...

Source file src/github.com/google/go-containerregistry/pkg/registry/blobs.go

Documentation: github.com/google/go-containerregistry/pkg/registry

     1  // Copyright 2018 Google LLC All Rights Reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //    http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    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  // Returns whether this url should be handled by the blob handler
    35  // This is complicated because blob is indicated by the trailing path, not the leading path.
    36  // https://github.com/opencontainers/distribution-spec/blob/master/spec.md#pulling-a-layer
    37  // https://github.com/opencontainers/distribution-spec/blob/master/spec.md#pushing-a-layer
    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  // BlobHandler represents a minimal blob storage backend, capable of serving
    52  // blob contents.
    53  type BlobHandler interface {
    54  	// Get gets the blob contents, or errNotFound if the blob wasn't found.
    55  	Get(ctx context.Context, repo string, h v1.Hash) (io.ReadCloser, error)
    56  }
    57  
    58  // BlobStatHandler is an extension interface representing a blob storage
    59  // backend that can serve metadata about blobs.
    60  type BlobStatHandler interface {
    61  	// Stat returns the size of the blob, or errNotFound if the blob wasn't
    62  	// found, or redirectError if the blob can be found elsewhere.
    63  	Stat(ctx context.Context, repo string, h v1.Hash) (int64, error)
    64  }
    65  
    66  // BlobPutHandler is an extension interface representing a blob storage backend
    67  // that can write blob contents.
    68  type BlobPutHandler interface {
    69  	// Put puts the blob contents.
    70  	//
    71  	// The contents will be verified against the expected size and digest
    72  	// as the contents are read, and an error will be returned if these
    73  	// don't match. Implementations should return that error, or a wrapper
    74  	// around that error, to return the correct error when these don't match.
    75  	Put(ctx context.Context, repo string, h v1.Hash, rc io.ReadCloser) error
    76  }
    77  
    78  // BlobDeleteHandler is an extension interface representing a blob storage
    79  // backend that can delete blob contents.
    80  type BlobDeleteHandler interface {
    81  	// Delete the blob contents.
    82  	Delete(ctx context.Context, repo string, h v1.Hash) error
    83  }
    84  
    85  // redirectError represents a signal that the blob handler doesn't have the blob
    86  // contents, but that those contents are at another location which registry
    87  // clients should redirect to.
    88  type redirectError struct {
    89  	// Location is the location to find the contents.
    90  	Location string
    91  
    92  	// Code is the HTTP redirect status code to return to clients.
    93  	Code int
    94  }
    95  
    96  func (e redirectError) Error() string { return fmt.Sprintf("redirecting (%d): %s", e.Code, e.Location) }
    97  
    98  // errNotFound represents an error locating the blob.
    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  // blobs
   153  type blobs struct {
   154  	blobHandler BlobHandler
   155  
   156  	// Each upload gets a unique id that writes occur to until finalized.
   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  	// Must have a path of form /v2/{name}/blobs/{upload,sha256:}
   169  	if len(elem) < 4 {
   170  		return &regError{
   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 &regError{
   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 &regError{
   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  		// It is weird that this is "target" instead of "service", but
   303  		// that's how the index math works out above.
   304  		if target != "uploads" {
   305  			return &regError{
   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 &regError{
   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 &regError{
   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 &regError{
   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 &regError{
   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 &regError{
   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 &regError{
   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 &regError{
   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 &regError{
   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 &regError{
   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