diff --git a/v1/download/oci_download.go b/v1/download/oci_download.go index 9dc1993fda..24fac482e2 100644 --- a/v1/download/oci_download.go +++ b/v1/download/oci_download.go @@ -371,12 +371,23 @@ func (t *ociTarget) url(path string) string { return fmt.Sprintf("%s://%s/v2/%s/%s", scheme, t.registry, t.repo, path) } +// manifestMediaTypes lists the OCI and Docker manifest media types that registries +// such as ghcr.io require in the Accept header to return manifests correctly. +var manifestMediaTypes = strings.Join([]string{ + "application/vnd.oci.image.manifest.v1+json", + "application/vnd.oci.image.index.v1+json", + "application/vnd.docker.distribution.manifest.v2+json", + "application/vnd.docker.distribution.manifest.list.v2+json", + "*/*", +}, ", ") + func (t *ociTarget) Resolve(ctx context.Context, reference string) (ocispec.Descriptor, error) { url := t.url("manifests/" + reference) req, err := http.NewRequestWithContext(ctx, http.MethodHead, url, nil) if err != nil { return ocispec.Descriptor{}, err } + req.Header.Set("Accept", manifestMediaTypes) resp, err := t.client.Do(req) if err != nil { @@ -408,12 +419,14 @@ func (t *ociTarget) Resolve(ctx context.Context, reference string) (ocispec.Desc func (t *ociTarget) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) { // Use blobs endpoint for non-manifest content, manifests endpoint for manifests var url string + isManifest := false switch target.MediaType { case "application/vnd.oci.image.manifest.v1+json", "application/vnd.oci.image.index.v1+json", "application/vnd.docker.distribution.manifest.v2+json", "application/vnd.docker.distribution.manifest.list.v2+json": url = t.url("manifests/" + target.Digest.String()) + isManifest = true default: url = t.url("blobs/" + target.Digest.String()) } @@ -422,6 +435,9 @@ func (t *ociTarget) Fetch(ctx context.Context, target ocispec.Descriptor) (io.Re if err != nil { return nil, err } + if isManifest { + req.Header.Set("Accept", target.MediaType) + } resp, err := t.client.Do(req) if err != nil { diff --git a/v1/download/oci_target_test.go b/v1/download/oci_target_test.go new file mode 100644 index 0000000000..460e853042 --- /dev/null +++ b/v1/download/oci_target_test.go @@ -0,0 +1,71 @@ +//go:build !opa_no_oci + +package download + +import ( + "context" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + digest "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras-go/v2/registry/remote/auth" +) + +// TestOCITargetAcceptHeaders verifies that ociTarget sends Accept headers on +// manifest requests. Registries such as ghcr.io return 404 for HEAD/GET requests +// that omit the Accept header; this test enforces the same behaviour. +func TestOCITargetAcceptHeaders(t *testing.T) { + const mediaType = "application/vnd.oci.image.manifest.v1+json" + digestHex := "sha256:" + strings.Repeat("a", 64) + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // reject any request missing Accept entirely + if r.Header.Get("Accept") == "" { + w.WriteHeader(http.StatusNotFound) + return + } + // for GET (Fetch), Accept must be the exact descriptor media type + if r.Method == http.MethodGet && r.Header.Get("Accept") != mediaType { + w.WriteHeader(http.StatusNotAcceptable) + return + } + w.Header().Set("Content-Type", mediaType) + w.Header().Set("Docker-Content-Digest", digestHex) + w.Header().Set("Content-Length", "100") + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + serverURL, err := url.Parse(server.URL) + if err != nil { + t.Fatal(err) + } + + target := &ociTarget{ + client: &auth.Client{Client: http.DefaultClient, Cache: auth.NewCache()}, + registry: serverURL.Host, + repo: "org/repo", + plainHTTP: true, + } + + // Resolve uses HEAD — any non-empty Accept is sufficient + if _, err := target.Resolve(context.Background(), "latest"); err != nil { + t.Fatalf("Resolve failed (Accept header likely missing): %v", err) + } + + // Fetch uses GET — Accept must match the descriptor's media type exactly + desc := ocispec.Descriptor{ + MediaType: mediaType, + Digest: digest.Digest(digestHex), + Size: 100, + } + rc, err := target.Fetch(context.Background(), desc) + if err != nil { + t.Fatalf("Fetch failed (Accept header likely missing or wrong): %v", err) + } + rc.Close() +}