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