From ea7dbf9e5cc8341ed67022fadf83622c18b146f0 Mon Sep 17 00:00:00 2001 From: alexstan12 <57507734+alexstan12@users.noreply.github.com> Date: Thu, 22 Jun 2023 14:29:45 +0300 Subject: [PATCH] refactor: move helper functions under common, in usage specific named files (#1540) Signed-off-by: Alex Stan --- pkg/api/authn.go | 10 +- pkg/api/authz.go | 24 +-- pkg/api/config/config.go | 14 ++ pkg/api/controller.go | 2 +- pkg/api/controller_test.go | 17 +- pkg/api/{ => errors}/errors.go | 2 +- pkg/api/{ => errors}/errors_test.go | 6 +- pkg/api/routes.go | 247 ++++++++++++-------------- pkg/common/common.go | 203 --------------------- pkg/common/common_test.go | 73 -------- pkg/common/http_client.go | 168 ++++++++++++++++++ pkg/common/http_client_test.go | 82 +++++++++ pkg/common/http_server.go | 68 +++++++ pkg/common/{utils.go => oci.go} | 8 + pkg/common/oci_test.go | 17 ++ pkg/extensions/search/cve/cve_test.go | 3 +- 16 files changed, 498 insertions(+), 446 deletions(-) rename pkg/api/{ => errors}/errors.go (99%) rename pkg/api/{ => errors}/errors_test.go (58%) create mode 100644 pkg/common/http_client.go create mode 100644 pkg/common/http_client_test.go create mode 100644 pkg/common/http_server.go rename pkg/common/{utils.go => oci.go} (88%) create mode 100644 pkg/common/oci_test.go diff --git a/pkg/api/authn.go b/pkg/api/authn.go index f705c417..af61cd65 100644 --- a/pkg/api/authn.go +++ b/pkg/api/authn.go @@ -19,6 +19,8 @@ import ( "zotregistry.io/zot/errors" "zotregistry.io/zot/pkg/api/config" "zotregistry.io/zot/pkg/api/constants" + apiErr "zotregistry.io/zot/pkg/api/errors" + "zotregistry.io/zot/pkg/common" localCtx "zotregistry.io/zot/pkg/requestcontext" ) @@ -76,7 +78,7 @@ func bearerAuthHandler(ctlr *Controller) mux.MiddlewareFunc { if err != nil { ctlr.Log.Error().Err(err).Msg("issue parsing Authorization header") response.Header().Set("Content-Type", "application/json") - WriteJSON(response, http.StatusInternalServerError, NewErrorList(NewError(UNSUPPORTED))) + common.WriteJSON(response, http.StatusInternalServerError, apiErr.NewErrorList(apiErr.NewError(apiErr.UNSUPPORTED))) return } @@ -205,7 +207,7 @@ func basicAuthHandler(ctlr *Controller) mux.MiddlewareFunc { isMgmtRequested := request.RequestURI == constants.FullMgmtPrefix if request.Header.Get("Authorization") == "" { - if anonymousPolicyExists(ctlr.Config.HTTP.AccessControl) || isMgmtRequested { + if ctlr.Config.HTTP.AccessControl.AnonymousPolicyExists() || isMgmtRequested { // Process request ctx := getReqContextWithAuthorization("", []string{}, request) next.ServeHTTP(response, request.WithContext(ctx)) //nolint:contextcheck @@ -225,7 +227,7 @@ func basicAuthHandler(ctlr *Controller) mux.MiddlewareFunc { // some client tools might send Authorization: Basic Og== (decoded into ":") // empty username and password if username == "" && passphrase == "" { - if anonymousPolicyExists(ctlr.Config.HTTP.AccessControl) || isMgmtRequested { + if ctlr.Config.HTTP.AccessControl.AnonymousPolicyExists() || isMgmtRequested { // Process request ctx := getReqContextWithAuthorization("", []string{}, request) next.ServeHTTP(response, request.WithContext(ctx)) //nolint:contextcheck @@ -316,7 +318,7 @@ func authFail(w http.ResponseWriter, realm string, delay int) { time.Sleep(time.Duration(delay) * time.Second) w.Header().Set("WWW-Authenticate", realm) w.Header().Set("Content-Type", "application/json") - WriteJSON(w, http.StatusUnauthorized, NewErrorList(NewError(UNAUTHORIZED))) + common.WriteJSON(w, http.StatusUnauthorized, apiErr.NewErrorList(apiErr.NewError(apiErr.UNAUTHORIZED))) } func getUsernamePasswordBasicAuth(request *http.Request) (string, string, error) { diff --git a/pkg/api/authz.go b/pkg/api/authz.go index 77b4d956..47eed818 100644 --- a/pkg/api/authz.go +++ b/pkg/api/authz.go @@ -5,7 +5,6 @@ import ( "fmt" "net/http" "strings" - "time" glob "github.com/bmatcuk/doublestar/v4" "github.com/gorilla/mux" @@ -323,7 +322,7 @@ func AuthzHandler(ctlr *Controller) mux.MiddlewareFunc { can := acCtrlr.can(ctx, identity, action, resource) //nolint:contextcheck if !can { - authzFail(response, ctlr.Config.HTTP.Realm, ctlr.Config.HTTP.Auth.FailDelay) + common.AuthzFail(response, ctlr.Config.HTTP.Realm, ctlr.Config.HTTP.Auth.FailDelay) } else { next.ServeHTTP(response, request.WithContext(ctx)) //nolint:contextcheck } @@ -335,24 +334,3 @@ func isExtensionURI(requestURI string) bool { return strings.Contains(requestURI, constants.ExtPrefix) || requestURI == fmt.Sprintf("%s%s", constants.RoutePrefix, constants.ExtCatalogPrefix) } - -func authzFail(w http.ResponseWriter, realm string, delay int) { - time.Sleep(time.Duration(delay) * time.Second) - w.Header().Set("WWW-Authenticate", realm) - w.Header().Set("Content-Type", "application/json") - WriteJSON(w, http.StatusForbidden, NewErrorList(NewError(DENIED))) -} - -func anonymousPolicyExists(config *config.AccessControlConfig) bool { - if config == nil { - return false - } - - for _, repository := range config.Repositories { - if len(repository.AnonymousPolicy) > 0 { - return true - } - } - - return false -} diff --git a/pkg/api/config/config.go b/pkg/api/config/config.go index 7edb5727..38b0d679 100644 --- a/pkg/api/config/config.go +++ b/pkg/api/config/config.go @@ -106,6 +106,20 @@ type AccessControlConfig struct { Groups Groups } +func (config *AccessControlConfig) AnonymousPolicyExists() bool { + if config == nil { + return false + } + + for _, repository := range config.Repositories { + if len(repository.AnonymousPolicy) > 0 { + return true + } + } + + return false +} + type ( Repositories map[string]PolicyGroup Groups map[string]Group diff --git a/pkg/api/controller.go b/pkg/api/controller.go index 107cd9c8..4340871f 100644 --- a/pkg/api/controller.go +++ b/pkg/api/controller.go @@ -178,7 +178,7 @@ func (c *Controller) Run(reloadCtx context.Context) error { if c.Config.HTTP.TLS.CACert != "" { clientAuth := tls.VerifyClientCertIfGiven if (c.Config.HTTP.Auth == nil || c.Config.HTTP.Auth.HTPasswd.Path == "") && - !anonymousPolicyExists(c.Config.HTTP.AccessControl) { + !c.Config.HTTP.AccessControl.AnonymousPolicyExists() { clientAuth = tls.RequireAndVerifyClientCert } diff --git a/pkg/api/controller_test.go b/pkg/api/controller_test.go index c352291d..35135e68 100644 --- a/pkg/api/controller_test.go +++ b/pkg/api/controller_test.go @@ -46,6 +46,7 @@ import ( "zotregistry.io/zot/pkg/api" "zotregistry.io/zot/pkg/api/config" "zotregistry.io/zot/pkg/api/constants" + apiErr "zotregistry.io/zot/pkg/api/errors" "zotregistry.io/zot/pkg/common" extconf "zotregistry.io/zot/pkg/extensions/config" "zotregistry.io/zot/pkg/log" @@ -798,7 +799,7 @@ func TestBasicAuth(t *testing.T) { So(err, ShouldBeNil) So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) - var e api.Error + var e apiErr.Error err = json.Unmarshal(resp.Body(), &e) So(err, ShouldBeNil) @@ -1102,7 +1103,7 @@ func TestMultipleInstance(t *testing.T) { So(err, ShouldBeNil) So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) - var e api.Error + var e apiErr.Error err = json.Unmarshal(resp.Body(), &e) So(err, ShouldBeNil) @@ -1165,7 +1166,7 @@ func TestMultipleInstance(t *testing.T) { So(err, ShouldBeNil) So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) - var e api.Error + var e apiErr.Error err = json.Unmarshal(resp.Body(), &e) So(err, ShouldBeNil) @@ -1224,7 +1225,7 @@ func TestTLSWithBasicAuth(t *testing.T) { So(err, ShouldBeNil) So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) - var e api.Error + var e apiErr.Error err = json.Unmarshal(resp.Body(), &e) So(err, ShouldBeNil) @@ -1887,7 +1888,7 @@ func TestBasicAuthWithLDAP(t *testing.T) { So(err, ShouldBeNil) So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) - var e api.Error + var e apiErr.Error err = json.Unmarshal(resp.Body(), &e) So(err, ShouldBeNil) @@ -2414,7 +2415,7 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { So(err, ShouldBeNil) So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, http.StatusOK) - var apiErr api.Error + var apiErr apiErr.Error err = json.Unmarshal(resp.Body(), &apiErr) So(err, ShouldBeNil) @@ -2958,7 +2959,7 @@ func TestGetUsername(t *testing.T) { So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) - var e api.Error + var e apiErr.Error err = json.Unmarshal(resp.Body(), &e) So(err, ShouldBeNil) }) @@ -2999,7 +3000,7 @@ func TestAuthorizationWithOnlyAnonymousPolicy(t *testing.T) { So(err, ShouldBeNil) So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, http.StatusOK) - var e api.Error + var e apiErr.Error err = json.Unmarshal(resp.Body(), &e) So(err, ShouldBeNil) diff --git a/pkg/api/errors.go b/pkg/api/errors/errors.go similarity index 99% rename from pkg/api/errors.go rename to pkg/api/errors/errors.go index 0ef63c7e..d218e71d 100644 --- a/pkg/api/errors.go +++ b/pkg/api/errors/errors.go @@ -1,4 +1,4 @@ -package api +package errors import ( "zotregistry.io/zot/errors" diff --git a/pkg/api/errors_test.go b/pkg/api/errors/errors_test.go similarity index 58% rename from pkg/api/errors_test.go rename to pkg/api/errors/errors_test.go index 641c3dbc..0a4744e3 100644 --- a/pkg/api/errors_test.go +++ b/pkg/api/errors/errors_test.go @@ -1,15 +1,15 @@ -package api_test +package errors_test import ( "testing" . "github.com/smartystreets/goconvey/convey" - "zotregistry.io/zot/pkg/api" + apiErr "zotregistry.io/zot/pkg/api/errors" ) func TestUnknownCodeError(t *testing.T) { Convey("Retrieve a new error with unknown code", t, func() { - So(func() { _ = api.NewError(123456789, nil) }, ShouldPanic) + So(func() { _ = apiErr.NewError(123456789, nil) }, ShouldPanic) }) } diff --git a/pkg/api/routes.go b/pkg/api/routes.go index 1d3cba0b..6510c6f5 100644 --- a/pkg/api/routes.go +++ b/pkg/api/routes.go @@ -21,7 +21,6 @@ import ( "strings" "github.com/gorilla/mux" - jsoniter "github.com/json-iterator/go" "github.com/opencontainers/distribution-spec/specs-go/v1/extensions" godigest "github.com/opencontainers/go-digest" ispec "github.com/opencontainers/image-spec/specs-go/v1" @@ -29,6 +28,7 @@ import ( zerr "zotregistry.io/zot/errors" "zotregistry.io/zot/pkg/api/constants" + apiErr "zotregistry.io/zot/pkg/api/errors" zcommon "zotregistry.io/zot/pkg/common" gqlPlayground "zotregistry.io/zot/pkg/debug/gqlplayground" debug "zotregistry.io/zot/pkg/debug/swagger" @@ -197,7 +197,7 @@ func (rh *RouteHandler) CheckVersionSupport(response http.ResponseWriter, reques } } - WriteData(response, http.StatusOK, "application/json", []byte{}) + zcommon.WriteData(response, http.StatusOK, "application/json", []byte{}) } type ImageTags struct { @@ -278,7 +278,8 @@ func (rh *RouteHandler) ListTags(response http.ResponseWriter, request *http.Req tags, err := imgStore.GetImageTags(name) if err != nil { - WriteJSON(response, http.StatusNotFound, NewErrorList(NewError(NAME_UNKNOWN, map[string]string{"name": name}))) + zcommon.WriteJSON(response, http.StatusNotFound, + apiErr.NewErrorList(apiErr.NewError(apiErr.NAME_UNKNOWN, map[string]string{"name": name}))) return } @@ -312,7 +313,7 @@ func (rh *RouteHandler) ListTags(response http.ResponseWriter, request *http.Req if numTags >= len(tags)-i { pTags.Tags = tags[i+1:] - WriteJSON(response, http.StatusOK, pTags) + zcommon.WriteJSON(response, http.StatusOK, pTags) return } @@ -327,12 +328,12 @@ func (rh *RouteHandler) ListTags(response http.ResponseWriter, request *http.Req } response.Header().Set("Link", fmt.Sprintf("/v2/%s/tags/list?n=%d&last=%s; rel=\"next\"", name, numTags, last)) - WriteJSON(response, http.StatusOK, pTags) + zcommon.WriteJSON(response, http.StatusOK, pTags) return } - WriteJSON(response, http.StatusOK, ImageTags{Name: name, Tags: tags}) + zcommon.WriteJSON(response, http.StatusOK, ImageTags{Name: name, Tags: tags}) } // CheckManifest godoc @@ -368,9 +369,9 @@ func (rh *RouteHandler) CheckManifest(response http.ResponseWriter, request *htt reference, ok := vars["reference"] if !ok || reference == "" { - WriteJSON(response, + zcommon.WriteJSON(response, http.StatusNotFound, - NewErrorList(NewError(MANIFEST_INVALID, map[string]string{"reference": reference}))) + apiErr.NewErrorList(apiErr.NewError(apiErr.MANIFEST_INVALID, map[string]string{"reference": reference}))) return } @@ -378,15 +379,15 @@ func (rh *RouteHandler) CheckManifest(response http.ResponseWriter, request *htt content, digest, mediaType, err := getImageManifest(rh, imgStore, name, reference) //nolint:contextcheck if err != nil { if errors.Is(err, zerr.ErrRepoNotFound) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain - WriteJSON(response, http.StatusNotFound, - NewErrorList(NewError(NAME_UNKNOWN, map[string]string{"reference": reference}))) + zcommon.WriteJSON(response, http.StatusNotFound, + apiErr.NewErrorList(apiErr.NewError(apiErr.NAME_UNKNOWN, map[string]string{"reference": reference}))) } else if errors.Is(err, zerr.ErrManifestNotFound) { - WriteJSON(response, http.StatusNotFound, - NewErrorList(NewError(MANIFEST_UNKNOWN, map[string]string{"reference": reference}))) + zcommon.WriteJSON(response, http.StatusNotFound, + apiErr.NewErrorList(apiErr.NewError(apiErr.MANIFEST_UNKNOWN, map[string]string{"reference": reference}))) } else { rh.c.Log.Error().Err(err).Msg("unexpected error") - WriteJSON(response, http.StatusInternalServerError, - NewErrorList(NewError(MANIFEST_INVALID, map[string]string{"reference": reference}))) + zcommon.WriteJSON(response, http.StatusInternalServerError, + apiErr.NewErrorList(apiErr.NewError(apiErr.MANIFEST_INVALID, map[string]string{"reference": reference}))) } return @@ -440,9 +441,9 @@ func (rh *RouteHandler) GetManifest(response http.ResponseWriter, request *http. reference, ok := vars["reference"] if !ok || reference == "" { - WriteJSON(response, + zcommon.WriteJSON(response, http.StatusNotFound, - NewErrorList(NewError(MANIFEST_UNKNOWN, map[string]string{"reference": reference}))) + apiErr.NewErrorList(apiErr.NewError(apiErr.MANIFEST_UNKNOWN, map[string]string{"reference": reference}))) return } @@ -450,14 +451,14 @@ func (rh *RouteHandler) GetManifest(response http.ResponseWriter, request *http. content, digest, mediaType, err := getImageManifest(rh, imgStore, name, reference) //nolint: contextcheck if err != nil { if errors.Is(err, zerr.ErrRepoNotFound) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain - WriteJSON(response, http.StatusNotFound, - NewErrorList(NewError(NAME_UNKNOWN, map[string]string{"name": name}))) + zcommon.WriteJSON(response, http.StatusNotFound, + apiErr.NewErrorList(apiErr.NewError(apiErr.NAME_UNKNOWN, map[string]string{"name": name}))) } else if errors.Is(err, zerr.ErrRepoBadVersion) { - WriteJSON(response, http.StatusNotFound, - NewErrorList(NewError(NAME_UNKNOWN, map[string]string{"name": name}))) + zcommon.WriteJSON(response, http.StatusNotFound, + apiErr.NewErrorList(apiErr.NewError(apiErr.NAME_UNKNOWN, map[string]string{"name": name}))) } else if errors.Is(err, zerr.ErrManifestNotFound) { - WriteJSON(response, http.StatusNotFound, - NewErrorList(NewError(MANIFEST_UNKNOWN, map[string]string{"reference": reference}))) + zcommon.WriteJSON(response, http.StatusNotFound, + apiErr.NewErrorList(apiErr.NewError(apiErr.MANIFEST_UNKNOWN, map[string]string{"reference": reference}))) } else { rh.c.Log.Error().Err(err).Msg("unexpected error") response.WriteHeader(http.StatusInternalServerError) @@ -478,7 +479,7 @@ func (rh *RouteHandler) GetManifest(response http.ResponseWriter, request *http. response.Header().Set(constants.DistContentDigestKey, digest.String()) response.Header().Set("Content-Length", fmt.Sprintf("%d", len(content))) response.Header().Set("Content-Type", mediaType) - WriteData(response, http.StatusOK, mediaType, content) + zcommon.WriteData(response, http.StatusOK, mediaType, content) } type ImageIndex struct { @@ -577,7 +578,7 @@ func (rh *RouteHandler) GetReferrers(response http.ResponseWriter, request *http response.Header().Set("OCI-Filters-Applied", strings.Join(artifactTypes, ",")) } - WriteData(response, http.StatusOK, ispec.MediaTypeImageIndex, out) + zcommon.WriteData(response, http.StatusOK, ispec.MediaTypeImageIndex, out) } // UpdateManifest godoc @@ -607,9 +608,9 @@ func (rh *RouteHandler) UpdateManifest(response http.ResponseWriter, request *ht reference, ok := vars["reference"] if !ok || reference == "" { - WriteJSON(response, + zcommon.WriteJSON(response, http.StatusNotFound, - NewErrorList(NewError(MANIFEST_INVALID, map[string]string{"reference": reference}))) + apiErr.NewErrorList(apiErr.NewError(apiErr.MANIFEST_INVALID, map[string]string{"reference": reference}))) return } @@ -617,8 +618,8 @@ func (rh *RouteHandler) UpdateManifest(response http.ResponseWriter, request *ht mediaType := request.Header.Get("Content-Type") if !storageCommon.IsSupportedMediaType(mediaType) { // response.WriteHeader(http.StatusUnsupportedMediaType) - WriteJSON(response, http.StatusUnsupportedMediaType, - NewErrorList(NewError(MANIFEST_INVALID, map[string]string{"mediaType": mediaType}))) + zcommon.WriteJSON(response, http.StatusUnsupportedMediaType, + apiErr.NewErrorList(apiErr.NewError(apiErr.MANIFEST_INVALID, map[string]string{"mediaType": mediaType}))) return } @@ -636,23 +637,24 @@ func (rh *RouteHandler) UpdateManifest(response http.ResponseWriter, request *ht digest, subjectDigest, err := imgStore.PutImageManifest(name, reference, mediaType, body) if err != nil { if errors.Is(err, zerr.ErrRepoNotFound) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain - WriteJSON(response, http.StatusNotFound, - NewErrorList(NewError(NAME_UNKNOWN, map[string]string{"name": name}))) + zcommon.WriteJSON(response, http.StatusNotFound, + apiErr.NewErrorList(apiErr.NewError(apiErr.NAME_UNKNOWN, map[string]string{"name": name}))) } else if errors.Is(err, zerr.ErrManifestNotFound) { - WriteJSON(response, http.StatusNotFound, - NewErrorList(NewError(MANIFEST_UNKNOWN, map[string]string{"reference": reference}))) + zcommon.WriteJSON(response, http.StatusNotFound, + apiErr.NewErrorList(apiErr.NewError(apiErr.MANIFEST_UNKNOWN, map[string]string{"reference": reference}))) } else if errors.Is(err, zerr.ErrBadManifest) { - WriteJSON(response, http.StatusBadRequest, - NewErrorList(NewError(MANIFEST_INVALID, map[string]string{"reference": reference}))) + zcommon.WriteJSON(response, http.StatusBadRequest, + apiErr.NewErrorList(apiErr.NewError(apiErr.MANIFEST_INVALID, map[string]string{"reference": reference}))) } else if errors.Is(err, zerr.ErrBlobNotFound) { - WriteJSON(response, http.StatusBadRequest, - NewErrorList(NewError(BLOB_UNKNOWN, map[string]string{"blob": digest.String()}))) + zcommon.WriteJSON(response, http.StatusBadRequest, + apiErr.NewErrorList(apiErr.NewError(apiErr.BLOB_UNKNOWN, map[string]string{"blob": digest.String()}))) } else if errors.Is(err, zerr.ErrRepoBadVersion) { - WriteJSON(response, http.StatusInternalServerError, - NewErrorList(NewError(INVALID_INDEX, map[string]string{"name": name}))) + zcommon.WriteJSON(response, http.StatusInternalServerError, + apiErr.NewErrorList(apiErr.NewError(apiErr.INVALID_INDEX, map[string]string{"name": name}))) } else if errors.Is(err, zerr.ErrImageLintAnnotations) { - WriteJSON(response, http.StatusBadRequest, - NewErrorList(NewError(MANIFEST_INVALID, map[string]string{"reference": reference}).WithMessage(err.Error()))) + zcommon.WriteJSON(response, http.StatusBadRequest, + apiErr.NewErrorList(apiErr.NewError( + apiErr.MANIFEST_INVALID, map[string]string{"reference": reference}).WithMessage(err.Error()))) } else { // could be syscall.EMFILE (Err:0x18 too many opened files), etc rh.c.Log.Error().Err(err).Msg("unexpected error: performing cleanup") @@ -734,14 +736,14 @@ func (rh *RouteHandler) DeleteManifest(response http.ResponseWriter, request *ht manifestBlob, manifestDigest, mediaType, err := imgStore.GetImageManifest(name, reference) if err != nil { if errors.Is(err, zerr.ErrRepoNotFound) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain - WriteJSON(response, http.StatusBadRequest, - NewErrorList(NewError(NAME_UNKNOWN, map[string]string{"name": name}))) + zcommon.WriteJSON(response, http.StatusBadRequest, + apiErr.NewErrorList(apiErr.NewError(apiErr.NAME_UNKNOWN, map[string]string{"name": name}))) } else if errors.Is(err, zerr.ErrManifestNotFound) { - WriteJSON(response, http.StatusNotFound, - NewErrorList(NewError(MANIFEST_UNKNOWN, map[string]string{"reference": reference}))) + zcommon.WriteJSON(response, http.StatusNotFound, + apiErr.NewErrorList(apiErr.NewError(apiErr.MANIFEST_UNKNOWN, map[string]string{"reference": reference}))) } else if errors.Is(err, zerr.ErrBadManifest) { - WriteJSON(response, http.StatusBadRequest, - NewErrorList(NewError(UNSUPPORTED, map[string]string{"reference": reference}))) + zcommon.WriteJSON(response, http.StatusBadRequest, + apiErr.NewErrorList(apiErr.NewError(apiErr.UNSUPPORTED, map[string]string{"reference": reference}))) } else { rh.c.Log.Error().Err(err).Msg("unexpected error") response.WriteHeader(http.StatusInternalServerError) @@ -753,17 +755,17 @@ func (rh *RouteHandler) DeleteManifest(response http.ResponseWriter, request *ht err = imgStore.DeleteImageManifest(name, reference, detectCollision) if err != nil { if errors.Is(err, zerr.ErrRepoNotFound) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain - WriteJSON(response, http.StatusBadRequest, - NewErrorList(NewError(NAME_UNKNOWN, map[string]string{"name": name}))) + zcommon.WriteJSON(response, http.StatusBadRequest, + apiErr.NewErrorList(apiErr.NewError(apiErr.NAME_UNKNOWN, map[string]string{"name": name}))) } else if errors.Is(err, zerr.ErrManifestNotFound) { - WriteJSON(response, http.StatusNotFound, - NewErrorList(NewError(MANIFEST_UNKNOWN, map[string]string{"reference": reference}))) + zcommon.WriteJSON(response, http.StatusNotFound, + apiErr.NewErrorList(apiErr.NewError(apiErr.MANIFEST_UNKNOWN, map[string]string{"reference": reference}))) } else if errors.Is(err, zerr.ErrManifestConflict) { - WriteJSON(response, http.StatusConflict, - NewErrorList(NewError(MANIFEST_INVALID, map[string]string{"reference": reference}))) + zcommon.WriteJSON(response, http.StatusConflict, + apiErr.NewErrorList(apiErr.NewError(apiErr.MANIFEST_INVALID, map[string]string{"reference": reference}))) } else if errors.Is(err, zerr.ErrBadManifest) { - WriteJSON(response, http.StatusBadRequest, - NewErrorList(NewError(UNSUPPORTED, map[string]string{"reference": reference}))) + zcommon.WriteJSON(response, http.StatusBadRequest, + apiErr.NewErrorList(apiErr.NewError(apiErr.UNSUPPORTED, map[string]string{"reference": reference}))) } else { rh.c.Log.Error().Err(err).Msg("unexpected error") response.WriteHeader(http.StatusInternalServerError) @@ -820,14 +822,16 @@ func (rh *RouteHandler) CheckBlob(response http.ResponseWriter, request *http.Re ok, blen, err := imgStore.CheckBlob(name, digest) if err != nil { if errors.Is(err, zerr.ErrBadBlobDigest) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain - WriteJSON(response, + zcommon.WriteJSON(response, http.StatusBadRequest, - NewErrorList(NewError(DIGEST_INVALID, map[string]string{"digest": digest.String()}))) + apiErr.NewErrorList(apiErr.NewError(apiErr.DIGEST_INVALID, map[string]string{"digest": digest.String()}))) } else if errors.Is(err, zerr.ErrRepoNotFound) { - WriteJSON(response, http.StatusNotFound, NewErrorList(NewError(NAME_UNKNOWN, map[string]string{"name": name}))) + zcommon.WriteJSON(response, http.StatusNotFound, + apiErr.NewErrorList(apiErr.NewError(apiErr.NAME_UNKNOWN, map[string]string{"name": name}))) } else if errors.Is(err, zerr.ErrBlobNotFound) { - WriteJSON(response, http.StatusNotFound, NewErrorList(NewError(BLOB_UNKNOWN, - map[string]string{"digest": digest.String()}))) + zcommon.WriteJSON(response, http.StatusNotFound, + apiErr.NewErrorList(apiErr.NewError(apiErr.BLOB_UNKNOWN, + map[string]string{"digest": digest.String()}))) } else { rh.c.Log.Error().Err(err).Msg("unexpected error") response.WriteHeader(http.StatusInternalServerError) @@ -837,7 +841,7 @@ func (rh *RouteHandler) CheckBlob(response http.ResponseWriter, request *http.Re } if !ok { - WriteJSON(response, http.StatusNotFound, NewErrorList(NewError(BLOB_UNKNOWN, + zcommon.WriteJSON(response, http.StatusNotFound, apiErr.NewErrorList(apiErr.NewError(apiErr.BLOB_UNKNOWN, map[string]string{"digest": digest.String()}))) return @@ -968,17 +972,17 @@ func (rh *RouteHandler) GetBlob(response http.ResponseWriter, request *http.Requ if err != nil { if errors.Is(err, zerr.ErrBadBlobDigest) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain - WriteJSON(response, + zcommon.WriteJSON(response, http.StatusBadRequest, - NewErrorList(NewError(DIGEST_INVALID, map[string]string{"digest": digest.String()}))) + apiErr.NewErrorList(apiErr.NewError(apiErr.DIGEST_INVALID, map[string]string{"digest": digest.String()}))) } else if errors.Is(err, zerr.ErrRepoNotFound) { - WriteJSON(response, + zcommon.WriteJSON(response, http.StatusNotFound, - NewErrorList(NewError(NAME_UNKNOWN, map[string]string{"name": name}))) + apiErr.NewErrorList(apiErr.NewError(apiErr.NAME_UNKNOWN, map[string]string{"name": name}))) } else if errors.Is(err, zerr.ErrBlobNotFound) { - WriteJSON(response, + zcommon.WriteJSON(response, http.StatusNotFound, - NewErrorList(NewError(BLOB_UNKNOWN, map[string]string{"digest": digest.String()}))) + apiErr.NewErrorList(apiErr.NewError(apiErr.BLOB_UNKNOWN, map[string]string{"digest": digest.String()}))) } else { rh.c.Log.Error().Err(err).Msg("unexpected error") response.WriteHeader(http.StatusInternalServerError) @@ -1037,17 +1041,17 @@ func (rh *RouteHandler) DeleteBlob(response http.ResponseWriter, request *http.R err = imgStore.DeleteBlob(name, digest) if err != nil { if errors.Is(err, zerr.ErrBadBlobDigest) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain - WriteJSON(response, + zcommon.WriteJSON(response, http.StatusBadRequest, - NewErrorList(NewError(DIGEST_INVALID, map[string]string{"digest": digest.String()}))) + apiErr.NewErrorList(apiErr.NewError(apiErr.DIGEST_INVALID, map[string]string{"digest": digest.String()}))) } else if errors.Is(err, zerr.ErrRepoNotFound) { - WriteJSON(response, + zcommon.WriteJSON(response, http.StatusNotFound, - NewErrorList(NewError(NAME_UNKNOWN, map[string]string{"name": name}))) + apiErr.NewErrorList(apiErr.NewError(apiErr.NAME_UNKNOWN, map[string]string{"name": name}))) } else if errors.Is(err, zerr.ErrBlobNotFound) { - WriteJSON(response, + zcommon.WriteJSON(response, http.StatusNotFound, - NewErrorList(NewError(BLOB_UNKNOWN, map[string]string{".String()": digest.String()}))) + apiErr.NewErrorList(apiErr.NewError(apiErr.BLOB_UNKNOWN, map[string]string{".String()": digest.String()}))) } else { rh.c.Log.Error().Err(err).Msg("unexpected error") response.WriteHeader(http.StatusInternalServerError) @@ -1100,7 +1104,8 @@ func (rh *RouteHandler) CreateBlobUpload(response http.ResponseWriter, request * upload, err := imgStore.NewBlobUpload(name) if err != nil { if errors.Is(err, zerr.ErrRepoNotFound) { - WriteJSON(response, http.StatusNotFound, NewErrorList(NewError(NAME_UNKNOWN, map[string]string{"name": name}))) + zcommon.WriteJSON(response, http.StatusNotFound, + apiErr.NewErrorList(apiErr.NewError(apiErr.NAME_UNKNOWN, map[string]string{"name": name}))) } else { rh.c.Log.Error().Err(err).Msg("unexpected error") response.WriteHeader(http.StatusInternalServerError) @@ -1155,8 +1160,8 @@ func (rh *RouteHandler) CreateBlobUpload(response http.ResponseWriter, request * contentLength, err := strconv.ParseInt(request.Header.Get("Content-Length"), 10, 64) if err != nil || contentLength <= 0 { rh.c.Log.Warn().Str("actual", request.Header.Get("Content-Length")).Msg("invalid content length") - WriteJSON(response, http.StatusBadRequest, - NewErrorList(NewError(BLOB_UPLOAD_INVALID, map[string]string{"digest": digest.String()}))) + zcommon.WriteJSON(response, http.StatusBadRequest, + apiErr.NewErrorList(apiErr.NewError(apiErr.BLOB_UPLOAD_INVALID, map[string]string{"digest": digest.String()}))) return } @@ -1186,7 +1191,8 @@ func (rh *RouteHandler) CreateBlobUpload(response http.ResponseWriter, request * upload, err := imgStore.NewBlobUpload(name) if err != nil { if errors.Is(err, zerr.ErrRepoNotFound) { - WriteJSON(response, http.StatusNotFound, NewErrorList(NewError(NAME_UNKNOWN, map[string]string{"name": name}))) + zcommon.WriteJSON(response, http.StatusNotFound, + apiErr.NewErrorList(apiErr.NewError(apiErr.NAME_UNKNOWN, map[string]string{"name": name}))) } else { rh.c.Log.Error().Err(err).Msg("unexpected error") response.WriteHeader(http.StatusInternalServerError) @@ -1235,17 +1241,17 @@ func (rh *RouteHandler) GetBlobUpload(response http.ResponseWriter, request *htt size, err := imgStore.GetBlobUpload(name, sessionID) if err != nil { if errors.Is(err, zerr.ErrBadUploadRange) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain - WriteJSON(response, http.StatusBadRequest, - NewErrorList(NewError(BLOB_UPLOAD_INVALID, map[string]string{"session_id": sessionID}))) + zcommon.WriteJSON(response, http.StatusBadRequest, + apiErr.NewErrorList(apiErr.NewError(apiErr.BLOB_UPLOAD_INVALID, map[string]string{"session_id": sessionID}))) } else if errors.Is(err, zerr.ErrBadBlobDigest) { - WriteJSON(response, http.StatusBadRequest, - NewErrorList(NewError(BLOB_UPLOAD_INVALID, map[string]string{"session_id": sessionID}))) + zcommon.WriteJSON(response, http.StatusBadRequest, + apiErr.NewErrorList(apiErr.NewError(apiErr.BLOB_UPLOAD_INVALID, map[string]string{"session_id": sessionID}))) } else if errors.Is(err, zerr.ErrRepoNotFound) { - WriteJSON(response, http.StatusNotFound, - NewErrorList(NewError(NAME_UNKNOWN, map[string]string{"name": name}))) + zcommon.WriteJSON(response, http.StatusNotFound, + apiErr.NewErrorList(apiErr.NewError(apiErr.NAME_UNKNOWN, map[string]string{"name": name}))) } else if errors.Is(err, zerr.ErrUploadNotFound) { - WriteJSON(response, http.StatusNotFound, - NewErrorList(NewError(BLOB_UPLOAD_UNKNOWN, map[string]string{"session_id": sessionID}))) + zcommon.WriteJSON(response, http.StatusNotFound, + apiErr.NewErrorList(apiErr.NewError(apiErr.BLOB_UPLOAD_UNKNOWN, map[string]string{"session_id": sessionID}))) } else { rh.c.Log.Error().Err(err).Msg("unexpected error") response.WriteHeader(http.StatusInternalServerError) @@ -1325,14 +1331,14 @@ func (rh *RouteHandler) PatchBlobUpload(response http.ResponseWriter, request *h if err != nil { if errors.Is(err, zerr.ErrBadUploadRange) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain - WriteJSON(response, http.StatusRequestedRangeNotSatisfiable, - NewErrorList(NewError(BLOB_UPLOAD_INVALID, map[string]string{"session_id": sessionID}))) + zcommon.WriteJSON(response, http.StatusRequestedRangeNotSatisfiable, + apiErr.NewErrorList(apiErr.NewError(apiErr.BLOB_UPLOAD_INVALID, map[string]string{"session_id": sessionID}))) } else if errors.Is(err, zerr.ErrRepoNotFound) { - WriteJSON(response, http.StatusNotFound, - NewErrorList(NewError(NAME_UNKNOWN, map[string]string{"name": name}))) + zcommon.WriteJSON(response, http.StatusNotFound, + apiErr.NewErrorList(apiErr.NewError(apiErr.NAME_UNKNOWN, map[string]string{"name": name}))) } else if errors.Is(err, zerr.ErrUploadNotFound) { - WriteJSON(response, http.StatusNotFound, - NewErrorList(NewError(BLOB_UPLOAD_UNKNOWN, map[string]string{"session_id": sessionID}))) + zcommon.WriteJSON(response, http.StatusNotFound, + apiErr.NewErrorList(apiErr.NewError(apiErr.BLOB_UPLOAD_UNKNOWN, map[string]string{"session_id": sessionID}))) } else { // could be io.ErrUnexpectedEOF, syscall.EMFILE (Err:0x18 too many opened files), etc rh.c.Log.Error().Err(err).Msg("unexpected error: removing .uploads/ files") @@ -1445,14 +1451,14 @@ func (rh *RouteHandler) UpdateBlobUpload(response http.ResponseWriter, request * _, err = imgStore.PutBlobChunk(name, sessionID, from, to, request.Body) if err != nil { if errors.Is(err, zerr.ErrBadUploadRange) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain - WriteJSON(response, http.StatusBadRequest, - NewErrorList(NewError(BLOB_UPLOAD_INVALID, map[string]string{"session_id": sessionID}))) + zcommon.WriteJSON(response, http.StatusBadRequest, + apiErr.NewErrorList(apiErr.NewError(apiErr.BLOB_UPLOAD_INVALID, map[string]string{"session_id": sessionID}))) } else if errors.Is(err, zerr.ErrRepoNotFound) { - WriteJSON(response, http.StatusNotFound, - NewErrorList(NewError(NAME_UNKNOWN, map[string]string{"name": name}))) + zcommon.WriteJSON(response, http.StatusNotFound, + apiErr.NewErrorList(apiErr.NewError(apiErr.NAME_UNKNOWN, map[string]string{"name": name}))) } else if errors.Is(err, zerr.ErrUploadNotFound) { - WriteJSON(response, http.StatusNotFound, - NewErrorList(NewError(BLOB_UPLOAD_UNKNOWN, map[string]string{"session_id": sessionID}))) + zcommon.WriteJSON(response, http.StatusNotFound, + apiErr.NewErrorList(apiErr.NewError(apiErr.BLOB_UPLOAD_UNKNOWN, map[string]string{"session_id": sessionID}))) } else { // could be io.ErrUnexpectedEOF, syscall.EMFILE (Err:0x18 too many opened files), etc rh.c.Log.Error().Err(err).Msg("unexpected error: removing .uploads/ files") @@ -1472,17 +1478,17 @@ finish: // blob chunks already transferred, just finish if err := imgStore.FinishBlobUpload(name, sessionID, request.Body, digest); err != nil { if errors.Is(err, zerr.ErrBadBlobDigest) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain - WriteJSON(response, http.StatusBadRequest, - NewErrorList(NewError(DIGEST_INVALID, map[string]string{"digest": digest.String()}))) + zcommon.WriteJSON(response, http.StatusBadRequest, + apiErr.NewErrorList(apiErr.NewError(apiErr.DIGEST_INVALID, map[string]string{"digest": digest.String()}))) } else if errors.Is(err, zerr.ErrBadUploadRange) { - WriteJSON(response, http.StatusBadRequest, - NewErrorList(NewError(BLOB_UPLOAD_INVALID, map[string]string{"session_id": sessionID}))) + zcommon.WriteJSON(response, http.StatusBadRequest, + apiErr.NewErrorList(apiErr.NewError(apiErr.BLOB_UPLOAD_INVALID, map[string]string{"session_id": sessionID}))) } else if errors.Is(err, zerr.ErrRepoNotFound) { - WriteJSON(response, http.StatusNotFound, - NewErrorList(NewError(NAME_UNKNOWN, map[string]string{"name": name}))) + zcommon.WriteJSON(response, http.StatusNotFound, + apiErr.NewErrorList(apiErr.NewError(apiErr.NAME_UNKNOWN, map[string]string{"name": name}))) } else if errors.Is(err, zerr.ErrUploadNotFound) { - WriteJSON(response, http.StatusNotFound, - NewErrorList(NewError(BLOB_UPLOAD_UNKNOWN, map[string]string{"session_id": sessionID}))) + zcommon.WriteJSON(response, http.StatusNotFound, + apiErr.NewErrorList(apiErr.NewError(apiErr.BLOB_UPLOAD_UNKNOWN, map[string]string{"session_id": sessionID}))) } else { // could be io.ErrUnexpectedEOF, syscall.EMFILE (Err:0x18 too many opened files), etc rh.c.Log.Error().Err(err).Msg("unexpected error: removing .uploads/ files") @@ -1535,11 +1541,11 @@ func (rh *RouteHandler) DeleteBlobUpload(response http.ResponseWriter, request * if err := imgStore.DeleteBlobUpload(name, sessionID); err != nil { if errors.Is(err, zerr.ErrRepoNotFound) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain - WriteJSON(response, http.StatusNotFound, - NewErrorList(NewError(NAME_UNKNOWN, map[string]string{"name": name}))) + zcommon.WriteJSON(response, http.StatusNotFound, + apiErr.NewErrorList(apiErr.NewError(apiErr.NAME_UNKNOWN, map[string]string{"name": name}))) } else if errors.Is(err, zerr.ErrUploadNotFound) { - WriteJSON(response, http.StatusNotFound, - NewErrorList(NewError(BLOB_UPLOAD_UNKNOWN, map[string]string{"session_id": sessionID}))) + zcommon.WriteJSON(response, http.StatusNotFound, + apiErr.NewErrorList(apiErr.NewError(apiErr.BLOB_UPLOAD_UNKNOWN, map[string]string{"session_id": sessionID}))) } else { rh.c.Log.Error().Err(err).Msg("unexpected error") response.WriteHeader(http.StatusInternalServerError) @@ -1619,7 +1625,7 @@ func (rh *RouteHandler) ListRepositories(response http.ResponseWriter, request * is := RepositoryList{Repositories: repos} - WriteJSON(response, http.StatusOK, is) + zcommon.WriteJSON(response, http.StatusOK, is) } // ListExtensions godoc @@ -1639,12 +1645,12 @@ func (rh *RouteHandler) ListExtensions(w http.ResponseWriter, r *http.Request) { extensionList := ext.GetExtensions(rh.c.Config) - WriteJSON(w, http.StatusOK, extensionList) + zcommon.WriteJSON(w, http.StatusOK, extensionList) } func (rh *RouteHandler) GetMetrics(w http.ResponseWriter, r *http.Request) { m := rh.c.Metrics.ReceiveMetrics() - WriteJSON(w, http.StatusOK, m) + zcommon.WriteJSON(w, http.StatusOK, m) } // helper routines @@ -1670,23 +1676,6 @@ func getContentRange(r *http.Request) (int64 /* from */, int64 /* to */, error) return rangeStart, rangeEnd, nil } -func WriteJSON(response http.ResponseWriter, status int, data interface{}) { - json := jsoniter.ConfigCompatibleWithStandardLibrary - - body, err := json.Marshal(data) - if err != nil { - panic(err) - } - - WriteData(response, status, constants.DefaultMediaType, body) -} - -func WriteData(w http.ResponseWriter, status int, mediaType string, data []byte) { - w.Header().Set("Content-Type", mediaType) - w.WriteHeader(status) - _, _ = w.Write(data) -} - func WriteDataFromReader(response http.ResponseWriter, status int, length int64, mediaType string, reader io.Reader, logger log.Logger, ) { @@ -1834,7 +1823,7 @@ func (rh *RouteHandler) GetOrasReferrers(response http.ResponseWriter, request * rs := ReferenceList{References: refs} - WriteJSON(response, http.StatusOK, rs) + zcommon.WriteJSON(response, http.StatusOK, rs) } // GetBlobUploadSessionLocation returns actual blob location to start/resume uploading blobs. diff --git a/pkg/common/common.go b/pkg/common/common.go index 897ca12b..5989d7a7 100644 --- a/pkg/common/common.go +++ b/pkg/common/common.go @@ -1,26 +1,14 @@ package common import ( - "crypto/tls" - "crypto/x509" "encoding/json" "errors" "fmt" - "io" "io/fs" - "net/http" "os" - "path" - "path/filepath" - "strings" "syscall" "time" "unicode/utf8" - - "github.com/gorilla/mux" - ispec "github.com/opencontainers/image-spec/specs-go/v1" - - "zotregistry.io/zot/pkg/log" ) const ( @@ -32,10 +20,6 @@ const ( caCertFilename = "ca.crt" ) -func AllowedMethods(methods ...string) []string { - return append(methods, http.MethodOptions) -} - func Contains[T comparable](elems []T, v T) bool { for _, s := range elems { if v == s { @@ -70,162 +54,10 @@ func RemoveFrom(inputSlice []string, item string) []string { return newSlice } -func GetTLSConfig(certsPath string, caCertPool *x509.CertPool) (*tls.Config, error) { - clientCert := filepath.Join(certsPath, clientCertFilename) - clientKey := filepath.Join(certsPath, clientKeyFilename) - caCertFile := filepath.Join(certsPath, caCertFilename) - - cert, err := tls.LoadX509KeyPair(clientCert, clientKey) - if err != nil { - return nil, err - } - - caCert, err := os.ReadFile(caCertFile) - if err != nil { - return nil, err - } - - caCertPool.AppendCertsFromPEM(caCert) - - return &tls.Config{ - Certificates: []tls.Certificate{cert}, - RootCAs: caCertPool, - MinVersion: tls.VersionTLS12, - }, nil -} - -func loadPerHostCerts(caCertPool *x509.CertPool, host string) *tls.Config { - // Check if the /home/user/.config/containers/certs.d/$IP:$PORT dir exists - home := os.Getenv("HOME") - clientCertsDir := filepath.Join(home, homeCertsDir, host) - - if DirExists(clientCertsDir) { - tlsConfig, err := GetTLSConfig(clientCertsDir, caCertPool) - - if err == nil { - return tlsConfig - } - } - - // Check if the /etc/containers/certs.d/$IP:$PORT dir exists - clientCertsDir = filepath.Join(certsPath, host) - if DirExists(clientCertsDir) { - tlsConfig, err := GetTLSConfig(clientCertsDir, caCertPool) - - if err == nil { - return tlsConfig - } - } - - return nil -} - -func CreateHTTPClient(verifyTLS bool, host string, certDir string) (*http.Client, error) { - htr := http.DefaultTransport.(*http.Transport).Clone() //nolint: forcetypeassert - if !verifyTLS { - htr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} //nolint: gosec - - return &http.Client{ - Timeout: httpTimeout, - Transport: htr, - }, nil - } - - // Add a copy of the system cert pool - caCertPool, _ := x509.SystemCertPool() - - tlsConfig := loadPerHostCerts(caCertPool, host) - if tlsConfig == nil { - tlsConfig = &tls.Config{RootCAs: caCertPool, MinVersion: tls.VersionTLS12} - } - - htr.TLSClientConfig = tlsConfig - - if certDir != "" { - clientCert := path.Join(certDir, "client.cert") - clientKey := path.Join(certDir, "client.key") - caCertPath := path.Join(certDir, "ca.crt") - - caCert, err := os.ReadFile(caCertPath) - if err != nil { - return nil, err - } - - caCertPool.AppendCertsFromPEM(caCert) - - cert, err := tls.LoadX509KeyPair(clientCert, clientKey) - if err != nil { - return nil, err - } - - htr.TLSClientConfig.Certificates = append(htr.TLSClientConfig.Certificates, cert) - } - - return &http.Client{ - Timeout: httpTimeout, - Transport: htr, - }, nil -} - func TypeOf(v interface{}) string { return fmt.Sprintf("%T", v) } -func MakeHTTPGetRequest(httpClient *http.Client, username string, password string, resultPtr interface{}, - blobURL string, mediaType string, log log.Logger, -) ([]byte, string, int, error) { - req, err := http.NewRequest(http.MethodGet, blobURL, nil) //nolint - if err != nil { - return nil, "", 0, err - } - - if mediaType != "" { - req.Header.Set("Accept", mediaType) - } - - if username != "" && password != "" { - req.SetBasicAuth(username, password) - } - - resp, err := httpClient.Do(req) - if err != nil { - log.Error().Str("errorType", TypeOf(err)). - Err(err).Str("blobURL", blobURL).Msg("couldn't get blob") - - return nil, "", -1, err - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - log.Error().Str("errorType", TypeOf(err)). - Err(err).Str("blobURL", blobURL).Msg("couldn't get blob") - - return nil, "", resp.StatusCode, err - } - - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - log.Error().Str("status code", fmt.Sprint(resp.StatusCode)). - Err(err).Str("blobURL", blobURL).Msg("couldn't get blob") - - return nil, "", resp.StatusCode, errors.New(string(body)) //nolint:goerr113 - } - - // read blob - if len(body) > 0 { - err = json.Unmarshal(body, &resultPtr) - if err != nil { - log.Error().Str("errorType", TypeOf(err)).Str("blobURL", blobURL). - Err(err).Msg("couldn't unmarshal remote blob") - - return body, "", resp.StatusCode, err - } - } - - return body, resp.Header.Get("Content-Type"), resp.StatusCode, err -} - func DirExists(d string) bool { if !utf8.ValidString(d) { return false @@ -269,38 +101,3 @@ func MarshalThroughStruct(obj interface{}, throughStruct interface{}) ([]byte, e return toJSON, nil } - -func GetManifestArtifactType(manifestContent ispec.Manifest) string { - if manifestContent.ArtifactType != "" { - return manifestContent.ArtifactType - } - - return manifestContent.Config.MediaType -} - -func AddExtensionSecurityHeaders() mux.MiddlewareFunc { //nolint:varnamelen - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - resp.Header().Set("X-Content-Type-Options", "nosniff") - - next.ServeHTTP(resp, req) - }) - } -} - -func ACHeadersHandler(allowedMethods ...string) mux.MiddlewareFunc { - headerValue := strings.Join(allowedMethods, ",") - - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - resp.Header().Set("Access-Control-Allow-Methods", headerValue) - resp.Header().Set("Access-Control-Allow-Headers", "Authorization,content-type") - - if req.Method == http.MethodOptions { - return - } - - next.ServeHTTP(resp, req) - }) - } -} diff --git a/pkg/common/common_test.go b/pkg/common/common_test.go index 4275fdcf..1df1dec9 100644 --- a/pkg/common/common_test.go +++ b/pkg/common/common_test.go @@ -1,19 +1,14 @@ package common_test import ( - "crypto/x509" "os" "path" "testing" - ispec "github.com/opencontainers/image-spec/specs-go/v1" . "github.com/smartystreets/goconvey/convey" - "zotregistry.io/zot/pkg/api" "zotregistry.io/zot/pkg/api/config" "zotregistry.io/zot/pkg/common" - "zotregistry.io/zot/pkg/log" - "zotregistry.io/zot/pkg/test" ) func TestCommon(t *testing.T) { @@ -46,21 +41,6 @@ func TestCommon(t *testing.T) { So(err, ShouldNotBeNil) }) - Convey("test getTLSConfig()", t, func() { - caCertPool, _ := x509.SystemCertPool() - tlsConfig, err := common.GetTLSConfig("wrongPath", caCertPool) - So(tlsConfig, ShouldBeNil) - So(err, ShouldNotBeNil) - - tempDir := t.TempDir() - err = test.CopyFiles("../../test/data", tempDir) - So(err, ShouldBeNil) - err = os.Chmod(path.Join(tempDir, "ca.crt"), 0o000) - So(err, ShouldBeNil) - _, err = common.GetTLSConfig(tempDir, caCertPool) - So(err, ShouldNotBeNil) - }) - Convey("test dirExists()", t, func() { exists := common.DirExists("testdir") So(exists, ShouldBeFalse) @@ -72,61 +52,8 @@ func TestCommon(t *testing.T) { So(isDir, ShouldBeFalse) }) - Convey("test CreateHTTPClient() no permissions on certificate", t, func() { - tempDir := t.TempDir() - err := test.CopyFiles("../../test/data", tempDir) - So(err, ShouldBeNil) - err = os.Chmod(path.Join(tempDir, "ca.crt"), 0o000) - So(err, ShouldBeNil) - - _, err = common.CreateHTTPClient(true, "localhost", tempDir) - So(err, ShouldNotBeNil) - }) - - Convey("test CreateHTTPClient() no permissions on key", t, func() { - tempDir := t.TempDir() - err := test.CopyFiles("../../test/data", tempDir) - So(err, ShouldBeNil) - err = os.Chmod(path.Join(tempDir, "client.key"), 0o000) - So(err, ShouldBeNil) - - _, err = common.CreateHTTPClient(true, "localhost", tempDir) - So(err, ShouldNotBeNil) - }) - - Convey("test MakeHTTPGetRequest() no permissions on key", t, func() { - port := test.GetFreePort() - baseURL := test.GetBaseURL(port) - - conf := config.New() - conf.HTTP.Port = port - - ctlr := api.NewController(conf) - tempDir := t.TempDir() - err := test.CopyFiles("../../test/data", tempDir) - So(err, ShouldBeNil) - ctlr.Config.Storage.RootDirectory = tempDir - - cm := test.NewControllerManager(ctlr) - cm.StartServer() - defer cm.StopServer() - test.WaitTillServerReady(baseURL) - - var resultPtr interface{} - httpClient, err := common.CreateHTTPClient(true, "localhost", tempDir) - So(err, ShouldBeNil) - _, _, _, err = common.MakeHTTPGetRequest(httpClient, "", "", - resultPtr, baseURL+"/v2/", ispec.MediaTypeImageManifest, log.NewLogger("", "")) - So(err, ShouldBeNil) - }) - Convey("Index func", t, func() { So(common.Index([]string{"a", "b"}, "b"), ShouldEqual, 1) So(common.Index([]string{"a", "b"}, "c"), ShouldEqual, -1) }) - Convey("Test image dir and digest", t, func() { - repo, digest := common.GetImageDirAndDigest("image") - So(repo, ShouldResemble, "image") - So(digest, ShouldResemble, "") - }) } diff --git a/pkg/common/http_client.go b/pkg/common/http_client.go new file mode 100644 index 00000000..03a3f210 --- /dev/null +++ b/pkg/common/http_client.go @@ -0,0 +1,168 @@ +package common + +import ( + "crypto/tls" + "crypto/x509" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "os" + "path" + "path/filepath" + + "zotregistry.io/zot/pkg/log" +) + +func GetTLSConfig(certsPath string, caCertPool *x509.CertPool) (*tls.Config, error) { + clientCert := filepath.Join(certsPath, clientCertFilename) + clientKey := filepath.Join(certsPath, clientKeyFilename) + caCertFile := filepath.Join(certsPath, caCertFilename) + + cert, err := tls.LoadX509KeyPair(clientCert, clientKey) + if err != nil { + return nil, err + } + + caCert, err := os.ReadFile(caCertFile) + if err != nil { + return nil, err + } + + caCertPool.AppendCertsFromPEM(caCert) + + return &tls.Config{ + Certificates: []tls.Certificate{cert}, + RootCAs: caCertPool, + MinVersion: tls.VersionTLS12, + }, nil +} + +func loadPerHostCerts(caCertPool *x509.CertPool, host string) *tls.Config { + // Check if the /home/user/.config/containers/certs.d/$IP:$PORT dir exists + home := os.Getenv("HOME") + clientCertsDir := filepath.Join(home, homeCertsDir, host) + + if DirExists(clientCertsDir) { + tlsConfig, err := GetTLSConfig(clientCertsDir, caCertPool) + + if err == nil { + return tlsConfig + } + } + + // Check if the /etc/containers/certs.d/$IP:$PORT dir exists + clientCertsDir = filepath.Join(certsPath, host) + if DirExists(clientCertsDir) { + tlsConfig, err := GetTLSConfig(clientCertsDir, caCertPool) + + if err == nil { + return tlsConfig + } + } + + return nil +} + +func CreateHTTPClient(verifyTLS bool, host string, certDir string) (*http.Client, error) { + htr := http.DefaultTransport.(*http.Transport).Clone() //nolint: forcetypeassert + if !verifyTLS { + htr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} //nolint: gosec + + return &http.Client{ + Timeout: httpTimeout, + Transport: htr, + }, nil + } + + // Add a copy of the system cert pool + caCertPool, _ := x509.SystemCertPool() + + tlsConfig := loadPerHostCerts(caCertPool, host) + if tlsConfig == nil { + tlsConfig = &tls.Config{RootCAs: caCertPool, MinVersion: tls.VersionTLS12} + } + + htr.TLSClientConfig = tlsConfig + + if certDir != "" { + clientCert := path.Join(certDir, "client.cert") + clientKey := path.Join(certDir, "client.key") + caCertPath := path.Join(certDir, "ca.crt") + + caCert, err := os.ReadFile(caCertPath) + if err != nil { + return nil, err + } + + caCertPool.AppendCertsFromPEM(caCert) + + cert, err := tls.LoadX509KeyPair(clientCert, clientKey) + if err != nil { + return nil, err + } + + htr.TLSClientConfig.Certificates = append(htr.TLSClientConfig.Certificates, cert) + } + + return &http.Client{ + Timeout: httpTimeout, + Transport: htr, + }, nil +} + +func MakeHTTPGetRequest(httpClient *http.Client, username string, password string, resultPtr interface{}, + blobURL string, mediaType string, log log.Logger, +) ([]byte, string, int, error) { + req, err := http.NewRequest(http.MethodGet, blobURL, nil) //nolint + if err != nil { + return nil, "", 0, err + } + + if mediaType != "" { + req.Header.Set("Accept", mediaType) + } + + if username != "" && password != "" { + req.SetBasicAuth(username, password) + } + + resp, err := httpClient.Do(req) + if err != nil { + log.Error().Str("errorType", TypeOf(err)). + Err(err).Str("blobURL", blobURL).Msg("couldn't get blob") + + return nil, "", -1, err + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Error().Str("errorType", TypeOf(err)). + Err(err).Str("blobURL", blobURL).Msg("couldn't get blob") + + return nil, "", resp.StatusCode, err + } + + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + log.Error().Str("status code", fmt.Sprint(resp.StatusCode)). + Err(err).Str("blobURL", blobURL).Msg("couldn't get blob") + + return nil, "", resp.StatusCode, errors.New(string(body)) //nolint:goerr113 + } + + // read blob + if len(body) > 0 { + err = json.Unmarshal(body, &resultPtr) + if err != nil { + log.Error().Str("errorType", TypeOf(err)).Str("blobURL", blobURL). + Err(err).Msg("couldn't unmarshal remote blob") + + return body, "", resp.StatusCode, err + } + } + + return body, resp.Header.Get("Content-Type"), resp.StatusCode, err +} diff --git a/pkg/common/http_client_test.go b/pkg/common/http_client_test.go new file mode 100644 index 00000000..4601bbfe --- /dev/null +++ b/pkg/common/http_client_test.go @@ -0,0 +1,82 @@ +package common_test + +import ( + "crypto/x509" + "os" + "path" + "testing" + + ispec "github.com/opencontainers/image-spec/specs-go/v1" + . "github.com/smartystreets/goconvey/convey" + + "zotregistry.io/zot/pkg/api" + "zotregistry.io/zot/pkg/api/config" + "zotregistry.io/zot/pkg/common" + "zotregistry.io/zot/pkg/log" + "zotregistry.io/zot/pkg/test" +) + +func TestHTTPClient(t *testing.T) { + Convey("test getTLSConfig()", t, func() { + caCertPool, _ := x509.SystemCertPool() + tlsConfig, err := common.GetTLSConfig("wrongPath", caCertPool) + So(tlsConfig, ShouldBeNil) + So(err, ShouldNotBeNil) + + tempDir := t.TempDir() + err = test.CopyFiles("../../test/data", tempDir) + So(err, ShouldBeNil) + err = os.Chmod(path.Join(tempDir, "ca.crt"), 0o000) + So(err, ShouldBeNil) + _, err = common.GetTLSConfig(tempDir, caCertPool) + So(err, ShouldNotBeNil) + }) + + Convey("test CreateHTTPClient() no permissions on certificate", t, func() { + tempDir := t.TempDir() + err := test.CopyFiles("../../test/data", tempDir) + So(err, ShouldBeNil) + err = os.Chmod(path.Join(tempDir, "ca.crt"), 0o000) + So(err, ShouldBeNil) + + _, err = common.CreateHTTPClient(true, "localhost", tempDir) + So(err, ShouldNotBeNil) + }) + + Convey("test CreateHTTPClient() no permissions on key", t, func() { + tempDir := t.TempDir() + err := test.CopyFiles("../../test/data", tempDir) + So(err, ShouldBeNil) + err = os.Chmod(path.Join(tempDir, "client.key"), 0o000) + So(err, ShouldBeNil) + + _, err = common.CreateHTTPClient(true, "localhost", tempDir) + So(err, ShouldNotBeNil) + }) + + Convey("test MakeHTTPGetRequest() no permissions on key", t, func() { + port := test.GetFreePort() + baseURL := test.GetBaseURL(port) + + conf := config.New() + conf.HTTP.Port = port + + ctlr := api.NewController(conf) + tempDir := t.TempDir() + err := test.CopyFiles("../../test/data", tempDir) + So(err, ShouldBeNil) + ctlr.Config.Storage.RootDirectory = tempDir + + cm := test.NewControllerManager(ctlr) + cm.StartServer() + defer cm.StopServer() + test.WaitTillServerReady(baseURL) + + var resultPtr interface{} + httpClient, err := common.CreateHTTPClient(true, "localhost", tempDir) + So(err, ShouldBeNil) + _, _, _, err = common.MakeHTTPGetRequest(httpClient, "", "", + resultPtr, baseURL+"/v2/", ispec.MediaTypeImageManifest, log.NewLogger("", "")) + So(err, ShouldBeNil) + }) +} diff --git a/pkg/common/http_server.go b/pkg/common/http_server.go new file mode 100644 index 00000000..e90db94e --- /dev/null +++ b/pkg/common/http_server.go @@ -0,0 +1,68 @@ +package common + +import ( + "net/http" + "strings" + "time" + + "github.com/gorilla/mux" + jsoniter "github.com/json-iterator/go" + + "zotregistry.io/zot/pkg/api/constants" + apiErr "zotregistry.io/zot/pkg/api/errors" +) + +func AllowedMethods(methods ...string) []string { + return append(methods, http.MethodOptions) +} + +func AddExtensionSecurityHeaders() mux.MiddlewareFunc { //nolint:varnamelen + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { + resp.Header().Set("X-Content-Type-Options", "nosniff") + + next.ServeHTTP(resp, req) + }) + } +} + +func ACHeadersHandler(allowedMethods ...string) mux.MiddlewareFunc { + headerValue := strings.Join(allowedMethods, ",") + + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { + resp.Header().Set("Access-Control-Allow-Methods", headerValue) + resp.Header().Set("Access-Control-Allow-Headers", "Authorization,content-type") + + if req.Method == http.MethodOptions { + return + } + + next.ServeHTTP(resp, req) + }) + } +} + +func AuthzFail(w http.ResponseWriter, realm string, delay int) { + time.Sleep(time.Duration(delay) * time.Second) + w.Header().Set("WWW-Authenticate", realm) + w.Header().Set("Content-Type", "application/json") + WriteJSON(w, http.StatusForbidden, apiErr.NewErrorList(apiErr.NewError(apiErr.DENIED))) +} + +func WriteJSON(response http.ResponseWriter, status int, data interface{}) { + json := jsoniter.ConfigCompatibleWithStandardLibrary + + body, err := json.Marshal(data) + if err != nil { + panic(err) + } + + WriteData(response, status, constants.DefaultMediaType, body) +} + +func WriteData(w http.ResponseWriter, status int, mediaType string, data []byte) { + w.Header().Set("Content-Type", mediaType) + w.WriteHeader(status) + _, _ = w.Write(data) +} diff --git a/pkg/common/utils.go b/pkg/common/oci.go similarity index 88% rename from pkg/common/utils.go rename to pkg/common/oci.go index 02840acb..3758019c 100644 --- a/pkg/common/utils.go +++ b/pkg/common/oci.go @@ -48,6 +48,14 @@ func GetImageDirAndReference(imageName string) (string, string, bool) { return repo, tag, true } +func GetManifestArtifactType(manifestContent ispec.Manifest) string { + if manifestContent.ArtifactType != "" { + return manifestContent.ArtifactType + } + + return manifestContent.Config.MediaType +} + // GetImageLastUpdated This method will return last updated timestamp. // The Created timestamp is used, but if it is missing, look at the // history field and, if provided, return the timestamp of last entry in history. diff --git a/pkg/common/oci_test.go b/pkg/common/oci_test.go new file mode 100644 index 00000000..446fa193 --- /dev/null +++ b/pkg/common/oci_test.go @@ -0,0 +1,17 @@ +package common_test + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" + + "zotregistry.io/zot/pkg/common" +) + +func TestOCI(t *testing.T) { + Convey("Test image dir and digest", t, func() { + repo, digest := common.GetImageDirAndDigest("image") + So(repo, ShouldResemble, "image") + So(digest, ShouldResemble, "") + }) +} diff --git a/pkg/extensions/search/cve/cve_test.go b/pkg/extensions/search/cve/cve_test.go index 45711447..3b594a19 100644 --- a/pkg/extensions/search/cve/cve_test.go +++ b/pkg/extensions/search/cve/cve_test.go @@ -24,6 +24,7 @@ import ( "zotregistry.io/zot/pkg/api" "zotregistry.io/zot/pkg/api/config" "zotregistry.io/zot/pkg/api/constants" + apiErr "zotregistry.io/zot/pkg/api/errors" extconf "zotregistry.io/zot/pkg/extensions/config" "zotregistry.io/zot/pkg/extensions/monitoring" cveinfo "zotregistry.io/zot/pkg/extensions/search/cve" @@ -537,7 +538,7 @@ func TestCVESearch(t *testing.T) { So(err, ShouldBeNil) So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 401) - var apiErr api.Error + var apiErr apiErr.Error err = json.Unmarshal(resp.Body(), &apiErr) So(err, ShouldBeNil)