1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package remote
16
17 import (
18 "bytes"
19 "context"
20 "fmt"
21 "io"
22 "net/http"
23 "net/url"
24 "strings"
25
26 "github.com/google/go-containerregistry/internal/redact"
27 "github.com/google/go-containerregistry/internal/verify"
28 "github.com/google/go-containerregistry/pkg/authn"
29 "github.com/google/go-containerregistry/pkg/name"
30 v1 "github.com/google/go-containerregistry/pkg/v1"
31 "github.com/google/go-containerregistry/pkg/v1/remote/transport"
32 "github.com/google/go-containerregistry/pkg/v1/types"
33 )
34
35 const (
36 kib = 1024
37 mib = 1024 * kib
38 manifestLimit = 100 * mib
39 )
40
41
42 type fetcher struct {
43 target resource
44 client *http.Client
45 }
46
47 func makeFetcher(ctx context.Context, target resource, o *options) (*fetcher, error) {
48 auth := o.auth
49 if o.keychain != nil {
50 kauth, err := o.keychain.Resolve(target)
51 if err != nil {
52 return nil, err
53 }
54 auth = kauth
55 }
56
57 reg, ok := target.(name.Registry)
58 if !ok {
59 repo, ok := target.(name.Repository)
60 if !ok {
61 return nil, fmt.Errorf("unexpected resource: %T", target)
62 }
63 reg = repo.Registry
64 }
65
66 tr, err := transport.NewWithContext(ctx, reg, auth, o.transport, []string{target.Scope(transport.PullScope)})
67 if err != nil {
68 return nil, err
69 }
70 return &fetcher{
71 target: target,
72 client: &http.Client{Transport: tr},
73 }, nil
74 }
75
76 func (f *fetcher) Do(req *http.Request) (*http.Response, error) {
77 return f.client.Do(req)
78 }
79
80 type resource interface {
81 Scheme() string
82 RegistryStr() string
83 Scope(string) string
84
85 authn.Resource
86 }
87
88
89 func (f *fetcher) url(resource, identifier string) url.URL {
90 u := url.URL{
91 Scheme: f.target.Scheme(),
92 Host: f.target.RegistryStr(),
93
94 Path: "/v2/_catalog",
95 }
96 if repo, ok := f.target.(name.Repository); ok {
97 u.Path = fmt.Sprintf("/v2/%s/%s/%s", repo.RepositoryStr(), resource, identifier)
98 }
99 return u
100 }
101
102 func (f *fetcher) get(ctx context.Context, ref name.Reference, acceptable []types.MediaType, platform v1.Platform) (*Descriptor, error) {
103 b, desc, err := f.fetchManifest(ctx, ref, acceptable)
104 if err != nil {
105 return nil, err
106 }
107 return &Descriptor{
108 ref: ref,
109 ctx: ctx,
110 fetcher: *f,
111 Manifest: b,
112 Descriptor: *desc,
113 platform: platform,
114 }, nil
115 }
116
117 func (f *fetcher) fetchManifest(ctx context.Context, ref name.Reference, acceptable []types.MediaType) ([]byte, *v1.Descriptor, error) {
118 u := f.url("manifests", ref.Identifier())
119 req, err := http.NewRequest(http.MethodGet, u.String(), nil)
120 if err != nil {
121 return nil, nil, err
122 }
123 accept := []string{}
124 for _, mt := range acceptable {
125 accept = append(accept, string(mt))
126 }
127 req.Header.Set("Accept", strings.Join(accept, ","))
128
129 resp, err := f.client.Do(req.WithContext(ctx))
130 if err != nil {
131 return nil, nil, err
132 }
133 defer resp.Body.Close()
134
135 if err := transport.CheckError(resp, http.StatusOK); err != nil {
136 return nil, nil, err
137 }
138
139 manifest, err := io.ReadAll(io.LimitReader(resp.Body, manifestLimit))
140 if err != nil {
141 return nil, nil, err
142 }
143
144 digest, size, err := v1.SHA256(bytes.NewReader(manifest))
145 if err != nil {
146 return nil, nil, err
147 }
148
149 mediaType := types.MediaType(resp.Header.Get("Content-Type"))
150 contentDigest, err := v1.NewHash(resp.Header.Get("Docker-Content-Digest"))
151 if err == nil && mediaType == types.DockerManifestSchema1Signed {
152
153
154 digest = contentDigest
155 }
156
157
158 if dgst, ok := ref.(name.Digest); ok {
159 if digest.String() != dgst.DigestStr() {
160 return nil, nil, fmt.Errorf("manifest digest: %q does not match requested digest: %q for %q", digest, dgst.DigestStr(), ref)
161 }
162 }
163
164 var artifactType string
165 mf, _ := v1.ParseManifest(bytes.NewReader(manifest))
166
167
168 if mf != nil && !mf.Config.MediaType.IsConfig() {
169 artifactType = string(mf.Config.MediaType)
170 }
171
172
173
174
175
176
177
178
179
180
181 desc := v1.Descriptor{
182 Digest: digest,
183 Size: size,
184 MediaType: mediaType,
185 ArtifactType: artifactType,
186 }
187
188 return manifest, &desc, nil
189 }
190
191 func (f *fetcher) headManifest(ctx context.Context, ref name.Reference, acceptable []types.MediaType) (*v1.Descriptor, error) {
192 u := f.url("manifests", ref.Identifier())
193 req, err := http.NewRequest(http.MethodHead, u.String(), nil)
194 if err != nil {
195 return nil, err
196 }
197 accept := []string{}
198 for _, mt := range acceptable {
199 accept = append(accept, string(mt))
200 }
201 req.Header.Set("Accept", strings.Join(accept, ","))
202
203 resp, err := f.client.Do(req.WithContext(ctx))
204 if err != nil {
205 return nil, err
206 }
207 defer resp.Body.Close()
208
209 if err := transport.CheckError(resp, http.StatusOK); err != nil {
210 return nil, err
211 }
212
213 mth := resp.Header.Get("Content-Type")
214 if mth == "" {
215 return nil, fmt.Errorf("HEAD %s: response did not include Content-Type header", u.String())
216 }
217 mediaType := types.MediaType(mth)
218
219 size := resp.ContentLength
220 if size == -1 {
221 return nil, fmt.Errorf("GET %s: response did not include Content-Length header", u.String())
222 }
223
224 dh := resp.Header.Get("Docker-Content-Digest")
225 if dh == "" {
226 return nil, fmt.Errorf("HEAD %s: response did not include Docker-Content-Digest header", u.String())
227 }
228 digest, err := v1.NewHash(dh)
229 if err != nil {
230 return nil, err
231 }
232
233
234 if dgst, ok := ref.(name.Digest); ok {
235 if digest.String() != dgst.DigestStr() {
236 return nil, fmt.Errorf("manifest digest: %q does not match requested digest: %q for %q", digest, dgst.DigestStr(), ref)
237 }
238 }
239
240
241 return &v1.Descriptor{
242 Digest: digest,
243 Size: size,
244 MediaType: mediaType,
245 }, nil
246 }
247
248 func (f *fetcher) fetchBlob(ctx context.Context, size int64, h v1.Hash) (io.ReadCloser, error) {
249 u := f.url("blobs", h.String())
250 req, err := http.NewRequest(http.MethodGet, u.String(), nil)
251 if err != nil {
252 return nil, err
253 }
254
255 resp, err := f.client.Do(req.WithContext(ctx))
256 if err != nil {
257 return nil, redact.Error(err)
258 }
259
260 if err := transport.CheckError(resp, http.StatusOK); err != nil {
261 resp.Body.Close()
262 return nil, err
263 }
264
265
266
267
268 if hsize := resp.ContentLength; hsize != -1 {
269 if size == verify.SizeUnknown {
270 size = hsize
271 } else if hsize != size {
272 return nil, fmt.Errorf("GET %s: Content-Length header %d does not match expected size %d", u.String(), hsize, size)
273 }
274 }
275
276 return verify.ReadCloser(resp.Body, size, h)
277 }
278
279 func (f *fetcher) headBlob(ctx context.Context, h v1.Hash) (*http.Response, error) {
280 u := f.url("blobs", h.String())
281 req, err := http.NewRequest(http.MethodHead, u.String(), nil)
282 if err != nil {
283 return nil, err
284 }
285
286 resp, err := f.client.Do(req.WithContext(ctx))
287 if err != nil {
288 return nil, redact.Error(err)
289 }
290
291 if err := transport.CheckError(resp, http.StatusOK); err != nil {
292 resp.Body.Close()
293 return nil, err
294 }
295
296 return resp, nil
297 }
298
299 func (f *fetcher) blobExists(ctx context.Context, h v1.Hash) (bool, error) {
300 u := f.url("blobs", h.String())
301 req, err := http.NewRequest(http.MethodHead, u.String(), nil)
302 if err != nil {
303 return false, err
304 }
305
306 resp, err := f.client.Do(req.WithContext(ctx))
307 if err != nil {
308 return false, redact.Error(err)
309 }
310 defer resp.Body.Close()
311
312 if err := transport.CheckError(resp, http.StatusOK, http.StatusNotFound); err != nil {
313 return false, err
314 }
315
316 return resp.StatusCode == http.StatusOK, nil
317 }
318
View as plain text