package oci import ( "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "io" "net/http" "strconv" "git.wntrmute.dev/kyle/mcr/internal/auth" "git.wntrmute.dev/kyle/mcr/internal/db" "git.wntrmute.dev/kyle/mcr/internal/policy" ) func (h *Handler) handleManifestGet(w http.ResponseWriter, r *http.Request, repo, reference string) { if !h.checkPolicy(w, r, policy.ActionPull, repo) { return } m, ok := h.resolveManifest(w, repo, reference) if !ok { return } h.audit(r, "manifest_pulled", repo, m.Digest) w.Header().Set("Content-Type", m.MediaType) w.Header().Set("Docker-Content-Digest", m.Digest) w.Header().Set("Content-Length", strconv.FormatInt(m.Size, 10)) w.WriteHeader(http.StatusOK) _, _ = w.Write(m.Content) } func (h *Handler) handleManifestHead(w http.ResponseWriter, r *http.Request, repo, reference string) { if !h.checkPolicy(w, r, policy.ActionPull, repo) { return } m, ok := h.resolveManifest(w, repo, reference) if !ok { return } w.Header().Set("Content-Type", m.MediaType) w.Header().Set("Docker-Content-Digest", m.Digest) w.Header().Set("Content-Length", strconv.FormatInt(m.Size, 10)) w.WriteHeader(http.StatusOK) } // resolveManifest looks up a manifest by tag or digest, writing OCI errors on failure. func (h *Handler) resolveManifest(w http.ResponseWriter, repo, reference string) (*db.ManifestRow, bool) { repoID, err := h.db.GetRepositoryByName(repo) if err != nil { if errors.Is(err, db.ErrRepoNotFound) { writeOCIError(w, "NAME_UNKNOWN", http.StatusNotFound, fmt.Sprintf("repository %q not found", repo)) return nil, false } h.log.Error("manifest resolve: lookup repository", "error", err, "repo", repo) writeOCIError(w, "UNKNOWN", http.StatusInternalServerError, "internal error") return nil, false } var m *db.ManifestRow if isDigest(reference) { m, err = h.db.GetManifestByDigest(repoID, reference) } else { m, err = h.db.GetManifestByTag(repoID, reference) } if err != nil { if errors.Is(err, db.ErrManifestNotFound) { writeOCIError(w, "MANIFEST_UNKNOWN", http.StatusNotFound, fmt.Sprintf("manifest %q not found", reference)) return nil, false } h.log.Error("manifest resolve: lookup manifest", "error", err, "repo", repo, "reference", reference) writeOCIError(w, "UNKNOWN", http.StatusInternalServerError, "internal error") return nil, false } return m, true } // ociManifest is the minimal structure for parsing an OCI image manifest. type ociManifest struct { SchemaVersion int `json:"schemaVersion"` MediaType string `json:"mediaType,omitempty"` Config ociDescriptor `json:"config"` Layers []ociDescriptor `json:"layers"` } // ociDescriptor is a content-addressed reference within a manifest. type ociDescriptor struct { MediaType string `json:"mediaType"` Digest string `json:"digest"` Size int64 `json:"size"` } // supportedManifestMediaTypes lists the manifest media types MCR accepts. var supportedManifestMediaTypes = map[string]bool{ "application/vnd.oci.image.manifest.v1+json": true, "application/vnd.docker.distribution.manifest.v2+json": true, } // handleManifestPut handles PUT /v2//manifests/ func (h *Handler) handleManifestPut(w http.ResponseWriter, r *http.Request, repo, reference string) { if !h.checkPolicy(w, r, policy.ActionPush, repo) { return } // Step 1: Read and parse manifest JSON. body, err := io.ReadAll(r.Body) if err != nil { writeOCIError(w, "MANIFEST_INVALID", http.StatusBadRequest, "failed to read request body") return } if len(body) == 0 { writeOCIError(w, "MANIFEST_INVALID", http.StatusBadRequest, "empty manifest") return } var manifest ociManifest if err := json.Unmarshal(body, &manifest); err != nil { writeOCIError(w, "MANIFEST_INVALID", http.StatusBadRequest, "malformed manifest JSON") return } if manifest.SchemaVersion != 2 { writeOCIError(w, "MANIFEST_INVALID", http.StatusBadRequest, "unsupported schema version") return } // Determine media type from Content-Type header, falling back to manifest body. mediaType := r.Header.Get("Content-Type") if mediaType == "" { mediaType = manifest.MediaType } if mediaType == "" { mediaType = "application/vnd.oci.image.manifest.v1+json" } if !supportedManifestMediaTypes[mediaType] { writeOCIError(w, "MANIFEST_INVALID", http.StatusBadRequest, fmt.Sprintf("unsupported media type: %s", mediaType)) return } // Step 2: Compute SHA-256 digest. sum := sha256.Sum256(body) computedDigest := "sha256:" + hex.EncodeToString(sum[:]) // Step 3: If reference is a digest, verify it matches. tag := "" if isDigest(reference) { if reference != computedDigest { writeOCIError(w, "DIGEST_INVALID", http.StatusBadRequest, fmt.Sprintf("digest mismatch: computed %s, got %s", computedDigest, reference)) return } } else { tag = reference } // Step 4: Collect all referenced blob digests. var blobDigests []string if manifest.Config.Digest != "" { blobDigests = append(blobDigests, manifest.Config.Digest) } for _, layer := range manifest.Layers { if layer.Digest != "" { blobDigests = append(blobDigests, layer.Digest) } } // Step 5: Verify all referenced blobs exist. for _, bd := range blobDigests { exists, err := h.db.BlobExists(bd) if err != nil { h.log.Error("manifest push: check blob exists", "error", err, "repo", repo, "blob_digest", bd) writeOCIError(w, "UNKNOWN", http.StatusInternalServerError, "internal error") return } if !exists { writeOCIError(w, "MANIFEST_BLOB_UNKNOWN", http.StatusBadRequest, fmt.Sprintf("blob %s not found", bd)) return } } // Step 6: Single transaction — create repo, insert manifest, populate // manifest_blobs, upsert tag. params := db.PushManifestParams{ RepoName: repo, Digest: computedDigest, MediaType: mediaType, Content: body, Size: int64(len(body)), Tag: tag, BlobDigests: blobDigests, } if err := h.db.PushManifest(params); err != nil { h.log.Error("manifest push: write to database", "error", err, "repo", repo, "digest", computedDigest, "tag", tag) writeOCIError(w, "UNKNOWN", http.StatusInternalServerError, "internal error") return } // Step 7: Audit and respond. details := map[string]string{} if tag != "" { details["tag"] = tag } if h.auditFn != nil { claims := auth.ClaimsFromContext(r.Context()) actorID := "" if claims != nil { actorID = claims.Subject } h.auditFn("manifest_pushed", actorID, repo, computedDigest, r.RemoteAddr, details) } h.log.Info("manifest pushed", "repo", repo, "digest", computedDigest, "tag", tag, "size", len(body)) w.Header().Set("Location", fmt.Sprintf("/v2/%s/manifests/%s", repo, computedDigest)) w.Header().Set("Docker-Content-Digest", computedDigest) w.Header().Set("Content-Type", mediaType) w.WriteHeader(http.StatusCreated) }