github.com/ralexstokes/docker@v1.6.2/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. 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 == 404 { 284 return nil, utils.NewHTTPRequestError(fmt.Sprintf("HTTP code: %d", res.StatusCode), res) 285 } else if res.StatusCode != 200 { 286 errBody, err := ioutil.ReadAll(res.Body) 287 if err != nil { 288 log.Debugf("Error reading response body: %s", err) 289 } 290 return nil, utils.NewHTTPRequestError(fmt.Sprintf("Error: Status %d trying to pull repository %s: %q", res.StatusCode, remote, errBody), res) 291 } 292 293 var tokens []string 294 if res.Header.Get("X-Docker-Token") != "" { 295 tokens = res.Header["X-Docker-Token"] 296 } 297 298 var endpoints []string 299 if res.Header.Get("X-Docker-Endpoints") != "" { 300 endpoints, err = buildEndpointsList(res.Header["X-Docker-Endpoints"], r.indexEndpoint.VersionString(1)) 301 if err != nil { 302 return nil, err 303 } 304 } else { 305 // Assume the endpoint is on the same host 306 endpoints = append(endpoints, fmt.Sprintf("%s://%s/v1/", r.indexEndpoint.URL.Scheme, req.URL.Host)) 307 } 308 309 remoteChecksums := []*ImgData{} 310 if err := json.NewDecoder(res.Body).Decode(&remoteChecksums); err != nil { 311 return nil, err 312 } 313 314 // Forge a better object from the retrieved data 315 imgsData := make(map[string]*ImgData) 316 for _, elem := range remoteChecksums { 317 imgsData[elem.ID] = elem 318 } 319 320 return &RepositoryData{ 321 ImgList: imgsData, 322 Endpoints: endpoints, 323 Tokens: tokens, 324 }, nil 325 } 326 327 func (r *Session) PushImageChecksumRegistry(imgData *ImgData, registry string, token []string) error { 328 329 log.Debugf("[registry] Calling PUT %s", registry+"images/"+imgData.ID+"/checksum") 330 331 req, err := r.reqFactory.NewRequest("PUT", registry+"images/"+imgData.ID+"/checksum", nil) 332 if err != nil { 333 return err 334 } 335 setTokenAuth(req, token) 336 req.Header.Set("X-Docker-Checksum", imgData.Checksum) 337 req.Header.Set("X-Docker-Checksum-Payload", imgData.ChecksumPayload) 338 339 res, _, err := r.doRequest(req) 340 if err != nil { 341 return fmt.Errorf("Failed to upload metadata: %s", err) 342 } 343 defer res.Body.Close() 344 if len(res.Cookies()) > 0 { 345 r.jar.SetCookies(req.URL, res.Cookies()) 346 } 347 if res.StatusCode != 200 { 348 errBody, err := ioutil.ReadAll(res.Body) 349 if err != nil { 350 return fmt.Errorf("HTTP code %d while uploading metadata and error when trying to parse response body: %s", res.StatusCode, err) 351 } 352 var jsonBody map[string]string 353 if err := json.Unmarshal(errBody, &jsonBody); err != nil { 354 errBody = []byte(err.Error()) 355 } else if jsonBody["error"] == "Image already exists" { 356 return ErrAlreadyExists 357 } 358 return fmt.Errorf("HTTP code %d while uploading metadata: %q", res.StatusCode, errBody) 359 } 360 return nil 361 } 362 363 // Push a local image to the registry 364 func (r *Session) PushImageJSONRegistry(imgData *ImgData, jsonRaw []byte, registry string, token []string) error { 365 366 log.Debugf("[registry] Calling PUT %s", registry+"images/"+imgData.ID+"/json") 367 368 req, err := r.reqFactory.NewRequest("PUT", registry+"images/"+imgData.ID+"/json", bytes.NewReader(jsonRaw)) 369 if err != nil { 370 return err 371 } 372 req.Header.Add("Content-type", "application/json") 373 setTokenAuth(req, token) 374 375 res, _, err := r.doRequest(req) 376 if err != nil { 377 return fmt.Errorf("Failed to upload metadata: %s", err) 378 } 379 defer res.Body.Close() 380 if res.StatusCode == 401 && strings.HasPrefix(registry, "http://") { 381 return utils.NewHTTPRequestError("HTTP code 401, Docker will not send auth headers over HTTP.", res) 382 } 383 if res.StatusCode != 200 { 384 errBody, err := ioutil.ReadAll(res.Body) 385 if err != nil { 386 return utils.NewHTTPRequestError(fmt.Sprintf("HTTP code %d while uploading metadata and error when trying to parse response body: %s", res.StatusCode, err), res) 387 } 388 var jsonBody map[string]string 389 if err := json.Unmarshal(errBody, &jsonBody); err != nil { 390 errBody = []byte(err.Error()) 391 } else if jsonBody["error"] == "Image already exists" { 392 return ErrAlreadyExists 393 } 394 return utils.NewHTTPRequestError(fmt.Sprintf("HTTP code %d while uploading metadata: %q", res.StatusCode, errBody), res) 395 } 396 return nil 397 } 398 399 func (r *Session) PushImageLayerRegistry(imgID string, layer io.Reader, registry string, token []string, jsonRaw []byte) (checksum string, checksumPayload string, err error) { 400 401 log.Debugf("[registry] Calling PUT %s", registry+"images/"+imgID+"/layer") 402 403 tarsumLayer, err := tarsum.NewTarSum(layer, false, tarsum.Version0) 404 if err != nil { 405 return "", "", err 406 } 407 h := sha256.New() 408 h.Write(jsonRaw) 409 h.Write([]byte{'\n'}) 410 checksumLayer := io.TeeReader(tarsumLayer, h) 411 412 req, err := r.reqFactory.NewRequest("PUT", registry+"images/"+imgID+"/layer", checksumLayer) 413 if err != nil { 414 return "", "", err 415 } 416 req.Header.Add("Content-Type", "application/octet-stream") 417 req.ContentLength = -1 418 req.TransferEncoding = []string{"chunked"} 419 setTokenAuth(req, token) 420 res, _, err := r.doRequest(req) 421 if err != nil { 422 return "", "", fmt.Errorf("Failed to upload layer: %s", err) 423 } 424 if rc, ok := layer.(io.Closer); ok { 425 if err := rc.Close(); err != nil { 426 return "", "", err 427 } 428 } 429 defer res.Body.Close() 430 431 if res.StatusCode != 200 { 432 errBody, err := ioutil.ReadAll(res.Body) 433 if err != nil { 434 return "", "", utils.NewHTTPRequestError(fmt.Sprintf("HTTP code %d while uploading metadata and error when trying to parse response body: %s", res.StatusCode, err), res) 435 } 436 return "", "", utils.NewHTTPRequestError(fmt.Sprintf("Received HTTP code %d while uploading layer: %q", res.StatusCode, errBody), res) 437 } 438 439 checksumPayload = "sha256:" + hex.EncodeToString(h.Sum(nil)) 440 return tarsumLayer.Sum(jsonRaw), checksumPayload, nil 441 } 442 443 // push a tag on the registry. 444 // Remote has the format '<user>/<repo> 445 func (r *Session) PushRegistryTag(remote, revision, tag, registry string, token []string) error { 446 // "jsonify" the string 447 revision = "\"" + revision + "\"" 448 path := fmt.Sprintf("repositories/%s/tags/%s", remote, tag) 449 450 req, err := r.reqFactory.NewRequest("PUT", registry+path, strings.NewReader(revision)) 451 if err != nil { 452 return err 453 } 454 req.Header.Add("Content-type", "application/json") 455 setTokenAuth(req, token) 456 req.ContentLength = int64(len(revision)) 457 res, _, err := r.doRequest(req) 458 if err != nil { 459 return err 460 } 461 res.Body.Close() 462 if res.StatusCode != 200 && res.StatusCode != 201 { 463 return utils.NewHTTPRequestError(fmt.Sprintf("Internal server error: %d trying to push tag %s on %s", res.StatusCode, tag, remote), res) 464 } 465 return nil 466 } 467 468 func (r *Session) PushImageJSONIndex(remote string, imgList []*ImgData, validate bool, regs []string) (*RepositoryData, error) { 469 cleanImgList := []*ImgData{} 470 if validate { 471 for _, elem := range imgList { 472 if elem.Checksum != "" { 473 cleanImgList = append(cleanImgList, elem) 474 } 475 } 476 } else { 477 cleanImgList = imgList 478 } 479 480 imgListJSON, err := json.Marshal(cleanImgList) 481 if err != nil { 482 return nil, err 483 } 484 var suffix string 485 if validate { 486 suffix = "images" 487 } 488 u := fmt.Sprintf("%srepositories/%s/%s", r.indexEndpoint.VersionString(1), remote, suffix) 489 log.Debugf("[registry] PUT %s", u) 490 log.Debugf("Image list pushed to index:\n%s", imgListJSON) 491 headers := map[string][]string{ 492 "Content-type": {"application/json"}, 493 "X-Docker-Token": {"true"}, 494 } 495 if validate { 496 headers["X-Docker-Endpoints"] = regs 497 } 498 499 // Redirect if necessary 500 var res *http.Response 501 for { 502 if res, err = r.putImageRequest(u, headers, imgListJSON); err != nil { 503 return nil, err 504 } 505 if !shouldRedirect(res) { 506 break 507 } 508 res.Body.Close() 509 u = res.Header.Get("Location") 510 log.Debugf("Redirected to %s", u) 511 } 512 defer res.Body.Close() 513 514 if res.StatusCode == 401 { 515 return nil, errLoginRequired 516 } 517 518 var tokens, endpoints []string 519 if !validate { 520 if res.StatusCode != 200 && res.StatusCode != 201 { 521 errBody, err := ioutil.ReadAll(res.Body) 522 if err != nil { 523 log.Debugf("Error reading response body: %s", err) 524 } 525 return nil, utils.NewHTTPRequestError(fmt.Sprintf("Error: Status %d trying to push repository %s: %q", res.StatusCode, remote, errBody), res) 526 } 527 if res.Header.Get("X-Docker-Token") != "" { 528 tokens = res.Header["X-Docker-Token"] 529 log.Debugf("Auth token: %v", tokens) 530 } else { 531 return nil, fmt.Errorf("Index response didn't contain an access token") 532 } 533 534 if res.Header.Get("X-Docker-Endpoints") != "" { 535 endpoints, err = buildEndpointsList(res.Header["X-Docker-Endpoints"], r.indexEndpoint.VersionString(1)) 536 if err != nil { 537 return nil, err 538 } 539 } else { 540 return nil, fmt.Errorf("Index response didn't contain any endpoints") 541 } 542 } 543 if validate { 544 if res.StatusCode != 204 { 545 errBody, err := ioutil.ReadAll(res.Body) 546 if err != nil { 547 log.Debugf("Error reading response body: %s", err) 548 } 549 return nil, utils.NewHTTPRequestError(fmt.Sprintf("Error: Status %d trying to push checksums %s: %q", res.StatusCode, remote, errBody), res) 550 } 551 } 552 553 return &RepositoryData{ 554 Tokens: tokens, 555 Endpoints: endpoints, 556 }, nil 557 } 558 559 func (r *Session) putImageRequest(u string, headers map[string][]string, body []byte) (*http.Response, error) { 560 req, err := r.reqFactory.NewRequest("PUT", u, bytes.NewReader(body)) 561 if err != nil { 562 return nil, err 563 } 564 req.SetBasicAuth(r.authConfig.Username, r.authConfig.Password) 565 req.ContentLength = int64(len(body)) 566 for k, v := range headers { 567 req.Header[k] = v 568 } 569 response, _, err := r.doRequest(req) 570 if err != nil { 571 return nil, err 572 } 573 return response, nil 574 } 575 576 func shouldRedirect(response *http.Response) bool { 577 return response.StatusCode >= 300 && response.StatusCode < 400 578 } 579 580 func (r *Session) SearchRepositories(term string) (*SearchResults, error) { 581 log.Debugf("Index server: %s", r.indexEndpoint) 582 u := r.indexEndpoint.VersionString(1) + "search?q=" + url.QueryEscape(term) 583 req, err := r.reqFactory.NewRequest("GET", u, nil) 584 if err != nil { 585 return nil, err 586 } 587 if r.authConfig != nil && len(r.authConfig.Username) > 0 { 588 req.SetBasicAuth(r.authConfig.Username, r.authConfig.Password) 589 } 590 req.Header.Set("X-Docker-Token", "true") 591 res, _, err := r.doRequest(req) 592 if err != nil { 593 return nil, err 594 } 595 defer res.Body.Close() 596 if res.StatusCode != 200 { 597 return nil, utils.NewHTTPRequestError(fmt.Sprintf("Unexpected status code %d", res.StatusCode), res) 598 } 599 result := new(SearchResults) 600 err = json.NewDecoder(res.Body).Decode(result) 601 return result, err 602 } 603 604 func (r *Session) GetAuthConfig(withPasswd bool) *AuthConfig { 605 password := "" 606 if withPasswd { 607 password = r.authConfig.Password 608 } 609 return &AuthConfig{ 610 Username: r.authConfig.Username, 611 Password: password, 612 Email: r.authConfig.Email, 613 } 614 } 615 616 func setTokenAuth(req *http.Request, token []string) { 617 if req.Header.Get("Authorization") == "" { // Don't override 618 req.Header.Set("Authorization", "Token "+strings.Join(token, ",")) 619 } 620 }