code.gitea.io/gitea@v1.22.3/routers/api/packages/container/container.go (about)

     1  // Copyright 2021 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package container
     5  
     6  import (
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"net/http"
    11  	"net/url"
    12  	"os"
    13  	"regexp"
    14  	"strconv"
    15  	"strings"
    16  
    17  	auth_model "code.gitea.io/gitea/models/auth"
    18  	packages_model "code.gitea.io/gitea/models/packages"
    19  	container_model "code.gitea.io/gitea/models/packages/container"
    20  	user_model "code.gitea.io/gitea/models/user"
    21  	"code.gitea.io/gitea/modules/httplib"
    22  	"code.gitea.io/gitea/modules/json"
    23  	"code.gitea.io/gitea/modules/log"
    24  	packages_module "code.gitea.io/gitea/modules/packages"
    25  	container_module "code.gitea.io/gitea/modules/packages/container"
    26  	"code.gitea.io/gitea/modules/setting"
    27  	"code.gitea.io/gitea/modules/sync"
    28  	"code.gitea.io/gitea/modules/util"
    29  	"code.gitea.io/gitea/routers/api/packages/helper"
    30  	auth_service "code.gitea.io/gitea/services/auth"
    31  	"code.gitea.io/gitea/services/context"
    32  	packages_service "code.gitea.io/gitea/services/packages"
    33  	container_service "code.gitea.io/gitea/services/packages/container"
    34  
    35  	digest "github.com/opencontainers/go-digest"
    36  )
    37  
    38  // maximum size of a container manifest
    39  // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-manifests
    40  const maxManifestSize = 10 * 1024 * 1024
    41  
    42  var (
    43  	imageNamePattern = regexp.MustCompile(`\A[a-z0-9]+([._-][a-z0-9]+)*(/[a-z0-9]+([._-][a-z0-9]+)*)*\z`)
    44  	referencePattern = regexp.MustCompile(`\A[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}\z`)
    45  )
    46  
    47  type containerHeaders struct {
    48  	Status        int
    49  	ContentDigest string
    50  	UploadUUID    string
    51  	Range         string
    52  	Location      string
    53  	ContentType   string
    54  	ContentLength int64
    55  }
    56  
    57  // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#legacy-docker-support-http-headers
    58  func setResponseHeaders(resp http.ResponseWriter, h *containerHeaders) {
    59  	if h.Location != "" {
    60  		resp.Header().Set("Location", h.Location)
    61  	}
    62  	if h.Range != "" {
    63  		resp.Header().Set("Range", h.Range)
    64  	}
    65  	if h.ContentType != "" {
    66  		resp.Header().Set("Content-Type", h.ContentType)
    67  	}
    68  	if h.ContentLength != 0 {
    69  		resp.Header().Set("Content-Length", strconv.FormatInt(h.ContentLength, 10))
    70  	}
    71  	if h.UploadUUID != "" {
    72  		resp.Header().Set("Docker-Upload-Uuid", h.UploadUUID)
    73  	}
    74  	if h.ContentDigest != "" {
    75  		resp.Header().Set("Docker-Content-Digest", h.ContentDigest)
    76  		resp.Header().Set("ETag", fmt.Sprintf(`"%s"`, h.ContentDigest))
    77  	}
    78  	resp.Header().Set("Docker-Distribution-Api-Version", "registry/2.0")
    79  	resp.WriteHeader(h.Status)
    80  }
    81  
    82  func jsonResponse(ctx *context.Context, status int, obj any) {
    83  	setResponseHeaders(ctx.Resp, &containerHeaders{
    84  		Status:      status,
    85  		ContentType: "application/json",
    86  	})
    87  	if err := json.NewEncoder(ctx.Resp).Encode(obj); err != nil {
    88  		log.Error("JSON encode: %v", err)
    89  	}
    90  }
    91  
    92  func apiError(ctx *context.Context, status int, err error) {
    93  	helper.LogAndProcessError(ctx, status, err, func(message string) {
    94  		setResponseHeaders(ctx.Resp, &containerHeaders{
    95  			Status: status,
    96  		})
    97  	})
    98  }
    99  
   100  // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#error-codes
   101  func apiErrorDefined(ctx *context.Context, err *namedError) {
   102  	type ContainerError struct {
   103  		Code    string `json:"code"`
   104  		Message string `json:"message"`
   105  	}
   106  
   107  	type ContainerErrors struct {
   108  		Errors []ContainerError `json:"errors"`
   109  	}
   110  
   111  	jsonResponse(ctx, err.StatusCode, ContainerErrors{
   112  		Errors: []ContainerError{
   113  			{
   114  				Code:    err.Code,
   115  				Message: err.Message,
   116  			},
   117  		},
   118  	})
   119  }
   120  
   121  func apiUnauthorizedError(ctx *context.Context) {
   122  	// container registry requires that the "/v2" must be in the root, so the sub-path in AppURL should be removed
   123  	realmURL := httplib.GuessCurrentHostURL(ctx) + "/v2/token"
   124  	ctx.Resp.Header().Add("WWW-Authenticate", `Bearer realm="`+realmURL+`",service="container_registry",scope="*"`)
   125  	apiErrorDefined(ctx, errUnauthorized)
   126  }
   127  
   128  // ReqContainerAccess is a middleware which checks the current user valid (real user or ghost if anonymous access is enabled)
   129  func ReqContainerAccess(ctx *context.Context) {
   130  	if ctx.Doer == nil || (setting.Service.RequireSignInView && ctx.Doer.IsGhost()) {
   131  		apiUnauthorizedError(ctx)
   132  	}
   133  }
   134  
   135  // VerifyImageName is a middleware which checks if the image name is allowed
   136  func VerifyImageName(ctx *context.Context) {
   137  	if !imageNamePattern.MatchString(ctx.Params("image")) {
   138  		apiErrorDefined(ctx, errNameInvalid)
   139  	}
   140  }
   141  
   142  // DetermineSupport is used to test if the registry supports OCI
   143  // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#determining-support
   144  func DetermineSupport(ctx *context.Context) {
   145  	setResponseHeaders(ctx.Resp, &containerHeaders{
   146  		Status: http.StatusOK,
   147  	})
   148  }
   149  
   150  // Authenticate creates a token for the current user
   151  // If the current user is anonymous, the ghost user is used unless RequireSignInView is enabled.
   152  func Authenticate(ctx *context.Context) {
   153  	u := ctx.Doer
   154  	packageScope := auth_service.GetAccessScope(ctx.Data)
   155  	if u == nil {
   156  		if setting.Service.RequireSignInView {
   157  			apiUnauthorizedError(ctx)
   158  			return
   159  		}
   160  
   161  		u = user_model.NewGhostUser()
   162  	} else {
   163  		if has, err := packageScope.HasAnyScope(
   164  			auth_model.AccessTokenScopeReadPackage,
   165  			auth_model.AccessTokenScopeWritePackage,
   166  			auth_model.AccessTokenScopeAll,
   167  		); !has {
   168  			if err != nil {
   169  				log.Error("Error checking access scope: %v", err)
   170  			}
   171  			apiUnauthorizedError(ctx)
   172  			return
   173  		}
   174  	}
   175  
   176  	token, err := packages_service.CreateAuthorizationToken(u, packageScope)
   177  	if err != nil {
   178  		apiError(ctx, http.StatusInternalServerError, err)
   179  		return
   180  	}
   181  
   182  	ctx.JSON(http.StatusOK, map[string]string{
   183  		"token": token,
   184  	})
   185  }
   186  
   187  // https://distribution.github.io/distribution/spec/auth/oauth/
   188  func AuthenticateNotImplemented(ctx *context.Context) {
   189  	// This optional endpoint can be used to authenticate a client.
   190  	// It must implement the specification described in:
   191  	// https://datatracker.ietf.org/doc/html/rfc6749
   192  	// https://distribution.github.io/distribution/spec/auth/oauth/
   193  	// Purpose of this stub is to respond with 404 Not Found instead of 405 Method Not Allowed.
   194  
   195  	ctx.Status(http.StatusNotFound)
   196  }
   197  
   198  // https://docs.docker.com/registry/spec/api/#listing-repositories
   199  func GetRepositoryList(ctx *context.Context) {
   200  	n := ctx.FormInt("n")
   201  	if n <= 0 || n > 100 {
   202  		n = 100
   203  	}
   204  	last := ctx.FormTrim("last")
   205  
   206  	repositories, err := container_model.GetRepositories(ctx, ctx.Doer, n, last)
   207  	if err != nil {
   208  		apiError(ctx, http.StatusInternalServerError, err)
   209  		return
   210  	}
   211  
   212  	type RepositoryList struct {
   213  		Repositories []string `json:"repositories"`
   214  	}
   215  
   216  	if len(repositories) == n {
   217  		v := url.Values{}
   218  		if n > 0 {
   219  			v.Add("n", strconv.Itoa(n))
   220  		}
   221  		v.Add("last", repositories[len(repositories)-1])
   222  
   223  		ctx.Resp.Header().Set("Link", fmt.Sprintf(`</v2/_catalog?%s>; rel="next"`, v.Encode()))
   224  	}
   225  
   226  	jsonResponse(ctx, http.StatusOK, RepositoryList{
   227  		Repositories: repositories,
   228  	})
   229  }
   230  
   231  // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#mounting-a-blob-from-another-repository
   232  // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#single-post
   233  // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks
   234  func InitiateUploadBlob(ctx *context.Context) {
   235  	image := ctx.Params("image")
   236  
   237  	mount := ctx.FormTrim("mount")
   238  	from := ctx.FormTrim("from")
   239  	if mount != "" {
   240  		blob, _ := workaroundGetContainerBlob(ctx, &container_model.BlobSearchOptions{
   241  			Repository: from,
   242  			Digest:     mount,
   243  		})
   244  		if blob != nil {
   245  			accessible, err := packages_model.IsBlobAccessibleForUser(ctx, blob.Blob.ID, ctx.Doer)
   246  			if err != nil {
   247  				apiError(ctx, http.StatusInternalServerError, err)
   248  				return
   249  			}
   250  
   251  			if accessible {
   252  				if err := mountBlob(ctx, &packages_service.PackageInfo{Owner: ctx.Package.Owner, Name: image}, blob.Blob); err != nil {
   253  					apiError(ctx, http.StatusInternalServerError, err)
   254  					return
   255  				}
   256  
   257  				setResponseHeaders(ctx.Resp, &containerHeaders{
   258  					Location:      fmt.Sprintf("/v2/%s/%s/blobs/%s", ctx.Package.Owner.LowerName, image, mount),
   259  					ContentDigest: mount,
   260  					Status:        http.StatusCreated,
   261  				})
   262  				return
   263  			}
   264  		}
   265  	}
   266  
   267  	digest := ctx.FormTrim("digest")
   268  	if digest != "" {
   269  		buf, err := packages_module.CreateHashedBufferFromReader(ctx.Req.Body)
   270  		if err != nil {
   271  			apiError(ctx, http.StatusInternalServerError, err)
   272  			return
   273  		}
   274  		defer buf.Close()
   275  
   276  		if digest != digestFromHashSummer(buf) {
   277  			apiErrorDefined(ctx, errDigestInvalid)
   278  			return
   279  		}
   280  
   281  		if _, err := saveAsPackageBlob(ctx,
   282  			buf,
   283  			&packages_service.PackageCreationInfo{
   284  				PackageInfo: packages_service.PackageInfo{
   285  					Owner: ctx.Package.Owner,
   286  					Name:  image,
   287  				},
   288  				Creator: ctx.Doer,
   289  			},
   290  		); err != nil {
   291  			switch err {
   292  			case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
   293  				apiError(ctx, http.StatusForbidden, err)
   294  			default:
   295  				apiError(ctx, http.StatusInternalServerError, err)
   296  			}
   297  			return
   298  		}
   299  
   300  		setResponseHeaders(ctx.Resp, &containerHeaders{
   301  			Location:      fmt.Sprintf("/v2/%s/%s/blobs/%s", ctx.Package.Owner.LowerName, image, digest),
   302  			ContentDigest: digest,
   303  			Status:        http.StatusCreated,
   304  		})
   305  		return
   306  	}
   307  
   308  	upload, err := packages_model.CreateBlobUpload(ctx)
   309  	if err != nil {
   310  		apiError(ctx, http.StatusInternalServerError, err)
   311  		return
   312  	}
   313  
   314  	setResponseHeaders(ctx.Resp, &containerHeaders{
   315  		Location:   fmt.Sprintf("/v2/%s/%s/blobs/uploads/%s", ctx.Package.Owner.LowerName, image, upload.ID),
   316  		Range:      "0-0",
   317  		UploadUUID: upload.ID,
   318  		Status:     http.StatusAccepted,
   319  	})
   320  }
   321  
   322  // https://docs.docker.com/registry/spec/api/#get-blob-upload
   323  func GetUploadBlob(ctx *context.Context) {
   324  	uuid := ctx.Params("uuid")
   325  
   326  	upload, err := packages_model.GetBlobUploadByID(ctx, uuid)
   327  	if err != nil {
   328  		if err == packages_model.ErrPackageBlobUploadNotExist {
   329  			apiErrorDefined(ctx, errBlobUploadUnknown)
   330  		} else {
   331  			apiError(ctx, http.StatusInternalServerError, err)
   332  		}
   333  		return
   334  	}
   335  
   336  	setResponseHeaders(ctx.Resp, &containerHeaders{
   337  		Range:      fmt.Sprintf("0-%d", upload.BytesReceived),
   338  		UploadUUID: upload.ID,
   339  		Status:     http.StatusNoContent,
   340  	})
   341  }
   342  
   343  // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks
   344  func UploadBlob(ctx *context.Context) {
   345  	image := ctx.Params("image")
   346  
   347  	uploader, err := container_service.NewBlobUploader(ctx, ctx.Params("uuid"))
   348  	if err != nil {
   349  		if err == packages_model.ErrPackageBlobUploadNotExist {
   350  			apiErrorDefined(ctx, errBlobUploadUnknown)
   351  		} else {
   352  			apiError(ctx, http.StatusInternalServerError, err)
   353  		}
   354  		return
   355  	}
   356  	defer uploader.Close()
   357  
   358  	contentRange := ctx.Req.Header.Get("Content-Range")
   359  	if contentRange != "" {
   360  		start, end := 0, 0
   361  		if _, err := fmt.Sscanf(contentRange, "%d-%d", &start, &end); err != nil {
   362  			apiErrorDefined(ctx, errBlobUploadInvalid)
   363  			return
   364  		}
   365  
   366  		if int64(start) != uploader.Size() {
   367  			apiErrorDefined(ctx, errBlobUploadInvalid.WithStatusCode(http.StatusRequestedRangeNotSatisfiable))
   368  			return
   369  		}
   370  	} else if uploader.Size() != 0 {
   371  		apiErrorDefined(ctx, errBlobUploadInvalid.WithMessage("Stream uploads after first write are not allowed"))
   372  		return
   373  	}
   374  
   375  	if err := uploader.Append(ctx, ctx.Req.Body); err != nil {
   376  		apiError(ctx, http.StatusInternalServerError, err)
   377  		return
   378  	}
   379  
   380  	setResponseHeaders(ctx.Resp, &containerHeaders{
   381  		Location:   fmt.Sprintf("/v2/%s/%s/blobs/uploads/%s", ctx.Package.Owner.LowerName, image, uploader.ID),
   382  		Range:      fmt.Sprintf("0-%d", uploader.Size()-1),
   383  		UploadUUID: uploader.ID,
   384  		Status:     http.StatusAccepted,
   385  	})
   386  }
   387  
   388  // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks
   389  func EndUploadBlob(ctx *context.Context) {
   390  	image := ctx.Params("image")
   391  
   392  	digest := ctx.FormTrim("digest")
   393  	if digest == "" {
   394  		apiErrorDefined(ctx, errDigestInvalid)
   395  		return
   396  	}
   397  
   398  	uploader, err := container_service.NewBlobUploader(ctx, ctx.Params("uuid"))
   399  	if err != nil {
   400  		if err == packages_model.ErrPackageBlobUploadNotExist {
   401  			apiErrorDefined(ctx, errBlobUploadUnknown)
   402  		} else {
   403  			apiError(ctx, http.StatusInternalServerError, err)
   404  		}
   405  		return
   406  	}
   407  	doClose := true
   408  	defer func() {
   409  		if doClose {
   410  			uploader.Close()
   411  		}
   412  	}()
   413  
   414  	if ctx.Req.Body != nil {
   415  		if err := uploader.Append(ctx, ctx.Req.Body); err != nil {
   416  			apiError(ctx, http.StatusInternalServerError, err)
   417  			return
   418  		}
   419  	}
   420  
   421  	if digest != digestFromHashSummer(uploader) {
   422  		apiErrorDefined(ctx, errDigestInvalid)
   423  		return
   424  	}
   425  
   426  	if _, err := saveAsPackageBlob(ctx,
   427  		uploader,
   428  		&packages_service.PackageCreationInfo{
   429  			PackageInfo: packages_service.PackageInfo{
   430  				Owner: ctx.Package.Owner,
   431  				Name:  image,
   432  			},
   433  			Creator: ctx.Doer,
   434  		},
   435  	); err != nil {
   436  		switch err {
   437  		case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
   438  			apiError(ctx, http.StatusForbidden, err)
   439  		default:
   440  			apiError(ctx, http.StatusInternalServerError, err)
   441  		}
   442  		return
   443  	}
   444  
   445  	if err := uploader.Close(); err != nil {
   446  		apiError(ctx, http.StatusInternalServerError, err)
   447  		return
   448  	}
   449  	doClose = false
   450  
   451  	if err := container_service.RemoveBlobUploadByID(ctx, uploader.ID); err != nil {
   452  		apiError(ctx, http.StatusInternalServerError, err)
   453  		return
   454  	}
   455  
   456  	setResponseHeaders(ctx.Resp, &containerHeaders{
   457  		Location:      fmt.Sprintf("/v2/%s/%s/blobs/%s", ctx.Package.Owner.LowerName, image, digest),
   458  		ContentDigest: digest,
   459  		Status:        http.StatusCreated,
   460  	})
   461  }
   462  
   463  // https://docs.docker.com/registry/spec/api/#delete-blob-upload
   464  func CancelUploadBlob(ctx *context.Context) {
   465  	uuid := ctx.Params("uuid")
   466  
   467  	_, err := packages_model.GetBlobUploadByID(ctx, uuid)
   468  	if err != nil {
   469  		if err == packages_model.ErrPackageBlobUploadNotExist {
   470  			apiErrorDefined(ctx, errBlobUploadUnknown)
   471  		} else {
   472  			apiError(ctx, http.StatusInternalServerError, err)
   473  		}
   474  		return
   475  	}
   476  
   477  	if err := container_service.RemoveBlobUploadByID(ctx, uuid); err != nil {
   478  		apiError(ctx, http.StatusInternalServerError, err)
   479  		return
   480  	}
   481  
   482  	setResponseHeaders(ctx.Resp, &containerHeaders{
   483  		Status: http.StatusNoContent,
   484  	})
   485  }
   486  
   487  func getBlobFromContext(ctx *context.Context) (*packages_model.PackageFileDescriptor, error) {
   488  	d := ctx.Params("digest")
   489  
   490  	if digest.Digest(d).Validate() != nil {
   491  		return nil, container_model.ErrContainerBlobNotExist
   492  	}
   493  
   494  	return workaroundGetContainerBlob(ctx, &container_model.BlobSearchOptions{
   495  		OwnerID: ctx.Package.Owner.ID,
   496  		Image:   ctx.Params("image"),
   497  		Digest:  d,
   498  	})
   499  }
   500  
   501  // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#checking-if-content-exists-in-the-registry
   502  func HeadBlob(ctx *context.Context) {
   503  	blob, err := getBlobFromContext(ctx)
   504  	if err != nil {
   505  		if err == container_model.ErrContainerBlobNotExist {
   506  			apiErrorDefined(ctx, errBlobUnknown)
   507  		} else {
   508  			apiError(ctx, http.StatusInternalServerError, err)
   509  		}
   510  		return
   511  	}
   512  
   513  	setResponseHeaders(ctx.Resp, &containerHeaders{
   514  		ContentDigest: blob.Properties.GetByName(container_module.PropertyDigest),
   515  		ContentLength: blob.Blob.Size,
   516  		Status:        http.StatusOK,
   517  	})
   518  }
   519  
   520  // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pulling-blobs
   521  func GetBlob(ctx *context.Context) {
   522  	blob, err := getBlobFromContext(ctx)
   523  	if err != nil {
   524  		if err == container_model.ErrContainerBlobNotExist {
   525  			apiErrorDefined(ctx, errBlobUnknown)
   526  		} else {
   527  			apiError(ctx, http.StatusInternalServerError, err)
   528  		}
   529  		return
   530  	}
   531  
   532  	serveBlob(ctx, blob)
   533  }
   534  
   535  // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#deleting-blobs
   536  func DeleteBlob(ctx *context.Context) {
   537  	d := ctx.Params("digest")
   538  
   539  	if digest.Digest(d).Validate() != nil {
   540  		apiErrorDefined(ctx, errBlobUnknown)
   541  		return
   542  	}
   543  
   544  	if err := deleteBlob(ctx, ctx.Package.Owner, ctx.Params("image"), d); err != nil {
   545  		apiError(ctx, http.StatusInternalServerError, err)
   546  		return
   547  	}
   548  
   549  	setResponseHeaders(ctx.Resp, &containerHeaders{
   550  		Status: http.StatusAccepted,
   551  	})
   552  }
   553  
   554  // TODO: use clustered lock
   555  var lockManifest = sync.NewExclusivePool()
   556  
   557  // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-manifests
   558  func UploadManifest(ctx *context.Context) {
   559  	reference := ctx.Params("reference")
   560  
   561  	mci := &manifestCreationInfo{
   562  		MediaType: ctx.Req.Header.Get("Content-Type"),
   563  		Owner:     ctx.Package.Owner,
   564  		Creator:   ctx.Doer,
   565  		Image:     ctx.Params("image"),
   566  		Reference: reference,
   567  		IsTagged:  digest.Digest(reference).Validate() != nil,
   568  	}
   569  
   570  	if mci.IsTagged && !referencePattern.MatchString(reference) {
   571  		apiErrorDefined(ctx, errManifestInvalid.WithMessage("Tag is invalid"))
   572  		return
   573  	}
   574  
   575  	maxSize := maxManifestSize + 1
   576  	buf, err := packages_module.CreateHashedBufferFromReaderWithSize(&io.LimitedReader{R: ctx.Req.Body, N: int64(maxSize)}, maxSize)
   577  	if err != nil {
   578  		apiError(ctx, http.StatusInternalServerError, err)
   579  		return
   580  	}
   581  	defer buf.Close()
   582  
   583  	if buf.Size() > maxManifestSize {
   584  		apiErrorDefined(ctx, errManifestInvalid.WithMessage("Manifest exceeds maximum size").WithStatusCode(http.StatusRequestEntityTooLarge))
   585  		return
   586  	}
   587  
   588  	imagePath := ctx.Package.Owner.Name + "/" + ctx.Params("image")
   589  	lockManifest.CheckIn(imagePath)
   590  	defer lockManifest.CheckOut(imagePath)
   591  
   592  	digest, err := processManifest(ctx, mci, buf)
   593  	if err != nil {
   594  		var namedError *namedError
   595  		if errors.As(err, &namedError) {
   596  			apiErrorDefined(ctx, namedError)
   597  		} else if errors.Is(err, container_model.ErrContainerBlobNotExist) {
   598  			apiErrorDefined(ctx, errBlobUnknown)
   599  		} else {
   600  			switch err {
   601  			case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
   602  				apiError(ctx, http.StatusForbidden, err)
   603  			default:
   604  				apiError(ctx, http.StatusInternalServerError, err)
   605  			}
   606  		}
   607  		return
   608  	}
   609  
   610  	setResponseHeaders(ctx.Resp, &containerHeaders{
   611  		Location:      fmt.Sprintf("/v2/%s/%s/manifests/%s", ctx.Package.Owner.LowerName, mci.Image, reference),
   612  		ContentDigest: digest,
   613  		Status:        http.StatusCreated,
   614  	})
   615  }
   616  
   617  func getBlobSearchOptionsFromContext(ctx *context.Context) (*container_model.BlobSearchOptions, error) {
   618  	reference := ctx.Params("reference")
   619  
   620  	opts := &container_model.BlobSearchOptions{
   621  		OwnerID:    ctx.Package.Owner.ID,
   622  		Image:      ctx.Params("image"),
   623  		IsManifest: true,
   624  	}
   625  
   626  	if digest.Digest(reference).Validate() == nil {
   627  		opts.Digest = reference
   628  	} else if referencePattern.MatchString(reference) {
   629  		opts.Tag = reference
   630  	} else {
   631  		return nil, container_model.ErrContainerBlobNotExist
   632  	}
   633  
   634  	return opts, nil
   635  }
   636  
   637  func getManifestFromContext(ctx *context.Context) (*packages_model.PackageFileDescriptor, error) {
   638  	opts, err := getBlobSearchOptionsFromContext(ctx)
   639  	if err != nil {
   640  		return nil, err
   641  	}
   642  
   643  	return workaroundGetContainerBlob(ctx, opts)
   644  }
   645  
   646  // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#checking-if-content-exists-in-the-registry
   647  func HeadManifest(ctx *context.Context) {
   648  	manifest, err := getManifestFromContext(ctx)
   649  	if err != nil {
   650  		if err == container_model.ErrContainerBlobNotExist {
   651  			apiErrorDefined(ctx, errManifestUnknown)
   652  		} else {
   653  			apiError(ctx, http.StatusInternalServerError, err)
   654  		}
   655  		return
   656  	}
   657  
   658  	setResponseHeaders(ctx.Resp, &containerHeaders{
   659  		ContentDigest: manifest.Properties.GetByName(container_module.PropertyDigest),
   660  		ContentType:   manifest.Properties.GetByName(container_module.PropertyMediaType),
   661  		ContentLength: manifest.Blob.Size,
   662  		Status:        http.StatusOK,
   663  	})
   664  }
   665  
   666  // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pulling-manifests
   667  func GetManifest(ctx *context.Context) {
   668  	manifest, err := getManifestFromContext(ctx)
   669  	if err != nil {
   670  		if err == container_model.ErrContainerBlobNotExist {
   671  			apiErrorDefined(ctx, errManifestUnknown)
   672  		} else {
   673  			apiError(ctx, http.StatusInternalServerError, err)
   674  		}
   675  		return
   676  	}
   677  
   678  	serveBlob(ctx, manifest)
   679  }
   680  
   681  // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#deleting-tags
   682  // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#deleting-manifests
   683  func DeleteManifest(ctx *context.Context) {
   684  	opts, err := getBlobSearchOptionsFromContext(ctx)
   685  	if err != nil {
   686  		apiErrorDefined(ctx, errManifestUnknown)
   687  		return
   688  	}
   689  
   690  	imagePath := ctx.Package.Owner.Name + "/" + ctx.Params("image")
   691  	lockManifest.CheckIn(imagePath)
   692  	defer lockManifest.CheckOut(imagePath)
   693  
   694  	pvs, err := container_model.GetManifestVersions(ctx, opts)
   695  	if err != nil {
   696  		apiError(ctx, http.StatusInternalServerError, err)
   697  		return
   698  	}
   699  
   700  	if len(pvs) == 0 {
   701  		apiErrorDefined(ctx, errManifestUnknown)
   702  		return
   703  	}
   704  
   705  	for _, pv := range pvs {
   706  		if err := packages_service.RemovePackageVersion(ctx, ctx.Doer, pv); err != nil {
   707  			apiError(ctx, http.StatusInternalServerError, err)
   708  			return
   709  		}
   710  	}
   711  
   712  	setResponseHeaders(ctx.Resp, &containerHeaders{
   713  		Status: http.StatusAccepted,
   714  	})
   715  }
   716  
   717  func serveBlob(ctx *context.Context, pfd *packages_model.PackageFileDescriptor) {
   718  	s, u, _, err := packages_service.GetPackageBlobStream(ctx, pfd.File, pfd.Blob)
   719  	if err != nil {
   720  		apiError(ctx, http.StatusInternalServerError, err)
   721  		return
   722  	}
   723  
   724  	headers := &containerHeaders{
   725  		ContentDigest: pfd.Properties.GetByName(container_module.PropertyDigest),
   726  		ContentType:   pfd.Properties.GetByName(container_module.PropertyMediaType),
   727  		ContentLength: pfd.Blob.Size,
   728  		Status:        http.StatusOK,
   729  	}
   730  
   731  	if u != nil {
   732  		headers.Status = http.StatusTemporaryRedirect
   733  		headers.Location = u.String()
   734  
   735  		setResponseHeaders(ctx.Resp, headers)
   736  		return
   737  	}
   738  
   739  	defer s.Close()
   740  
   741  	setResponseHeaders(ctx.Resp, headers)
   742  	if _, err := io.Copy(ctx.Resp, s); err != nil {
   743  		log.Error("Error whilst copying content to response: %v", err)
   744  	}
   745  }
   746  
   747  // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#content-discovery
   748  func GetTagList(ctx *context.Context) {
   749  	image := ctx.Params("image")
   750  
   751  	if _, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.TypeContainer, image); err != nil {
   752  		if err == packages_model.ErrPackageNotExist {
   753  			apiErrorDefined(ctx, errNameUnknown)
   754  		} else {
   755  			apiError(ctx, http.StatusInternalServerError, err)
   756  		}
   757  		return
   758  	}
   759  
   760  	n := -1
   761  	if ctx.FormTrim("n") != "" {
   762  		n = ctx.FormInt("n")
   763  	}
   764  	last := ctx.FormTrim("last")
   765  
   766  	tags, err := container_model.GetImageTags(ctx, ctx.Package.Owner.ID, image, n, last)
   767  	if err != nil {
   768  		apiError(ctx, http.StatusInternalServerError, err)
   769  		return
   770  	}
   771  
   772  	type TagList struct {
   773  		Name string   `json:"name"`
   774  		Tags []string `json:"tags"`
   775  	}
   776  
   777  	if len(tags) > 0 {
   778  		v := url.Values{}
   779  		if n > 0 {
   780  			v.Add("n", strconv.Itoa(n))
   781  		}
   782  		v.Add("last", tags[len(tags)-1])
   783  
   784  		ctx.Resp.Header().Set("Link", fmt.Sprintf(`</v2/%s/%s/tags/list?%s>; rel="next"`, ctx.Package.Owner.LowerName, image, v.Encode()))
   785  	}
   786  
   787  	jsonResponse(ctx, http.StatusOK, TagList{
   788  		Name: strings.ToLower(ctx.Package.Owner.LowerName + "/" + image),
   789  		Tags: tags,
   790  	})
   791  }
   792  
   793  // FIXME: Workaround to be removed in v1.20
   794  // https://github.com/go-gitea/gitea/issues/19586
   795  func workaroundGetContainerBlob(ctx *context.Context, opts *container_model.BlobSearchOptions) (*packages_model.PackageFileDescriptor, error) {
   796  	blob, err := container_model.GetContainerBlob(ctx, opts)
   797  	if err != nil {
   798  		return nil, err
   799  	}
   800  
   801  	err = packages_module.NewContentStore().Has(packages_module.BlobHash256Key(blob.Blob.HashSHA256))
   802  	if err != nil {
   803  		if errors.Is(err, util.ErrNotExist) || errors.Is(err, os.ErrNotExist) {
   804  			log.Debug("Package registry inconsistent: blob %s does not exist on file system", blob.Blob.HashSHA256)
   805  			return nil, container_model.ErrContainerBlobNotExist
   806  		}
   807  		return nil, err
   808  	}
   809  
   810  	return blob, nil
   811  }