1// SPDX-License-Identifier: MIT
2
3package registryproxy
4
5import (
6 "fmt"
7 "io"
8 "net/http"
9 "net/http/httptest"
10 "os"
11 "path/filepath"
12 "strings"
13 "testing"
14)
15
16// A pulled manifest and blob are served from the store the second time: a tag
17// is revalidated by HEAD, a digest is not fetched again.
18func TestPullCached(t *testing.T) {
19 f := fake(t)
20 p := proxyFor(t, f)
21 for range 2 {
22 w := request(t, p, http.MethodGet, "/v2/library/alpine/manifests/3")
23 if w.Code != http.StatusOK || w.Body.String() != manifestBody {
24 t.Fatalf("manifest: %d %s", w.Code, w.Body)
25 }
26 w = request(t, p, http.MethodGet, "/v2/library/alpine/blobs/"+sum(blobBody))
27 if w.Code != http.StatusOK || w.Body.String() != blobBody {
28 t.Fatalf("blob: %d %s", w.Code, w.Body)
29 }
30 }
31 if f.heads.Load() != 2 || f.gets.Load() != 1 || f.blobs.Load() != 1 || f.tokens.Load() != 1 {
32 t.Fatalf("counts HEAD=%d GET=%d blobs=%d tokens=%d", f.heads.Load(), f.gets.Load(), f.blobs.Load(), f.tokens.Load())
33 }
34}
35
36// HEAD of a blob answers its length, cold or stored, with no body; a stored
37// blob serves ranges.
38func TestBlobHeadAndRange(t *testing.T) {
39 f := fake(t)
40 p := proxyFor(t, f)
41 path := "/v2/library/alpine/blobs/" + sum(blobBody)
42 for _, cached := range []bool{false, true} {
43 if cached {
44 if w := request(t, p, http.MethodGet, path); w.Code != http.StatusOK {
45 t.Fatal(w.Code)
46 }
47 }
48 w := request(t, p, http.MethodHead, path)
49 if w.Code != http.StatusOK || w.Body.Len() != 0 || w.Header().Get("Content-Length") != fmt.Sprint(len(blobBody)) {
50 t.Fatalf("HEAD %d %v %q", w.Code, w.Header(), w.Body.String())
51 }
52 }
53 w := request(t, p, http.MethodGet, path, "Range", "bytes=2-5")
54 if w.Code != http.StatusPartialContent || w.Body.String() != blobBody[2:6] {
55 t.Fatalf("range: %d %s", w.Code, w.Body)
56 }
57}
58
59// A blob is verified before any byte of it is sent: a corrupt one is a 502
60// with no body, and is not stored.
61func TestCorruptBlobRejected(t *testing.T) {
62 f := fake(t)
63 f.corrupt.Store(true)
64 p := proxyFor(t, f)
65 w := request(t, p, http.MethodGet, "/v2/library/alpine/blobs/"+sum(blobBody))
66 if w.Code != http.StatusBadGateway || strings.Contains(w.Body.String(), "xxxx") {
67 t.Fatalf("corrupt blob: %d %q", w.Code, w.Body)
68 }
69 if _, err := os.Stat(filepath.Join(p.s.dir, kindBlob.key(sum(blobBody)[7:]))); !os.IsNotExist(err) {
70 t.Fatal("corrupt blob stored")
71 }
72 if ds, _ := os.ReadDir(filepath.Join(p.s.dir, "tmp")); len(ds) != 0 {
73 t.Fatal("partial temp retained")
74 }
75}
76
77// Only GET and HEAD of valid names, tags and digests under /v2/ are served.
78func TestRouteValidation(t *testing.T) {
79 p := proxyFor(t, fake(t))
80 for _, path := range []string{"/v2/../library/alpine/manifests/3", "/v2/Library/alpine/manifests/3", "/v2/library/alpine/manifests/.bad", "/v2/library/alpine/manifests/sha256:bad", "/v2/library/alpine/blobs/sha256:bad", "/v2/library/alpine/tags/list", "/v3/library/alpine/manifests/3"} {
81 if w := request(t, p, http.MethodGet, path); w.Code != http.StatusNotFound {
82 t.Errorf("%s: %d", path, w.Code)
83 }
84 }
85 if w := request(t, p, http.MethodPut, "/v2/"); w.Code != http.StatusMethodNotAllowed {
86 t.Fatal(w.Code)
87 }
88 if w := request(t, p, http.MethodGet, "/v2/"); w.Code != http.StatusOK || w.Body.String() != "{}" {
89 t.Fatalf("/v2/: %d %s", w.Code, w.Body)
90 }
91}
92
93// A manifest whose bytes do not match its digest, or over the size limit, is
94// a 502 and is not stored.
95func TestManifestLimitsAndDigest(t *testing.T) {
96 for _, body := range []string{"tampered", strings.Repeat("x", manifestLimit+1)} {
97 up := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
98 if r.URL.Path == "/token" {
99 _, _ = io.WriteString(w, `{"token":"public"}`)
100 return
101 }
102 _, _ = io.WriteString(w, body)
103 }))
104 p, err := New(Config{Dir: t.TempDir(), Max: 8 << 20, Upstream: up.URL, AuthURL: up.URL + "/token"})
105 if err != nil {
106 t.Fatal(err)
107 }
108 w := request(t, p, http.MethodGet, "/v2/library/alpine/manifests/"+sum(manifestBody))
109 up.Close()
110 if w.Code != http.StatusBadGateway {
111 t.Fatal(w.Code)
112 }
113 if _, _, err := p.s.open(kindManifest, sum(manifestBody)); err == nil {
114 t.Fatal("invalid manifest stored")
115 }
116 }
117}