From 6d600e64b5a99bc7223908363dd57240647950da Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Thu, 24 Sep 2026 15:01:43 +0800 Subject: [PATCH] fix: use correct content-type for container registry response (#39398) (#39407) --- routers/api/packages/container/container.go | 108 ++++++++---------- .../api_packages_container_test.go | 19 ++- 2 files changed, 55 insertions(+), 72 deletions(-) diff --git a/routers/api/packages/container/container.go b/routers/api/packages/container/container.go index a293cac08d8..b706ab0bea4 100644 --- a/routers/api/packages/container/container.go +++ b/routers/api/packages/container/container.go @@ -57,21 +57,17 @@ type containerHeaders struct { UploadUUID string Range string Location string - ContentType string ContentLength optional.Option[int64] } // 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 != "" { resp.Header().Set("Location", h.Location) } if h.Range != "" { resp.Header().Set("Range", h.Range) } - if h.ContentType != "" { - resp.Header().Set("Content-Type", h.ContentType) - } if h.ContentLength.Has() { resp.Header().Set("Content-Length", strconv.FormatInt(h.ContentLength.Value(), 10)) } @@ -87,18 +83,14 @@ func setResponseHeaders(resp http.ResponseWriter, h *containerHeaders) { } func jsonResponse(ctx *context.Context, status int, obj any) { - setResponseHeaders(ctx.Resp, &containerHeaders{ - Status: status, - ContentType: "application/json", - }) + ctx.Resp.Header().Set("Content-Type", "application/json") + writeResponseHeaders(ctx.Resp, &containerHeaders{Status: status}) _ = json.NewEncoder(ctx.Resp).Encode(obj) // ignore network errors } func apiError(ctx *context.Context, status int, err error) { _ = helper.ProcessErrorForUser(ctx, status, err) - setResponseHeaders(ctx.Resp, &containerHeaders{ - Status: status, - }) + writeResponseHeaders(ctx.Resp, &containerHeaders{Status: status}) } // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#error-codes @@ -155,9 +147,7 @@ func VerifyImageName(ctx *context.Context) { // DetermineSupport is used to test if the registry supports OCI // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#determining-support func DetermineSupport(ctx *context.Context) { - setResponseHeaders(ctx.Resp, &containerHeaders{ - Status: http.StatusOK, - }) + writeResponseHeaders(ctx.Resp, &containerHeaders{Status: http.StatusOK}) } // Authenticate creates a token for the current user @@ -267,7 +257,7 @@ func PostBlobsUploads(ctx *context.Context) { return } - setResponseHeaders(ctx.Resp, &containerHeaders{ + writeResponseHeaders(ctx.Resp, &containerHeaders{ Location: fmt.Sprintf("/v2/%s/%s/blobs/%s", ctx.Package.Owner.LowerName, image, mount), ContentDigest: mount, Status: http.StatusCreated, @@ -310,7 +300,7 @@ func PostBlobsUploads(ctx *context.Context) { return } - setResponseHeaders(ctx.Resp, &containerHeaders{ + writeResponseHeaders(ctx.Resp, &containerHeaders{ Location: fmt.Sprintf("/v2/%s/%s/blobs/%s", ctx.Package.Owner.LowerName, image, digest), ContentDigest: digest, Status: http.StatusCreated, @@ -324,7 +314,7 @@ func PostBlobsUploads(ctx *context.Context) { 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), UploadUUID: upload.ID, Status: http.StatusAccepted, @@ -355,7 +345,7 @@ func GetBlobsUpload(ctx *context.Context) { if upload.BytesReceived > 0 { 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 @@ -404,7 +394,7 @@ func PatchBlobsUpload(ctx *context.Context) { if uploader.Size() > 0 { 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 @@ -469,7 +459,7 @@ func PutBlobsUpload(ctx *context.Context) { return } - setResponseHeaders(ctx.Resp, &containerHeaders{ + writeResponseHeaders(ctx.Resp, &containerHeaders{ Location: fmt.Sprintf("/v2/%s/%s/blobs/%s", ctx.Package.Owner.LowerName, image, digest), ContentDigest: digest, Status: http.StatusCreated, @@ -495,9 +485,7 @@ func DeleteBlobsUpload(ctx *context.Context) { return } - setResponseHeaders(ctx.Resp, &containerHeaders{ - Status: http.StatusNoContent, - }) + writeResponseHeaders(ctx.Resp, &containerHeaders{Status: http.StatusNoContent}) } func getBlobFromContext(ctx *context.Context) (*packages_model.PackageFileDescriptor, error) { @@ -524,8 +512,8 @@ func HeadBlob(ctx *context.Context) { } return } - - setResponseHeaders(ctx.Resp, &containerHeaders{ + ctx.Resp.Header().Set("Content-Type", "application/octet-stream") + writeResponseHeaders(ctx.Resp, &containerHeaders{ ContentDigest: blob.Properties.GetByName(container_module.PropertyDigest), ContentLength: optional.Some(blob.Blob.Size), Status: http.StatusOK, @@ -543,8 +531,9 @@ func GetBlob(ctx *context.Context) { } return } - - serveBlob(ctx, blob) + // "/v2//blobs/" : no need to use descriptor.mediaType, just respond as binary content. + // 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 @@ -560,9 +549,7 @@ func DeleteBlob(ctx *context.Context) { return } - setResponseHeaders(ctx.Resp, &containerHeaders{ - Status: http.StatusAccepted, - }) + writeResponseHeaders(ctx.Resp, &containerHeaders{Status: http.StatusAccepted}) } // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-manifests @@ -611,7 +598,7 @@ func PutManifest(ctx *context.Context) { return } - setResponseHeaders(ctx.Resp, &containerHeaders{ + writeResponseHeaders(ctx.Resp, &containerHeaders{ Location: fmt.Sprintf("/v2/%s/%s/manifests/%s", ctx.Package.Owner.LowerName, mci.Image, reference), ContentDigest: digest, Status: http.StatusCreated, @@ -638,18 +625,25 @@ func getBlobSearchOptionsFromContext(ctx *context.Context) (*container_model.Blo 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) if err != nil { - return nil, err + return nil, "", err } - return workaroundGetContainerBlob(ctx, opts) + pfd, err := workaroundGetContainerBlob(ctx, opts) + if err != nil { + return nil, "", err + } + // "/v2//manifests/": 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 func HeadManifest(ctx *context.Context) { - manifest, err := getManifestFromContext(ctx) + manifest, contentType, err := getManifestFromContext(ctx) if err != nil { if errors.Is(err, container_model.ErrContainerBlobNotExist) { apiErrorDefined(ctx, errManifestUnknown) @@ -658,10 +652,9 @@ func HeadManifest(ctx *context.Context) { } return } - - setResponseHeaders(ctx.Resp, &containerHeaders{ + ctx.Resp.Header().Set("Content-Type", contentType) + writeResponseHeaders(ctx.Resp, &containerHeaders{ ContentDigest: manifest.Properties.GetByName(container_module.PropertyDigest), - ContentType: manifest.Properties.GetByName(container_module.PropertyMediaType), ContentLength: optional.Some(manifest.Blob.Size), Status: http.StatusOK, }) @@ -669,7 +662,7 @@ func HeadManifest(ctx *context.Context) { // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pulling-manifests func GetManifest(ctx *context.Context) { - manifest, err := getManifestFromContext(ctx) + manifest, contentType, err := getManifestFromContext(ctx) if err != nil { if errors.Is(err, container_model.ErrContainerBlobNotExist) { apiErrorDefined(ctx, errManifestUnknown) @@ -678,8 +671,7 @@ func GetManifest(ctx *context.Context) { } return } - - serveBlob(ctx, manifest) + serveBlob(ctx, manifest, contentType) } // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#deleting-tags @@ -709,38 +701,34 @@ func DeleteManifest(ctx *context.Context) { } } - setResponseHeaders(ctx.Resp, &containerHeaders{ - Status: http.StatusAccepted, - }) + writeResponseHeaders(ctx.Resp, &containerHeaders{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{ - ContentType: pfd.Properties.GetByName(container_module.PropertyMediaType), + ContentType: contentType, }) if err != nil { apiError(ctx, http.StatusInternalServerError, err) 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 { - headers.Status = http.StatusTemporaryRedirect - headers.Location = u.String() - headers.ContentLength = optional.None[int64]() // do not set Content-Length for redirect responses - setResponseHeaders(ctx.Resp, headers) + writeResponseHeaders(ctx.Resp, &containerHeaders{ + ContentDigest: pfd.Properties.GetByName(container_module.PropertyDigest), // legacy logic: not sure whether it is right when redirecting + Location: u.String(), + Status: http.StatusTemporaryRedirect, + }) return } 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) } diff --git a/tests/integration/api_packages_container_test.go b/tests/integration/api_packages_container_test.go index 298064de57c..b0bcf848916 100644 --- a/tests/integration/api_packages_container_test.go +++ b/tests/integration/api_packages_container_test.go @@ -610,33 +610,28 @@ func TestPackageContainer(t *testing.T) { t.Run("HeadBlob", func(t *testing.T) { defer tests.PrintCurrentTest(t)() - req := NewRequest(t, "HEAD", fmt.Sprintf("%s/blobs/%s", url, unknownDigest)). - AddTokenAuth(userToken) + req := NewRequest(t, "HEAD", fmt.Sprintf("%s/blobs/%s", url, unknownDigest)).AddTokenAuth(userToken) MakeRequest(t, req, http.StatusNotFound) - req = NewRequest(t, "HEAD", fmt.Sprintf("%s/blobs/%s", url, blobDigest)). - AddTokenAuth(userToken) + req = NewRequest(t, "HEAD", fmt.Sprintf("%s/blobs/%s", url, blobDigest)).AddTokenAuth(userToken) 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, blobDigest, resp.Header().Get("Docker-Content-Digest")) - req = NewRequest(t, "HEAD", fmt.Sprintf("%s/blobs/%s", url, blobDigest)). - AddTokenAuth(anonymousToken) + req = NewRequest(t, "HEAD", fmt.Sprintf("%s/blobs/%s", url, blobDigest)).AddTokenAuth(anonymousToken) MakeRequest(t, req, http.StatusOK) }) t.Run("GetBlob", func(t *testing.T) { defer tests.PrintCurrentTest(t)() - req := NewRequest(t, "GET", fmt.Sprintf("%s/blobs/%s", url, unknownDigest)). - AddTokenAuth(userToken) + req := NewRequest(t, "GET", fmt.Sprintf("%s/blobs/%s", url, unknownDigest)).AddTokenAuth(userToken) MakeRequest(t, req, http.StatusNotFound) - req = NewRequest(t, "GET", fmt.Sprintf("%s/blobs/%s", url, blobDigest)). - AddTokenAuth(userToken) + req = NewRequest(t, "GET", fmt.Sprintf("%s/blobs/%s", url, blobDigest)).AddTokenAuth(userToken) 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, blobDigest, resp.Header().Get("Docker-Content-Digest")) assert.Equal(t, blobContent, resp.Body.Bytes())