zotregistry.io/zot@v1.4.4-0.20231124084042-02a8ed785457/pkg/api/routes.go (about)

     1  // @title Open Container Initiative Distribution Specification
     2  // @version v1.1.0-dev
     3  // @description APIs for Open Container Initiative Distribution Specification
     4  
     5  // @license.name Apache 2.0
     6  // @license.url http://www.apache.org/licenses/LICENSE-2.0.html
     7  
     8  package api
     9  
    10  import (
    11  	"context"
    12  	"encoding/json"
    13  	"errors"
    14  	"fmt"
    15  	"io"
    16  	"net/http"
    17  	"net/url"
    18  	"path"
    19  	"regexp"
    20  	"sort"
    21  	"strconv"
    22  	"strings"
    23  	"time"
    24  
    25  	guuid "github.com/gofrs/uuid"
    26  	"github.com/google/go-github/v52/github"
    27  	"github.com/gorilla/mux"
    28  	jsoniter "github.com/json-iterator/go"
    29  	"github.com/opencontainers/distribution-spec/specs-go/v1/extensions"
    30  	godigest "github.com/opencontainers/go-digest"
    31  	ispec "github.com/opencontainers/image-spec/specs-go/v1"
    32  	artifactspec "github.com/oras-project/artifacts-spec/specs-go/v1"
    33  	"github.com/zitadel/oidc/pkg/client/rp"
    34  	"github.com/zitadel/oidc/pkg/oidc"
    35  
    36  	zerr "zotregistry.io/zot/errors"
    37  	"zotregistry.io/zot/pkg/api/config"
    38  	"zotregistry.io/zot/pkg/api/constants"
    39  	apiErr "zotregistry.io/zot/pkg/api/errors"
    40  	zcommon "zotregistry.io/zot/pkg/common"
    41  	gqlPlayground "zotregistry.io/zot/pkg/debug/gqlplayground"
    42  	pprof "zotregistry.io/zot/pkg/debug/pprof"
    43  	debug "zotregistry.io/zot/pkg/debug/swagger"
    44  	ext "zotregistry.io/zot/pkg/extensions"
    45  	syncConstants "zotregistry.io/zot/pkg/extensions/sync/constants"
    46  	"zotregistry.io/zot/pkg/log"
    47  	"zotregistry.io/zot/pkg/meta"
    48  	mTypes "zotregistry.io/zot/pkg/meta/types"
    49  	zreg "zotregistry.io/zot/pkg/regexp"
    50  	reqCtx "zotregistry.io/zot/pkg/requestcontext"
    51  	storageCommon "zotregistry.io/zot/pkg/storage/common"
    52  	storageTypes "zotregistry.io/zot/pkg/storage/types"
    53  	"zotregistry.io/zot/pkg/test/inject"
    54  )
    55  
    56  type RouteHandler struct {
    57  	c *Controller
    58  }
    59  
    60  func NewRouteHandler(c *Controller) *RouteHandler {
    61  	rh := &RouteHandler{c: c}
    62  	rh.SetupRoutes()
    63  
    64  	return rh
    65  }
    66  
    67  func (rh *RouteHandler) SetupRoutes() {
    68  	// first get Auth middleware in order to first setup openid/ldap/htpasswd, before oidc provider routes are setup
    69  	authHandler := AuthHandler(rh.c)
    70  
    71  	applyCORSHeaders := getCORSHeadersHandler(rh.c.Config.HTTP.AllowOrigin)
    72  
    73  	if rh.c.Config.IsOpenIDAuthEnabled() {
    74  		// login path for openID
    75  		rh.c.Router.HandleFunc(constants.LoginPath, rh.AuthURLHandler())
    76  
    77  		// callback path for openID
    78  		for provider, relyingParty := range rh.c.RelyingParties {
    79  			if config.IsOauth2Supported(provider) {
    80  				rh.c.Router.HandleFunc(constants.CallbackBasePath+fmt.Sprintf("/%s", provider),
    81  					rp.CodeExchangeHandler(rh.GithubCodeExchangeCallback(), relyingParty))
    82  			} else if config.IsOpenIDSupported(provider) {
    83  				rh.c.Router.HandleFunc(constants.CallbackBasePath+fmt.Sprintf("/%s", provider),
    84  					rp.CodeExchangeHandler(rp.UserinfoCallback(rh.OpenIDCodeExchangeCallback()), relyingParty))
    85  			}
    86  		}
    87  	}
    88  
    89  	if rh.c.Config.IsAPIKeyEnabled() {
    90  		// enable api key management urls
    91  		apiKeyRouter := rh.c.Router.PathPrefix(constants.APIKeyPath).Subrouter()
    92  		apiKeyRouter.Use(authHandler)
    93  		apiKeyRouter.Use(BaseAuthzHandler(rh.c))
    94  
    95  		// Always use CORSHeadersMiddleware before ACHeadersMiddleware
    96  		apiKeyRouter.Use(zcommon.CORSHeadersMiddleware(rh.c.Config.HTTP.AllowOrigin))
    97  		apiKeyRouter.Use(zcommon.ACHeadersMiddleware(rh.c.Config,
    98  			http.MethodGet, http.MethodPost, http.MethodDelete, http.MethodOptions))
    99  
   100  		apiKeyRouter.Methods(http.MethodPost, http.MethodOptions).HandlerFunc(rh.CreateAPIKey)
   101  		apiKeyRouter.Methods(http.MethodGet).HandlerFunc(rh.GetAPIKeys)
   102  		apiKeyRouter.Methods(http.MethodDelete).HandlerFunc(rh.RevokeAPIKey)
   103  	}
   104  
   105  	/* on every route which may be used by UI we set OPTIONS as allowed METHOD
   106  	to enable preflight request from UI to backend */
   107  	if rh.c.Config.IsBasicAuthnEnabled() {
   108  		// logout path for openID
   109  		rh.c.Router.HandleFunc(constants.LogoutPath,
   110  			getUIHeadersHandler(rh.c.Config, http.MethodPost, http.MethodOptions)(applyCORSHeaders(rh.Logout))).
   111  			Methods(http.MethodPost, http.MethodOptions)
   112  	}
   113  
   114  	prefixedRouter := rh.c.Router.PathPrefix(constants.RoutePrefix).Subrouter()
   115  	prefixedRouter.Use(authHandler)
   116  
   117  	prefixedDistSpecRouter := prefixedRouter.NewRoute().Subrouter()
   118  	// authz is being enabled if AccessControl is specified
   119  	// if Authn is not present AccessControl will have only default policies
   120  	if rh.c.Config.HTTP.AccessControl != nil {
   121  		if rh.c.Config.IsBasicAuthnEnabled() {
   122  			rh.c.Log.Info().Msg("access control is being enabled")
   123  		} else {
   124  			rh.c.Log.Info().Msg("anonymous policy only access control is being enabled")
   125  		}
   126  
   127  		prefixedRouter.Use(BaseAuthzHandler(rh.c))
   128  		prefixedDistSpecRouter.Use(DistSpecAuthzHandler(rh.c))
   129  	}
   130  
   131  	// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#endpoints
   132  	{
   133  		prefixedDistSpecRouter.HandleFunc(fmt.Sprintf("/{name:%s}/tags/list", zreg.NameRegexp.String()),
   134  			getUIHeadersHandler(rh.c.Config, http.MethodGet, http.MethodOptions)(
   135  				applyCORSHeaders(rh.ListTags))).Methods(http.MethodGet, http.MethodOptions)
   136  		prefixedDistSpecRouter.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", zreg.NameRegexp.String()),
   137  			getUIHeadersHandler(rh.c.Config, http.MethodHead, http.MethodGet, http.MethodOptions)(
   138  				applyCORSHeaders(rh.CheckManifest))).Methods(http.MethodHead, http.MethodOptions)
   139  		prefixedDistSpecRouter.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", zreg.NameRegexp.String()),
   140  			applyCORSHeaders(rh.GetManifest)).Methods(http.MethodGet)
   141  		prefixedDistSpecRouter.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", zreg.NameRegexp.String()),
   142  			rh.UpdateManifest).Methods(http.MethodPut)
   143  		prefixedDistSpecRouter.HandleFunc(fmt.Sprintf("/{name:%s}/manifests/{reference}", zreg.NameRegexp.String()),
   144  			rh.DeleteManifest).Methods(http.MethodDelete)
   145  		prefixedDistSpecRouter.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/{digest}", zreg.NameRegexp.String()),
   146  			rh.CheckBlob).Methods(http.MethodHead)
   147  		prefixedDistSpecRouter.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/{digest}", zreg.NameRegexp.String()),
   148  			rh.GetBlob).Methods(http.MethodGet)
   149  		prefixedDistSpecRouter.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/{digest}", zreg.NameRegexp.String()),
   150  			rh.DeleteBlob).Methods(http.MethodDelete)
   151  		prefixedDistSpecRouter.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/uploads/", zreg.NameRegexp.String()),
   152  			rh.CreateBlobUpload).Methods(http.MethodPost)
   153  		prefixedDistSpecRouter.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/uploads/{session_id}", zreg.NameRegexp.String()),
   154  			rh.GetBlobUpload).Methods(http.MethodGet)
   155  		prefixedDistSpecRouter.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/uploads/{session_id}", zreg.NameRegexp.String()),
   156  			rh.PatchBlobUpload).Methods(http.MethodPatch)
   157  		prefixedDistSpecRouter.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/uploads/{session_id}", zreg.NameRegexp.String()),
   158  			rh.UpdateBlobUpload).Methods(http.MethodPut)
   159  		prefixedDistSpecRouter.HandleFunc(fmt.Sprintf("/{name:%s}/blobs/uploads/{session_id}", zreg.NameRegexp.String()),
   160  			rh.DeleteBlobUpload).Methods(http.MethodDelete)
   161  		// support for OCI artifact references
   162  		prefixedDistSpecRouter.HandleFunc(fmt.Sprintf("/{name:%s}/referrers/{digest}", zreg.NameRegexp.String()),
   163  			getUIHeadersHandler(rh.c.Config, http.MethodGet, http.MethodOptions)(
   164  				applyCORSHeaders(rh.GetReferrers))).Methods(http.MethodGet, http.MethodOptions)
   165  		prefixedRouter.HandleFunc(constants.ExtCatalogPrefix,
   166  			getUIHeadersHandler(rh.c.Config, http.MethodGet, http.MethodOptions)(
   167  				applyCORSHeaders(rh.ListRepositories))).Methods(http.MethodGet, http.MethodOptions)
   168  		prefixedRouter.HandleFunc(constants.ExtOciDiscoverPrefix,
   169  			getUIHeadersHandler(rh.c.Config, http.MethodGet, http.MethodOptions)(
   170  				applyCORSHeaders(rh.ListExtensions))).Methods(http.MethodGet, http.MethodOptions)
   171  		prefixedRouter.HandleFunc("/",
   172  			getUIHeadersHandler(rh.c.Config, http.MethodGet, http.MethodOptions)(
   173  				applyCORSHeaders(rh.CheckVersionSupport))).Methods(http.MethodGet, http.MethodOptions)
   174  	}
   175  
   176  	// support for ORAS artifact reference types (alpha 1) - image signature use case
   177  	rh.c.Router.HandleFunc(fmt.Sprintf("%s/{name:%s}/manifests/{digest}/referrers",
   178  		constants.ArtifactSpecRoutePrefix, zreg.NameRegexp.String()), rh.GetOrasReferrers).Methods("GET")
   179  
   180  	// swagger
   181  	debug.SetupSwaggerRoutes(rh.c.Config, rh.c.Router, authHandler, rh.c.Log)
   182  	// gql playground
   183  	gqlPlayground.SetupGQLPlaygroundRoutes(prefixedRouter, rh.c.StoreController, rh.c.Log)
   184  	// pprof
   185  	pprof.SetupPprofRoutes(rh.c.Config, prefixedRouter, authHandler, rh.c.Log)
   186  
   187  	// Preconditions for enabling the actual extension routes are part of extensions themselves
   188  	ext.SetupMetricsRoutes(rh.c.Config, rh.c.Router, authHandler, MetricsAuthzHandler(rh.c), rh.c.Log, rh.c.Metrics)
   189  	ext.SetupSearchRoutes(rh.c.Config, prefixedRouter, rh.c.StoreController, rh.c.MetaDB, rh.c.CveScanner,
   190  		rh.c.Log)
   191  	ext.SetupImageTrustRoutes(rh.c.Config, prefixedRouter, rh.c.MetaDB, rh.c.Log)
   192  	ext.SetupMgmtRoutes(rh.c.Config, prefixedRouter, rh.c.Log)
   193  	ext.SetupUserPreferencesRoutes(rh.c.Config, prefixedRouter, rh.c.MetaDB, rh.c.Log)
   194  	// last should always be UI because it will setup a http.FileServer and paths will be resolved by this FileServer.
   195  	ext.SetupUIRoutes(rh.c.Config, rh.c.Router, rh.c.Log)
   196  }
   197  
   198  func getCORSHeadersHandler(allowOrigin string) func(http.HandlerFunc) http.HandlerFunc {
   199  	return func(next http.HandlerFunc) http.HandlerFunc {
   200  		return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
   201  			zcommon.AddCORSHeaders(allowOrigin, response)
   202  
   203  			next.ServeHTTP(response, request)
   204  		})
   205  	}
   206  }
   207  
   208  func getUIHeadersHandler(config *config.Config, allowedMethods ...string) func(http.HandlerFunc) http.HandlerFunc {
   209  	allowedMethodsValue := strings.Join(allowedMethods, ",")
   210  
   211  	return func(next http.HandlerFunc) http.HandlerFunc {
   212  		return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
   213  			response.Header().Set("Access-Control-Allow-Methods", allowedMethodsValue)
   214  			response.Header().Set("Access-Control-Allow-Headers",
   215  				"Authorization,content-type,"+constants.SessionClientHeaderName)
   216  
   217  			if config.IsBasicAuthnEnabled() {
   218  				response.Header().Set("Access-Control-Allow-Credentials", "true")
   219  			}
   220  
   221  			next.ServeHTTP(response, request)
   222  		})
   223  	}
   224  }
   225  
   226  // Method handlers
   227  
   228  // CheckVersionSupport godoc
   229  // @Summary Check API support
   230  // @Description Check if this API version is supported
   231  // @Router  /v2/ [get]
   232  // @Accept  json
   233  // @Produce json
   234  // @Success 200 {string} string "ok".
   235  func (rh *RouteHandler) CheckVersionSupport(response http.ResponseWriter, request *http.Request) {
   236  	if request.Method == http.MethodOptions {
   237  		return
   238  	}
   239  
   240  	response.Header().Set(constants.DistAPIVersion, "registry/2.0")
   241  	// NOTE: compatibility workaround - return this header in "allowed-read" mode to allow for clients to
   242  	// work correctly
   243  	if rh.c.Config.HTTP.Auth != nil {
   244  		// don't send auth headers if request is coming from UI
   245  		if request.Header.Get(constants.SessionClientHeaderName) != constants.SessionClientHeaderValue {
   246  			if rh.c.Config.HTTP.Auth.Bearer != nil {
   247  				response.Header().Set("WWW-Authenticate", fmt.Sprintf("bearer realm=%s", rh.c.Config.HTTP.Auth.Bearer.Realm))
   248  			} else {
   249  				response.Header().Set("WWW-Authenticate", fmt.Sprintf("basic realm=%s", rh.c.Config.HTTP.Realm))
   250  			}
   251  		}
   252  	}
   253  
   254  	zcommon.WriteData(response, http.StatusOK, "application/json", []byte{})
   255  }
   256  
   257  // ListTags godoc
   258  // @Summary List image tags
   259  // @Description List all image tags in a repository
   260  // @Router  /v2/{name}/tags/list [get]
   261  // @Accept  json
   262  // @Produce json
   263  // @Param   name  path   string   true   "repository name"
   264  // @Param   n     query  integer  true   "limit entries for pagination"
   265  // @Param   last  query  string   true   "last tag value for pagination"
   266  // @Success 200 {object}     common.ImageTags
   267  // @Failure 404 {string}     string                 "not found"
   268  // @Failure 400 {string}     string                 "bad request".
   269  func (rh *RouteHandler) ListTags(response http.ResponseWriter, request *http.Request) {
   270  	if request.Method == http.MethodOptions {
   271  		return
   272  	}
   273  
   274  	vars := mux.Vars(request)
   275  
   276  	name, ok := vars["name"]
   277  
   278  	if !ok || name == "" {
   279  		response.WriteHeader(http.StatusNotFound)
   280  
   281  		return
   282  	}
   283  
   284  	paginate := false
   285  	numTags := -1
   286  
   287  	nQuery, ok := request.URL.Query()["n"]
   288  
   289  	if ok {
   290  		if len(nQuery) != 1 {
   291  			response.WriteHeader(http.StatusBadRequest)
   292  
   293  			return
   294  		}
   295  
   296  		var nQuery1 int64
   297  
   298  		var err error
   299  
   300  		if nQuery1, err = strconv.ParseInt(nQuery[0], 10, 0); err != nil {
   301  			response.WriteHeader(http.StatusBadRequest)
   302  
   303  			return
   304  		}
   305  
   306  		numTags = int(nQuery1)
   307  		paginate = true
   308  
   309  		if numTags < 0 {
   310  			response.WriteHeader(http.StatusBadRequest)
   311  
   312  			return
   313  		}
   314  	}
   315  
   316  	last := ""
   317  	lastQuery, ok := request.URL.Query()["last"]
   318  
   319  	if ok {
   320  		if len(lastQuery) != 1 {
   321  			response.WriteHeader(http.StatusBadRequest)
   322  
   323  			return
   324  		}
   325  
   326  		last = lastQuery[0]
   327  	}
   328  
   329  	imgStore := rh.getImageStore(name)
   330  
   331  	tags, err := imgStore.GetImageTags(name)
   332  	if err != nil {
   333  		e := apiErr.NewError(apiErr.NAME_UNKNOWN).AddDetail(map[string]string{"name": name})
   334  		zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e))
   335  
   336  		return
   337  	}
   338  
   339  	// Tags need to be sorted regardless of pagination parameters
   340  	sort.Strings(tags)
   341  
   342  	// Determine index of first tag returned
   343  	startIndex := 0
   344  
   345  	if last != "" {
   346  		found := false
   347  
   348  		for i, tag := range tags {
   349  			if tag == last {
   350  				found = true
   351  				startIndex = i + 1
   352  
   353  				break
   354  			}
   355  		}
   356  
   357  		if !found {
   358  			response.WriteHeader(http.StatusNotFound)
   359  
   360  			return
   361  		}
   362  	}
   363  
   364  	pTags := zcommon.ImageTags{Name: name}
   365  
   366  	if paginate && numTags == 0 {
   367  		pTags.Tags = []string{}
   368  		zcommon.WriteJSON(response, http.StatusOK, pTags)
   369  
   370  		return
   371  	}
   372  
   373  	stopIndex := len(tags) - 1
   374  	if paginate && (startIndex+numTags < len(tags)) {
   375  		stopIndex = startIndex + numTags - 1
   376  		response.Header().Set(
   377  			"Link",
   378  			fmt.Sprintf("/v2/%s/tags/list?n=%d&last=%s; rel=\"next\"",
   379  				name,
   380  				numTags,
   381  				tags[stopIndex],
   382  			),
   383  		)
   384  	}
   385  
   386  	pTags.Tags = tags[startIndex : stopIndex+1]
   387  
   388  	zcommon.WriteJSON(response, http.StatusOK, pTags)
   389  }
   390  
   391  // CheckManifest godoc
   392  // @Summary Check image manifest
   393  // @Description Check an image's manifest given a reference or a digest
   394  // @Router  /v2/{name}/manifests/{reference} [head]
   395  // @Accept  json
   396  // @Produce json
   397  // @Param   name          path    string     true        "repository name"
   398  // @Param   reference     path    string     true        "image reference or digest"
   399  // @Success 200 {string} string "ok"
   400  // @Header  200 {object} constants.DistContentDigestKey
   401  // @Failure 404 {string} string "not found"
   402  // @Failure 500 {string} string "internal server error".
   403  func (rh *RouteHandler) CheckManifest(response http.ResponseWriter, request *http.Request) {
   404  	if request.Method == http.MethodOptions {
   405  		return
   406  	}
   407  
   408  	vars := mux.Vars(request)
   409  	name, ok := vars["name"]
   410  
   411  	if !ok || name == "" {
   412  		response.WriteHeader(http.StatusNotFound)
   413  
   414  		return
   415  	}
   416  
   417  	imgStore := rh.getImageStore(name)
   418  
   419  	reference, ok := vars["reference"]
   420  	if !ok || reference == "" {
   421  		e := apiErr.NewError(apiErr.MANIFEST_INVALID).AddDetail(map[string]string{"reference": reference})
   422  		zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e))
   423  
   424  		return
   425  	}
   426  
   427  	content, digest, mediaType, err := getImageManifest(request.Context(), rh, imgStore, name, reference)
   428  	if err != nil {
   429  		details := zerr.GetDetails(err)
   430  		details["reference"] = reference
   431  
   432  		if errors.Is(err, zerr.ErrRepoNotFound) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain
   433  			e := apiErr.NewError(apiErr.NAME_UNKNOWN).AddDetail(details)
   434  			zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e))
   435  		} else if errors.Is(err, zerr.ErrManifestNotFound) {
   436  			e := apiErr.NewError(apiErr.MANIFEST_UNKNOWN).AddDetail(details)
   437  			zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e))
   438  		} else {
   439  			rh.c.Log.Error().Err(err).Msg("unexpected error")
   440  			e := apiErr.NewError(apiErr.MANIFEST_INVALID).AddDetail(details)
   441  			zcommon.WriteJSON(response, http.StatusInternalServerError, apiErr.NewErrorList(e))
   442  		}
   443  
   444  		return
   445  	}
   446  
   447  	response.Header().Set(constants.DistContentDigestKey, digest.String())
   448  	response.Header().Set("Content-Length", fmt.Sprintf("%d", len(content)))
   449  	response.Header().Set("Content-Type", mediaType)
   450  	response.WriteHeader(http.StatusOK)
   451  }
   452  
   453  type ImageManifest struct {
   454  	ispec.Manifest
   455  }
   456  
   457  type ExtensionList struct {
   458  	extensions.ExtensionList
   459  }
   460  
   461  // GetManifest godoc
   462  // @Summary Get image manifest
   463  // @Description Get an image's manifest given a reference or a digest
   464  // @Accept  json
   465  // @Produce application/vnd.oci.image.manifest.v1+json
   466  // @Param   name         path    string     true        "repository name"
   467  // @Param   reference    path    string     true        "image reference or digest"
   468  // @Success 200 {object} api.ImageManifest
   469  // @Header  200 {object} constants.DistContentDigestKey
   470  // @Failure 404 {string} string "not found"
   471  // @Failure 500 {string} string "internal server error"
   472  // @Router /v2/{name}/manifests/{reference} [get].
   473  func (rh *RouteHandler) GetManifest(response http.ResponseWriter, request *http.Request) {
   474  	if rh.c.Config.IsBasicAuthnEnabled() {
   475  		response.Header().Set("Access-Control-Allow-Credentials", "true")
   476  	}
   477  
   478  	vars := mux.Vars(request)
   479  	name, ok := vars["name"]
   480  
   481  	if !ok || name == "" {
   482  		response.WriteHeader(http.StatusNotFound)
   483  
   484  		return
   485  	}
   486  
   487  	imgStore := rh.getImageStore(name)
   488  
   489  	reference, ok := vars["reference"]
   490  	if !ok || reference == "" {
   491  		err := apiErr.NewError(apiErr.MANIFEST_UNKNOWN).AddDetail(map[string]string{"reference": reference})
   492  		zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(err))
   493  
   494  		return
   495  	}
   496  
   497  	content, digest, mediaType, err := getImageManifest(request.Context(), rh, imgStore, name, reference)
   498  	if err != nil {
   499  		details := zerr.GetDetails(err)
   500  		if errors.Is(err, zerr.ErrRepoNotFound) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain
   501  			details["name"] = name
   502  			e := apiErr.NewError(apiErr.NAME_UNKNOWN).AddDetail(details)
   503  			zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e))
   504  		} else if errors.Is(err, zerr.ErrRepoBadVersion) {
   505  			details["name"] = name
   506  			e := apiErr.NewError(apiErr.NAME_UNKNOWN).AddDetail(details)
   507  			zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e))
   508  		} else if errors.Is(err, zerr.ErrManifestNotFound) {
   509  			details["reference"] = reference
   510  			e := apiErr.NewError(apiErr.MANIFEST_UNKNOWN).AddDetail(details)
   511  			zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e))
   512  		} else {
   513  			rh.c.Log.Error().Err(err).Msg("unexpected error")
   514  			response.WriteHeader(http.StatusInternalServerError)
   515  		}
   516  
   517  		return
   518  	}
   519  
   520  	if rh.c.MetaDB != nil {
   521  		err := meta.OnGetManifest(name, reference, mediaType, content, rh.c.StoreController, rh.c.MetaDB, rh.c.Log)
   522  		if err != nil {
   523  			response.WriteHeader(http.StatusInternalServerError)
   524  
   525  			return
   526  		}
   527  	}
   528  
   529  	response.Header().Set(constants.DistContentDigestKey, digest.String())
   530  	response.Header().Set("Content-Length", fmt.Sprintf("%d", len(content)))
   531  	response.Header().Set("Content-Type", mediaType)
   532  	zcommon.WriteData(response, http.StatusOK, mediaType, content)
   533  }
   534  
   535  type ImageIndex struct {
   536  	ispec.Index
   537  }
   538  
   539  func getReferrers(ctx context.Context, routeHandler *RouteHandler,
   540  	imgStore storageTypes.ImageStore, name string, digest godigest.Digest,
   541  	artifactTypes []string,
   542  ) (ispec.Index, error) {
   543  	refs, err := imgStore.GetReferrers(name, digest, artifactTypes)
   544  	if err != nil || len(refs.Manifests) == 0 {
   545  		if isSyncOnDemandEnabled(*routeHandler.c) {
   546  			routeHandler.c.Log.Info().Str("repository", name).Str("reference", digest.String()).
   547  				Msg("referrers not found, trying to get reference by syncing on demand")
   548  
   549  			if errSync := routeHandler.c.SyncOnDemand.SyncReference(ctx, name, digest.String(),
   550  				syncConstants.OCI); errSync != nil {
   551  				routeHandler.c.Log.Err(errSync).Str("repository", name).Str("reference", digest.String()).
   552  					Msg("error encounter while syncing OCI reference for image")
   553  			}
   554  
   555  			refs, err = imgStore.GetReferrers(name, digest, artifactTypes)
   556  		}
   557  	}
   558  
   559  	return refs, err
   560  }
   561  
   562  // GetReferrers godoc
   563  // @Summary Get referrers for a given digest
   564  // @Description Get referrers given a digest
   565  // @Accept  json
   566  // @Produce application/vnd.oci.image.index.v1+json
   567  // @Param   name       path    string     true        "repository name"
   568  // @Param   digest     path    string     true        "digest"
   569  // @Param artifactType query string false "artifact type"
   570  // @Success 200 {object} api.ImageIndex
   571  // @Failure 404 {string} string "not found"
   572  // @Failure 500 {string} string "internal server error"
   573  // @Router /v2/{name}/referrers/{digest} [get].
   574  func (rh *RouteHandler) GetReferrers(response http.ResponseWriter, request *http.Request) {
   575  	if request.Method == http.MethodOptions {
   576  		return
   577  	}
   578  
   579  	vars := mux.Vars(request)
   580  
   581  	name, ok := vars["name"]
   582  	if !ok || name == "" {
   583  		response.WriteHeader(http.StatusNotFound)
   584  
   585  		return
   586  	}
   587  
   588  	digestStr, ok := vars["digest"]
   589  	digest, err := godigest.Parse(digestStr)
   590  
   591  	if !ok || digestStr == "" || err != nil {
   592  		response.WriteHeader(http.StatusBadRequest)
   593  
   594  		return
   595  	}
   596  
   597  	// filter by artifact type (more than one can be specified)
   598  	artifactTypes := request.URL.Query()["artifactType"]
   599  
   600  	rh.c.Log.Info().Str("digest", digest.String()).Interface("artifactType", artifactTypes).Msg("getting manifest")
   601  
   602  	imgStore := rh.getImageStore(name)
   603  
   604  	referrers, err := getReferrers(request.Context(), rh, imgStore, name, digest, artifactTypes)
   605  	if err != nil {
   606  		if errors.Is(err, zerr.ErrManifestNotFound) || errors.Is(err, zerr.ErrRepoNotFound) {
   607  			rh.c.Log.Error().Err(err).Str("name", name).Str("digest", digest.String()).Msg("manifest not found")
   608  			response.WriteHeader(http.StatusNotFound)
   609  		} else {
   610  			rh.c.Log.Error().Err(err).Str("name", name).Str("digest", digest.String()).Msg("unable to get references")
   611  			response.WriteHeader(http.StatusInternalServerError)
   612  		}
   613  
   614  		return
   615  	}
   616  
   617  	out, err := json.Marshal(referrers)
   618  	if err != nil {
   619  		rh.c.Log.Error().Err(err).Str("name", name).Str("digest", digest.String()).Msg("unable to marshal json")
   620  		response.WriteHeader(http.StatusInternalServerError)
   621  
   622  		return
   623  	}
   624  
   625  	if len(artifactTypes) > 0 {
   626  		// currently, the only filter supported and on this end-point
   627  		response.Header().Set("OCI-Filters-Applied", "artifactType")
   628  	}
   629  
   630  	zcommon.WriteData(response, http.StatusOK, ispec.MediaTypeImageIndex, out)
   631  }
   632  
   633  // UpdateManifest godoc
   634  // @Summary Update image manifest
   635  // @Description Update an image's manifest given a reference or a digest
   636  // @Accept  json
   637  // @Produce json
   638  // @Param   name         path    string     true        "repository name"
   639  // @Param   reference    path    string     true        "image reference or digest"
   640  // @Header  201 {object} constants.DistContentDigestKey
   641  // @Success 201 {string} string "created"
   642  // @Failure 400 {string} string "bad request"
   643  // @Failure 404 {string} string "not found"
   644  // @Failure 500 {string} string "internal server error"
   645  // @Router /v2/{name}/manifests/{reference} [put].
   646  func (rh *RouteHandler) UpdateManifest(response http.ResponseWriter, request *http.Request) {
   647  	vars := mux.Vars(request)
   648  	name, ok := vars["name"]
   649  
   650  	if !ok || name == "" {
   651  		response.WriteHeader(http.StatusNotFound)
   652  
   653  		return
   654  	}
   655  
   656  	imgStore := rh.getImageStore(name)
   657  
   658  	reference, ok := vars["reference"]
   659  	if !ok || reference == "" {
   660  		err := apiErr.NewError(apiErr.MANIFEST_INVALID).AddDetail(map[string]string{"reference": reference})
   661  		zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(err))
   662  
   663  		return
   664  	}
   665  
   666  	mediaType := request.Header.Get("Content-Type")
   667  	if !storageCommon.IsSupportedMediaType(mediaType) {
   668  		err := apiErr.NewError(apiErr.MANIFEST_INVALID).AddDetail(map[string]string{"mediaType": mediaType})
   669  		zcommon.WriteJSON(response, http.StatusUnsupportedMediaType, apiErr.NewErrorList(err))
   670  
   671  		return
   672  	}
   673  
   674  	body, err := io.ReadAll(request.Body)
   675  	// hard to reach test case, injected error (simulates an interrupted image manifest upload)
   676  	// err could be io.ErrUnexpectedEOF
   677  	if err := inject.Error(err); err != nil {
   678  		rh.c.Log.Error().Err(err).Msg("unexpected error")
   679  		response.WriteHeader(http.StatusInternalServerError)
   680  
   681  		return
   682  	}
   683  
   684  	digest, subjectDigest, err := imgStore.PutImageManifest(name, reference, mediaType, body)
   685  	if err != nil {
   686  		details := zerr.GetDetails(err)
   687  		if errors.Is(err, zerr.ErrRepoNotFound) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain
   688  			details["name"] = name
   689  			e := apiErr.NewError(apiErr.NAME_UNKNOWN).AddDetail(details)
   690  			zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e))
   691  		} else if errors.Is(err, zerr.ErrManifestNotFound) {
   692  			details["reference"] = reference
   693  			e := apiErr.NewError(apiErr.MANIFEST_UNKNOWN).AddDetail(details)
   694  			zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e))
   695  		} else if errors.Is(err, zerr.ErrBadManifest) {
   696  			details["reference"] = reference
   697  			e := apiErr.NewError(apiErr.MANIFEST_INVALID).AddDetail(details)
   698  			zcommon.WriteJSON(response, http.StatusBadRequest, apiErr.NewErrorList(e))
   699  		} else if errors.Is(err, zerr.ErrBlobNotFound) {
   700  			details["blob"] = digest.String()
   701  			e := apiErr.NewError(apiErr.BLOB_UNKNOWN).AddDetail(details)
   702  			zcommon.WriteJSON(response, http.StatusBadRequest, apiErr.NewErrorList(e))
   703  		} else if errors.Is(err, zerr.ErrImageLintAnnotations) {
   704  			details["reference"] = reference
   705  			e := apiErr.NewError(apiErr.MANIFEST_INVALID).AddDetail(details)
   706  			zcommon.WriteJSON(response, http.StatusBadRequest, apiErr.NewErrorList(e))
   707  		} else {
   708  			// could be syscall.EMFILE (Err:0x18 too many opened files), etc
   709  			rh.c.Log.Error().Err(err).Msg("unexpected error: performing cleanup")
   710  
   711  			if err = imgStore.DeleteImageManifest(name, reference, false); err != nil {
   712  				// deletion of image manifest is important, but not critical for image repo consistency
   713  				// in the worst scenario a partial manifest file written to disk will not affect the repo because
   714  				// the new manifest was not added to "index.json" file (it is possible that GC will take care of it)
   715  				rh.c.Log.Error().Err(err).Str("repository", name).Str("reference", reference).
   716  					Msg("couldn't remove image manifest in repo")
   717  			}
   718  
   719  			response.WriteHeader(http.StatusInternalServerError)
   720  		}
   721  
   722  		return
   723  	}
   724  
   725  	if rh.c.MetaDB != nil {
   726  		err := meta.OnUpdateManifest(request.Context(), name, reference, mediaType,
   727  			digest, body, rh.c.StoreController, rh.c.MetaDB, rh.c.Log)
   728  		if err != nil {
   729  			response.WriteHeader(http.StatusInternalServerError)
   730  
   731  			return
   732  		}
   733  	}
   734  
   735  	if subjectDigest.String() != "" {
   736  		response.Header().Set(constants.SubjectDigestKey, subjectDigest.String())
   737  	}
   738  
   739  	response.Header().Set("Location", fmt.Sprintf("/v2/%s/manifests/%s", name, digest))
   740  	response.Header().Set(constants.DistContentDigestKey, digest.String())
   741  	response.WriteHeader(http.StatusCreated)
   742  }
   743  
   744  // DeleteManifest godoc
   745  // @Summary Delete image manifest
   746  // @Description Delete an image's manifest given a reference or a digest
   747  // @Accept  json
   748  // @Produce json
   749  // @Param   name          path    string     true        "repository name"
   750  // @Param   reference     path    string     true        "image reference or digest"
   751  // @Success 200 {string} string "ok"
   752  // @Router /v2/{name}/manifests/{reference} [delete].
   753  func (rh *RouteHandler) DeleteManifest(response http.ResponseWriter, request *http.Request) {
   754  	vars := mux.Vars(request)
   755  	name, ok := vars["name"]
   756  
   757  	if !ok || name == "" {
   758  		response.WriteHeader(http.StatusNotFound)
   759  
   760  		return
   761  	}
   762  
   763  	imgStore := rh.getImageStore(name)
   764  
   765  	reference, ok := vars["reference"]
   766  	if !ok || reference == "" {
   767  		response.WriteHeader(http.StatusNotFound)
   768  
   769  		return
   770  	}
   771  
   772  	// user authz request context (set in authz middleware)
   773  	userAc, err := reqCtx.UserAcFromContext(request.Context())
   774  	if err != nil {
   775  		response.WriteHeader(http.StatusInternalServerError)
   776  
   777  		return
   778  	}
   779  
   780  	var detectCollision bool
   781  	if userAc != nil {
   782  		detectCollision = userAc.Can(constants.DetectManifestCollisionPermission, name)
   783  	}
   784  
   785  	manifestBlob, manifestDigest, mediaType, err := imgStore.GetImageManifest(name, reference)
   786  	if err != nil {
   787  		details := zerr.GetDetails(err)
   788  		if errors.Is(err, zerr.ErrRepoNotFound) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain
   789  			details["name"] = name
   790  			e := apiErr.NewError(apiErr.NAME_UNKNOWN).AddDetail(details)
   791  			zcommon.WriteJSON(response, http.StatusBadRequest, apiErr.NewErrorList(e))
   792  		} else if errors.Is(err, zerr.ErrManifestNotFound) {
   793  			details["reference"] = reference
   794  			e := apiErr.NewError(apiErr.MANIFEST_UNKNOWN).AddDetail(details)
   795  			zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e))
   796  		} else if errors.Is(err, zerr.ErrBadManifest) {
   797  			details["reference"] = reference
   798  			e := apiErr.NewError(apiErr.UNSUPPORTED).AddDetail(details)
   799  			zcommon.WriteJSON(response, http.StatusBadRequest, apiErr.NewErrorList(e))
   800  		} else {
   801  			rh.c.Log.Error().Err(err).Msg("unexpected error")
   802  			response.WriteHeader(http.StatusInternalServerError)
   803  		}
   804  
   805  		return
   806  	}
   807  
   808  	err = imgStore.DeleteImageManifest(name, reference, detectCollision)
   809  	if err != nil { //nolint: dupl
   810  		details := zerr.GetDetails(err)
   811  		if errors.Is(err, zerr.ErrRepoNotFound) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain
   812  			details["name"] = name
   813  			e := apiErr.NewError(apiErr.NAME_UNKNOWN).AddDetail(details)
   814  			zcommon.WriteJSON(response, http.StatusBadRequest, apiErr.NewErrorList(e))
   815  		} else if errors.Is(err, zerr.ErrManifestNotFound) {
   816  			details["reference"] = reference
   817  			e := apiErr.NewError(apiErr.MANIFEST_UNKNOWN).AddDetail(details)
   818  			zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e))
   819  		} else if errors.Is(err, zerr.ErrManifestConflict) {
   820  			details["reference"] = reference
   821  			e := apiErr.NewError(apiErr.MANIFEST_INVALID).AddDetail(details)
   822  			zcommon.WriteJSON(response, http.StatusConflict, apiErr.NewErrorList(e))
   823  		} else if errors.Is(err, zerr.ErrBadManifest) {
   824  			details["reference"] = reference
   825  			e := apiErr.NewError(apiErr.UNSUPPORTED).AddDetail(details)
   826  			zcommon.WriteJSON(response, http.StatusBadRequest, apiErr.NewErrorList(e))
   827  		} else if errors.Is(err, zerr.ErrManifestReferenced) {
   828  			// manifest is part of an index image, don't allow index manipulations.
   829  			details["reference"] = reference
   830  			e := apiErr.NewError(apiErr.DENIED).AddDetail(details)
   831  			zcommon.WriteJSON(response, http.StatusMethodNotAllowed, apiErr.NewErrorList(e))
   832  		} else {
   833  			rh.c.Log.Error().Err(err).Msg("unexpected error")
   834  			response.WriteHeader(http.StatusInternalServerError)
   835  		}
   836  
   837  		return
   838  	}
   839  
   840  	if rh.c.MetaDB != nil {
   841  		err := meta.OnDeleteManifest(name, reference, mediaType, manifestDigest, manifestBlob,
   842  			rh.c.StoreController, rh.c.MetaDB, rh.c.Log)
   843  		if err != nil {
   844  			response.WriteHeader(http.StatusInternalServerError)
   845  
   846  			return
   847  		}
   848  	}
   849  
   850  	response.WriteHeader(http.StatusAccepted)
   851  }
   852  
   853  // CheckBlob godoc
   854  // @Summary Check image blob/layer
   855  // @Description Check an image's blob/layer given a digest
   856  // @Accept  json
   857  // @Produce json
   858  // @Param   name     path    string     true        "repository name"
   859  // @Param   digest   path    string     true        "blob/layer digest"
   860  // @Success 200 {object} api.ImageManifest
   861  // @Header  200 {object} constants.DistContentDigestKey
   862  // @Router /v2/{name}/blobs/{digest} [head].
   863  func (rh *RouteHandler) CheckBlob(response http.ResponseWriter, request *http.Request) {
   864  	vars := mux.Vars(request)
   865  	name, ok := vars["name"]
   866  
   867  	if !ok || name == "" {
   868  		response.WriteHeader(http.StatusNotFound)
   869  
   870  		return
   871  	}
   872  
   873  	imgStore := rh.getImageStore(name)
   874  
   875  	digestStr, ok := vars["digest"]
   876  
   877  	if !ok || digestStr == "" {
   878  		response.WriteHeader(http.StatusNotFound)
   879  
   880  		return
   881  	}
   882  
   883  	digest := godigest.Digest(digestStr)
   884  
   885  	ok, blen, err := imgStore.CheckBlob(name, digest)
   886  	if err != nil {
   887  		details := zerr.GetDetails(err)
   888  		if errors.Is(err, zerr.ErrBadBlobDigest) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain
   889  			details["digest"] = digest.String()
   890  			e := apiErr.NewError(apiErr.DIGEST_INVALID).AddDetail(details)
   891  			zcommon.WriteJSON(response, http.StatusBadRequest, apiErr.NewErrorList(e))
   892  		} else if errors.Is(err, zerr.ErrRepoNotFound) {
   893  			details["name"] = name
   894  			e := apiErr.NewError(apiErr.NAME_UNKNOWN).AddDetail(details)
   895  			zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e))
   896  		} else if errors.Is(err, zerr.ErrBlobNotFound) {
   897  			details["digest"] = digest.String()
   898  			e := apiErr.NewError(apiErr.BLOB_UNKNOWN).AddDetail(details)
   899  			zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e))
   900  		} else {
   901  			rh.c.Log.Error().Err(err).Msg("unexpected error")
   902  			response.WriteHeader(http.StatusInternalServerError)
   903  		}
   904  
   905  		return
   906  	}
   907  
   908  	if !ok {
   909  		e := apiErr.NewError(apiErr.BLOB_UNKNOWN).AddDetail(map[string]string{"digest": digest.String()})
   910  		zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e))
   911  
   912  		return
   913  	}
   914  
   915  	response.Header().Set("Content-Length", fmt.Sprintf("%d", blen))
   916  	response.Header().Set("Accept-Ranges", "bytes")
   917  	response.Header().Set(constants.DistContentDigestKey, digest.String())
   918  	response.WriteHeader(http.StatusOK)
   919  }
   920  
   921  /* parseRangeHeader validates the "Range" HTTP header and returns the range. */
   922  func parseRangeHeader(contentRange string) (int64, int64, error) {
   923  	/* bytes=<start>- and bytes=<start>-<end> formats are supported */
   924  	pattern := `bytes=(?P<rangeFrom>\d+)-(?P<rangeTo>\d*$)`
   925  
   926  	regex, err := regexp.Compile(pattern)
   927  	if err != nil {
   928  		return -1, -1, zerr.ErrParsingHTTPHeader
   929  	}
   930  
   931  	match := regex.FindStringSubmatch(contentRange)
   932  
   933  	paramsMap := make(map[string]string)
   934  
   935  	for i, name := range regex.SubexpNames() {
   936  		if i > 0 && i <= len(match) {
   937  			paramsMap[name] = match[i]
   938  		}
   939  	}
   940  
   941  	var from int64
   942  	to := int64(-1)
   943  
   944  	rangeFrom := paramsMap["rangeFrom"]
   945  	if rangeFrom == "" {
   946  		return -1, -1, zerr.ErrParsingHTTPHeader
   947  	}
   948  
   949  	if from, err = strconv.ParseInt(rangeFrom, 10, 64); err != nil {
   950  		return -1, -1, zerr.ErrParsingHTTPHeader
   951  	}
   952  
   953  	rangeTo := paramsMap["rangeTo"]
   954  	if rangeTo != "" {
   955  		if to, err = strconv.ParseInt(rangeTo, 10, 64); err != nil {
   956  			return -1, -1, zerr.ErrParsingHTTPHeader
   957  		}
   958  
   959  		if to < from {
   960  			return -1, -1, zerr.ErrParsingHTTPHeader
   961  		}
   962  	}
   963  
   964  	return from, to, nil
   965  }
   966  
   967  // GetBlob godoc
   968  // @Summary Get image blob/layer
   969  // @Description Get an image's blob/layer given a digest
   970  // @Accept  json
   971  // @Produce application/vnd.oci.image.layer.v1.tar+gzip
   972  // @Param   name     path    string     true        "repository name"
   973  // @Param   digest   path    string     true        "blob/layer digest"
   974  // @Header  200 {object} constants.DistContentDigestKey
   975  // @Success 200 {object} api.ImageManifest
   976  // @Router /v2/{name}/blobs/{digest} [get].
   977  func (rh *RouteHandler) GetBlob(response http.ResponseWriter, request *http.Request) {
   978  	vars := mux.Vars(request)
   979  	name, ok := vars["name"]
   980  
   981  	if !ok || name == "" {
   982  		response.WriteHeader(http.StatusNotFound)
   983  
   984  		return
   985  	}
   986  
   987  	imgStore := rh.getImageStore(name)
   988  
   989  	digestStr, ok := vars["digest"]
   990  
   991  	if !ok || digestStr == "" {
   992  		response.WriteHeader(http.StatusNotFound)
   993  
   994  		return
   995  	}
   996  
   997  	digest := godigest.Digest(digestStr)
   998  
   999  	mediaType := request.Header.Get("Accept")
  1000  
  1001  	/* content range is supported for resumbale pulls */
  1002  	partial := false
  1003  
  1004  	var from, to int64
  1005  
  1006  	var err error
  1007  
  1008  	contentRange := request.Header.Get("Range")
  1009  
  1010  	_, ok = request.Header["Range"]
  1011  	if ok && contentRange == "" {
  1012  		response.WriteHeader(http.StatusRequestedRangeNotSatisfiable)
  1013  
  1014  		return
  1015  	}
  1016  
  1017  	if contentRange != "" {
  1018  		from, to, err = parseRangeHeader(contentRange)
  1019  		if err != nil {
  1020  			response.WriteHeader(http.StatusRequestedRangeNotSatisfiable)
  1021  
  1022  			return
  1023  		}
  1024  
  1025  		partial = true
  1026  	}
  1027  
  1028  	var repo io.ReadCloser
  1029  
  1030  	var blen, bsize int64
  1031  
  1032  	if partial {
  1033  		repo, blen, bsize, err = imgStore.GetBlobPartial(name, digest, mediaType, from, to)
  1034  	} else {
  1035  		repo, blen, err = imgStore.GetBlob(name, digest, mediaType)
  1036  	}
  1037  
  1038  	if err != nil {
  1039  		details := zerr.GetDetails(err)
  1040  		if errors.Is(err, zerr.ErrBadBlobDigest) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain
  1041  			details["digest"] = digest.String()
  1042  			e := apiErr.NewError(apiErr.DIGEST_INVALID).AddDetail(details)
  1043  			zcommon.WriteJSON(response, http.StatusBadRequest, apiErr.NewErrorList(e))
  1044  		} else if errors.Is(err, zerr.ErrRepoNotFound) {
  1045  			details["name"] = name
  1046  			e := apiErr.NewError(apiErr.NAME_UNKNOWN).AddDetail(details)
  1047  			zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e))
  1048  		} else if errors.Is(err, zerr.ErrBlobNotFound) {
  1049  			details["digest"] = digest.String()
  1050  			e := apiErr.NewError(apiErr.BLOB_UNKNOWN).AddDetail(details)
  1051  			zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e))
  1052  		} else {
  1053  			rh.c.Log.Error().Err(err).Msg("unexpected error")
  1054  			response.WriteHeader(http.StatusInternalServerError)
  1055  		}
  1056  
  1057  		return
  1058  	}
  1059  	defer repo.Close()
  1060  
  1061  	response.Header().Set("Content-Length", fmt.Sprintf("%d", blen))
  1062  
  1063  	status := http.StatusOK
  1064  
  1065  	if partial {
  1066  		status = http.StatusPartialContent
  1067  
  1068  		response.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", from, from+blen-1, bsize))
  1069  	} else {
  1070  		response.Header().Set(constants.DistContentDigestKey, digest.String())
  1071  	}
  1072  
  1073  	// return the blob data
  1074  	WriteDataFromReader(response, status, blen, mediaType, repo, rh.c.Log)
  1075  }
  1076  
  1077  // DeleteBlob godoc
  1078  // @Summary Delete image blob/layer
  1079  // @Description Delete an image's blob/layer given a digest
  1080  // @Accept  json
  1081  // @Produce json
  1082  // @Param   name      path    string     true        "repository name"
  1083  // @Param   digest    path    string     true        "blob/layer digest"
  1084  // @Success 202 {string} string "accepted"
  1085  // @Router /v2/{name}/blobs/{digest} [delete].
  1086  func (rh *RouteHandler) DeleteBlob(response http.ResponseWriter, request *http.Request) {
  1087  	vars := mux.Vars(request)
  1088  	name, ok := vars["name"]
  1089  
  1090  	if !ok || name == "" {
  1091  		response.WriteHeader(http.StatusNotFound)
  1092  
  1093  		return
  1094  	}
  1095  
  1096  	digestStr, ok := vars["digest"]
  1097  	digest, err := godigest.Parse(digestStr)
  1098  
  1099  	if !ok || digestStr == "" || err != nil {
  1100  		response.WriteHeader(http.StatusNotFound)
  1101  
  1102  		return
  1103  	}
  1104  
  1105  	imgStore := rh.getImageStore(name)
  1106  
  1107  	err = imgStore.DeleteBlob(name, digest)
  1108  	if err != nil {
  1109  		details := zerr.GetDetails(err)
  1110  		if errors.Is(err, zerr.ErrBadBlobDigest) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain
  1111  			details["digest"] = digest.String()
  1112  			e := apiErr.NewError(apiErr.DIGEST_INVALID).AddDetail(details)
  1113  			zcommon.WriteJSON(response, http.StatusBadRequest, apiErr.NewErrorList(e))
  1114  		} else if errors.Is(err, zerr.ErrRepoNotFound) {
  1115  			details["name"] = name
  1116  			e := apiErr.NewError(apiErr.NAME_UNKNOWN).AddDetail(map[string]string{"name": name})
  1117  			zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e))
  1118  		} else if errors.Is(err, zerr.ErrBlobNotFound) {
  1119  			details["digest"] = digest.String()
  1120  			e := apiErr.NewError(apiErr.BLOB_UNKNOWN).AddDetail(details)
  1121  			zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e))
  1122  		} else if errors.Is(err, zerr.ErrBlobReferenced) {
  1123  			details["digest"] = digest.String()
  1124  			e := apiErr.NewError(apiErr.DENIED).AddDetail(details)
  1125  			zcommon.WriteJSON(response, http.StatusMethodNotAllowed, apiErr.NewErrorList(e))
  1126  		} else {
  1127  			rh.c.Log.Error().Err(err).Msg("unexpected error")
  1128  			response.WriteHeader(http.StatusInternalServerError)
  1129  		}
  1130  
  1131  		return
  1132  	}
  1133  
  1134  	response.WriteHeader(http.StatusAccepted)
  1135  }
  1136  
  1137  // CreateBlobUpload godoc
  1138  // @Summary Create image blob/layer upload
  1139  // @Description Create a new image blob/layer upload
  1140  // @Accept  json
  1141  // @Produce json
  1142  // @Param   name    path    string     true        "repository name"
  1143  // @Success 202 {string} string "accepted"
  1144  // @Header  202 {string} Location "/v2/{name}/blobs/uploads/{session_id}"
  1145  // @Header  202 {string} Range "0-0"
  1146  // @Failure 401 {string} string "unauthorized"
  1147  // @Failure 404 {string} string "not found"
  1148  // @Failure 500 {string} string "internal server error"
  1149  // @Router /v2/{name}/blobs/uploads [post].
  1150  func (rh *RouteHandler) CreateBlobUpload(response http.ResponseWriter, request *http.Request) {
  1151  	vars := mux.Vars(request)
  1152  	name, ok := vars["name"]
  1153  
  1154  	if !ok || name == "" {
  1155  		response.WriteHeader(http.StatusNotFound)
  1156  
  1157  		return
  1158  	}
  1159  
  1160  	imgStore := rh.getImageStore(name)
  1161  
  1162  	// currently zot does not support cross-repository mounting, following dist-spec and returning 202
  1163  	if mountDigests, ok := request.URL.Query()["mount"]; ok {
  1164  		if len(mountDigests) != 1 {
  1165  			response.WriteHeader(http.StatusBadRequest)
  1166  
  1167  			return
  1168  		}
  1169  
  1170  		mountDigest := godigest.Digest(mountDigests[0])
  1171  		// zot does not support cross mounting directly and do a workaround creating using hard link.
  1172  		// check blob looks for actual path (name+mountDigests[0]) first then look for cache and
  1173  		// if found in cache, will do hard link and if fails we will start new upload.
  1174  		_, _, err := imgStore.CheckBlob(name, mountDigest)
  1175  		if err != nil {
  1176  			upload, err := imgStore.NewBlobUpload(name)
  1177  			if err != nil {
  1178  				details := zerr.GetDetails(err)
  1179  				if errors.Is(err, zerr.ErrRepoNotFound) {
  1180  					details["name"] = name
  1181  					e := apiErr.NewError(apiErr.NAME_UNKNOWN).AddDetail(details)
  1182  					zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e))
  1183  				} else {
  1184  					rh.c.Log.Error().Err(err).Msg("unexpected error")
  1185  					response.WriteHeader(http.StatusInternalServerError)
  1186  				}
  1187  
  1188  				return
  1189  			}
  1190  
  1191  			response.Header().Set("Location", getBlobUploadSessionLocation(request.URL, upload))
  1192  			response.Header().Set("Range", "0-0")
  1193  			response.WriteHeader(http.StatusAccepted)
  1194  
  1195  			return
  1196  		}
  1197  
  1198  		response.Header().Set("Location", getBlobUploadLocation(request.URL, name, mountDigest))
  1199  		response.WriteHeader(http.StatusCreated)
  1200  
  1201  		return
  1202  	}
  1203  
  1204  	if _, ok := request.URL.Query()["from"]; ok {
  1205  		response.WriteHeader(http.StatusMethodNotAllowed)
  1206  
  1207  		return
  1208  	}
  1209  
  1210  	// a full blob upload if "digest" is present
  1211  	digests, ok := request.URL.Query()["digest"]
  1212  	if ok {
  1213  		if len(digests) != 1 {
  1214  			response.WriteHeader(http.StatusBadRequest)
  1215  
  1216  			return
  1217  		}
  1218  
  1219  		if contentType := request.Header.Get("Content-Type"); contentType != constants.BinaryMediaType {
  1220  			rh.c.Log.Warn().Str("actual", contentType).Str("expected", constants.BinaryMediaType).Msg("invalid media type")
  1221  			response.WriteHeader(http.StatusUnsupportedMediaType)
  1222  
  1223  			return
  1224  		}
  1225  
  1226  		rh.c.Log.Info().Int64("r.ContentLength", request.ContentLength).Msg("DEBUG")
  1227  
  1228  		digestStr := digests[0]
  1229  
  1230  		digest := godigest.Digest(digestStr)
  1231  
  1232  		var contentLength int64
  1233  
  1234  		contentLength, err := strconv.ParseInt(request.Header.Get("Content-Length"), 10, 64)
  1235  		if err != nil || contentLength <= 0 {
  1236  			rh.c.Log.Warn().Str("actual", request.Header.Get("Content-Length")).Msg("invalid content length")
  1237  			details := map[string]string{"digest": digest.String()}
  1238  
  1239  			if err != nil {
  1240  				details["conversion error"] = err.Error()
  1241  			} else {
  1242  				details["Content-Length"] = request.Header.Get("Content-Length")
  1243  			}
  1244  			e := apiErr.NewError(apiErr.BLOB_UPLOAD_INVALID).AddDetail(details)
  1245  			zcommon.WriteJSON(response, http.StatusBadRequest, apiErr.NewErrorList(e))
  1246  
  1247  			return
  1248  		}
  1249  
  1250  		sessionID, size, err := imgStore.FullBlobUpload(name, request.Body, digest)
  1251  		if err != nil {
  1252  			rh.c.Log.Error().Err(err).Int64("actual", size).Int64("expected", contentLength).Msg("failed full upload")
  1253  			response.WriteHeader(http.StatusInternalServerError)
  1254  
  1255  			return
  1256  		}
  1257  
  1258  		if size != contentLength {
  1259  			rh.c.Log.Warn().Int64("actual", size).Int64("expected", contentLength).Msg("invalid content length")
  1260  			response.WriteHeader(http.StatusInternalServerError)
  1261  
  1262  			return
  1263  		}
  1264  
  1265  		response.Header().Set("Location", getBlobUploadLocation(request.URL, name, digest))
  1266  		response.Header().Set(constants.BlobUploadUUID, sessionID)
  1267  		response.WriteHeader(http.StatusCreated)
  1268  
  1269  		return
  1270  	}
  1271  
  1272  	upload, err := imgStore.NewBlobUpload(name)
  1273  	if err != nil {
  1274  		details := zerr.GetDetails(err)
  1275  		if errors.Is(err, zerr.ErrRepoNotFound) {
  1276  			details["name"] = name
  1277  			e := apiErr.NewError(apiErr.NAME_UNKNOWN).AddDetail(details)
  1278  			zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e))
  1279  		} else {
  1280  			rh.c.Log.Error().Err(err).Msg("unexpected error")
  1281  			response.WriteHeader(http.StatusInternalServerError)
  1282  		}
  1283  
  1284  		return
  1285  	}
  1286  
  1287  	response.Header().Set("Location", getBlobUploadSessionLocation(request.URL, upload))
  1288  	response.Header().Set("Range", "0-0")
  1289  	response.WriteHeader(http.StatusAccepted)
  1290  }
  1291  
  1292  // GetBlobUpload godoc
  1293  // @Summary Get image blob/layer upload
  1294  // @Description Get an image's blob/layer upload given a session_id
  1295  // @Accept  json
  1296  // @Produce json
  1297  // @Param   name          path    string     true        "repository name"
  1298  // @Param   session_id    path    string     true        "upload session_id"
  1299  // @Success 204 {string} string "no content"
  1300  // @Header  202 {string} Location "/v2/{name}/blobs/uploads/{session_id}"
  1301  // @Header  202 {string} Range "0-128"
  1302  // @Failure 404 {string} string "not found"
  1303  // @Failure 500 {string} string "internal server error"
  1304  // @Router /v2/{name}/blobs/uploads/{session_id} [get].
  1305  func (rh *RouteHandler) GetBlobUpload(response http.ResponseWriter, request *http.Request) {
  1306  	vars := mux.Vars(request)
  1307  	name, ok := vars["name"]
  1308  
  1309  	if !ok || name == "" {
  1310  		response.WriteHeader(http.StatusNotFound)
  1311  
  1312  		return
  1313  	}
  1314  
  1315  	imgStore := rh.getImageStore(name)
  1316  
  1317  	sessionID, ok := vars["session_id"]
  1318  	if !ok || sessionID == "" {
  1319  		response.WriteHeader(http.StatusNotFound)
  1320  
  1321  		return
  1322  	}
  1323  
  1324  	size, err := imgStore.GetBlobUpload(name, sessionID)
  1325  	if err != nil {
  1326  		details := zerr.GetDetails(err)
  1327  		//nolint:gocritic // errorslint conflicts with gocritic:IfElseChain
  1328  		if errors.Is(err, zerr.ErrBadUploadRange) || errors.Is(err, zerr.ErrBadBlobDigest) {
  1329  			details["session_id"] = sessionID
  1330  			e := apiErr.NewError(apiErr.BLOB_UPLOAD_INVALID).AddDetail(details)
  1331  			zcommon.WriteJSON(response, http.StatusBadRequest, apiErr.NewErrorList(e))
  1332  		} else if errors.Is(err, zerr.ErrRepoNotFound) {
  1333  			details["name"] = name
  1334  			e := apiErr.NewError(apiErr.NAME_UNKNOWN).AddDetail(details)
  1335  			zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e))
  1336  		} else if errors.Is(err, zerr.ErrUploadNotFound) {
  1337  			details["session_id"] = sessionID
  1338  			e := apiErr.NewError(apiErr.BLOB_UPLOAD_UNKNOWN).AddDetail(details)
  1339  			zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e))
  1340  		} else {
  1341  			rh.c.Log.Error().Err(err).Msg("unexpected error")
  1342  			response.WriteHeader(http.StatusInternalServerError)
  1343  		}
  1344  
  1345  		return
  1346  	}
  1347  
  1348  	response.Header().Set("Location", getBlobUploadSessionLocation(request.URL, sessionID))
  1349  	response.Header().Set("Range", fmt.Sprintf("0-%d", size-1))
  1350  	response.WriteHeader(http.StatusNoContent)
  1351  }
  1352  
  1353  // PatchBlobUpload godoc
  1354  // @Summary Resume image blob/layer upload
  1355  // @Description Resume an image's blob/layer upload given an session_id
  1356  // @Accept  json
  1357  // @Produce json
  1358  // @Param   name         path    string     true        "repository name"
  1359  // @Param   session_id   path    string     true        "upload session_id"
  1360  // @Success 202 {string} string "accepted"
  1361  // @Header  202 {string} Location "/v2/{name}/blobs/uploads/{session_id}"
  1362  // @Header  202 {string} Range "0-128"
  1363  // @Header  200 {object} api.BlobUploadUUID
  1364  // @Failure 400 {string} string "bad request"
  1365  // @Failure 404 {string} string "not found"
  1366  // @Failure 416 {string} string "range not satisfiable"
  1367  // @Failure 500 {string} string "internal server error"
  1368  // @Router /v2/{name}/blobs/uploads/{session_id} [patch].
  1369  func (rh *RouteHandler) PatchBlobUpload(response http.ResponseWriter, request *http.Request) {
  1370  	vars := mux.Vars(request)
  1371  	name, ok := vars["name"]
  1372  
  1373  	if !ok || name == "" {
  1374  		response.WriteHeader(http.StatusNotFound)
  1375  
  1376  		return
  1377  	}
  1378  
  1379  	imgStore := rh.getImageStore(name)
  1380  
  1381  	sessionID, ok := vars["session_id"]
  1382  	if !ok || sessionID == "" {
  1383  		response.WriteHeader(http.StatusNotFound)
  1384  
  1385  		return
  1386  	}
  1387  
  1388  	var clen int64
  1389  
  1390  	var err error
  1391  
  1392  	if request.Header.Get("Content-Length") == "" || request.Header.Get("Content-Range") == "" {
  1393  		// streamed blob upload
  1394  		clen, err = imgStore.PutBlobChunkStreamed(name, sessionID, request.Body)
  1395  	} else {
  1396  		// chunked blob upload
  1397  
  1398  		var contentLength int64
  1399  
  1400  		if contentLength, err = strconv.ParseInt(request.Header.Get("Content-Length"), 10, 64); err != nil {
  1401  			rh.c.Log.Warn().Str("actual", request.Header.Get("Content-Length")).Msg("invalid content length")
  1402  			response.WriteHeader(http.StatusBadRequest)
  1403  
  1404  			return
  1405  		}
  1406  
  1407  		var from, to int64
  1408  		if from, to, err = getContentRange(request); err != nil || (to-from)+1 != contentLength {
  1409  			response.WriteHeader(http.StatusRequestedRangeNotSatisfiable)
  1410  
  1411  			return
  1412  		}
  1413  
  1414  		clen, err = imgStore.PutBlobChunk(name, sessionID, from, to, request.Body)
  1415  	}
  1416  
  1417  	if err != nil { //nolint: dupl
  1418  		details := zerr.GetDetails(err)
  1419  		if errors.Is(err, zerr.ErrBadUploadRange) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain
  1420  			details["session_id"] = sessionID
  1421  			e := apiErr.NewError(apiErr.BLOB_UPLOAD_INVALID).AddDetail(details)
  1422  			zcommon.WriteJSON(response, http.StatusRequestedRangeNotSatisfiable, apiErr.NewErrorList(e))
  1423  		} else if errors.Is(err, zerr.ErrRepoNotFound) {
  1424  			details["name"] = name
  1425  			e := apiErr.NewError(apiErr.NAME_UNKNOWN).AddDetail(details)
  1426  			zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e))
  1427  		} else if errors.Is(err, zerr.ErrUploadNotFound) {
  1428  			details["session_id"] = sessionID
  1429  			e := apiErr.NewError(apiErr.BLOB_UPLOAD_UNKNOWN).AddDetail(details)
  1430  			zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e))
  1431  		} else {
  1432  			// could be io.ErrUnexpectedEOF, syscall.EMFILE (Err:0x18 too many opened files), etc
  1433  			rh.c.Log.Error().Err(err).Msg("unexpected error: removing .uploads/ files")
  1434  
  1435  			if err = imgStore.DeleteBlobUpload(name, sessionID); err != nil {
  1436  				rh.c.Log.Error().Err(err).Str("blobUpload", sessionID).Str("repository", name).
  1437  					Msg("couldn't remove blobUpload in repo")
  1438  			}
  1439  			response.WriteHeader(http.StatusInternalServerError)
  1440  		}
  1441  
  1442  		return
  1443  	}
  1444  
  1445  	response.Header().Set("Location", getBlobUploadSessionLocation(request.URL, sessionID))
  1446  	response.Header().Set("Range", fmt.Sprintf("0-%d", clen-1))
  1447  	response.Header().Set("Content-Length", "0")
  1448  	response.Header().Set(constants.BlobUploadUUID, sessionID)
  1449  	response.WriteHeader(http.StatusAccepted)
  1450  }
  1451  
  1452  // UpdateBlobUpload godoc
  1453  // @Summary Update image blob/layer upload
  1454  // @Description Update and finish an image's blob/layer upload given a digest
  1455  // @Accept  json
  1456  // @Produce json
  1457  // @Param   name         path    string     true        "repository name"
  1458  // @Param   session_id   path    string     true        "upload session_id"
  1459  // @Param   digest       query   string     true        "blob/layer digest"
  1460  // @Success 201 {string} string "created"
  1461  // @Header  202 {string} Location "/v2/{name}/blobs/uploads/{digest}"
  1462  // @Header  200 {object} constants.DistContentDigestKey
  1463  // @Failure 404 {string} string "not found"
  1464  // @Failure 500 {string} string "internal server error"
  1465  // @Router /v2/{name}/blobs/uploads/{session_id} [put].
  1466  func (rh *RouteHandler) UpdateBlobUpload(response http.ResponseWriter, request *http.Request) {
  1467  	vars := mux.Vars(request)
  1468  	name, ok := vars["name"]
  1469  
  1470  	if !ok || name == "" {
  1471  		response.WriteHeader(http.StatusNotFound)
  1472  
  1473  		return
  1474  	}
  1475  
  1476  	imgStore := rh.getImageStore(name)
  1477  
  1478  	sessionID, ok := vars["session_id"]
  1479  	if !ok || sessionID == "" {
  1480  		response.WriteHeader(http.StatusNotFound)
  1481  
  1482  		return
  1483  	}
  1484  
  1485  	digests, ok := request.URL.Query()["digest"]
  1486  	if !ok || len(digests) != 1 {
  1487  		response.WriteHeader(http.StatusBadRequest)
  1488  
  1489  		return
  1490  	}
  1491  
  1492  	digest, err := godigest.Parse(digests[0])
  1493  	if err != nil {
  1494  		response.WriteHeader(http.StatusBadRequest)
  1495  
  1496  		return
  1497  	}
  1498  
  1499  	rh.c.Log.Info().Int64("r.ContentLength", request.ContentLength).Msg("DEBUG")
  1500  
  1501  	contentPresent := true
  1502  
  1503  	contentLen, err := strconv.ParseInt(request.Header.Get("Content-Length"), 10, 64)
  1504  	if err != nil {
  1505  		contentPresent = false
  1506  	}
  1507  
  1508  	contentRangePresent := true
  1509  
  1510  	if request.Header.Get("Content-Range") == "" {
  1511  		contentRangePresent = false
  1512  	}
  1513  
  1514  	// we expect at least one of "Content-Length" or "Content-Range" to be
  1515  	// present
  1516  	if !contentPresent && !contentRangePresent {
  1517  		response.WriteHeader(http.StatusBadRequest)
  1518  
  1519  		return
  1520  	}
  1521  
  1522  	var from, to int64
  1523  
  1524  	if contentPresent {
  1525  		contentRange := request.Header.Get("Content-Range")
  1526  		if contentRange == "" { // monolithic upload
  1527  			from = 0
  1528  
  1529  			if contentLen == 0 {
  1530  				goto finish
  1531  			}
  1532  
  1533  			to = contentLen
  1534  		} else if from, to, err = getContentRange(request); err != nil { // finish chunked upload
  1535  			response.WriteHeader(http.StatusRequestedRangeNotSatisfiable)
  1536  
  1537  			return
  1538  		}
  1539  
  1540  		_, err = imgStore.PutBlobChunk(name, sessionID, from, to, request.Body)
  1541  		if err != nil { //nolint:dupl
  1542  			details := zerr.GetDetails(err)
  1543  			if errors.Is(err, zerr.ErrBadUploadRange) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain
  1544  				details["session_id"] = sessionID
  1545  				e := apiErr.NewError(apiErr.BLOB_UPLOAD_INVALID).AddDetail(details)
  1546  				zcommon.WriteJSON(response, http.StatusBadRequest, apiErr.NewErrorList(e))
  1547  			} else if errors.Is(err, zerr.ErrRepoNotFound) {
  1548  				details["name"] = name
  1549  				e := apiErr.NewError(apiErr.NAME_UNKNOWN).AddDetail(details)
  1550  				zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e))
  1551  			} else if errors.Is(err, zerr.ErrUploadNotFound) {
  1552  				details["session_id"] = sessionID
  1553  				e := apiErr.NewError(apiErr.BLOB_UPLOAD_UNKNOWN).AddDetail(details)
  1554  				zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e))
  1555  			} else {
  1556  				// could be io.ErrUnexpectedEOF, syscall.EMFILE (Err:0x18 too many opened files), etc
  1557  				rh.c.Log.Error().Err(err).Msg("unexpected error: removing .uploads/ files")
  1558  
  1559  				if err = imgStore.DeleteBlobUpload(name, sessionID); err != nil {
  1560  					rh.c.Log.Error().Err(err).Str("blobUpload", sessionID).Str("repository", name).
  1561  						Msg("couldn't remove blobUpload in repo")
  1562  				}
  1563  				response.WriteHeader(http.StatusInternalServerError)
  1564  			}
  1565  
  1566  			return
  1567  		}
  1568  	}
  1569  
  1570  finish:
  1571  	// blob chunks already transferred, just finish
  1572  	if err := imgStore.FinishBlobUpload(name, sessionID, request.Body, digest); err != nil {
  1573  		details := zerr.GetDetails(err)
  1574  		if errors.Is(err, zerr.ErrBadBlobDigest) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain
  1575  			details["digest"] = digest.String()
  1576  			e := apiErr.NewError(apiErr.DIGEST_INVALID).AddDetail(details)
  1577  			zcommon.WriteJSON(response, http.StatusBadRequest, apiErr.NewErrorList(e))
  1578  		} else if errors.Is(err, zerr.ErrBadUploadRange) {
  1579  			details["session_id"] = sessionID
  1580  			e := apiErr.NewError(apiErr.BLOB_UPLOAD_INVALID).AddDetail(details)
  1581  			zcommon.WriteJSON(response, http.StatusBadRequest, apiErr.NewErrorList(e))
  1582  		} else if errors.Is(err, zerr.ErrRepoNotFound) {
  1583  			details["name"] = name
  1584  			e := apiErr.NewError(apiErr.NAME_UNKNOWN).AddDetail(details)
  1585  			zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e))
  1586  		} else if errors.Is(err, zerr.ErrUploadNotFound) {
  1587  			details["session_id"] = sessionID
  1588  			e := apiErr.NewError(apiErr.BLOB_UPLOAD_UNKNOWN).AddDetail(details)
  1589  			zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e))
  1590  		} else {
  1591  			// could be io.ErrUnexpectedEOF, syscall.EMFILE (Err:0x18 too many opened files), etc
  1592  			rh.c.Log.Error().Err(err).Msg("unexpected error: removing .uploads/ files")
  1593  
  1594  			if err = imgStore.DeleteBlobUpload(name, sessionID); err != nil {
  1595  				rh.c.Log.Error().Err(err).Str("blobUpload", sessionID).Str("repository", name).
  1596  					Msg("couldn't remove blobUpload in repo")
  1597  			}
  1598  			response.WriteHeader(http.StatusInternalServerError)
  1599  		}
  1600  
  1601  		return
  1602  	}
  1603  
  1604  	response.Header().Set("Location", getBlobUploadLocation(request.URL, name, digest))
  1605  	response.Header().Set("Content-Length", "0")
  1606  	response.Header().Set(constants.DistContentDigestKey, digest.String())
  1607  	response.WriteHeader(http.StatusCreated)
  1608  }
  1609  
  1610  // DeleteBlobUpload godoc
  1611  // @Summary Delete image blob/layer
  1612  // @Description Delete an image's blob/layer given a digest
  1613  // @Accept  json
  1614  // @Produce json
  1615  // @Param   name         path    string     true        "repository name"
  1616  // @Param   session_id   path    string     true        "upload session_id"
  1617  // @Success 200 {string} string "ok"
  1618  // @Failure 404 {string} string "not found"
  1619  // @Failure 500 {string} string "internal server error"
  1620  // @Router /v2/{name}/blobs/uploads/{session_id} [delete].
  1621  func (rh *RouteHandler) DeleteBlobUpload(response http.ResponseWriter, request *http.Request) {
  1622  	vars := mux.Vars(request)
  1623  	name, ok := vars["name"]
  1624  
  1625  	if !ok || name == "" {
  1626  		response.WriteHeader(http.StatusNotFound)
  1627  
  1628  		return
  1629  	}
  1630  
  1631  	imgStore := rh.getImageStore(name)
  1632  
  1633  	sessionID, ok := vars["session_id"]
  1634  	if !ok || sessionID == "" {
  1635  		response.WriteHeader(http.StatusNotFound)
  1636  
  1637  		return
  1638  	}
  1639  
  1640  	if err := imgStore.DeleteBlobUpload(name, sessionID); err != nil {
  1641  		details := zerr.GetDetails(err)
  1642  		if errors.Is(err, zerr.ErrRepoNotFound) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain
  1643  			details["name"] = name
  1644  			e := apiErr.NewError(apiErr.NAME_UNKNOWN).AddDetail(details)
  1645  			zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e))
  1646  		} else if errors.Is(err, zerr.ErrUploadNotFound) {
  1647  			details["session_id"] = sessionID
  1648  			e := apiErr.NewError(apiErr.BLOB_UPLOAD_UNKNOWN).AddDetail(details)
  1649  			zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(e))
  1650  		} else {
  1651  			rh.c.Log.Error().Err(err).Msg("unexpected error")
  1652  			response.WriteHeader(http.StatusInternalServerError)
  1653  		}
  1654  
  1655  		return
  1656  	}
  1657  
  1658  	response.WriteHeader(http.StatusNoContent)
  1659  }
  1660  
  1661  type RepositoryList struct {
  1662  	Repositories []string `json:"repositories"`
  1663  }
  1664  
  1665  // ListRepositories godoc
  1666  // @Summary List image repositories
  1667  // @Description List all image repositories
  1668  // @Accept  json
  1669  // @Produce json
  1670  // @Success 200 {object} api.RepositoryList
  1671  // @Failure 500 {string} string "internal server error"
  1672  // @Router /v2/_catalog [get].
  1673  func (rh *RouteHandler) ListRepositories(response http.ResponseWriter, request *http.Request) {
  1674  	if request.Method == http.MethodOptions {
  1675  		return
  1676  	}
  1677  
  1678  	combineRepoList := make([]string, 0)
  1679  
  1680  	subStore := rh.c.StoreController.SubStore
  1681  
  1682  	for _, imgStore := range subStore {
  1683  		repos, err := imgStore.GetRepositories()
  1684  		if err != nil {
  1685  			response.WriteHeader(http.StatusInternalServerError)
  1686  
  1687  			return
  1688  		}
  1689  
  1690  		combineRepoList = append(combineRepoList, repos...)
  1691  	}
  1692  
  1693  	singleStore := rh.c.StoreController.DefaultStore
  1694  	if singleStore != nil {
  1695  		repos, err := singleStore.GetRepositories()
  1696  		if err != nil {
  1697  			response.WriteHeader(http.StatusInternalServerError)
  1698  
  1699  			return
  1700  		}
  1701  
  1702  		combineRepoList = append(combineRepoList, repos...)
  1703  	}
  1704  
  1705  	repos := make([]string, 0)
  1706  	// authz context
  1707  	userAc, err := reqCtx.UserAcFromContext(request.Context())
  1708  	if err != nil {
  1709  		response.WriteHeader(http.StatusInternalServerError)
  1710  
  1711  		return
  1712  	}
  1713  
  1714  	if userAc != nil {
  1715  		for _, r := range combineRepoList {
  1716  			if userAc.Can(constants.ReadPermission, r) {
  1717  				repos = append(repos, r)
  1718  			}
  1719  		}
  1720  	} else {
  1721  		repos = combineRepoList
  1722  	}
  1723  
  1724  	is := RepositoryList{Repositories: repos}
  1725  
  1726  	zcommon.WriteJSON(response, http.StatusOK, is)
  1727  }
  1728  
  1729  // ListExtensions godoc
  1730  // @Summary List Registry level extensions
  1731  // @Description List all extensions present on registry
  1732  // @Accept  json
  1733  // @Produce json
  1734  // @Success 200 {object}   api.ExtensionList
  1735  // @Router /v2/_oci/ext/discover [get].
  1736  func (rh *RouteHandler) ListExtensions(w http.ResponseWriter, r *http.Request) {
  1737  	if r.Method == http.MethodOptions {
  1738  		return
  1739  	}
  1740  
  1741  	extensionList := ext.GetExtensions(rh.c.Config)
  1742  
  1743  	zcommon.WriteJSON(w, http.StatusOK, extensionList)
  1744  }
  1745  
  1746  // The following routes are specific to zot and NOT part of the OCI dist-spec
  1747  
  1748  // Logout godoc
  1749  // @Summary Logout by removing current session
  1750  // @Description Logout by removing current session
  1751  // @Router  /zot/auth/logout [post]
  1752  // @Accept  json
  1753  // @Produce json
  1754  // @Success 200 {string} string "ok".
  1755  // @Failure 500 {string} string "internal server error".
  1756  func (rh *RouteHandler) Logout(response http.ResponseWriter, request *http.Request) {
  1757  	if request.Method == http.MethodOptions {
  1758  		return
  1759  	}
  1760  
  1761  	session, _ := rh.c.CookieStore.Get(request, "session")
  1762  	session.Options.MaxAge = -1
  1763  
  1764  	err := session.Save(request, response)
  1765  	if err != nil {
  1766  		response.WriteHeader(http.StatusInternalServerError)
  1767  
  1768  		return
  1769  	}
  1770  
  1771  	response.WriteHeader(http.StatusOK)
  1772  }
  1773  
  1774  // github Oauth2 CodeExchange callback.
  1775  func (rh *RouteHandler) GithubCodeExchangeCallback() rp.CodeExchangeCallback {
  1776  	return func(w http.ResponseWriter, r *http.Request,
  1777  		tokens *oidc.Tokens, state string, relyingParty rp.RelyingParty,
  1778  	) {
  1779  		ctx := r.Context()
  1780  
  1781  		client := github.NewClient(relyingParty.OAuthConfig().Client(ctx, tokens.Token))
  1782  
  1783  		email, groups, err := GetGithubUserInfo(ctx, client, rh.c.Log)
  1784  		if email == "" || err != nil {
  1785  			w.WriteHeader(http.StatusUnauthorized)
  1786  
  1787  			return
  1788  		}
  1789  
  1790  		callbackUI, err := OAuth2Callback(rh.c, w, r, state, email, groups) //nolint: contextcheck
  1791  		if err != nil {
  1792  			if errors.Is(err, zerr.ErrInvalidStateCookie) {
  1793  				w.WriteHeader(http.StatusUnauthorized)
  1794  			}
  1795  
  1796  			w.WriteHeader(http.StatusInternalServerError)
  1797  		}
  1798  
  1799  		if callbackUI != "" {
  1800  			http.Redirect(w, r, callbackUI, http.StatusFound)
  1801  
  1802  			return
  1803  		}
  1804  
  1805  		w.WriteHeader(http.StatusCreated)
  1806  	}
  1807  }
  1808  
  1809  // Openid CodeExchange callback.
  1810  func (rh *RouteHandler) OpenIDCodeExchangeCallback() rp.CodeExchangeUserinfoCallback {
  1811  	return func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens, state string,
  1812  		relyingParty rp.RelyingParty, info oidc.UserInfo,
  1813  	) {
  1814  		email := info.GetEmail()
  1815  		if email == "" {
  1816  			rh.c.Log.Error().Msg("couldn't set user record for empty email value")
  1817  			w.WriteHeader(http.StatusUnauthorized)
  1818  
  1819  			return
  1820  		}
  1821  
  1822  		var groups []string
  1823  
  1824  		val, ok := info.GetClaim("groups").([]interface{})
  1825  		if !ok {
  1826  			rh.c.Log.Info().Msgf("couldn't find any 'groups' claim for user %s", email)
  1827  		}
  1828  
  1829  		for _, group := range val {
  1830  			groups = append(groups, fmt.Sprint(group))
  1831  		}
  1832  
  1833  		callbackUI, err := OAuth2Callback(rh.c, w, r, state, email, groups)
  1834  		if err != nil {
  1835  			if errors.Is(err, zerr.ErrInvalidStateCookie) {
  1836  				w.WriteHeader(http.StatusUnauthorized)
  1837  			}
  1838  
  1839  			w.WriteHeader(http.StatusInternalServerError)
  1840  		}
  1841  
  1842  		if callbackUI != "" {
  1843  			http.Redirect(w, r, callbackUI, http.StatusFound)
  1844  
  1845  			return
  1846  		}
  1847  
  1848  		w.WriteHeader(http.StatusCreated)
  1849  	}
  1850  }
  1851  
  1852  // helper routines
  1853  
  1854  func getContentRange(r *http.Request) (int64 /* from */, int64 /* to */, error) {
  1855  	contentRange := r.Header.Get("Content-Range")
  1856  	tokens := strings.Split(contentRange, "-")
  1857  
  1858  	rangeStart, err := strconv.ParseInt(tokens[0], 10, 64)
  1859  	if err != nil {
  1860  		return -1, -1, zerr.ErrBadUploadRange
  1861  	}
  1862  
  1863  	rangeEnd, err := strconv.ParseInt(tokens[1], 10, 64)
  1864  	if err != nil {
  1865  		return -1, -1, zerr.ErrBadUploadRange
  1866  	}
  1867  
  1868  	if rangeStart > rangeEnd {
  1869  		return -1, -1, zerr.ErrBadUploadRange
  1870  	}
  1871  
  1872  	return rangeStart, rangeEnd, nil
  1873  }
  1874  
  1875  func WriteDataFromReader(response http.ResponseWriter, status int, length int64, mediaType string,
  1876  	reader io.Reader, logger log.Logger,
  1877  ) {
  1878  	response.Header().Set("Content-Type", mediaType)
  1879  	response.Header().Set("Content-Length", strconv.FormatInt(length, 10))
  1880  	response.WriteHeader(status)
  1881  
  1882  	const maxSize = 10 * 1024 * 1024
  1883  
  1884  	for {
  1885  		_, err := io.CopyN(response, reader, maxSize)
  1886  		if errors.Is(err, io.EOF) {
  1887  			break
  1888  		} else if err != nil {
  1889  			// other kinds of intermittent errors can occur, e.g, io.ErrShortWrite
  1890  			logger.Error().Err(err).Msg("copying data into http response")
  1891  
  1892  			return
  1893  		}
  1894  	}
  1895  }
  1896  
  1897  // will return image storage corresponding to subpath provided in config.
  1898  func (rh *RouteHandler) getImageStore(name string) storageTypes.ImageStore {
  1899  	return rh.c.StoreController.GetImageStore(name)
  1900  }
  1901  
  1902  // will sync on demand if an image is not found, in case sync extensions is enabled.
  1903  func getImageManifest(ctx context.Context, routeHandler *RouteHandler, imgStore storageTypes.ImageStore, name,
  1904  	reference string,
  1905  ) ([]byte, godigest.Digest, string, error) {
  1906  	syncEnabled := isSyncOnDemandEnabled(*routeHandler.c)
  1907  
  1908  	_, digestErr := godigest.Parse(reference)
  1909  	if digestErr == nil {
  1910  		// if it's a digest then return local cached image, if not found and sync enabled, then try to sync
  1911  		content, digest, mediaType, err := imgStore.GetImageManifest(name, reference)
  1912  		if err == nil || !syncEnabled {
  1913  			return content, digest, mediaType, err
  1914  		}
  1915  	}
  1916  
  1917  	if syncEnabled {
  1918  		routeHandler.c.Log.Info().Str("repository", name).Str("reference", reference).
  1919  			Msg("trying to get updated image by syncing on demand")
  1920  
  1921  		if errSync := routeHandler.c.SyncOnDemand.SyncImage(ctx, name, reference); errSync != nil {
  1922  			routeHandler.c.Log.Err(errSync).Str("repository", name).Str("reference", reference).
  1923  				Msg("error encounter while syncing image")
  1924  		}
  1925  	}
  1926  
  1927  	return imgStore.GetImageManifest(name, reference)
  1928  }
  1929  
  1930  // will sync referrers on demand if they are not found, in case sync extensions is enabled.
  1931  func getOrasReferrers(ctx context.Context, routeHandler *RouteHandler,
  1932  	imgStore storageTypes.ImageStore, name string, digest godigest.Digest,
  1933  	artifactType string,
  1934  ) ([]artifactspec.Descriptor, error) {
  1935  	refs, err := imgStore.GetOrasReferrers(name, digest, artifactType)
  1936  	if err != nil {
  1937  		if isSyncOnDemandEnabled(*routeHandler.c) {
  1938  			routeHandler.c.Log.Info().Str("repository", name).Str("reference", digest.String()).
  1939  				Msg("artifact not found, trying to get artifact by syncing on demand")
  1940  
  1941  			if errSync := routeHandler.c.SyncOnDemand.SyncReference(ctx, name, digest.String(),
  1942  				syncConstants.Oras); errSync != nil {
  1943  				routeHandler.c.Log.Error().Err(err).Str("name", name).Str("digest", digest.String()).
  1944  					Msg("unable to get references")
  1945  			}
  1946  
  1947  			refs, err = imgStore.GetOrasReferrers(name, digest, artifactType)
  1948  		}
  1949  	}
  1950  
  1951  	return refs, err
  1952  }
  1953  
  1954  type ReferenceList struct {
  1955  	References []artifactspec.Descriptor `json:"references"`
  1956  }
  1957  
  1958  // GetOrasReferrers godoc
  1959  // @Summary Get references for an image
  1960  // @Description Get references for an image given a digest and artifact type
  1961  // @Accept  json
  1962  // @Produce json
  1963  // @Param   name            path    string     true        "repository name"
  1964  // @Param   digest          path    string     true        "image digest"
  1965  // @Param   artifactType    query   string     true        "artifact type"
  1966  // @Success 200 {string} string "ok"
  1967  // @Failure 404 {string} string "not found"
  1968  // @Failure 500 {string} string "internal server error"
  1969  // @Router /oras/artifacts/v1/{name}/manifests/{digest}/referrers [get].
  1970  func (rh *RouteHandler) GetOrasReferrers(response http.ResponseWriter, request *http.Request) {
  1971  	vars := mux.Vars(request)
  1972  	name, ok := vars["name"]
  1973  
  1974  	if !ok || name == "" {
  1975  		response.WriteHeader(http.StatusNotFound)
  1976  
  1977  		return
  1978  	}
  1979  
  1980  	digestStr, ok := vars["digest"]
  1981  	digest, err := godigest.Parse(digestStr)
  1982  
  1983  	if !ok || digestStr == "" || err != nil {
  1984  		response.WriteHeader(http.StatusBadRequest)
  1985  
  1986  		return
  1987  	}
  1988  
  1989  	// filter by artifact type
  1990  	artifactType := ""
  1991  
  1992  	artifactTypes, ok := request.URL.Query()["artifactType"]
  1993  	if ok {
  1994  		if len(artifactTypes) != 1 {
  1995  			rh.c.Log.Error().Msg("invalid artifact types")
  1996  			response.WriteHeader(http.StatusBadRequest)
  1997  
  1998  			return
  1999  		}
  2000  
  2001  		artifactType = artifactTypes[0]
  2002  	}
  2003  
  2004  	imgStore := rh.getImageStore(name)
  2005  
  2006  	rh.c.Log.Info().Str("digest", digest.String()).Str("artifactType", artifactType).Msg("getting manifest")
  2007  
  2008  	refs, err := getOrasReferrers(request.Context(), rh, imgStore, name, digest, artifactType) //nolint:contextcheck
  2009  	if err != nil {
  2010  		if errors.Is(err, zerr.ErrManifestNotFound) || errors.Is(err, zerr.ErrRepoNotFound) {
  2011  			rh.c.Log.Error().Err(err).Str("name", name).Str("digest", digest.String()).Msg("manifest not found")
  2012  			response.WriteHeader(http.StatusNotFound)
  2013  		} else {
  2014  			rh.c.Log.Error().Err(err).Str("name", name).Str("digest", digest.String()).Msg("unable to get references")
  2015  			response.WriteHeader(http.StatusInternalServerError)
  2016  		}
  2017  
  2018  		return
  2019  	}
  2020  
  2021  	rs := ReferenceList{References: refs}
  2022  
  2023  	zcommon.WriteJSON(response, http.StatusOK, rs)
  2024  }
  2025  
  2026  type APIKeyPayload struct { //nolint:revive
  2027  	Label          string   `json:"label"`
  2028  	Scopes         []string `json:"scopes"`
  2029  	ExpirationDate string   `json:"expirationDate"`
  2030  }
  2031  
  2032  // GetAPIKeys godoc
  2033  // @Summary Get list of API keys for the current user
  2034  // @Description Get list of all API keys for a logged in user
  2035  // @Accept  json
  2036  // @Produce json
  2037  // @Success 200 {string} string "ok"
  2038  // @Failure 401 {string} string "unauthorized"
  2039  // @Failure 500 {string} string "internal server error"
  2040  // @Router  /zot/auth/apikey  [get].
  2041  func (rh *RouteHandler) GetAPIKeys(resp http.ResponseWriter, req *http.Request) {
  2042  	apiKeys, err := rh.c.MetaDB.GetUserAPIKeys(req.Context())
  2043  	if err != nil {
  2044  		rh.c.Log.Error().Err(err).Msg("error getting list of API keys for user")
  2045  		resp.WriteHeader(http.StatusInternalServerError)
  2046  
  2047  		return
  2048  	}
  2049  
  2050  	apiKeyResponse := struct {
  2051  		APIKeys []mTypes.APIKeyDetails `json:"apiKeys"`
  2052  	}{
  2053  		APIKeys: apiKeys,
  2054  	}
  2055  
  2056  	json := jsoniter.ConfigCompatibleWithStandardLibrary
  2057  
  2058  	data, err := json.Marshal(apiKeyResponse)
  2059  	if err != nil {
  2060  		rh.c.Log.Error().Err(err).Msg("unable to marshal api key response")
  2061  
  2062  		resp.WriteHeader(http.StatusInternalServerError)
  2063  
  2064  		return
  2065  	}
  2066  
  2067  	resp.Header().Set("Content-Type", constants.DefaultMediaType)
  2068  	resp.WriteHeader(http.StatusOK)
  2069  	_, _ = resp.Write(data)
  2070  }
  2071  
  2072  // CreateAPIKey godoc
  2073  // @Summary Create an API key for the current user
  2074  // @Description Can create an api key for a logged in user, based on the provided label and scopes.
  2075  // @Accept  json
  2076  // @Produce json
  2077  // @Param   id  body  APIKeyPayload  true  "api token id (UUID)"
  2078  // @Success 201 {string} string "created"
  2079  // @Failure 400 {string} string "bad request"
  2080  // @Failure 401 {string} string "unauthorized"
  2081  // @Failure 500 {string} string "internal server error"
  2082  // @Router  /zot/auth/apikey  [post].
  2083  func (rh *RouteHandler) CreateAPIKey(resp http.ResponseWriter, req *http.Request) {
  2084  	var payload APIKeyPayload
  2085  
  2086  	body, err := io.ReadAll(req.Body)
  2087  	if err != nil {
  2088  		rh.c.Log.Error().Msg("unable to read request body")
  2089  		resp.WriteHeader(http.StatusInternalServerError)
  2090  
  2091  		return
  2092  	}
  2093  
  2094  	err = json.Unmarshal(body, &payload)
  2095  	if err != nil {
  2096  		resp.WriteHeader(http.StatusBadRequest)
  2097  
  2098  		return
  2099  	}
  2100  
  2101  	apiKey, apiKeyID, err := GenerateAPIKey(guuid.DefaultGenerator, rh.c.Log)
  2102  	if err != nil {
  2103  		resp.WriteHeader(http.StatusInternalServerError)
  2104  
  2105  		return
  2106  	}
  2107  
  2108  	hashedAPIKey := hashUUID(apiKey)
  2109  
  2110  	createdAt := time.Now()
  2111  
  2112  	// won't expire if no value provided
  2113  	expirationDate := time.Time{}
  2114  
  2115  	if payload.ExpirationDate != "" {
  2116  		//nolint: gosmopolitan
  2117  		expirationDate, err = time.ParseInLocation(constants.APIKeyTimeFormat, payload.ExpirationDate, time.Local)
  2118  		if err != nil {
  2119  			resp.WriteHeader(http.StatusBadRequest)
  2120  
  2121  			return
  2122  		}
  2123  
  2124  		if createdAt.After(expirationDate) {
  2125  			resp.WriteHeader(http.StatusBadRequest)
  2126  
  2127  			return
  2128  		}
  2129  	}
  2130  
  2131  	apiKeyDetails := &mTypes.APIKeyDetails{
  2132  		CreatedAt:      createdAt,
  2133  		ExpirationDate: expirationDate,
  2134  		IsExpired:      false,
  2135  		CreatorUA:      req.UserAgent(),
  2136  		GeneratedBy:    "manual",
  2137  		Label:          payload.Label,
  2138  		Scopes:         payload.Scopes,
  2139  		UUID:           apiKeyID,
  2140  	}
  2141  
  2142  	err = rh.c.MetaDB.AddUserAPIKey(req.Context(), hashedAPIKey, apiKeyDetails)
  2143  	if err != nil {
  2144  		rh.c.Log.Error().Err(err).Msg("error storing API key")
  2145  		resp.WriteHeader(http.StatusInternalServerError)
  2146  
  2147  		return
  2148  	}
  2149  
  2150  	apiKeyResponse := struct {
  2151  		mTypes.APIKeyDetails
  2152  		APIKey string `json:"apiKey"`
  2153  	}{
  2154  		APIKey:        fmt.Sprintf("%s%s", constants.APIKeysPrefix, apiKey),
  2155  		APIKeyDetails: *apiKeyDetails,
  2156  	}
  2157  
  2158  	json := jsoniter.ConfigCompatibleWithStandardLibrary
  2159  
  2160  	data, err := json.Marshal(apiKeyResponse)
  2161  	if err != nil {
  2162  		rh.c.Log.Error().Err(err).Msg("unable to marshal api key response")
  2163  
  2164  		resp.WriteHeader(http.StatusInternalServerError)
  2165  
  2166  		return
  2167  	}
  2168  
  2169  	resp.Header().Set("Content-Type", constants.DefaultMediaType)
  2170  	resp.WriteHeader(http.StatusCreated)
  2171  	_, _ = resp.Write(data)
  2172  }
  2173  
  2174  // RevokeAPIKey godoc
  2175  // @Summary Revokes one current user API key
  2176  // @Description Revokes one current user API key based on given key ID
  2177  // @Accept  json
  2178  // @Produce json
  2179  // @Param   id  query  string   true   "api token id (UUID)"
  2180  // @Success 200 {string} string "ok"
  2181  // @Failure 500 {string} string "internal server error"
  2182  // @Failure 401 {string} string "unauthorized"
  2183  // @Failure 400 {string} string "bad request"
  2184  // @Router  /zot/auth/apikey [delete].
  2185  func (rh *RouteHandler) RevokeAPIKey(resp http.ResponseWriter, req *http.Request) {
  2186  	ids, ok := req.URL.Query()["id"]
  2187  	if !ok || len(ids) != 1 {
  2188  		resp.WriteHeader(http.StatusBadRequest)
  2189  
  2190  		return
  2191  	}
  2192  
  2193  	keyID := ids[0]
  2194  
  2195  	err := rh.c.MetaDB.DeleteUserAPIKey(req.Context(), keyID)
  2196  	if err != nil {
  2197  		rh.c.Log.Error().Err(err).Str("keyID", keyID).Msg("error deleting API key")
  2198  		resp.WriteHeader(http.StatusInternalServerError)
  2199  
  2200  		return
  2201  	}
  2202  
  2203  	resp.WriteHeader(http.StatusOK)
  2204  }
  2205  
  2206  // GetBlobUploadSessionLocation returns actual blob location to start/resume uploading blobs.
  2207  // e.g. /v2/<name>/blobs/uploads/<session-id>.
  2208  func getBlobUploadSessionLocation(url *url.URL, sessionID string) string {
  2209  	url.RawQuery = ""
  2210  
  2211  	if !strings.Contains(url.Path, sessionID) {
  2212  		url.Path = path.Join(url.Path, sessionID)
  2213  	}
  2214  
  2215  	return url.String()
  2216  }
  2217  
  2218  // GetBlobUploadLocation returns actual blob location on registry
  2219  // e.g /v2/<name>/blobs/<digest>.
  2220  func getBlobUploadLocation(url *url.URL, name string, digest godigest.Digest) string {
  2221  	url.RawQuery = ""
  2222  
  2223  	// we are relying on request URL to set location and
  2224  	// if request URL contains uploads either we are resuming blob upload or starting a new blob upload.
  2225  	// getBlobUploadLocation will be called only when blob upload is completed and
  2226  	// location should be set as blob url <v2/<name>/blobs/<digest>>.
  2227  	if strings.Contains(url.Path, "uploads") {
  2228  		url.Path = path.Join(constants.RoutePrefix, name, constants.Blobs, digest.String())
  2229  	}
  2230  
  2231  	return url.String()
  2232  }
  2233  
  2234  func isSyncOnDemandEnabled(ctlr Controller) bool {
  2235  	if ctlr.Config.IsSyncEnabled() &&
  2236  		fmt.Sprintf("%v", ctlr.SyncOnDemand) != fmt.Sprintf("%v", nil) {
  2237  		return true
  2238  	}
  2239  
  2240  	return false
  2241  }