1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package registry
16
17 import (
18 "bytes"
19 "context"
20 "errors"
21 "fmt"
22 "io"
23 "log"
24 "math/rand"
25 "net/http"
26 "path"
27 "strings"
28 "sync"
29
30 "github.com/google/go-containerregistry/internal/verify"
31 v1 "github.com/google/go-containerregistry/pkg/v1"
32 )
33
34
35
36
37
38 func isBlob(req *http.Request) bool {
39 elem := strings.Split(req.URL.Path, "/")
40 elem = elem[1:]
41 if elem[len(elem)-1] == "" {
42 elem = elem[:len(elem)-1]
43 }
44 if len(elem) < 3 {
45 return false
46 }
47 return elem[len(elem)-2] == "blobs" || (elem[len(elem)-3] == "blobs" &&
48 elem[len(elem)-2] == "uploads")
49 }
50
51
52
53 type BlobHandler interface {
54
55 Get(ctx context.Context, repo string, h v1.Hash) (io.ReadCloser, error)
56 }
57
58
59
60 type BlobStatHandler interface {
61
62
63 Stat(ctx context.Context, repo string, h v1.Hash) (int64, error)
64 }
65
66
67
68 type BlobPutHandler interface {
69
70
71
72
73
74
75 Put(ctx context.Context, repo string, h v1.Hash, rc io.ReadCloser) error
76 }
77
78
79
80 type BlobDeleteHandler interface {
81
82 Delete(ctx context.Context, repo string, h v1.Hash) error
83 }
84
85
86
87
88 type redirectError struct {
89
90 Location string
91
92
93 Code int
94 }
95
96 func (e redirectError) Error() string { return fmt.Sprintf("redirecting (%d): %s", e.Code, e.Location) }
97
98
99 var errNotFound = errors.New("not found")
100
101 type memHandler struct {
102 m map[string][]byte
103 lock sync.Mutex
104 }
105
106 func NewInMemoryBlobHandler() BlobHandler { return &memHandler{m: map[string][]byte{}} }
107
108 func (m *memHandler) Stat(_ context.Context, _ string, h v1.Hash) (int64, error) {
109 m.lock.Lock()
110 defer m.lock.Unlock()
111
112 b, found := m.m[h.String()]
113 if !found {
114 return 0, errNotFound
115 }
116 return int64(len(b)), nil
117 }
118 func (m *memHandler) Get(_ context.Context, _ string, h v1.Hash) (io.ReadCloser, error) {
119 m.lock.Lock()
120 defer m.lock.Unlock()
121
122 b, found := m.m[h.String()]
123 if !found {
124 return nil, errNotFound
125 }
126 return io.NopCloser(bytes.NewReader(b)), nil
127 }
128 func (m *memHandler) Put(_ context.Context, _ string, h v1.Hash, rc io.ReadCloser) error {
129 m.lock.Lock()
130 defer m.lock.Unlock()
131
132 defer rc.Close()
133 all, err := io.ReadAll(rc)
134 if err != nil {
135 return err
136 }
137 m.m[h.String()] = all
138 return nil
139 }
140 func (m *memHandler) Delete(_ context.Context, _ string, h v1.Hash) error {
141 m.lock.Lock()
142 defer m.lock.Unlock()
143
144 if _, found := m.m[h.String()]; !found {
145 return errNotFound
146 }
147
148 delete(m.m, h.String())
149 return nil
150 }
151
152
153 type blobs struct {
154 blobHandler BlobHandler
155
156
157 uploads map[string][]byte
158 lock sync.Mutex
159 log *log.Logger
160 }
161
162 func (b *blobs) handle(resp http.ResponseWriter, req *http.Request) *regError {
163 elem := strings.Split(req.URL.Path, "/")
164 elem = elem[1:]
165 if elem[len(elem)-1] == "" {
166 elem = elem[:len(elem)-1]
167 }
168
169 if len(elem) < 4 {
170 return ®Error{
171 Status: http.StatusBadRequest,
172 Code: "NAME_INVALID",
173 Message: "blobs must be attached to a repo",
174 }
175 }
176 target := elem[len(elem)-1]
177 service := elem[len(elem)-2]
178 digest := req.URL.Query().Get("digest")
179 contentRange := req.Header.Get("Content-Range")
180
181 repo := req.URL.Host + path.Join(elem[1:len(elem)-2]...)
182
183 switch req.Method {
184 case http.MethodHead:
185 h, err := v1.NewHash(target)
186 if err != nil {
187 return ®Error{
188 Status: http.StatusBadRequest,
189 Code: "NAME_INVALID",
190 Message: "invalid digest",
191 }
192 }
193
194 var size int64
195 if bsh, ok := b.blobHandler.(BlobStatHandler); ok {
196 size, err = bsh.Stat(req.Context(), repo, h)
197 if errors.Is(err, errNotFound) {
198 return regErrBlobUnknown
199 } else if err != nil {
200 var rerr redirectError
201 if errors.As(err, &rerr) {
202 http.Redirect(resp, req, rerr.Location, rerr.Code)
203 return nil
204 }
205 return regErrInternal(err)
206 }
207 } else {
208 rc, err := b.blobHandler.Get(req.Context(), repo, h)
209 if errors.Is(err, errNotFound) {
210 return regErrBlobUnknown
211 } else if err != nil {
212 var rerr redirectError
213 if errors.As(err, &rerr) {
214 http.Redirect(resp, req, rerr.Location, rerr.Code)
215 return nil
216 }
217 return regErrInternal(err)
218 }
219 defer rc.Close()
220 size, err = io.Copy(io.Discard, rc)
221 if err != nil {
222 return regErrInternal(err)
223 }
224 }
225
226 resp.Header().Set("Content-Length", fmt.Sprint(size))
227 resp.Header().Set("Docker-Content-Digest", h.String())
228 resp.WriteHeader(http.StatusOK)
229 return nil
230
231 case http.MethodGet:
232 h, err := v1.NewHash(target)
233 if err != nil {
234 return ®Error{
235 Status: http.StatusBadRequest,
236 Code: "NAME_INVALID",
237 Message: "invalid digest",
238 }
239 }
240
241 var size int64
242 var r io.Reader
243 if bsh, ok := b.blobHandler.(BlobStatHandler); ok {
244 size, err = bsh.Stat(req.Context(), repo, h)
245 if errors.Is(err, errNotFound) {
246 return regErrBlobUnknown
247 } else if err != nil {
248 var rerr redirectError
249 if errors.As(err, &rerr) {
250 http.Redirect(resp, req, rerr.Location, rerr.Code)
251 return nil
252 }
253 return regErrInternal(err)
254 }
255
256 rc, err := b.blobHandler.Get(req.Context(), repo, h)
257 if errors.Is(err, errNotFound) {
258 return regErrBlobUnknown
259 } else if err != nil {
260 var rerr redirectError
261 if errors.As(err, &rerr) {
262 http.Redirect(resp, req, rerr.Location, rerr.Code)
263 return nil
264 }
265
266 return regErrInternal(err)
267 }
268 defer rc.Close()
269 r = rc
270 } else {
271 tmp, err := b.blobHandler.Get(req.Context(), repo, h)
272 if errors.Is(err, errNotFound) {
273 return regErrBlobUnknown
274 } else if err != nil {
275 var rerr redirectError
276 if errors.As(err, &rerr) {
277 http.Redirect(resp, req, rerr.Location, rerr.Code)
278 return nil
279 }
280
281 return regErrInternal(err)
282 }
283 defer tmp.Close()
284 var buf bytes.Buffer
285 io.Copy(&buf, tmp)
286 size = int64(buf.Len())
287 r = &buf
288 }
289
290 resp.Header().Set("Content-Length", fmt.Sprint(size))
291 resp.Header().Set("Docker-Content-Digest", h.String())
292 resp.WriteHeader(http.StatusOK)
293 io.Copy(resp, r)
294 return nil
295
296 case http.MethodPost:
297 bph, ok := b.blobHandler.(BlobPutHandler)
298 if !ok {
299 return regErrUnsupported
300 }
301
302
303
304 if target != "uploads" {
305 return ®Error{
306 Status: http.StatusBadRequest,
307 Code: "METHOD_UNKNOWN",
308 Message: fmt.Sprintf("POST to /blobs must be followed by /uploads, got %s", target),
309 }
310 }
311
312 if digest != "" {
313 h, err := v1.NewHash(digest)
314 if err != nil {
315 return regErrDigestInvalid
316 }
317
318 vrc, err := verify.ReadCloser(req.Body, req.ContentLength, h)
319 if err != nil {
320 return regErrInternal(err)
321 }
322 defer vrc.Close()
323
324 if err = bph.Put(req.Context(), repo, h, vrc); err != nil {
325 if errors.As(err, &verify.Error{}) {
326 log.Printf("Digest mismatch: %v", err)
327 return regErrDigestMismatch
328 }
329 return regErrInternal(err)
330 }
331 resp.Header().Set("Docker-Content-Digest", h.String())
332 resp.WriteHeader(http.StatusCreated)
333 return nil
334 }
335
336 id := fmt.Sprint(rand.Int63())
337 resp.Header().Set("Location", "/"+path.Join("v2", path.Join(elem[1:len(elem)-2]...), "blobs/uploads", id))
338 resp.Header().Set("Range", "0-0")
339 resp.WriteHeader(http.StatusAccepted)
340 return nil
341
342 case http.MethodPatch:
343 if service != "uploads" {
344 return ®Error{
345 Status: http.StatusBadRequest,
346 Code: "METHOD_UNKNOWN",
347 Message: fmt.Sprintf("PATCH to /blobs must be followed by /uploads, got %s", service),
348 }
349 }
350
351 if contentRange != "" {
352 start, end := 0, 0
353 if _, err := fmt.Sscanf(contentRange, "%d-%d", &start, &end); err != nil {
354 return ®Error{
355 Status: http.StatusRequestedRangeNotSatisfiable,
356 Code: "BLOB_UPLOAD_UNKNOWN",
357 Message: "We don't understand your Content-Range",
358 }
359 }
360 b.lock.Lock()
361 defer b.lock.Unlock()
362 if start != len(b.uploads[target]) {
363 return ®Error{
364 Status: http.StatusRequestedRangeNotSatisfiable,
365 Code: "BLOB_UPLOAD_UNKNOWN",
366 Message: "Your content range doesn't match what we have",
367 }
368 }
369 l := bytes.NewBuffer(b.uploads[target])
370 io.Copy(l, req.Body)
371 b.uploads[target] = l.Bytes()
372 resp.Header().Set("Location", "/"+path.Join("v2", path.Join(elem[1:len(elem)-3]...), "blobs/uploads", target))
373 resp.Header().Set("Range", fmt.Sprintf("0-%d", len(l.Bytes())-1))
374 resp.WriteHeader(http.StatusNoContent)
375 return nil
376 }
377
378 b.lock.Lock()
379 defer b.lock.Unlock()
380 if _, ok := b.uploads[target]; ok {
381 return ®Error{
382 Status: http.StatusBadRequest,
383 Code: "BLOB_UPLOAD_INVALID",
384 Message: "Stream uploads after first write are not allowed",
385 }
386 }
387
388 l := &bytes.Buffer{}
389 io.Copy(l, req.Body)
390
391 b.uploads[target] = l.Bytes()
392 resp.Header().Set("Location", "/"+path.Join("v2", path.Join(elem[1:len(elem)-3]...), "blobs/uploads", target))
393 resp.Header().Set("Range", fmt.Sprintf("0-%d", len(l.Bytes())-1))
394 resp.WriteHeader(http.StatusNoContent)
395 return nil
396
397 case http.MethodPut:
398 bph, ok := b.blobHandler.(BlobPutHandler)
399 if !ok {
400 return regErrUnsupported
401 }
402
403 if service != "uploads" {
404 return ®Error{
405 Status: http.StatusBadRequest,
406 Code: "METHOD_UNKNOWN",
407 Message: fmt.Sprintf("PUT to /blobs must be followed by /uploads, got %s", service),
408 }
409 }
410
411 if digest == "" {
412 return ®Error{
413 Status: http.StatusBadRequest,
414 Code: "DIGEST_INVALID",
415 Message: "digest not specified",
416 }
417 }
418
419 b.lock.Lock()
420 defer b.lock.Unlock()
421
422 h, err := v1.NewHash(digest)
423 if err != nil {
424 return ®Error{
425 Status: http.StatusBadRequest,
426 Code: "NAME_INVALID",
427 Message: "invalid digest",
428 }
429 }
430
431 defer req.Body.Close()
432 in := io.NopCloser(io.MultiReader(bytes.NewBuffer(b.uploads[target]), req.Body))
433
434 size := int64(verify.SizeUnknown)
435 if req.ContentLength > 0 {
436 size = int64(len(b.uploads[target])) + req.ContentLength
437 }
438
439 vrc, err := verify.ReadCloser(in, size, h)
440 if err != nil {
441 return regErrInternal(err)
442 }
443 defer vrc.Close()
444
445 if err := bph.Put(req.Context(), repo, h, vrc); err != nil {
446 if errors.As(err, &verify.Error{}) {
447 log.Printf("Digest mismatch: %v", err)
448 return regErrDigestMismatch
449 }
450 return regErrInternal(err)
451 }
452
453 delete(b.uploads, target)
454 resp.Header().Set("Docker-Content-Digest", h.String())
455 resp.WriteHeader(http.StatusCreated)
456 return nil
457
458 case http.MethodDelete:
459 bdh, ok := b.blobHandler.(BlobDeleteHandler)
460 if !ok {
461 return regErrUnsupported
462 }
463
464 h, err := v1.NewHash(target)
465 if err != nil {
466 return ®Error{
467 Status: http.StatusBadRequest,
468 Code: "NAME_INVALID",
469 Message: "invalid digest",
470 }
471 }
472 if err := bdh.Delete(req.Context(), repo, h); err != nil {
473 return regErrInternal(err)
474 }
475 resp.WriteHeader(http.StatusAccepted)
476 return nil
477
478 default:
479 return ®Error{
480 Status: http.StatusBadRequest,
481 Code: "METHOD_UNKNOWN",
482 Message: "We don't understand your method + url",
483 }
484 }
485 }
486
View as plain text