mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-08-01 01:58:09 -04:00
251 lines
8.2 KiB
Go
251 lines
8.2 KiB
Go
package handler
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"database/sql"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/git-pkgs/proxy/internal/database"
|
|
)
|
|
|
|
const (
|
|
containerManifestCacheEcosystem = "oci-manifest"
|
|
containerStaleWarning = `110 - "Response is Stale"`
|
|
)
|
|
|
|
var manifestDigestReferencePattern = regexp.MustCompile(`^[a-z0-9]+:[a-f0-9]+$`)
|
|
|
|
type cachedContainerManifest struct {
|
|
body []byte
|
|
contentType string
|
|
contentDigest string
|
|
etag string
|
|
size int64
|
|
fetchedAt time.Time
|
|
}
|
|
|
|
func (h *ContainerHandler) serveManifest(w http.ResponseWriter, r *http.Request, name, reference string) {
|
|
accept := containerManifestAccept(r)
|
|
cacheKey := h.containerManifestCacheKey(name, reference, accept)
|
|
cached, err := h.loadContainerManifest(r.Context(), cacheKey)
|
|
if err != nil {
|
|
h.proxy.Logger.Warn("failed to read cached container manifest", "error", err)
|
|
cached = nil
|
|
}
|
|
|
|
immutable := manifestDigestReferencePattern.MatchString(reference)
|
|
if cached != nil && (immutable || h.containerManifestFresh(cached)) {
|
|
writeContainerManifest(w, r.Method, cached, false)
|
|
return
|
|
}
|
|
|
|
upstreamURL := fmt.Sprintf("%s/v2/%s/manifests/%s", h.registryURL, name, reference)
|
|
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()
|
|
if err := h.storeContainerManifest(r.Context(), cacheKey, cached); err != nil {
|
|
h.proxy.Logger.Warn("failed to refresh cached container manifest", "error", err)
|
|
}
|
|
writeContainerManifest(w, r.Method, cached, false)
|
|
return
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
if cached != nil && shouldServeStaleManifest(resp.StatusCode) {
|
|
writeContainerManifest(w, r.Method, cached, true)
|
|
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
|
|
}
|
|
manifest := &cachedContainerManifest{
|
|
body: body,
|
|
contentType: resp.Header.Get("Content-Type"),
|
|
contentDigest: resp.Header.Get("Docker-Content-Digest"),
|
|
etag: resp.Header.Get("ETag"),
|
|
size: int64(len(body)),
|
|
fetchedAt: time.Now(),
|
|
}
|
|
if manifest.contentDigest == "" {
|
|
manifest.contentDigest = sha256Digest(body)
|
|
}
|
|
if err := h.storeContainerManifest(r.Context(), cacheKey, manifest); err != nil {
|
|
h.proxy.Logger.Warn("failed to cache container manifest", "error", err)
|
|
}
|
|
if manifest.contentDigest != reference && manifestDigestReferencePattern.MatchString(manifest.contentDigest) {
|
|
digestKey := h.containerManifestCacheKey(name, manifest.contentDigest, accept)
|
|
if err := h.storeContainerManifest(r.Context(), digestKey, manifest); err != nil {
|
|
h.proxy.Logger.Warn("failed to cache container manifest by digest", "error", err)
|
|
}
|
|
}
|
|
writeContainerManifest(w, r.Method, manifest, false)
|
|
}
|
|
|
|
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)
|
|
writeContainerManifest(w, r.Method, cached, true)
|
|
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
|
|
}
|
|
|
|
func (h *ContainerHandler) containerManifestCacheKey(name, reference, accept string) string {
|
|
identity := strings.Join([]string{h.registryURL, name, reference, accept}, "\x00")
|
|
sum := sha256.Sum256([]byte(identity))
|
|
return hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
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
|
|
}
|
|
if entry.FetchedAt.Valid {
|
|
manifest.fetchedAt = entry.FetchedAt.Time
|
|
}
|
|
return manifest, nil
|
|
}
|
|
|
|
func (h *ContainerHandler) storeContainerManifest(ctx context.Context, cacheKey string, manifest *cachedContainerManifest) error {
|
|
if h.proxy.DB == nil || h.proxy.Storage == nil {
|
|
return nil
|
|
}
|
|
storagePath := metadataStoragePath(containerManifestCacheEcosystem, cacheKey)
|
|
size, _, err := h.proxy.Storage.Store(ctx, storagePath, bytes.NewReader(manifest.body))
|
|
if err != nil {
|
|
return fmt.Errorf("storing manifest: %w", err)
|
|
}
|
|
manifest.size = size
|
|
return h.proxy.DB.UpsertMetadataCache(&database.MetadataCacheEntry{
|
|
Ecosystem: containerManifestCacheEcosystem,
|
|
Name: cacheKey,
|
|
StoragePath: storagePath,
|
|
ETag: sql.NullString{String: manifest.etag, Valid: manifest.etag != ""},
|
|
ContentType: sql.NullString{String: manifest.contentType, Valid: manifest.contentType != ""},
|
|
ContentDigest: sql.NullString{String: manifest.contentDigest, Valid: manifest.contentDigest != ""},
|
|
Size: sql.NullInt64{Int64: size, Valid: true},
|
|
FetchedAt: sql.NullTime{Time: manifest.fetchedAt, Valid: !manifest.fetchedAt.IsZero()},
|
|
})
|
|
}
|
|
|
|
func writeContainerManifest(w http.ResponseWriter, method string, manifest *cachedContainerManifest, stale bool) {
|
|
if manifest.contentType != "" {
|
|
w.Header().Set("Content-Type", manifest.contentType)
|
|
}
|
|
w.Header().Set("Content-Length", strconv.FormatInt(manifest.size, 10))
|
|
if manifest.contentDigest != "" {
|
|
w.Header().Set("Docker-Content-Digest", manifest.contentDigest)
|
|
}
|
|
if manifest.etag != "" {
|
|
w.Header().Set("ETag", manifest.etag)
|
|
}
|
|
if stale {
|
|
w.Header().Set("Warning", containerStaleWarning)
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
if method != http.MethodHead {
|
|
_, _ = 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",
|
|
}, ", ")
|
|
}
|
|
|
|
func copyContainerManifestHeaders(destination, source http.Header) {
|
|
for _, header := range []string{"Content-Type", "Content-Length", "Docker-Content-Digest", "ETag", "WWW-Authenticate"} {
|
|
if value := source.Get(header); value != "" {
|
|
destination.Set(header, value)
|
|
}
|
|
}
|
|
}
|
|
|
|
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[:])
|
|
}
|