// Copyright 2018 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package remote import ( "context" "errors" "fmt" "net/http" "net/http/httptest" "net/url" "strconv" "strings" "testing" "github.com/google/go-cmp/cmp" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/types" ) var fakeDigest = "sha256:0000000000000000000000000000000000000000000000000000000000000000" func TestGetSchema1(t *testing.T) { expectedRepo := "foo/bar" manifestPath := fmt.Sprintf("/v2/%s/manifests/latest", expectedRepo) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/v2/": w.WriteHeader(http.StatusOK) case manifestPath: if r.Method != http.MethodGet { t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet) } w.Header().Set("Content-Type", string(types.DockerManifestSchema1Signed)) w.Header().Set("Docker-Content-Digest", fakeDigest) w.Write([]byte("doesn't matter")) default: t.Fatalf("Unexpected path: %v", r.URL.Path) } })) defer server.Close() u, err := url.Parse(server.URL) if err != nil { t.Fatalf("url.Parse(%v) = %v", server.URL, err) } tag := mustNewTag(t, fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo)) // Get should succeed even for invalid json. We don't parse the response. desc, err := Get(tag) if err != nil { t.Fatalf("Get(%s) = %v", tag, err) } if desc.Digest.String() != fakeDigest { t.Errorf("Descriptor.Digest = %q, expected %q", desc.Digest, fakeDigest) } want := `unsupported MediaType: "application/vnd.docker.distribution.manifest.v1+prettyjws", see https://github.com/google/go-containerregistry/issues/377` // Should fail based on media type. if _, err := desc.Image(); err != nil { if !errors.Is(err, ErrSchema1) { t.Errorf("Image() = %v, expected remote.ErrSchema1", err) } if diff := cmp.Diff(want, err.Error()); diff != "" { t.Errorf("Image() error message (-want +got) = %v", diff) } } else { t.Errorf("Image() = %v, expected err", err) } // Should fail based on media type. if _, err := desc.ImageIndex(); err != nil { if !errors.Is(err, ErrSchema1) { t.Errorf("ImageImage() = %v, expected remote.ErrSchema1", err) } } else { t.Errorf("ImageIndex() = %v, expected err", err) } } func TestGetImageAsIndex(t *testing.T) { expectedRepo := "foo/bar" manifestPath := fmt.Sprintf("/v2/%s/manifests/latest", expectedRepo) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/v2/": w.WriteHeader(http.StatusOK) case manifestPath: if r.Method != http.MethodGet { t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet) } w.Header().Set("Content-Type", string(types.DockerManifestSchema2)) w.Write([]byte("doesn't matter")) default: t.Fatalf("Unexpected path: %v", r.URL.Path) } })) defer server.Close() u, err := url.Parse(server.URL) if err != nil { t.Fatalf("url.Parse(%v) = %v", server.URL, err) } tag := mustNewTag(t, fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo)) // Get should succeed even for invalid json. We don't parse the response. desc, err := Get(tag) if err != nil { t.Fatalf("Get(%s) = %v", tag, err) } // Should fail based on media type. if _, err := desc.ImageIndex(); err == nil { t.Errorf("ImageIndex() = %v, expected err", err) } } func TestHeadSchema1(t *testing.T) { expectedRepo := "foo/bar" mediaType := types.DockerManifestSchema1Signed response := []byte("doesn't matter") manifestPath := fmt.Sprintf("/v2/%s/manifests/latest", expectedRepo) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/v2/": w.WriteHeader(http.StatusOK) case manifestPath: if r.Method != http.MethodHead { t.Errorf("Method; got %v, want %v", r.Method, http.MethodHead) } w.Header().Set("Content-Type", string(mediaType)) w.Header().Set("Content-Length", strconv.Itoa(len(response))) w.Header().Set("Docker-Content-Digest", fakeDigest) w.Write(response) default: t.Fatalf("Unexpected path: %v", r.URL.Path) } })) defer server.Close() u, err := url.Parse(server.URL) if err != nil { t.Fatalf("url.Parse(%v) = %v", server.URL, err) } tag := mustNewTag(t, fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo)) // Head should succeed even for invalid json. We don't parse the response. desc, err := Head(tag) if err != nil { t.Fatalf("Head(%s) = %v", tag, err) } if desc.MediaType != mediaType { t.Errorf("Descriptor.MediaType = %q, expected %q", desc.MediaType, mediaType) } if desc.Digest.String() != fakeDigest { t.Errorf("Descriptor.Digest = %q, expected %q", desc.Digest, fakeDigest) } if desc.Size != int64(len(response)) { t.Errorf("Descriptor.Size = %q, expected %q", desc.Size, len(response)) } } // TestHead_MissingHeaders tests that HEAD responses missing necessary headers // result in errors. func TestHead_MissingHeaders(t *testing.T) { missingType := "missing-type" missingLength := "missing-length" missingDigest := "missing-digest" server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/v2/" { w.WriteHeader(http.StatusOK) return } if r.Method != http.MethodHead { t.Errorf("Method; got %v, want %v", r.Method, http.MethodHead) } if !strings.Contains(r.URL.Path, missingType) { w.Header().Set("Content-Type", "My-Media-Type") } if !strings.Contains(r.URL.Path, missingLength) { w.Header().Set("Content-Length", "10") } if !strings.Contains(r.URL.Path, missingDigest) { w.Header().Set("Docker-Content-Digest", fakeDigest) } })) defer server.Close() u, err := url.Parse(server.URL) if err != nil { t.Fatalf("url.Parse(%v) = %v", server.URL, err) } for _, repo := range []string{missingType, missingLength, missingDigest} { tag := mustNewTag(t, fmt.Sprintf("%s/%s:latest", u.Host, repo)) if _, err := Head(tag); err == nil { t.Errorf("Head(%q): expected error, got nil", tag) } } } // TestRedactFetchBlob tests that a request to fetchBlob that gets redirected // to a URL that contains sensitive information has that information redacted // if the subsequent request fails. func TestRedactFetchBlob(t *testing.T) { ctx := context.Background() f := fetcher{ target: mustNewTag(t, "original.com/repo:latest").Context(), client: &http.Client{ Transport: errTransport{}, }, } h, err := v1.NewHash(fakeDigest) if err != nil { t.Fatal("NewHash:", err) } if _, err := f.fetchBlob(ctx, 0, h); err == nil { t.Fatalf("fetchBlob: expected error, got nil") } else if !strings.Contains(err.Error(), "access_token=REDACTED") { t.Fatalf("fetchBlob: expected error to contain redacted access token, got %v", err) } } type errTransport struct{} func (errTransport) RoundTrip(req *http.Request) (*http.Response, error) { // This simulates a registry that returns a redirect upon the first // request, and then returns an error upon subsequent requests. This helps // test whether error redaction takes into account URLs in error messasges // that are not the original request URL. if req.URL.Host == "original.com" { return &http.Response{ StatusCode: http.StatusSeeOther, Header: http.Header{"Location": []string{"https://redirected.com?access_token=SECRET"}}, }, nil } return nil, fmt.Errorf("error reaching %s", req.URL.String()) }