github.com/containerd/containerd@v22.0.0-20200918172823-438c87b8e050+incompatible/remotes/docker/resolver_test.go (about) 1 /* 2 Copyright The containerd Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package docker 18 19 import ( 20 "context" 21 "crypto/tls" 22 "crypto/x509" 23 "encoding/json" 24 "fmt" 25 "io" 26 "io/ioutil" 27 "net/http" 28 "net/http/httptest" 29 "strconv" 30 "strings" 31 "testing" 32 "time" 33 34 "github.com/containerd/containerd/remotes" 35 digest "github.com/opencontainers/go-digest" 36 specs "github.com/opencontainers/image-spec/specs-go" 37 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 38 "github.com/pkg/errors" 39 ) 40 41 func TestHTTPResolver(t *testing.T) { 42 s := func(h http.Handler) (string, ResolverOptions, func()) { 43 s := httptest.NewServer(h) 44 45 options := ResolverOptions{} 46 base := s.URL[7:] // strip "http://" 47 return base, options, s.Close 48 } 49 50 runBasicTest(t, "testname", s) 51 } 52 53 func TestHTTPSResolver(t *testing.T) { 54 runBasicTest(t, "testname", tlsServer) 55 } 56 57 func TestBasicResolver(t *testing.T) { 58 basicAuth := func(h http.Handler) (string, ResolverOptions, func()) { 59 // Wrap with basic auth 60 wrapped := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { 61 username, password, ok := r.BasicAuth() 62 if !ok || username != "user1" || password != "password1" { 63 rw.Header().Set("WWW-Authenticate", "Basic realm=localhost") 64 rw.WriteHeader(http.StatusUnauthorized) 65 return 66 } 67 h.ServeHTTP(rw, r) 68 }) 69 70 base, options, close := tlsServer(wrapped) 71 options.Hosts = ConfigureDefaultRegistries( 72 WithClient(options.Client), 73 WithAuthorizer(NewAuthorizer(options.Client, func(string) (string, string, error) { 74 return "user1", "password1", nil 75 })), 76 ) 77 return base, options, close 78 } 79 runBasicTest(t, "testname", basicAuth) 80 } 81 82 func TestAnonymousTokenResolver(t *testing.T) { 83 th := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { 84 if r.Method != http.MethodGet { 85 rw.WriteHeader(http.StatusMethodNotAllowed) 86 return 87 } 88 rw.Header().Set("Content-Type", "application/json") 89 rw.WriteHeader(http.StatusOK) 90 rw.Write([]byte(`{"access_token":"perfectlyvalidopaquetoken"}`)) 91 }) 92 93 runBasicTest(t, "testname", withTokenServer(th, nil)) 94 } 95 96 func TestBasicAuthTokenResolver(t *testing.T) { 97 th := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { 98 if r.Method != http.MethodGet { 99 rw.WriteHeader(http.StatusMethodNotAllowed) 100 return 101 } 102 rw.Header().Set("Content-Type", "application/json") 103 rw.WriteHeader(http.StatusOK) 104 username, password, ok := r.BasicAuth() 105 if !ok || username != "user1" || password != "password1" { 106 rw.Write([]byte(`{"access_token":"insufficientscope"}`)) 107 } else { 108 rw.Write([]byte(`{"access_token":"perfectlyvalidopaquetoken"}`)) 109 } 110 }) 111 creds := func(string) (string, string, error) { 112 return "user1", "password1", nil 113 } 114 115 runBasicTest(t, "testname", withTokenServer(th, creds)) 116 } 117 118 func TestRefreshTokenResolver(t *testing.T) { 119 th := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { 120 if r.Method != http.MethodPost { 121 rw.WriteHeader(http.StatusMethodNotAllowed) 122 return 123 } 124 rw.Header().Set("Content-Type", "application/json") 125 rw.WriteHeader(http.StatusOK) 126 127 r.ParseForm() 128 if r.PostForm.Get("grant_type") != "refresh_token" || r.PostForm.Get("refresh_token") != "somerefreshtoken" { 129 rw.Write([]byte(`{"access_token":"insufficientscope"}`)) 130 } else { 131 rw.Write([]byte(`{"access_token":"perfectlyvalidopaquetoken"}`)) 132 } 133 }) 134 creds := func(string) (string, string, error) { 135 return "", "somerefreshtoken", nil 136 } 137 138 runBasicTest(t, "testname", withTokenServer(th, creds)) 139 } 140 141 func TestPostBasicAuthTokenResolver(t *testing.T) { 142 th := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { 143 if r.Method != http.MethodPost { 144 rw.WriteHeader(http.StatusMethodNotAllowed) 145 return 146 } 147 rw.Header().Set("Content-Type", "application/json") 148 rw.WriteHeader(http.StatusOK) 149 150 r.ParseForm() 151 if r.PostForm.Get("grant_type") != "password" || r.PostForm.Get("username") != "user1" || r.PostForm.Get("password") != "password1" { 152 rw.Write([]byte(`{"access_token":"insufficientscope"}`)) 153 } else { 154 rw.Write([]byte(`{"access_token":"perfectlyvalidopaquetoken"}`)) 155 } 156 }) 157 creds := func(string) (string, string, error) { 158 return "user1", "password1", nil 159 } 160 161 runBasicTest(t, "testname", withTokenServer(th, creds)) 162 } 163 164 func TestBadTokenResolver(t *testing.T) { 165 th := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { 166 if r.Method != http.MethodPost { 167 rw.WriteHeader(http.StatusMethodNotAllowed) 168 return 169 } 170 rw.Header().Set("Content-Type", "application/json") 171 rw.WriteHeader(http.StatusOK) 172 rw.Write([]byte(`{"access_token":"insufficientscope"}`)) 173 }) 174 creds := func(string) (string, string, error) { 175 return "", "somerefreshtoken", nil 176 } 177 178 ctx := context.Background() 179 h := newContent(ocispec.MediaTypeImageManifest, []byte("not anything parse-able")) 180 181 base, ro, close := withTokenServer(th, creds)(logHandler{t, h}) 182 defer close() 183 184 resolver := NewResolver(ro) 185 image := fmt.Sprintf("%s/doesntmatter:sometatg", base) 186 187 _, _, err := resolver.Resolve(ctx, image) 188 if err == nil { 189 t.Fatal("Expected error getting token with inssufficient scope") 190 } 191 if !errors.Is(err, ErrInvalidAuthorization) { 192 t.Fatal(err) 193 } 194 } 195 196 func TestHostFailureFallbackResolver(t *testing.T) { 197 sf := func(h http.Handler) (string, ResolverOptions, func()) { 198 s := httptest.NewServer(h) 199 base := s.URL[7:] // strip "http://" 200 201 options := ResolverOptions{} 202 createHost := func(host string) RegistryHost { 203 return RegistryHost{ 204 Client: &http.Client{ 205 // Set the timeout so we timeout waiting for the non-responsive HTTP server 206 Timeout: 500 * time.Millisecond, 207 }, 208 Host: host, 209 Scheme: "http", 210 Path: "/v2", 211 Capabilities: HostCapabilityPull | HostCapabilityResolve | HostCapabilityPush, 212 } 213 } 214 215 // Create an unstarted HTTP server. We use this to generate a random port. 216 notRunning := httptest.NewUnstartedServer(nil) 217 notRunningBase := notRunning.Listener.Addr().String() 218 219 // Override hosts with two hosts 220 options.Hosts = func(host string) ([]RegistryHost, error) { 221 return []RegistryHost{ 222 createHost(notRunningBase), // This host IS running, but with a non-responsive HTTP server 223 createHost(base), // This host IS running 224 }, nil 225 } 226 227 return base, options, s.Close 228 } 229 230 runBasicTest(t, "testname", sf) 231 } 232 233 func TestHostTLSFailureFallbackResolver(t *testing.T) { 234 sf := func(h http.Handler) (string, ResolverOptions, func()) { 235 // Start up two servers 236 server := httptest.NewServer(h) 237 httpBase := server.URL[7:] // strip "http://" 238 239 tlsServer := httptest.NewUnstartedServer(h) 240 tlsServer.StartTLS() 241 httpsBase := tlsServer.URL[8:] // strip "https://" 242 243 capool := x509.NewCertPool() 244 cert, _ := x509.ParseCertificate(tlsServer.TLS.Certificates[0].Certificate[0]) 245 capool.AddCert(cert) 246 247 client := &http.Client{ 248 Transport: &http.Transport{ 249 TLSClientConfig: &tls.Config{ 250 RootCAs: capool, 251 }, 252 }, 253 } 254 255 options := ResolverOptions{} 256 createHost := func(host string) RegistryHost { 257 return RegistryHost{ 258 Client: client, 259 Host: host, 260 Scheme: "https", 261 Path: "/v2", 262 Capabilities: HostCapabilityPull | HostCapabilityResolve | HostCapabilityPush, 263 } 264 } 265 266 // Override hosts with two hosts 267 options.Hosts = func(host string) ([]RegistryHost, error) { 268 return []RegistryHost{ 269 createHost(httpBase), // This host is serving plain HTTP 270 createHost(httpsBase), // This host is serving TLS 271 }, nil 272 } 273 274 return httpBase, options, func() { 275 server.Close() 276 tlsServer.Close() 277 } 278 } 279 280 runBasicTest(t, "testname", sf) 281 } 282 283 func TestResolveProxy(t *testing.T) { 284 var ( 285 ctx = context.Background() 286 tag = "latest" 287 r = http.NewServeMux() 288 name = "testname" 289 ns = "upstream.example.com" 290 ) 291 292 m := newManifest( 293 newContent(ocispec.MediaTypeImageConfig, []byte("1")), 294 newContent(ocispec.MediaTypeImageLayerGzip, []byte("2")), 295 ) 296 mc := newContent(ocispec.MediaTypeImageManifest, m.OCIManifest()) 297 m.RegisterHandler(r, name) 298 r.Handle(fmt.Sprintf("/v2/%s/manifests/%s", name, tag), mc) 299 r.Handle(fmt.Sprintf("/v2/%s/manifests/%s", name, mc.Digest()), mc) 300 301 nr := namespaceRouter{ 302 "upstream.example.com": r, 303 } 304 305 base, ro, close := tlsServer(logHandler{t, nr}) 306 defer close() 307 308 ro.Hosts = func(host string) ([]RegistryHost, error) { 309 return []RegistryHost{{ 310 Client: ro.Client, 311 Host: base, 312 Scheme: "https", 313 Path: "/v2", 314 Capabilities: HostCapabilityPull | HostCapabilityResolve, 315 }}, nil 316 } 317 318 resolver := NewResolver(ro) 319 image := fmt.Sprintf("%s/%s:%s", ns, name, tag) 320 321 _, d, err := resolver.Resolve(ctx, image) 322 if err != nil { 323 t.Fatal(err) 324 } 325 f, err := resolver.Fetcher(ctx, image) 326 if err != nil { 327 t.Fatal(err) 328 } 329 330 refs, err := testocimanifest(ctx, f, d) 331 if err != nil { 332 t.Fatal(err) 333 } 334 335 if len(refs) != 2 { 336 t.Fatalf("Unexpected number of references: %d, expected 2", len(refs)) 337 } 338 339 for _, ref := range refs { 340 if err := testFetch(ctx, f, ref); err != nil { 341 t.Fatal(err) 342 } 343 } 344 } 345 346 func TestResolveProxyFallback(t *testing.T) { 347 var ( 348 ctx = context.Background() 349 tag = "latest" 350 r = http.NewServeMux() 351 name = "testname" 352 ) 353 354 m := newManifest( 355 newContent(ocispec.MediaTypeImageConfig, []byte("1")), 356 newContent(ocispec.MediaTypeImageLayerGzip, []byte("2")), 357 ) 358 mc := newContent(ocispec.MediaTypeImageManifest, m.OCIManifest()) 359 m.RegisterHandler(r, name) 360 r.Handle(fmt.Sprintf("/v2/%s/manifests/%s", name, tag), mc) 361 r.Handle(fmt.Sprintf("/v2/%s/manifests/%s", name, mc.Digest()), mc) 362 363 nr := namespaceRouter{ 364 "": r, 365 } 366 s := httptest.NewServer(logHandler{t, nr}) 367 defer s.Close() 368 369 base := s.URL[7:] // strip "http://" 370 371 ro := ResolverOptions{ 372 Hosts: func(host string) ([]RegistryHost, error) { 373 return []RegistryHost{ 374 { 375 Host: flipLocalhost(host), 376 Scheme: "http", 377 Path: "/v2", 378 Capabilities: HostCapabilityPull | HostCapabilityResolve, 379 }, 380 { 381 Host: host, 382 Scheme: "http", 383 Path: "/v2", 384 Capabilities: HostCapabilityPull | HostCapabilityResolve | HostCapabilityPush, 385 }, 386 }, nil 387 }, 388 } 389 390 resolver := NewResolver(ro) 391 image := fmt.Sprintf("%s/%s:%s", base, name, tag) 392 393 _, d, err := resolver.Resolve(ctx, image) 394 if err != nil { 395 t.Fatal(err) 396 } 397 f, err := resolver.Fetcher(ctx, image) 398 if err != nil { 399 t.Fatal(err) 400 } 401 402 refs, err := testocimanifest(ctx, f, d) 403 if err != nil { 404 t.Fatal(err) 405 } 406 407 if len(refs) != 2 { 408 t.Fatalf("Unexpected number of references: %d, expected 2", len(refs)) 409 } 410 411 for _, ref := range refs { 412 if err := testFetch(ctx, f, ref); err != nil { 413 t.Fatal(err) 414 } 415 } 416 } 417 418 func flipLocalhost(host string) string { 419 if strings.HasPrefix(host, "127.0.0.1") { 420 return "localhost" + host[9:] 421 422 } else if strings.HasPrefix(host, "localhost") { 423 return "127.0.0.1" + host[9:] 424 } 425 return host 426 } 427 428 func withTokenServer(th http.Handler, creds func(string) (string, string, error)) func(h http.Handler) (string, ResolverOptions, func()) { 429 return func(h http.Handler) (string, ResolverOptions, func()) { 430 s := httptest.NewUnstartedServer(th) 431 s.StartTLS() 432 433 cert, _ := x509.ParseCertificate(s.TLS.Certificates[0].Certificate[0]) 434 tokenBase := s.URL + "/token" 435 436 // Wrap with token auth 437 wrapped := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { 438 auth := strings.ToLower(r.Header.Get("Authorization")) 439 if auth != "bearer perfectlyvalidopaquetoken" { 440 authHeader := fmt.Sprintf("Bearer realm=%q,service=registry,scope=\"repository:testname:pull,pull\"", tokenBase) 441 if strings.HasPrefix(auth, "bearer ") { 442 authHeader = authHeader + ",error=" + auth[7:] 443 } 444 rw.Header().Set("WWW-Authenticate", authHeader) 445 rw.WriteHeader(http.StatusUnauthorized) 446 return 447 } 448 h.ServeHTTP(rw, r) 449 }) 450 451 base, options, close := tlsServer(wrapped) 452 options.Hosts = ConfigureDefaultRegistries( 453 WithClient(options.Client), 454 WithAuthorizer(NewDockerAuthorizer( 455 WithAuthClient(options.Client), 456 WithAuthCreds(creds), 457 )), 458 ) 459 options.Client.Transport.(*http.Transport).TLSClientConfig.RootCAs.AddCert(cert) 460 return base, options, func() { 461 s.Close() 462 close() 463 } 464 } 465 } 466 467 func tlsServer(h http.Handler) (string, ResolverOptions, func()) { 468 s := httptest.NewUnstartedServer(h) 469 s.StartTLS() 470 471 capool := x509.NewCertPool() 472 cert, _ := x509.ParseCertificate(s.TLS.Certificates[0].Certificate[0]) 473 capool.AddCert(cert) 474 475 client := &http.Client{ 476 Transport: &http.Transport{ 477 TLSClientConfig: &tls.Config{ 478 RootCAs: capool, 479 }, 480 }, 481 } 482 options := ResolverOptions{ 483 Hosts: ConfigureDefaultRegistries(WithClient(client)), 484 // Set deprecated field for tests to use for configuration 485 Client: client, 486 } 487 base := s.URL[8:] // strip "https://" 488 return base, options, s.Close 489 } 490 491 type logHandler struct { 492 t *testing.T 493 handler http.Handler 494 } 495 496 func (h logHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { 497 h.handler.ServeHTTP(rw, r) 498 } 499 500 type namespaceRouter map[string]http.Handler 501 502 func (nr namespaceRouter) ServeHTTP(rw http.ResponseWriter, r *http.Request) { 503 h, ok := nr[r.URL.Query().Get("ns")] 504 if !ok { 505 rw.WriteHeader(http.StatusNotFound) 506 return 507 } 508 h.ServeHTTP(rw, r) 509 } 510 511 func runBasicTest(t *testing.T, name string, sf func(h http.Handler) (string, ResolverOptions, func())) { 512 var ( 513 ctx = context.Background() 514 tag = "latest" 515 r = http.NewServeMux() 516 ) 517 518 m := newManifest( 519 newContent(ocispec.MediaTypeImageConfig, []byte("1")), 520 newContent(ocispec.MediaTypeImageLayerGzip, []byte("2")), 521 ) 522 mc := newContent(ocispec.MediaTypeImageManifest, m.OCIManifest()) 523 m.RegisterHandler(r, name) 524 r.Handle(fmt.Sprintf("/v2/%s/manifests/%s", name, tag), mc) 525 r.Handle(fmt.Sprintf("/v2/%s/manifests/%s", name, mc.Digest()), mc) 526 527 base, ro, close := sf(logHandler{t, r}) 528 defer close() 529 530 resolver := NewResolver(ro) 531 image := fmt.Sprintf("%s/%s:%s", base, name, tag) 532 533 _, d, err := resolver.Resolve(ctx, image) 534 if err != nil { 535 t.Fatal(err) 536 } 537 f, err := resolver.Fetcher(ctx, image) 538 if err != nil { 539 t.Fatal(err) 540 } 541 542 refs, err := testocimanifest(ctx, f, d) 543 if err != nil { 544 t.Fatal(err) 545 } 546 547 if len(refs) != 2 { 548 t.Fatalf("Unexpected number of references: %d, expected 2", len(refs)) 549 } 550 551 for _, ref := range refs { 552 if err := testFetch(ctx, f, ref); err != nil { 553 t.Fatal(err) 554 } 555 } 556 } 557 558 func testFetch(ctx context.Context, f remotes.Fetcher, desc ocispec.Descriptor) error { 559 r, err := f.Fetch(ctx, desc) 560 if err != nil { 561 return err 562 } 563 dgstr := desc.Digest.Algorithm().Digester() 564 io.Copy(dgstr.Hash(), r) 565 if dgstr.Digest() != desc.Digest { 566 return errors.Errorf("content mismatch: %s != %s", dgstr.Digest(), desc.Digest) 567 } 568 569 return nil 570 } 571 572 func testocimanifest(ctx context.Context, f remotes.Fetcher, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { 573 r, err := f.Fetch(ctx, desc) 574 if err != nil { 575 return nil, errors.Wrapf(err, "failed to fetch %s", desc.Digest) 576 } 577 p, err := ioutil.ReadAll(r) 578 if err != nil { 579 return nil, err 580 } 581 if dgst := desc.Digest.Algorithm().FromBytes(p); dgst != desc.Digest { 582 return nil, errors.Errorf("digest mismatch: %s != %s", dgst, desc.Digest) 583 } 584 585 var manifest ocispec.Manifest 586 if err := json.Unmarshal(p, &manifest); err != nil { 587 return nil, err 588 } 589 590 var descs []ocispec.Descriptor 591 592 descs = append(descs, manifest.Config) 593 descs = append(descs, manifest.Layers...) 594 595 return descs, nil 596 } 597 598 type testContent struct { 599 mediaType string 600 content []byte 601 } 602 603 func newContent(mediaType string, b []byte) testContent { 604 return testContent{ 605 mediaType: mediaType, 606 content: b, 607 } 608 } 609 610 func (tc testContent) Descriptor() ocispec.Descriptor { 611 return ocispec.Descriptor{ 612 MediaType: tc.mediaType, 613 Digest: digest.FromBytes(tc.content), 614 Size: int64(len(tc.content)), 615 } 616 } 617 618 func (tc testContent) Digest() digest.Digest { 619 return digest.FromBytes(tc.content) 620 } 621 622 func (tc testContent) ServeHTTP(w http.ResponseWriter, r *http.Request) { 623 w.Header().Add("Content-Type", tc.mediaType) 624 w.Header().Add("Content-Length", strconv.Itoa(len(tc.content))) 625 w.Header().Add("Docker-Content-Digest", tc.Digest().String()) 626 w.WriteHeader(http.StatusOK) 627 w.Write(tc.content) 628 } 629 630 type testManifest struct { 631 config testContent 632 references []testContent 633 } 634 635 func newManifest(config testContent, refs ...testContent) testManifest { 636 return testManifest{ 637 config: config, 638 references: refs, 639 } 640 } 641 642 func (m testManifest) OCIManifest() []byte { 643 manifest := ocispec.Manifest{ 644 Versioned: specs.Versioned{ 645 SchemaVersion: 1, 646 }, 647 Config: m.config.Descriptor(), 648 Layers: make([]ocispec.Descriptor, len(m.references)), 649 } 650 for i, c := range m.references { 651 manifest.Layers[i] = c.Descriptor() 652 } 653 b, _ := json.Marshal(manifest) 654 return b 655 } 656 657 func (m testManifest) RegisterHandler(r *http.ServeMux, name string) { 658 for _, c := range append(m.references, m.config) { 659 r.Handle(fmt.Sprintf("/v2/%s/blobs/%s", name, c.Digest()), c) 660 } 661 }