mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-09-16 07:42:05 -04:00
* Use shared artifacts at cache boundaries * Address artifact cache review * Defer cached artifact validation to checkCache Construct artifacts.Artifact from the row without validation so a malformed content hash reaches newIntegrityChecks in checkCache, which clears the row and treats the request as a miss. Erroring at the DB boundary instead surfaced a 500 and left the bad row in place. * Build stored Artifact after digest and scan checks Construct the shared artifact struct from trusted storage output as a literal, after the hash-mismatch and scanner paths that delete failed downloads, so no path between Store and updateCacheDB can leave bytes in storage without a database row.
186 lines
6.7 KiB
Go
186 lines
6.7 KiB
Go
package handler
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"net/http"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
apkEcosystem = "alpine"
|
|
// defaultAPKRepositoryName is the repository name used when no
|
|
// upstream.apk repositories are configured.
|
|
defaultAPKRepositoryName = "alpine"
|
|
// defaultAPKUpstream is the official Alpine Linux mirror.
|
|
defaultAPKUpstream = "https://dl-cdn.alpinelinux.org/alpine"
|
|
apkMatchCount = 3 // full match + name + version
|
|
)
|
|
|
|
// APKHandler handles Alpine APK repository protocol requests. Each configured
|
|
// upstream repository is mounted at /apk/{repository}/ and the remaining path
|
|
// mirrors the upstream layout ({release}/{repo}/{arch}/{file}).
|
|
//
|
|
// Repository indexes (v2 APKINDEX.tar.gz, v3 Packages.adb) and detached
|
|
// signatures are served byte-for-byte unchanged through the metadata cache so
|
|
// apk signature verification keeps working. Package files are cached in the
|
|
// shared artifact cache and stay available when the upstream is unreachable.
|
|
type APKHandler struct {
|
|
proxy *Proxy
|
|
proxyURL string
|
|
repositories map[string]string
|
|
}
|
|
|
|
// NewAPKHandler creates an Alpine APK repository protocol handler.
|
|
// When repositories is empty, a single repository named "alpine" pointing at
|
|
// the official Alpine mirror is used.
|
|
func NewAPKHandler(proxy *Proxy, proxyURL string, repositories map[string]string) *APKHandler {
|
|
h := &APKHandler{
|
|
proxy: proxy,
|
|
proxyURL: strings.TrimSuffix(proxyURL, "/"),
|
|
repositories: make(map[string]string, len(repositories)),
|
|
}
|
|
for name, repositoryURL := range repositories {
|
|
h.repositories[name] = strings.TrimSuffix(repositoryURL, "/")
|
|
}
|
|
if len(h.repositories) == 0 {
|
|
h.repositories[defaultAPKRepositoryName] = defaultAPKUpstream
|
|
}
|
|
return h
|
|
}
|
|
|
|
// Routes returns the HTTP handler for APK requests.
|
|
// Mount this at /apk on your router.
|
|
func (h *APKHandler) Routes() http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet && r.Method != http.MethodHead {
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
path := strings.TrimPrefix(r.URL.Path, "/")
|
|
|
|
if containsPathTraversal(path) {
|
|
http.Error(w, "invalid path", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
repository, rest, ok := strings.Cut(path, "/")
|
|
upstreamURL, found := h.repositories[repository]
|
|
if !ok || rest == "" || !found {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
switch {
|
|
case isAPKIndex(rest) || isAPKSignature(rest):
|
|
// Indexes and detached signatures are signed upstream metadata.
|
|
// Cache them with the metadata TTL and serve the stored bytes
|
|
// unchanged so apk verification continues to work.
|
|
h.handleMetadata(w, r, repository, upstreamURL, rest)
|
|
case strings.HasSuffix(rest, ".apk"):
|
|
// Package downloads - cache these in the artifact cache.
|
|
h.handlePackageDownload(w, r, repository, upstreamURL, rest)
|
|
default:
|
|
// Other files - proxy directly.
|
|
h.proxyFile(w, r, upstreamURL, rest)
|
|
}
|
|
})
|
|
}
|
|
|
|
// isAPKIndex reports whether the path names a repository index:
|
|
// APKINDEX.tar.gz (apk v2) or Packages.adb (apk v3).
|
|
func isAPKIndex(path string) bool {
|
|
base := path[strings.LastIndex(path, "/")+1:]
|
|
return base == "APKINDEX.tar.gz" || base == "Packages.adb"
|
|
}
|
|
|
|
// isAPKSignature reports whether the path names a detached signature file.
|
|
func isAPKSignature(path string) bool {
|
|
return strings.HasSuffix(path, ".sig") || strings.HasSuffix(path, ".rsa.pub")
|
|
}
|
|
|
|
// handlePackageDownload fetches and caches .apk packages.
|
|
// Path format: {release}/{repo}/{arch}/{name}-{version}-r{rel}.apk
|
|
// Example: v3.22/main/x86_64/busybox-1.37.0-r12.apk
|
|
//
|
|
// APK filenames do not include the architecture, so the same filename can hold
|
|
// different bytes per architecture (and per release). The full request path is
|
|
// therefore part of the cache identity.
|
|
func (h *APKHandler) handlePackageDownload(w http.ResponseWriter, r *http.Request, repository, upstreamURL, path string) {
|
|
name, version, arch := h.parseAPKPath(path)
|
|
if name == "" {
|
|
// Can't parse, just proxy directly
|
|
h.proxyFile(w, r, upstreamURL, path)
|
|
return
|
|
}
|
|
|
|
downloadURL := upstreamURL + "/" + path
|
|
cacheFilename := repository + "/" + path
|
|
|
|
h.proxy.Logger.Info("apk package download",
|
|
"repository", repository, "name", name, "version", version, "arch", arch)
|
|
|
|
result, err := h.proxy.GetOrFetchArtifactFromURL(
|
|
r.Context(), apkEcosystem, name, version, cacheFilename, downloadURL)
|
|
if err != nil {
|
|
h.proxy.serveArtifactError(w, err, "failed to fetch package")
|
|
return
|
|
}
|
|
|
|
if result.Artifact.MediaType == "" {
|
|
result.Artifact.MediaType = "application/octet-stream"
|
|
}
|
|
serveArtifact(w, r.Method, result)
|
|
}
|
|
|
|
// handleMetadata serves repository indexes and signatures through the
|
|
// metadata cache. Stored bytes are re-served verbatim, which keeps embedded
|
|
// and detached signatures valid.
|
|
func (h *APKHandler) handleMetadata(w http.ResponseWriter, r *http.Request, repository, upstreamURL, path string) {
|
|
h.proxy.ProxyCached(w, r, upstreamURL+"/"+path, apkEcosystem,
|
|
h.metadataCacheKey(repository, upstreamURL, path), "*/*")
|
|
}
|
|
|
|
// metadataCacheKey derives the metadata cache key from the repository name,
|
|
// its upstream URL, and the request path. Hashing the identity keeps distinct
|
|
// repositories from sharing cache entries (repository names may contain '_'
|
|
// and a separator-based key would be ambiguous) and drops cached entries when
|
|
// a repository is repointed at a different upstream, mirroring
|
|
// HelmHandler.indexCacheKey.
|
|
func (h *APKHandler) metadataCacheKey(repository, upstreamURL, path string) string {
|
|
identity := repository + "\x00" + upstreamURL + "\x00" + path
|
|
digest := sha256.Sum256([]byte(identity))
|
|
return hex.EncodeToString(digest[:])
|
|
}
|
|
|
|
// proxyFile proxies any file directly without caching.
|
|
func (h *APKHandler) proxyFile(w http.ResponseWriter, r *http.Request, upstreamURL, path string) {
|
|
h.proxy.ProxyFile(w, r, upstreamURL+"/"+path)
|
|
}
|
|
|
|
// apkPackagePattern matches .apk filenames to extract name and version.
|
|
// Format: {name}-{version}-r{rel}.apk where version starts with a digit.
|
|
// Examples:
|
|
// - busybox-1.37.0-r12.apk
|
|
// - alpine-baselayout-data-3.7.0-r0.apk
|
|
var apkPackagePattern = regexp.MustCompile(`^(.+)-(\d[^-]*-r\d+)\.apk$`)
|
|
|
|
// parseAPKPath extracts package info from a path containing an APK filename.
|
|
// The architecture is taken from the parent directory since APK filenames do
|
|
// not include it.
|
|
func (h *APKHandler) parseAPKPath(path string) (name, version, arch string) {
|
|
segments := strings.Split(path, "/")
|
|
filename := segments[len(segments)-1]
|
|
if len(segments) > 1 {
|
|
arch = segments[len(segments)-2]
|
|
}
|
|
|
|
matches := apkPackagePattern.FindStringSubmatch(filename)
|
|
if len(matches) != apkMatchCount {
|
|
return "", "", ""
|
|
}
|
|
|
|
return matches[1], matches[2], arch
|
|
}
|