github.com/lxpollitt/docker@v1.5.0/registry/session.go (about) 1 package registry 2 3 import ( 4 "bytes" 5 "crypto/sha256" 6 // this is required for some certificates 7 _ "crypto/sha512" 8 "encoding/hex" 9 "encoding/json" 10 "fmt" 11 "io" 12 "io/ioutil" 13 "net/http" 14 "net/http/cookiejar" 15 "net/url" 16 "strconv" 17 "strings" 18 "time" 19 20 log "github.com/Sirupsen/logrus" 21 "github.com/docker/docker/pkg/httputils" 22 "github.com/docker/docker/pkg/tarsum" 23 "github.com/docker/docker/utils" 24 ) 25 26 type Session struct { 27 authConfig *AuthConfig 28 reqFactory *utils.HTTPRequestFactory 29 indexEndpoint *Endpoint 30 jar *cookiejar.Jar 31 timeout TimeoutType 32 } 33 34 func NewSession(authConfig *AuthConfig, factory *utils.HTTPRequestFactory, endpoint *Endpoint, timeout bool) (r *Session, err error) { 35 r = &Session{ 36 authConfig: authConfig, 37 indexEndpoint: endpoint, 38 } 39 40 if timeout { 41 r.timeout = ReceiveTimeout 42 } 43 44 r.jar, err = cookiejar.New(nil) 45 if err != nil { 46 return nil, err 47 } 48 49 // If we're working with a standalone private registry over HTTPS, send Basic Auth headers 50 // alongside our requests. 51 if r.indexEndpoint.VersionString(1) != IndexServerAddress() && r.indexEndpoint.URL.Scheme == "https" { 52 info, err := r.indexEndpoint.Ping() 53 if err != nil { 54 return nil, err 55 } 56 if info.Standalone { 57 log.Debugf("Endpoint %s is eligible for private registry registry. Enabling decorator.", r.indexEndpoint.String()) 58 dec := utils.NewHTTPAuthDecorator(authConfig.Username, authConfig.Password) 59 factory.AddDecorator(dec) 60 } 61 } 62 63 r.reqFactory = factory 64 return r, nil 65 } 66 67 func (r *Session) doRequest(req *http.Request) (*http.Response, *http.Client, error) { 68 return doRequest(req, r.jar, r.timeout, r.indexEndpoint.IsSecure) 69 } 70 71 // Retrieve the history of a given image from the Registry. 72 // Return a list of the parent's json (requested image included) 73 func (r *Session) GetRemoteHistory(imgID, registry string, token []string) ([]string, error) { 74 req, err := r.reqFactory.NewRequest("GET", registry+"images/"+imgID+"/ancestry", nil) 75 if err != nil { 76 return nil, err 77 } 78 setTokenAuth(req, token) 79 res, _, err := r.doRequest(req) 80 if err != nil { 81 return nil, err 82 } 83 defer res.Body.Close() 84 if res.StatusCode != 200 { 85 if res.StatusCode == 401 { 86 return nil, errLoginRequired 87 } 88 return nil, utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to fetch remote history for %s", res.StatusCode, imgID), res) 89 } 90 91 jsonString, err := ioutil.ReadAll(res.Body) 92 if err != nil { 93 return nil, fmt.Errorf("Error while reading the http response: %s", err) 94 } 95 96 log.Debugf("Ancestry: %s", jsonString) 97 history := new([]string) 98 if err := json.Unmarshal(jsonString, history); err != nil { 99 return nil, err 100 } 101 return *history, nil 102 } 103 104 // Check if an image exists in the Registry 105 func (r *Session) LookupRemoteImage(imgID, registry string, token []string) error { 106 req, err := r.reqFactory.NewRequest("GET", registry+"images/"+imgID+"/json", nil) 107 if err != nil { 108 return err 109 } 110 setTokenAuth(req, token) 111 res, _, err := r.doRequest(req) 112 if err != nil { 113 return err 114 } 115 res.Body.Close() 116 if res.StatusCode != 200 { 117 return utils.NewHTTPRequestError(fmt.Sprintf("HTTP code %d", res.StatusCode), res) 118 } 119 return nil 120 } 121 122 // Retrieve an image from the Registry. 123 func (r *Session) GetRemoteImageJSON(imgID, registry string, token []string) ([]byte, int, error) { 124 // Get the JSON 125 req, err := r.reqFactory.NewRequest("GET", registry+"images/"+imgID+"/json", nil) 126 if err != nil { 127 return nil, -1, fmt.Errorf("Failed to download json: %s", err) 128 } 129 setTokenAuth(req, token) 130 res, _, err := r.doRequest(req) 131 if err != nil { 132 return nil, -1, fmt.Errorf("Failed to download json: %s", err) 133 } 134 defer res.Body.Close() 135 if res.StatusCode != 200 { 136 return nil, -1, utils.NewHTTPRequestError(fmt.Sprintf("HTTP code %d", res.StatusCode), res) 137 } 138 // if the size header is not present, then set it to '-1' 139 imageSize := -1 140 if hdr := res.Header.Get("X-Docker-Size"); hdr != "" { 141 imageSize, err = strconv.Atoi(hdr) 142 if err != nil { 143 return nil, -1, err 144 } 145 } 146 147 jsonString, err := ioutil.ReadAll(res.Body) 148 if err != nil { 149 return nil, -1, fmt.Errorf("Failed to parse downloaded json: %s (%s)", err, jsonString) 150 } 151 return jsonString, imageSize, nil 152 } 153 154 func (r *Session) GetRemoteImageLayer(imgID, registry string, token []string, imgSize int64) (io.ReadCloser, error) { 155 var ( 156 retries = 5 157 statusCode = 0 158 client *http.Client 159 res *http.Response 160 imageURL = fmt.Sprintf("%simages/%s/layer", registry, imgID) 161 ) 162 163 req, err := r.reqFactory.NewRequest("GET", imageURL, nil) 164 if err != nil { 165 return nil, fmt.Errorf("Error while getting from the server: %s\n", err) 166 } 167 setTokenAuth(req, token) 168 for i := 1; i <= retries; i++ { 169 statusCode = 0 170 res, client, err = r.doRequest(req) 171 if err != nil { 172 log.Debugf("Error contacting registry: %s", err) 173 if res != nil { 174 if res.Body != nil { 175 res.Body.Close() 176 } 177 statusCode = res.StatusCode 178 } 179 if i == retries { 180 return nil, fmt.Errorf("Server error: Status %d while fetching image layer (%s)", 181 statusCode, imgID) 182 } 183 time.Sleep(time.Duration(i) * 5 * time.Second) 184 continue 185 } 186 break 187 } 188 189 if res.StatusCode != 200 { 190 res.Body.Close() 191 return nil, fmt.Errorf("Server error: Status %d while fetching image layer (%s)", 192 res.StatusCode, imgID) 193 } 194 195 if res.Header.Get("Accept-Ranges") == "bytes" && imgSize > 0 { 196 log.Debugf("server supports resume") 197 return httputils.ResumableRequestReaderWithInitialResponse(client, req, 5, imgSize, res), nil 198 } 199 log.Debugf("server doesn't support resume") 200 return res.Body, nil 201 } 202 203 func (r *Session) GetRemoteTags(registries []string, repository string, token []string) (map[string]string, error) { 204 if strings.Count(repository, "/") == 0 { 205 // This will be removed once the Registry supports auto-resolution on 206 // the "library" namespace 207 repository = "library/" + repository 208 } 209 for _, host := range registries { 210 endpoint := fmt.Sprintf("%srepositories/%s/tags", host, repository) 211 req, err := r.reqFactory.NewRequest("GET", endpoint, nil) 212 213 if err != nil { 214 return nil, err 215 } 216 setTokenAuth(req, token) 217 res, _, err := r.doRequest(req) 218 if err != nil { 219 return nil, err 220 } 221 222 log.Debugf("Got status code %d from %s", res.StatusCode, endpoint) 223 defer res.Body.Close() 224 225 if res.StatusCode != 200 && res.StatusCode != 404 { 226 continue 227 } else if res.StatusCode == 404 { 228 return nil, fmt.Errorf("Repository not found") 229 } 230 231 result := make(map[string]string) 232 if err := json.NewDecoder(res.Body).Decode(&result); err != nil { 233 return nil, err 234 } 235 return result, nil 236 } 237 return nil, fmt.Errorf("Could not reach any registry endpoint") 238 } 239 240 func buildEndpointsList(headers []string, indexEp string) ([]string, error) { 241 var endpoints []string 242 parsedURL, err := url.Parse(indexEp) 243 if err != nil { 244 return nil, err 245 } 246 var urlScheme = parsedURL.Scheme 247 // The Registry's URL scheme has to match the Index' 248 for _, ep := range headers { 249 epList := strings.Split(ep, ",") 250 for _, epListElement := range epList { 251 endpoints = append( 252 endpoints, 253 fmt.Sprintf("%s://%s/v1/", urlScheme, strings.TrimSpace(epListElement))) 254 } 255 } 256 return endpoints, nil 257 } 258 259 func (r *Session) GetRepositoryData(remote string) (*RepositoryData, error) { 260 repositoryTarget := fmt.Sprintf("%srepositories/%s/images", r.indexEndpoint.VersionString(1), remote) 261 262 log.Debugf("[registry] Calling GET %s", repositoryTarget) 263 264 req, err := r.reqFactory.NewRequest("GET", repositoryTarget, nil) 265 if err != nil { 266 return nil, err 267 } 268 if r.authConfig != nil && len(r.authConfig.Username) > 0 { 269 req.SetBasicAuth(r.authConfig.Username, r.authConfig.Password) 270 } 271 req.Header.Set("X-Docker-Token", "true") 272 273 res, _, err := r.doRequest(req) 274 if err != nil { 275 return nil, err 276 } 277 defer res.Body.Close() 278 if res.StatusCode == 401 { 279 return nil, errLoginRequired 280 } 281 // TODO: Right now we're ignoring checksums in the response body. 282 // In the future, we need to use them to check image validity. 283 if res.StatusCode != 200 { 284 return nil, utils.NewHTTPRequestError(fmt.Sprintf("HTTP code: %d", res.StatusCode), res) 285 } 286 287 var tokens []string 288 if res.Header.Get("X-Docker-Token") != "" { 289 tokens = res.Header["X-Docker-Token"] 290 } 291 292 var endpoints []string 293 if res.Header.Get("X-Docker-Endpoints") != "" { 294 endpoints, err = buildEndpointsList(res.Header["X-Docker-Endpoints"], r.indexEndpoint.VersionString(1)) 295 if err != nil { 296 return nil, err 297 } 298 } else { 299 // Assume the endpoint is on the same host 300 endpoints = append(endpoints, fmt.Sprintf("%s://%s/v1/", r.indexEndpoint.URL.Scheme, req.URL.Host)) 301 } 302 303 remoteChecksums := []*ImgData{} 304 if err := json.NewDecoder(res.Body).Decode(&remoteChecksums); err != nil { 305 return nil, err 306 } 307 308 // Forge a better object from the retrieved data 309 imgsData := make(map[string]*ImgData) 310 for _, elem := range remoteChecksums { 311 imgsData[elem.ID] = elem 312 } 313 314 return &RepositoryData{ 315 ImgList: imgsData, 316 Endpoints: endpoints, 317 Tokens: tokens, 318 }, nil 319 } 320 321 func (r *Session) PushImageChecksumRegistry(imgData *ImgData, registry string, token []string) error { 322 323 log.Debugf("[registry] Calling PUT %s", registry+"images/"+imgData.ID+"/checksum") 324 325 req, err := r.reqFactory.NewRequest("PUT", registry+"images/"+imgData.ID+"/checksum", nil) 326 if err != nil { 327 return err 328 } 329 setTokenAuth(req, token) 330 req.Header.Set("X-Docker-Checksum", imgData.Checksum) 331 req.Header.Set("X-Docker-Checksum-Payload", imgData.ChecksumPayload) 332 333 res, _, err := r.doRequest(req) 334 if err != nil { 335 return fmt.Errorf("Failed to upload metadata: %s", err) 336 } 337 defer res.Body.Close() 338 if len(res.Cookies()) > 0 { 339 r.jar.SetCookies(req.URL, res.Cookies()) 340 } 341 if res.StatusCode != 200 { 342 errBody, err := ioutil.ReadAll(res.Body) 343 if err != nil { 344 return fmt.Errorf("HTTP code %d while uploading metadata and error when trying to parse response body: %s", res.StatusCode, err) 345 } 346 var jsonBody map[string]string 347 if err := json.Unmarshal(errBody, &jsonBody); err != nil { 348 errBody = []byte(err.Error()) 349 } else if jsonBody["error"] == "Image already exists" { 350 return ErrAlreadyExists 351 } 352 return fmt.Errorf("HTTP code %d while uploading metadata: %s", res.StatusCode, errBody) 353 } 354 return nil 355 } 356 357 // Push a local image to the registry 358 func (r *Session) PushImageJSONRegistry(imgData *ImgData, jsonRaw []byte, registry string, token []string) error { 359 360 log.Debugf("[registry] Calling PUT %s", registry+"images/"+imgData.ID+"/json") 361 362 req, err := r.reqFactory.NewRequest("PUT", registry+"images/"+imgData.ID+"/json", bytes.NewReader(jsonRaw)) 363 if err != nil { 364 return err 365 } 366 req.Header.Add("Content-type", "application/json") 367 setTokenAuth(req, token) 368 369 res, _, err := r.doRequest(req) 370 if err != nil { 371 return fmt.Errorf("Failed to upload metadata: %s", err) 372 } 373 defer res.Body.Close() 374 if res.StatusCode == 401 && strings.HasPrefix(registry, "http://") { 375 return utils.NewHTTPRequestError("HTTP code 401, Docker will not send auth headers over HTTP.", res) 376 } 377 if res.StatusCode != 200 { 378 errBody, err := ioutil.ReadAll(res.Body) 379 if err != nil { 380 return utils.NewHTTPRequestError(fmt.Sprintf("HTTP code %d while uploading metadata and error when trying to parse response body: %s", res.StatusCode, err), res) 381 } 382 var jsonBody map[string]string 383 if err := json.Unmarshal(errBody, &jsonBody); err != nil { 384 errBody = []byte(err.Error()) 385 } else if jsonBody["error"] == "Image already exists" { 386 return ErrAlreadyExists 387 } 388 return utils.NewHTTPRequestError(fmt.Sprintf("HTTP code %d while uploading metadata: %s", res.StatusCode, errBody), res) 389 } 390 return nil 391 } 392 393 func (r *Session) PushImageLayerRegistry(imgID string, layer io.Reader, registry string, token []string, jsonRaw []byte) (checksum string, checksumPayload string, err error) { 394 395 log.Debugf("[registry] Calling PUT %s", registry+"images/"+imgID+"/layer") 396 397 tarsumLayer, err := tarsum.NewTarSum(layer, false, tarsum.Version0) 398 if err != nil { 399 return "", "", err 400 } 401 h := sha256.New() 402 h.Write(jsonRaw) 403 h.Write([]byte{'\n'}) 404 checksumLayer := io.TeeReader(tarsumLayer, h) 405 406 req, err := r.reqFactory.NewRequest("PUT", registry+"images/"+imgID+"/layer", checksumLayer) 407 if err != nil { 408 return "", "", err 409 } 410 req.Header.Add("Content-Type", "application/octet-stream") 411 req.ContentLength = -1 412 req.TransferEncoding = []string{"chunked"} 413 setTokenAuth(req, token) 414 res, _, err := r.doRequest(req) 415 if err != nil { 416 return "", "", fmt.Errorf("Failed to upload layer: %s", err) 417 } 418 if rc, ok := layer.(io.Closer); ok { 419 if err := rc.Close(); err != nil { 420 return "", "", err 421 } 422 } 423 defer res.Body.Close() 424 425 if res.StatusCode != 200 { 426 errBody, err := ioutil.ReadAll(res.Body) 427 if err != nil { 428 return "", "", utils.NewHTTPRequestError(fmt.Sprintf("HTTP code %d while uploading metadata and error when trying to parse response body: %s", res.StatusCode, err), res) 429 } 430 return "", "", utils.NewHTTPRequestError(fmt.Sprintf("Received HTTP code %d while uploading layer: %s", res.StatusCode, errBody), res) 431 } 432 433 checksumPayload = "sha256:" + hex.EncodeToString(h.Sum(nil)) 434 return tarsumLayer.Sum(jsonRaw), checksumPayload, nil 435 } 436 437 // push a tag on the registry. 438 // Remote has the format '<user>/<repo> 439 func (r *Session) PushRegistryTag(remote, revision, tag, registry string, token []string) error { 440 // "jsonify" the string 441 revision = "\"" + revision + "\"" 442 path := fmt.Sprintf("repositories/%s/tags/%s", remote, tag) 443 444 req, err := r.reqFactory.NewRequest("PUT", registry+path, strings.NewReader(revision)) 445 if err != nil { 446 return err 447 } 448 req.Header.Add("Content-type", "application/json") 449 setTokenAuth(req, token) 450 req.ContentLength = int64(len(revision)) 451 res, _, err := r.doRequest(req) 452 if err != nil { 453 return err 454 } 455 res.Body.Close() 456 if res.StatusCode != 200 && res.StatusCode != 201 { 457 return utils.NewHTTPRequestError(fmt.Sprintf("Internal server error: %d trying to push tag %s on %s", res.StatusCode, tag, remote), res) 458 } 459 return nil 460 } 461 462 func (r *Session) PushImageJSONIndex(remote string, imgList []*ImgData, validate bool, regs []string) (*RepositoryData, error) { 463 cleanImgList := []*ImgData{} 464 if validate { 465 for _, elem := range imgList { 466 if elem.Checksum != "" { 467 cleanImgList = append(cleanImgList, elem) 468 } 469 } 470 } else { 471 cleanImgList = imgList 472 } 473 474 imgListJSON, err := json.Marshal(cleanImgList) 475 if err != nil { 476 return nil, err 477 } 478 var suffix string 479 if validate { 480 suffix = "images" 481 } 482 u := fmt.Sprintf("%srepositories/%s/%s", r.indexEndpoint.VersionString(1), remote, suffix) 483 log.Debugf("[registry] PUT %s", u) 484 log.Debugf("Image list pushed to index:\n%s", imgListJSON) 485 headers := map[string][]string{ 486 "Content-type": {"application/json"}, 487 "X-Docker-Token": {"true"}, 488 } 489 if validate { 490 headers["X-Docker-Endpoints"] = regs 491 } 492 493 // Redirect if necessary 494 var res *http.Response 495 for { 496 if res, err = r.putImageRequest(u, headers, imgListJSON); err != nil { 497 return nil, err 498 } 499 if !shouldRedirect(res) { 500 break 501 } 502 res.Body.Close() 503 u = res.Header.Get("Location") 504 log.Debugf("Redirected to %s", u) 505 } 506 defer res.Body.Close() 507 508 var tokens, endpoints []string 509 if !validate { 510 if res.StatusCode != 200 && res.StatusCode != 201 { 511 errBody, err := ioutil.ReadAll(res.Body) 512 if err != nil { 513 return nil, err 514 } 515 return nil, utils.NewHTTPRequestError(fmt.Sprintf("Error: Status %d trying to push repository %s: %s", res.StatusCode, remote, errBody), res) 516 } 517 if res.Header.Get("X-Docker-Token") != "" { 518 tokens = res.Header["X-Docker-Token"] 519 log.Debugf("Auth token: %v", tokens) 520 } else { 521 return nil, fmt.Errorf("Index response didn't contain an access token") 522 } 523 524 if res.Header.Get("X-Docker-Endpoints") != "" { 525 endpoints, err = buildEndpointsList(res.Header["X-Docker-Endpoints"], r.indexEndpoint.VersionString(1)) 526 if err != nil { 527 return nil, err 528 } 529 } else { 530 return nil, fmt.Errorf("Index response didn't contain any endpoints") 531 } 532 } 533 if validate { 534 if res.StatusCode != 204 { 535 errBody, err := ioutil.ReadAll(res.Body) 536 if err != nil { 537 return nil, err 538 } 539 return nil, utils.NewHTTPRequestError(fmt.Sprintf("Error: Status %d trying to push checksums %s: %s", res.StatusCode, remote, errBody), res) 540 } 541 } 542 543 return &RepositoryData{ 544 Tokens: tokens, 545 Endpoints: endpoints, 546 }, nil 547 } 548 549 func (r *Session) putImageRequest(u string, headers map[string][]string, body []byte) (*http.Response, error) { 550 req, err := r.reqFactory.NewRequest("PUT", u, bytes.NewReader(body)) 551 if err != nil { 552 return nil, err 553 } 554 req.SetBasicAuth(r.authConfig.Username, r.authConfig.Password) 555 req.ContentLength = int64(len(body)) 556 for k, v := range headers { 557 req.Header[k] = v 558 } 559 response, _, err := r.doRequest(req) 560 if err != nil { 561 return nil, err 562 } 563 return response, nil 564 } 565 566 func shouldRedirect(response *http.Response) bool { 567 return response.StatusCode >= 300 && response.StatusCode < 400 568 } 569 570 func (r *Session) SearchRepositories(term string) (*SearchResults, error) { 571 log.Debugf("Index server: %s", r.indexEndpoint) 572 u := r.indexEndpoint.VersionString(1) + "search?q=" + url.QueryEscape(term) 573 req, err := r.reqFactory.NewRequest("GET", u, nil) 574 if err != nil { 575 return nil, err 576 } 577 if r.authConfig != nil && len(r.authConfig.Username) > 0 { 578 req.SetBasicAuth(r.authConfig.Username, r.authConfig.Password) 579 } 580 req.Header.Set("X-Docker-Token", "true") 581 res, _, err := r.doRequest(req) 582 if err != nil { 583 return nil, err 584 } 585 defer res.Body.Close() 586 if res.StatusCode != 200 { 587 return nil, utils.NewHTTPRequestError(fmt.Sprintf("Unexpected status code %d", res.StatusCode), res) 588 } 589 result := new(SearchResults) 590 err = json.NewDecoder(res.Body).Decode(result) 591 return result, err 592 } 593 594 func (r *Session) GetAuthConfig(withPasswd bool) *AuthConfig { 595 password := "" 596 if withPasswd { 597 password = r.authConfig.Password 598 } 599 return &AuthConfig{ 600 Username: r.authConfig.Username, 601 Password: password, 602 Email: r.authConfig.Email, 603 } 604 } 605 606 func setTokenAuth(req *http.Request, token []string) { 607 if req.Header.Get("Authorization") == "" { // Don't override 608 req.Header.Set("Authorization", "Token "+strings.Join(token, ",")) 609 } 610 }