fix: use correct content-type for container registry response (#39398)

spec:
https://github.com/opencontainers/distribution-spec/blob/main/spec.md

* `/v2/<name>/manifests/<reference>`: media type
* `/v2/<name>/blobs/<digest>` : binary content (spec doesn't require
content type)
This commit is contained in:
wxiaoguang
2026-09-24 02:45:56 +08:00
committed by GitHub
parent 06e334121f
commit 71065941a9
2 changed files with 56 additions and 73 deletions
+49 -61
View File
@@ -27,6 +27,7 @@ import (
"gitea.dev/modules/setting" "gitea.dev/modules/setting"
"gitea.dev/modules/storage" "gitea.dev/modules/storage"
"gitea.dev/modules/structs" "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"gitea.dev/routers/api/packages/helper" "gitea.dev/routers/api/packages/helper"
auth_service "gitea.dev/services/auth" auth_service "gitea.dev/services/auth"
"gitea.dev/services/context" "gitea.dev/services/context"
@@ -55,21 +56,17 @@ type containerHeaders struct {
UploadUUID string UploadUUID string
Range string Range string
Location string Location string
ContentType string
ContentLength optional.Option[int64] ContentLength optional.Option[int64]
} }
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#legacy-docker-support-http-headers // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#legacy-docker-support-http-headers
func setResponseHeaders(resp http.ResponseWriter, h *containerHeaders) { func writeResponseHeaders(resp http.ResponseWriter, h *containerHeaders) {
if h.Location != "" { if h.Location != "" {
resp.Header().Set("Location", h.Location) resp.Header().Set("Location", h.Location)
} }
if h.Range != "" { if h.Range != "" {
resp.Header().Set("Range", h.Range) resp.Header().Set("Range", h.Range)
} }
if h.ContentType != "" {
resp.Header().Set("Content-Type", h.ContentType)
}
if h.ContentLength.Has() { if h.ContentLength.Has() {
resp.Header().Set("Content-Length", strconv.FormatInt(h.ContentLength.Value(), 10)) resp.Header().Set("Content-Length", strconv.FormatInt(h.ContentLength.Value(), 10))
} }
@@ -85,18 +82,14 @@ func setResponseHeaders(resp http.ResponseWriter, h *containerHeaders) {
} }
func jsonResponse(ctx *context.Context, status int, obj any) { func jsonResponse(ctx *context.Context, status int, obj any) {
setResponseHeaders(ctx.Resp, &containerHeaders{ ctx.Resp.Header().Set("Content-Type", "application/json")
Status: status, writeResponseHeaders(ctx.Resp, &containerHeaders{Status: status})
ContentType: "application/json",
})
_ = json.NewEncoder(ctx.Resp).Encode(obj) // ignore network errors _ = json.NewEncoder(ctx.Resp).Encode(obj) // ignore network errors
} }
func apiError(ctx *context.Context, status int, err error) { func apiError(ctx *context.Context, status int, err error) {
_ = helper.ProcessErrorForUser(ctx, status, err) _ = helper.ProcessErrorForUser(ctx, status, err)
setResponseHeaders(ctx.Resp, &containerHeaders{ writeResponseHeaders(ctx.Resp, &containerHeaders{Status: status})
Status: status,
})
} }
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#error-codes // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#error-codes
@@ -153,9 +146,7 @@ func VerifyImageName(ctx *context.Context) {
// DetermineSupport is used to test if the registry supports OCI // DetermineSupport is used to test if the registry supports OCI
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#determining-support // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#determining-support
func DetermineSupport(ctx *context.Context) { func DetermineSupport(ctx *context.Context) {
setResponseHeaders(ctx.Resp, &containerHeaders{ writeResponseHeaders(ctx.Resp, &containerHeaders{Status: http.StatusOK})
Status: http.StatusOK,
})
} }
// Authenticate creates a token for the current user // Authenticate creates a token for the current user
@@ -265,7 +256,7 @@ func PostBlobsUploads(ctx *context.Context) {
return return
} }
setResponseHeaders(ctx.Resp, &containerHeaders{ writeResponseHeaders(ctx.Resp, &containerHeaders{
Location: fmt.Sprintf("/v2/%s/%s/blobs/%s", ctx.Package.Owner.LowerName, image, mount), Location: fmt.Sprintf("/v2/%s/%s/blobs/%s", ctx.Package.Owner.LowerName, image, mount),
ContentDigest: mount, ContentDigest: mount,
Status: http.StatusCreated, Status: http.StatusCreated,
@@ -308,7 +299,7 @@ func PostBlobsUploads(ctx *context.Context) {
return return
} }
setResponseHeaders(ctx.Resp, &containerHeaders{ writeResponseHeaders(ctx.Resp, &containerHeaders{
Location: fmt.Sprintf("/v2/%s/%s/blobs/%s", ctx.Package.Owner.LowerName, image, digest), Location: fmt.Sprintf("/v2/%s/%s/blobs/%s", ctx.Package.Owner.LowerName, image, digest),
ContentDigest: digest, ContentDigest: digest,
Status: http.StatusCreated, Status: http.StatusCreated,
@@ -322,7 +313,7 @@ func PostBlobsUploads(ctx *context.Context) {
return return
} }
setResponseHeaders(ctx.Resp, &containerHeaders{ writeResponseHeaders(ctx.Resp, &containerHeaders{
Location: fmt.Sprintf("/v2/%s/%s/blobs/uploads/%s", ctx.Package.Owner.LowerName, image, upload.ID), Location: fmt.Sprintf("/v2/%s/%s/blobs/uploads/%s", ctx.Package.Owner.LowerName, image, upload.ID),
UploadUUID: upload.ID, UploadUUID: upload.ID,
Status: http.StatusAccepted, Status: http.StatusAccepted,
@@ -353,7 +344,7 @@ func GetBlobsUpload(ctx *context.Context) {
if upload.BytesReceived > 0 { if upload.BytesReceived > 0 {
respHeaders.Range = fmt.Sprintf("0-%d", upload.BytesReceived-1) respHeaders.Range = fmt.Sprintf("0-%d", upload.BytesReceived-1)
} }
setResponseHeaders(ctx.Resp, respHeaders) writeResponseHeaders(ctx.Resp, respHeaders)
} }
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#single-post // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#single-post
@@ -402,7 +393,7 @@ func PatchBlobsUpload(ctx *context.Context) {
if uploader.Size() > 0 { if uploader.Size() > 0 {
respHeaders.Range = fmt.Sprintf("0-%d", uploader.Size()-1) respHeaders.Range = fmt.Sprintf("0-%d", uploader.Size()-1)
} }
setResponseHeaders(ctx.Resp, respHeaders) writeResponseHeaders(ctx.Resp, respHeaders)
} }
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks
@@ -467,7 +458,7 @@ func PutBlobsUpload(ctx *context.Context) {
return return
} }
setResponseHeaders(ctx.Resp, &containerHeaders{ writeResponseHeaders(ctx.Resp, &containerHeaders{
Location: fmt.Sprintf("/v2/%s/%s/blobs/%s", ctx.Package.Owner.LowerName, image, digest), Location: fmt.Sprintf("/v2/%s/%s/blobs/%s", ctx.Package.Owner.LowerName, image, digest),
ContentDigest: digest, ContentDigest: digest,
Status: http.StatusCreated, Status: http.StatusCreated,
@@ -493,9 +484,7 @@ func DeleteBlobsUpload(ctx *context.Context) {
return return
} }
setResponseHeaders(ctx.Resp, &containerHeaders{ writeResponseHeaders(ctx.Resp, &containerHeaders{Status: http.StatusNoContent})
Status: http.StatusNoContent,
})
} }
func getBlobFromContext(ctx *context.Context) (*packages_model.PackageFileDescriptor, error) { func getBlobFromContext(ctx *context.Context) (*packages_model.PackageFileDescriptor, error) {
@@ -522,8 +511,8 @@ func HeadBlob(ctx *context.Context) {
} }
return return
} }
ctx.Resp.Header().Set("Content-Type", "application/octet-stream")
setResponseHeaders(ctx.Resp, &containerHeaders{ writeResponseHeaders(ctx.Resp, &containerHeaders{
ContentDigest: blob.Properties.GetByName(container_module.PropertyDigest), ContentDigest: blob.Properties.GetByName(container_module.PropertyDigest),
ContentLength: optional.Some(blob.Blob.Size), ContentLength: optional.Some(blob.Blob.Size),
Status: http.StatusOK, Status: http.StatusOK,
@@ -541,8 +530,9 @@ func GetBlob(ctx *context.Context) {
} }
return return
} }
// "/v2/<name>/blobs/<digest>" : no need to use descriptor.mediaType, just respond as binary content.
serveBlob(ctx, blob) // don't trust or use the blob's media type, it is not validated.
serveBlob(ctx, blob, "application/octet-stream")
} }
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#deleting-blobs // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#deleting-blobs
@@ -558,9 +548,7 @@ func DeleteBlob(ctx *context.Context) {
return return
} }
setResponseHeaders(ctx.Resp, &containerHeaders{ writeResponseHeaders(ctx.Resp, &containerHeaders{Status: http.StatusAccepted})
Status: http.StatusAccepted,
})
} }
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-manifests // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-manifests
@@ -608,7 +596,7 @@ func PutManifest(ctx *context.Context) {
return return
} }
setResponseHeaders(ctx.Resp, &containerHeaders{ writeResponseHeaders(ctx.Resp, &containerHeaders{
Location: fmt.Sprintf("/v2/%s/%s/manifests/%s", ctx.Package.Owner.LowerName, mci.Image, reference), Location: fmt.Sprintf("/v2/%s/%s/manifests/%s", ctx.Package.Owner.LowerName, mci.Image, reference),
ContentDigest: digest, ContentDigest: digest,
Status: http.StatusCreated, Status: http.StatusCreated,
@@ -635,18 +623,24 @@ func getBlobSearchOptionsFromContext(ctx *context.Context) (*container_model.Blo
return opts, nil return opts, nil
} }
func getManifestFromContext(ctx *context.Context) (*packages_model.PackageFileDescriptor, error) { func getManifestFromContext(ctx *context.Context) (_ *packages_model.PackageFileDescriptor, contentType string, _ error) {
opts, err := getBlobSearchOptionsFromContext(ctx) opts, err := getBlobSearchOptionsFromContext(ctx)
if err != nil { if err != nil {
return nil, err return nil, "", err
} }
pfd, err := getExistingContainerBlob(ctx, opts)
return getExistingContainerBlob(ctx, opts) if err != nil {
return nil, "", err
}
// "/v2/<name>/manifests/<reference>": must use valid manifest.mediaType as HTTP Content-Type
mediaType := pfd.Properties.GetByName(container_module.PropertyMediaType)
mediaType = util.Iif(container_module.IsMediaTypeValid(mediaType), mediaType, "application/octet-stream")
return pfd, mediaType, nil
} }
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#checking-if-content-exists-in-the-registry // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#checking-if-content-exists-in-the-registry
func HeadManifest(ctx *context.Context) { func HeadManifest(ctx *context.Context) {
manifest, err := getManifestFromContext(ctx) manifest, contentType, err := getManifestFromContext(ctx)
if err != nil { if err != nil {
if errors.Is(err, container_model.ErrContainerBlobNotExist) { if errors.Is(err, container_model.ErrContainerBlobNotExist) {
apiErrorDefined(ctx, errManifestUnknown) apiErrorDefined(ctx, errManifestUnknown)
@@ -655,10 +649,9 @@ func HeadManifest(ctx *context.Context) {
} }
return return
} }
ctx.Resp.Header().Set("Content-Type", contentType)
setResponseHeaders(ctx.Resp, &containerHeaders{ writeResponseHeaders(ctx.Resp, &containerHeaders{
ContentDigest: manifest.Properties.GetByName(container_module.PropertyDigest), ContentDigest: manifest.Properties.GetByName(container_module.PropertyDigest),
ContentType: manifest.Properties.GetByName(container_module.PropertyMediaType),
ContentLength: optional.Some(manifest.Blob.Size), ContentLength: optional.Some(manifest.Blob.Size),
Status: http.StatusOK, Status: http.StatusOK,
}) })
@@ -666,7 +659,7 @@ func HeadManifest(ctx *context.Context) {
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pulling-manifests // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pulling-manifests
func GetManifest(ctx *context.Context) { func GetManifest(ctx *context.Context) {
manifest, err := getManifestFromContext(ctx) manifest, contentType, err := getManifestFromContext(ctx)
if err != nil { if err != nil {
if errors.Is(err, container_model.ErrContainerBlobNotExist) { if errors.Is(err, container_model.ErrContainerBlobNotExist) {
apiErrorDefined(ctx, errManifestUnknown) apiErrorDefined(ctx, errManifestUnknown)
@@ -675,8 +668,7 @@ func GetManifest(ctx *context.Context) {
} }
return return
} }
serveBlob(ctx, manifest, contentType)
serveBlob(ctx, manifest)
} }
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#deleting-tags // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#deleting-tags
@@ -706,38 +698,34 @@ func DeleteManifest(ctx *context.Context) {
} }
} }
setResponseHeaders(ctx.Resp, &containerHeaders{ writeResponseHeaders(ctx.Resp, &containerHeaders{Status: http.StatusAccepted})
Status: http.StatusAccepted,
})
} }
func serveBlob(ctx *context.Context, pfd *packages_model.PackageFileDescriptor) { func serveBlob(ctx *context.Context, pfd *packages_model.PackageFileDescriptor, contentType string) {
s, u, _, err := packages_service.OpenBlobForDownload(ctx, pfd.File, pfd.Blob, ctx.Req.Method, &storage.ServeDirectOptions{ s, u, _, err := packages_service.OpenBlobForDownload(ctx, pfd.File, pfd.Blob, ctx.Req.Method, &storage.ServeDirectOptions{
ContentType: pfd.Properties.GetByName(container_module.PropertyMediaType), ContentType: contentType,
}) })
if err != nil { if err != nil {
apiError(ctx, http.StatusInternalServerError, err) apiError(ctx, http.StatusInternalServerError, err)
return return
} }
headers := &containerHeaders{
ContentDigest: pfd.Properties.GetByName(container_module.PropertyDigest),
ContentType: pfd.Properties.GetByName(container_module.PropertyMediaType),
ContentLength: optional.Some(pfd.Blob.Size),
Status: http.StatusOK,
}
if u != nil { if u != nil {
headers.Status = http.StatusTemporaryRedirect writeResponseHeaders(ctx.Resp, &containerHeaders{
headers.Location = u.String() ContentDigest: pfd.Properties.GetByName(container_module.PropertyDigest), // legacy logic: not sure whether it is right when redirecting
headers.ContentLength = optional.None[int64]() // do not set Content-Length for redirect responses Location: u.String(),
setResponseHeaders(ctx.Resp, headers) Status: http.StatusTemporaryRedirect,
})
return return
} }
defer s.Close() defer s.Close()
setResponseHeaders(ctx.Resp, headers) ctx.Resp.Header().Set("Content-Type", contentType)
writeResponseHeaders(ctx.Resp, &containerHeaders{
ContentDigest: pfd.Properties.GetByName(container_module.PropertyDigest),
ContentLength: optional.Some(pfd.Blob.Size),
Status: http.StatusOK,
})
_, _ = io.Copy(ctx.Resp, s) _, _ = io.Copy(ctx.Resp, s)
} }
@@ -610,33 +610,28 @@ func TestPackageContainer(t *testing.T) {
t.Run("HeadBlob", func(t *testing.T) { t.Run("HeadBlob", func(t *testing.T) {
defer tests.PrintCurrentTest(t)() defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "HEAD", fmt.Sprintf("%s/blobs/%s", url, unknownDigest)). req := NewRequest(t, "HEAD", fmt.Sprintf("%s/blobs/%s", url, unknownDigest)).AddTokenAuth(userToken)
AddTokenAuth(userToken)
MakeRequest(t, req, http.StatusNotFound) MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "HEAD", fmt.Sprintf("%s/blobs/%s", url, blobDigest)). req = NewRequest(t, "HEAD", fmt.Sprintf("%s/blobs/%s", url, blobDigest)).AddTokenAuth(userToken)
AddTokenAuth(userToken)
resp := MakeRequest(t, req, http.StatusOK) resp := MakeRequest(t, req, http.StatusOK)
assert.Equal(t, "application/octet-stream", resp.Header().Get("Content-Type"))
assert.Equal(t, strconv.Itoa(len(blobContent)), resp.Header().Get("Content-Length")) assert.Equal(t, strconv.Itoa(len(blobContent)), resp.Header().Get("Content-Length"))
assert.Equal(t, blobDigest, resp.Header().Get("Docker-Content-Digest")) assert.Equal(t, blobDigest, resp.Header().Get("Docker-Content-Digest"))
req = NewRequest(t, "HEAD", fmt.Sprintf("%s/blobs/%s", url, blobDigest)). req = NewRequest(t, "HEAD", fmt.Sprintf("%s/blobs/%s", url, blobDigest)).AddTokenAuth(anonymousToken)
AddTokenAuth(anonymousToken)
MakeRequest(t, req, http.StatusOK) MakeRequest(t, req, http.StatusOK)
}) })
t.Run("GetBlob", func(t *testing.T) { t.Run("GetBlob", func(t *testing.T) {
defer tests.PrintCurrentTest(t)() defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", fmt.Sprintf("%s/blobs/%s", url, unknownDigest)). req := NewRequest(t, "GET", fmt.Sprintf("%s/blobs/%s", url, unknownDigest)).AddTokenAuth(userToken)
AddTokenAuth(userToken)
MakeRequest(t, req, http.StatusNotFound) MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "GET", fmt.Sprintf("%s/blobs/%s", url, blobDigest)). req = NewRequest(t, "GET", fmt.Sprintf("%s/blobs/%s", url, blobDigest)).AddTokenAuth(userToken)
AddTokenAuth(userToken)
resp := MakeRequest(t, req, http.StatusOK) resp := MakeRequest(t, req, http.StatusOK)
assert.Equal(t, "application/octet-stream", resp.Header().Get("Content-Type"))
assert.Equal(t, strconv.Itoa(len(blobContent)), resp.Header().Get("Content-Length")) assert.Equal(t, strconv.Itoa(len(blobContent)), resp.Header().Get("Content-Length"))
assert.Equal(t, blobDigest, resp.Header().Get("Docker-Content-Digest")) assert.Equal(t, blobDigest, resp.Header().Get("Docker-Content-Digest"))
assert.Equal(t, blobContent, resp.Body.Bytes()) assert.Equal(t, blobContent, resp.Body.Bytes())