diff --git a/app/handler/manifest.go b/app/handler/manifest.go index f201371..1f1be80 100644 --- a/app/handler/manifest.go +++ b/app/handler/manifest.go @@ -6,25 +6,6 @@ import ( "mstore/app/router" ) -// https://github.com/opencontainers/distribution-spec/blob/main/spec.md -// -// Open Container Initiative Distribution Specification -// -// Existing Manifests -// -// The image manifest can be checked for existence with the following url: -// -// HEAD /v2//manifests/ -// -// The name and reference parameter identify the image and are required. The reference may include a tag or digest. -// -// A 404 Not Found response will be returned if the image is unknown to the registry. -// If the image exists and the response is successful the response will be as follows: -// -// 200 OK -// Content-Length: -// Docker-Content-Digest: - func (hand *Handler) ManifestExists(rctx *router.Context) { name, _ := rctx.GetSubpath("name") reference, _ := rctx.GetSubpath("reference") @@ -44,49 +25,7 @@ func (hand *Handler) ManifestExists(rctx *router.Context) { rctx.SetStatus(code) } -// https://github.com/opencontainers/distribution-spec/blob/main/spec.md -// -// Pushing Manifests -// -// To push a manifest, perform a PUT request to a path in the following format, -// and with the following headers and body: /v2//manifests/ -// -// Clients SHOULD set the Content-Type header to the type of the manifest being pushed. -// The client SHOULD NOT include parameters on the Content-Type header (see RFC7231). -// The registry SHOULD ignore parameters on the Content-Type header. -// -// All manifests SHOULD include a mediaType field declaring the type of the manifest being pushed. -// If a manifest includes a mediaType field, clients MUST set the Content-Type header to -// the value specified by the mediaType field. -// -// Content-Type: application/vnd.oci.image.manifest.v1+json -// -// is the namespace of the repository, and the MUST be either a) a digest or b) a tag. -// -// The uploaded manifest MUST reference any blobs that make up the object. -// However, the list of blobs MAY be empty. -// -// The registry MUST store the manifest in the exact byte representation provided by the client. -// Upon a successful upload, the registry MUST return response code 201 Created, -// and MUST have the following header: -// -// Location: -// -// The is a pullable manifest URL. The Docker-Content-Digest header returns -// the canonical digest of the uploaded blob, and MUST be equal to the client provided digest. -// Clients MAY ignore the value but if it is used, the client SHOULD verify -// the value against the uploaded blob data. -// -// An attempt to pull a nonexistent repository MUST return response code 404 Not Found. -// -// A registry SHOULD enforce some limit on the maximum manifest size that it can accept. -// A registry that enforces this limit SHOULD respond to a request to push a manifest over this -// limit with a response code 413 Payload Too Large. -// Client and registry implementations SHOULD expect to be able to support manifest -// pushes of at least 4 megabytes. - // PUT /v2//manifests/ 201 404 - func (hand *Handler) PutManifest(rctx *router.Context) { //hand.DumpHeaders("PutManifest headers", rctx) @@ -113,3 +52,28 @@ func (hand *Handler) PutManifest(rctx *router.Context) { } rctx.SetStatus(code) } + +// GET /v2//manifests/ 200 404 +func (hand *Handler) GetManifest(rctx *router.Context) { + name, _ := rctx.GetSubpath("name") + reference, _ := rctx.GetSubpath("reference") + + params := &operator.GetManifestParams{ + Name: name, + Reference: reference, + } + ctx := rctx.GetContext() + res, code, err := hand.oper.GetManifest(ctx, params) + if err != nil { + hand.logg.Errorf("GetManifest error: %v", err) + rctx.SetStatus(code) + return + } + + rctx.SetHeader("Content-Length", res.ContentLength) + rctx.SetHeader("Content-Type", res.ContentType) + rctx.SetHeader("Docker-Content-Digest", res.DockerContentDigest) + rctx.SetStatus(code) + + rctx.SendBytes([]byte(res.Payload)) +} diff --git a/app/operator/imgaux.go b/app/operator/imgaux.go index 22aa801..e5bd414 100644 --- a/app/operator/imgaux.go +++ b/app/operator/imgaux.go @@ -6,6 +6,20 @@ import ( ) const sha256prefix = "sha256:" +const sha512prefix = "sha512:" + +func normalizeSHADigest(digest string) string { + if stringLikeSHA256Digest(digest) && !strings.HasPrefix(digest, sha256prefix) { + digest = sha256prefix + digest + } else if stringLikeSHA512Digest(digest) && !strings.HasPrefix(digest, sha512prefix) { + digest = sha512prefix + digest + } + return digest +} + +func stringLikeSHADigest(some string) bool { + return stringLikeSHA256Digest(some) || stringLikeSHA512Digest(some) +} func stringLikeSHA256Digest(some string) bool { if strings.HasPrefix(some, sha256prefix) { @@ -20,3 +34,17 @@ func stringLikeSHA256Digest(some string) bool { } return false } + +func stringLikeSHA512Digest(some string) bool { + if strings.HasPrefix(some, sha512prefix) { + some = strings.TrimPrefix(some, sha512prefix) + } + _, err := hex.DecodeString(some) + if err != nil { + return false + } + if len(some) == 128 { + return true + } + return false +} diff --git a/app/operator/manifest.go b/app/operator/manifest.go index db225b6..3da92c4 100644 --- a/app/operator/manifest.go +++ b/app/operator/manifest.go @@ -11,8 +11,6 @@ import ( "mstore/app/descr" "mstore/pkg/auxoci" - - ocidigest "github.com/opencontainers/go-digest" ) const ( @@ -69,7 +67,7 @@ func (oper *Operator) ManifestExists(ctx context.Context, params *ManifestExists } } - digest := ocidigest.SHA256.FromString(manifest.Payload) + digest := auxoci.SHA256DigestFromString(manifest.Payload) payloadSize := len(manifest.Payload) res.ContentLength = strconv.FormatInt(int64(payloadSize), 10) res.ContentType = manifest.ContentType @@ -227,3 +225,52 @@ func (oper *Operator) PutManifest(ctx context.Context, params *PutManifestParams return res, http.StatusCreated, err } + +type GetManifestParams struct { + Name string + Reference string +} +type GetManifestResult struct { + ContentLength string + ContentType string + DockerContentDigest string + Payload string +} + +func (oper *Operator) GetManifest(ctx context.Context, params *GetManifestParams) (*GetManifestResult, int, error) { + var err error + res := &GetManifestResult{} + oper.logg.Debugf("Get manifest %s:%s", params.Name, params.Reference) + + manifestDescr := descr.Manifest{} + var exists bool + if stringLikeSHADigest(params.Reference) { + digest := normalizeSHADigest(params.Reference) + oper.logg.Debugf("Get manifest %s with digest %s", params.Name, params.Reference) + exists, manifestDescr, err = oper.mdb.GetManifestByDigest(ctx, params.Name, digest) + if err != nil { + return res, http.StatusInternalServerError, err + } + if !exists { + return res, http.StatusNotFound, err + } + } else { + oper.logg.Debugf("Get manifest %s with tag %s", params.Name, params.Reference) + exists, manifestDescr, err = oper.mdb.GetManifestByReference(ctx, params.Name, params.Reference) + if err != nil { + return res, http.StatusInternalServerError, err + } + if !exists { + return res, http.StatusNotFound, err + } + } + + manifestDigest := auxoci.SHA256DigestFromString(manifestDescr.Payload) + res.DockerContentDigest = manifestDigest.String() + + res.ContentLength = strconv.FormatInt(int64(len(manifestDescr.Payload)), 10) + res.ContentType = manifestDescr.ContentType + res.Payload = manifestDescr.Payload + + return res, http.StatusOK, err +} diff --git a/app/router/context.go b/app/router/context.go index e44ad1e..0fcb42c 100644 --- a/app/router/context.go +++ b/app/router/context.go @@ -66,3 +66,7 @@ func (rctx *Context) SendText(payload string) { rctx.Writer.Header().Set("Content-Type", "text/plain") rctx.Writer.Write([]byte(payload)) } + +func (rctx *Context) SendBytes(payload []byte) { + rctx.Writer.Write(payload) +} diff --git a/app/service/service.go b/app/service/service.go index 272d575..94de53c 100644 --- a/app/service/service.go +++ b/app/service/service.go @@ -95,10 +95,68 @@ func (svc *Service) Build() error { // and with the following headers and body: /v2//manifests/ const reference = `{reference:[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}}` + svc.rout.Head(`/v2/{name}/manifests/{reference}`, svc.hand.ManifestExists) svc.rout.Put(`/v2/{name}/manifests/{reference}`, svc.hand.PutManifest) + // Pulling manifests + // + // To pull a manifest, perform a GET request to a URL in the following form: + // /v2//manifests/ end-3 + // + // refers to the namespace of the repository. MUST be either + // (a) the digest of the manifest or (b) a tag. The MUST NOT be in + // any other format. Throughout this document, MUST match the following + // regular expression: + // + // [a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*(\/[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*)* + // + // Implementers note: Many clients impose a limit of 255 characters on the length of + // the concatenation of the registry hostname (and optional port), /, and value. + // If the registry name is registry.example.org:5000, those clients would be limited + // to a of 229 characters (255 minus 25 for the registry hostname and port + // and minus 1 for a / separator). For compatibility with those clients, registries should + // avoid values of that would cause this limit to be exceeded. + // + // Throughout this document, as a tag MUST be at most 128 characters + // in length and MUST match the following regular expression: + // + // [a-zA-Z0-9_][a-zA-Z0-9._-]{0,127} + // + // The client SHOULD include an Accept header indicating which manifest content types + // it supports. In a successful response, the Content-Type header will indicate + // the type of the returned manifest. The registry SHOULD NOT include parameters + // on the Content-Type header. The client SHOULD ignore parameters on the Content-Type + // header. + // The Content-Type header SHOULD match what the client pushed as the manifest's + // Content-Type. If the manifest has a mediaType field, clients SHOULD reject + // unless the mediaType field's value matches the type specified by the Content-Type header. + // + // For more information on the use of Accept headers and content negotiation, + // please see Content Negotiation and RFC7231. + // + // A GET request to an existing manifest URL MUST provide the expected manifest, + // with a response code that MUST be 200 OK. A successful response SHOULD contain + // the digest of the uploaded blob in the header Docker-Content-Digest. + // + // The Docker-Content-Digest header, if present on the response, + // returns the canonical digest of the uploaded blob which MAY differ + // from the provided digest. If the digest does differ, + // it MAY be the case that the hashing algorithms used do not match. + // + // See Content Digests apdx-3 for information on how to detect the hashing + // algorithm in use. Most clients MAY ignore the value, but if it is used, + // the client MUST verify the value matches the returned manifest. + // + // If the part of a manifest request is a digest, clients SHOULD verify + // the returned manifest matches this digest. + // + // If the manifest is not found in the repository, the response code + // MUST be 404 Not Found. + + svc.rout.Get(`/v2/{name}/manifests/{reference}`, svc.hand.GetManifest) + // Pulling blobs // // To pull a blob, perform a GET request to a URL in the following form: diff --git a/pkg/auxoci/ociaux.go b/pkg/auxoci/ociaux.go index 541375d..965eaba 100644 --- a/pkg/auxoci/ociaux.go +++ b/pkg/auxoci/ociaux.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" + ocidigest "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) @@ -16,3 +17,7 @@ func ParseOCIManifest(rawManifest []byte) (*ocispec.Manifest, error) { } return manifest, err } + +func SHA256DigestFromString(payload string) ocidigest.Digest { + return ocidigest.SHA256.FromString(payload) +}