2026-08-13 07:35:07 +01:00
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"crypto/sha256"
|
|
|
|
|
"encoding/hex"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
2026-09-02 17:10:35 +05:30
|
|
|
"mime"
|
2026-08-13 07:35:07 +01:00
|
|
|
"net/http"
|
|
|
|
|
"regexp"
|
2026-09-02 17:10:35 +05:30
|
|
|
"sort"
|
2026-08-13 07:35:07 +01:00
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
containerManifestCacheEcosystem = "oci-manifest"
|
|
|
|
|
containerStaleWarning = `110 - "Response is Stale"`
|
2026-09-02 17:10:35 +05:30
|
|
|
|
|
|
|
|
containerAcceptWildcardSpecificity = iota
|
|
|
|
|
containerAcceptTypeWildcardSpecificity
|
|
|
|
|
containerAcceptExactSpecificity
|
2026-08-13 07:35:07 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var manifestDigestReferencePattern = regexp.MustCompile(`^[a-z0-9]+:[a-f0-9]+$`)
|
|
|
|
|
|
|
|
|
|
type cachedContainerManifest struct {
|
|
|
|
|
body []byte
|
|
|
|
|
contentType string
|
|
|
|
|
contentDigest string
|
|
|
|
|
etag string
|
|
|
|
|
size int64
|
2026-09-03 16:59:12 +01:00
|
|
|
lastModified time.Time
|
2026-08-13 07:35:07 +01:00
|
|
|
fetchedAt time.Time
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-16 22:42:55 +05:30
|
|
|
func (h *ContainerHandler) serveManifest(w http.ResponseWriter, r *http.Request, registryURL, name, reference string) {
|
2026-08-13 07:35:07 +01:00
|
|
|
accept := containerManifestAccept(r)
|
2026-09-02 17:10:35 +05:30
|
|
|
cacheAccept := normalizeContainerManifestAccept(accept)
|
|
|
|
|
cacheKey := h.containerManifestCacheKey(registryURL, name, reference, cacheAccept)
|
|
|
|
|
cached := h.loadContainerManifestForAccept(r.Context(), registryURL, name, reference, accept, cacheKey)
|
2026-08-13 07:35:07 +01:00
|
|
|
|
|
|
|
|
immutable := manifestDigestReferencePattern.MatchString(reference)
|
|
|
|
|
if cached != nil && (immutable || h.containerManifestFresh(cached)) {
|
2026-09-03 16:59:12 +01:00
|
|
|
writeContainerManifest(w, r, cached, false)
|
2026-08-13 07:35:07 +01:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-16 22:42:55 +05:30
|
|
|
upstreamURL := fmt.Sprintf("%s/v2/%s/manifests/%s", registryURL, name, reference)
|
2026-08-13 07:35:07 +01:00
|
|
|
req, err := http.NewRequestWithContext(r.Context(), r.Method, upstreamURL, nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
h.containerError(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to create request")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
req.Header.Set("Accept", accept)
|
|
|
|
|
if cached != nil && cached.etag != "" {
|
|
|
|
|
req.Header.Set("If-None-Match", cached.etag)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resp, err := h.proxy.HTTPClient.Do(req)
|
|
|
|
|
if err != nil {
|
|
|
|
|
h.serveStaleManifestOrError(w, r, cached, err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
|
|
|
|
|
|
if resp.StatusCode == http.StatusNotModified && cached != nil {
|
|
|
|
|
cached.fetchedAt = time.Now()
|
2026-09-02 17:10:35 +05:30
|
|
|
h.storeContainerManifestForAccept(r.Context(), registryURL, name, reference, accept, cacheAccept, cached)
|
2026-09-03 16:59:12 +01:00
|
|
|
writeContainerManifest(w, r, cached, false)
|
2026-08-13 07:35:07 +01:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
|
|
|
if cached != nil && shouldServeStaleManifest(resp.StatusCode) {
|
2026-09-03 16:59:12 +01:00
|
|
|
writeContainerManifest(w, r, cached, true)
|
2026-08-13 07:35:07 +01:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
copyContainerManifestHeaders(w.Header(), resp.Header)
|
|
|
|
|
w.WriteHeader(resp.StatusCode)
|
|
|
|
|
_, _ = io.Copy(w, resp.Body)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if r.Method == http.MethodHead {
|
|
|
|
|
copyContainerManifestHeaders(w.Header(), resp.Header)
|
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
body, err := h.proxy.ReadMetadata(resp.Body)
|
|
|
|
|
if err != nil {
|
|
|
|
|
h.serveStaleManifestOrError(w, r, cached, fmt.Errorf("reading manifest: %w", err))
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-09-03 16:59:12 +01:00
|
|
|
computedDigest := sha256Digest(body)
|
|
|
|
|
contentDigest := resp.Header.Get("Docker-Content-Digest")
|
|
|
|
|
for _, expected := range []string{reference, contentDigest} {
|
|
|
|
|
if strings.HasPrefix(expected, "sha256:") && expected != computedDigest {
|
|
|
|
|
h.proxy.Logger.Error("upstream manifest failed digest verification",
|
|
|
|
|
"name", name, "reference", reference, "expected", expected, "actual", computedDigest)
|
|
|
|
|
h.containerError(w, http.StatusBadGateway, "DIGEST_INVALID", "manifest digest verification failed")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if contentDigest == "" {
|
|
|
|
|
contentDigest = computedDigest
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-13 07:35:07 +01:00
|
|
|
manifest := &cachedContainerManifest{
|
|
|
|
|
body: body,
|
2026-09-02 13:22:02 +01:00
|
|
|
contentType: resp.Header.Get(headerContentType),
|
2026-09-03 16:59:12 +01:00
|
|
|
contentDigest: contentDigest,
|
|
|
|
|
etag: resp.Header.Get(headerETag),
|
2026-08-13 07:35:07 +01:00
|
|
|
size: int64(len(body)),
|
2026-09-03 16:59:12 +01:00
|
|
|
lastModified: parseHTTPTime(resp.Header.Get(headerLastModified)),
|
2026-08-13 07:35:07 +01:00
|
|
|
fetchedAt: time.Now(),
|
|
|
|
|
}
|
2026-09-02 17:10:35 +05:30
|
|
|
h.storeContainerManifestForAccept(r.Context(), registryURL, name, reference, accept, cacheAccept, manifest)
|
2026-08-13 07:35:07 +01:00
|
|
|
if manifest.contentDigest != reference && manifestDigestReferencePattern.MatchString(manifest.contentDigest) {
|
2026-09-02 17:10:35 +05:30
|
|
|
h.storeContainerManifestForAccept(r.Context(), registryURL, name, manifest.contentDigest, accept, cacheAccept, manifest)
|
2026-08-13 07:35:07 +01:00
|
|
|
}
|
2026-09-03 16:59:12 +01:00
|
|
|
writeContainerManifest(w, r, manifest, false)
|
2026-08-13 07:35:07 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *ContainerHandler) serveStaleManifestOrError(w http.ResponseWriter, r *http.Request, cached *cachedContainerManifest, err error) {
|
|
|
|
|
if cached != nil {
|
|
|
|
|
h.proxy.Logger.Warn("upstream manifest fetch failed, serving stale cache", "error", err)
|
2026-09-03 16:59:12 +01:00
|
|
|
writeContainerManifest(w, r, cached, true)
|
2026-08-13 07:35:07 +01:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
h.proxy.Logger.Error("failed to fetch manifest", "error", err)
|
|
|
|
|
h.containerError(w, http.StatusBadGateway, "INTERNAL_ERROR", "failed to fetch from upstream")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *ContainerHandler) containerManifestFresh(manifest *cachedContainerManifest) bool {
|
|
|
|
|
return h.proxy.MetadataTTL > 0 && !manifest.fetchedAt.IsZero() && time.Since(manifest.fetchedAt) < h.proxy.MetadataTTL
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-16 22:42:55 +05:30
|
|
|
func (h *ContainerHandler) containerManifestCacheKey(registryURL, name, reference, accept string) string {
|
|
|
|
|
identity := strings.Join([]string{registryURL, name, reference, accept}, "\x00")
|
2026-08-13 07:35:07 +01:00
|
|
|
sum := sha256.Sum256([]byte(identity))
|
|
|
|
|
return hex.EncodeToString(sum[:])
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-02 17:10:35 +05:30
|
|
|
func (h *ContainerHandler) loadContainerManifestForAccept(ctx context.Context, registryURL, name, reference, accept, cacheKey string) *cachedContainerManifest {
|
|
|
|
|
cached, err := h.loadContainerManifest(ctx, cacheKey)
|
|
|
|
|
if err != nil {
|
|
|
|
|
h.proxy.Logger.Warn("failed to read cached container manifest", "error", err)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
if cached != nil {
|
|
|
|
|
if containerManifestCacheCompatible(accept, cached) {
|
|
|
|
|
return cached
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
legacyCacheKey := h.containerManifestCacheKey(registryURL, name, reference, accept)
|
|
|
|
|
if legacyCacheKey == cacheKey {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
cached, err = h.loadContainerManifest(ctx, legacyCacheKey)
|
|
|
|
|
if err != nil {
|
|
|
|
|
h.proxy.Logger.Warn("failed to read legacy cached container manifest", "error", err)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
if cached == nil || !containerManifestCacheCompatible(accept, cached) {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
if err := h.storeContainerManifest(ctx, cacheKey, cached); err != nil {
|
|
|
|
|
h.proxy.Logger.Warn("failed to migrate cached container manifest", "error", err)
|
|
|
|
|
}
|
|
|
|
|
return cached
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *ContainerHandler) storeContainerManifestForAccept(ctx context.Context, registryURL, name, reference, accept, cacheAccept string, manifest *cachedContainerManifest) {
|
|
|
|
|
cacheKey := h.containerManifestCacheKey(registryURL, name, reference, cacheAccept)
|
|
|
|
|
if err := h.storeContainerManifest(ctx, cacheKey, manifest); err != nil {
|
|
|
|
|
h.proxy.Logger.Warn("failed to cache container manifest", "error", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
legacyCacheKey := h.containerManifestCacheKey(registryURL, name, reference, accept)
|
|
|
|
|
if legacyCacheKey == cacheKey {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err := h.storeContainerManifest(ctx, legacyCacheKey, manifest); err != nil {
|
|
|
|
|
h.proxy.Logger.Warn("failed to cache legacy container manifest", "error", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-13 07:35:07 +01:00
|
|
|
func (h *ContainerHandler) loadContainerManifest(ctx context.Context, cacheKey string) (*cachedContainerManifest, error) {
|
|
|
|
|
if h.proxy.DB == nil || h.proxy.Storage == nil {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
entry, err := h.proxy.DB.GetMetadataCache(containerManifestCacheEcosystem, cacheKey)
|
|
|
|
|
if err != nil || entry == nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
reader, err := h.proxy.Storage.Open(ctx, entry.StoragePath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
defer func() { _ = reader.Close() }()
|
|
|
|
|
body, err := h.proxy.ReadMetadata(reader)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
manifest := &cachedContainerManifest{body: body, size: int64(len(body))}
|
|
|
|
|
if entry.ContentType.Valid {
|
|
|
|
|
manifest.contentType = entry.ContentType.String
|
|
|
|
|
}
|
|
|
|
|
if entry.ContentDigest.Valid {
|
|
|
|
|
manifest.contentDigest = entry.ContentDigest.String
|
|
|
|
|
} else {
|
|
|
|
|
manifest.contentDigest = sha256Digest(body)
|
|
|
|
|
}
|
|
|
|
|
if entry.ETag.Valid {
|
|
|
|
|
manifest.etag = entry.ETag.String
|
|
|
|
|
}
|
|
|
|
|
if entry.Size.Valid {
|
|
|
|
|
manifest.size = entry.Size.Int64
|
|
|
|
|
}
|
2026-09-03 16:59:12 +01:00
|
|
|
if entry.LastModified.Valid {
|
|
|
|
|
manifest.lastModified = entry.LastModified.Time
|
|
|
|
|
}
|
2026-08-13 07:35:07 +01:00
|
|
|
if entry.FetchedAt.Valid {
|
|
|
|
|
manifest.fetchedAt = entry.FetchedAt.Time
|
|
|
|
|
}
|
|
|
|
|
return manifest, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *ContainerHandler) storeContainerManifest(ctx context.Context, cacheKey string, manifest *cachedContainerManifest) error {
|
2026-09-02 17:10:35 +05:30
|
|
|
size, err := h.storeContainerMetadata(ctx, containerManifestCacheEcosystem, cacheKey, manifest.body,
|
2026-09-03 16:59:12 +01:00
|
|
|
manifest.etag, "", manifest.contentType, manifest.contentDigest, manifest.lastModified, manifest.fetchedAt)
|
2026-08-13 07:35:07 +01:00
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("storing manifest: %w", err)
|
|
|
|
|
}
|
|
|
|
|
manifest.size = size
|
2026-09-02 17:10:35 +05:30
|
|
|
return nil
|
2026-08-13 07:35:07 +01:00
|
|
|
}
|
|
|
|
|
|
2026-09-03 16:59:12 +01:00
|
|
|
func writeContainerManifest(w http.ResponseWriter, r *http.Request, manifest *cachedContainerManifest, stale bool) {
|
2026-08-13 07:35:07 +01:00
|
|
|
if manifest.contentType != "" {
|
2026-09-02 13:22:02 +01:00
|
|
|
w.Header().Set(headerContentType, manifest.contentType)
|
2026-08-13 07:35:07 +01:00
|
|
|
}
|
2026-09-02 13:22:02 +01:00
|
|
|
w.Header().Set(headerContentLength, strconv.FormatInt(manifest.size, 10))
|
2026-08-13 07:35:07 +01:00
|
|
|
if manifest.contentDigest != "" {
|
|
|
|
|
w.Header().Set("Docker-Content-Digest", manifest.contentDigest)
|
|
|
|
|
}
|
|
|
|
|
if manifest.etag != "" {
|
2026-09-03 16:59:12 +01:00
|
|
|
w.Header().Set(headerETag, manifest.etag)
|
|
|
|
|
}
|
|
|
|
|
if !manifest.lastModified.IsZero() {
|
|
|
|
|
w.Header().Set(headerLastModified, manifest.lastModified.UTC().Format(http.TimeFormat))
|
2026-08-13 07:35:07 +01:00
|
|
|
}
|
|
|
|
|
if stale {
|
|
|
|
|
w.Header().Set("Warning", containerStaleWarning)
|
|
|
|
|
}
|
2026-09-03 16:59:12 +01:00
|
|
|
if ifNoneMatchHits(r.Header.Get("If-None-Match"), manifest.etag) {
|
|
|
|
|
w.WriteHeader(http.StatusNotModified)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if !manifest.lastModified.IsZero() {
|
|
|
|
|
if modifiedSince, err := http.ParseTime(r.Header.Get("If-Modified-Since")); err == nil && !manifest.lastModified.After(modifiedSince) {
|
|
|
|
|
w.WriteHeader(http.StatusNotModified)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-13 07:35:07 +01:00
|
|
|
w.WriteHeader(http.StatusOK)
|
2026-09-03 16:59:12 +01:00
|
|
|
if r.Method != http.MethodHead {
|
2026-08-13 07:35:07 +01:00
|
|
|
_, _ = w.Write(manifest.body)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func containerManifestAccept(r *http.Request) string {
|
|
|
|
|
if accept := r.Header.Get("Accept"); accept != "" {
|
|
|
|
|
return accept
|
|
|
|
|
}
|
|
|
|
|
return 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",
|
|
|
|
|
"application/vnd.docker.distribution.manifest.v1+prettyjws",
|
|
|
|
|
}, ", ")
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-02 17:10:35 +05:30
|
|
|
func normalizeContainerManifestAccept(accept string) string {
|
|
|
|
|
mediaTypes := make(map[string]struct{})
|
|
|
|
|
for _, value := range strings.Split(accept, ",") {
|
|
|
|
|
value = strings.TrimSpace(value)
|
|
|
|
|
if value == "" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
mediaType, params, err := mime.ParseMediaType(value)
|
|
|
|
|
if err != nil {
|
|
|
|
|
mediaTypes[strings.ToLower(value)] = struct{}{}
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
paramKeys := make([]string, 0, len(params))
|
|
|
|
|
for key := range params {
|
|
|
|
|
paramKeys = append(paramKeys, key)
|
|
|
|
|
}
|
|
|
|
|
sort.Strings(paramKeys)
|
|
|
|
|
canonical := strings.ToLower(mediaType)
|
|
|
|
|
for _, key := range paramKeys {
|
|
|
|
|
value := params[key]
|
|
|
|
|
if strings.EqualFold(key, "q") {
|
|
|
|
|
if quality, err := strconv.ParseFloat(value, 64); err == nil {
|
|
|
|
|
if quality == 1 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
value = strconv.FormatFloat(quality, 'g', -1, 64)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
canonical += ";" + strings.ToLower(key) + "=" + value
|
|
|
|
|
}
|
|
|
|
|
mediaTypes[canonical] = struct{}{}
|
|
|
|
|
}
|
|
|
|
|
canonicalMediaTypes := make([]string, 0, len(mediaTypes))
|
|
|
|
|
for mediaType := range mediaTypes {
|
|
|
|
|
canonicalMediaTypes = append(canonicalMediaTypes, mediaType)
|
|
|
|
|
}
|
|
|
|
|
sort.Strings(canonicalMediaTypes)
|
|
|
|
|
return strings.Join(canonicalMediaTypes, ",")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func containerManifestAccepts(accept, contentType string) bool {
|
|
|
|
|
contentType, contentParams, err := mime.ParseMediaType(contentType)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
contentType = strings.ToLower(contentType)
|
|
|
|
|
contentMajor, contentMinor, found := strings.Cut(contentType, "/")
|
|
|
|
|
if !found {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bestMediaTypeSpecificity := -1
|
|
|
|
|
bestParameterSpecificity := 0
|
|
|
|
|
bestQuality := 0.0
|
|
|
|
|
for _, value := range strings.Split(accept, ",") {
|
|
|
|
|
mediaType, params, err := mime.ParseMediaType(strings.TrimSpace(value))
|
|
|
|
|
if err != nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
mediaType = strings.ToLower(mediaType)
|
|
|
|
|
major, minor, found := strings.Cut(mediaType, "/")
|
|
|
|
|
if found && containerAcceptRangeMatches(major, minor, params, contentMajor, contentMinor, contentParams) {
|
|
|
|
|
mediaTypeSpecificity, parameterSpecificity := containerAcceptSpecificity(major, minor, params)
|
|
|
|
|
if mediaTypeSpecificity > bestMediaTypeSpecificity ||
|
|
|
|
|
(mediaTypeSpecificity == bestMediaTypeSpecificity && parameterSpecificity > bestParameterSpecificity) {
|
|
|
|
|
bestMediaTypeSpecificity = mediaTypeSpecificity
|
|
|
|
|
bestParameterSpecificity = parameterSpecificity
|
|
|
|
|
bestQuality = containerAcceptQuality(params)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return bestQuality > 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func containerManifestCacheCompatible(accept string, manifest *cachedContainerManifest) bool {
|
|
|
|
|
return manifest.contentType == "" || containerManifestAccepts(accept, manifest.contentType)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func containerAcceptRangeMatches(major, minor string, params map[string]string, contentMajor, contentMinor string, contentParams map[string]string) bool {
|
|
|
|
|
if (major != "*" && major != contentMajor) || (minor != "*" && minor != contentMinor) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
for key, value := range params {
|
|
|
|
|
if strings.EqualFold(key, "q") {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if contentParams[key] != value {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func containerAcceptSpecificity(major, minor string, params map[string]string) (int, int) {
|
|
|
|
|
parameterSpecificity := 0
|
|
|
|
|
for key := range params {
|
|
|
|
|
if !strings.EqualFold(key, "q") {
|
|
|
|
|
parameterSpecificity++
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch {
|
|
|
|
|
case major == "*" && minor == "*":
|
|
|
|
|
return containerAcceptWildcardSpecificity, parameterSpecificity
|
|
|
|
|
case major == "*" || minor == "*":
|
|
|
|
|
return containerAcceptTypeWildcardSpecificity, parameterSpecificity
|
|
|
|
|
default:
|
|
|
|
|
return containerAcceptExactSpecificity, parameterSpecificity
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func containerAcceptQuality(params map[string]string) float64 {
|
|
|
|
|
value, ok := params["q"]
|
|
|
|
|
if !ok {
|
|
|
|
|
return 1
|
|
|
|
|
}
|
|
|
|
|
quality, err := strconv.ParseFloat(value, 64)
|
|
|
|
|
if err != nil || quality < 0 || quality > 1 {
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
return quality
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-13 07:35:07 +01:00
|
|
|
func copyContainerManifestHeaders(destination, source http.Header) {
|
2026-09-03 16:59:12 +01:00
|
|
|
for _, header := range []string{headerContentType, headerContentLength, "Docker-Content-Digest", headerETag, headerLastModified, "WWW-Authenticate"} {
|
2026-08-13 07:35:07 +01:00
|
|
|
if value := source.Get(header); value != "" {
|
|
|
|
|
destination.Set(header, value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-03 16:59:12 +01:00
|
|
|
func parseHTTPTime(value string) time.Time {
|
|
|
|
|
parsed, _ := http.ParseTime(value)
|
|
|
|
|
return parsed
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-13 07:35:07 +01:00
|
|
|
func shouldServeStaleManifest(status int) bool {
|
|
|
|
|
return status == http.StatusTooManyRequests || status >= http.StatusInternalServerError
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func sha256Digest(body []byte) string {
|
|
|
|
|
digest := sha256.Sum256(body)
|
|
|
|
|
return "sha256:" + hex.EncodeToString(digest[:])
|
|
|
|
|
}
|