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.
160 lines
5.9 KiB
Go
160 lines
5.9 KiB
Go
package handler
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"net/http"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
genericEcosystem = "generic"
|
|
// genericAcceptAny is always sent for metadata so every client shares the
|
|
// same cached representation, regardless of its Accept header.
|
|
genericAcceptAny = "*/*"
|
|
// githubReleaseAssetMatchCount is the full match plus owner, repository,
|
|
// tag and asset filename.
|
|
githubReleaseAssetMatchCount = 5
|
|
)
|
|
|
|
// githubReleaseAssetPattern matches the path of a GitHub release asset
|
|
// download, {owner}/{repo}/releases/download/{tag}/{asset}. A tag pins the
|
|
// asset to one release, so these downloads are cached in the artifact cache
|
|
// and served without revalidation once fetched.
|
|
var githubReleaseAssetPattern = regexp.MustCompile(`^([^/]+)/([^/]+)/releases/download/([^/]+)/([^/]+)$`)
|
|
|
|
// GenericHandler proxies plain HTTP downloads from configured upstream base
|
|
// URLs. Each configured upstream is mounted at /generic/{name}/ and the
|
|
// remaining request path (and query string) is appended to the upstream URL.
|
|
//
|
|
// Only configured upstreams are reachable, so the proxy is not an open HTTP
|
|
// proxy. The handler is the caching layer behind tools that download from
|
|
// fixed URL shapes, such as mise's aqua backend fetching GitHub release
|
|
// assets, and is pointed at by URL-rewriting settings on the client.
|
|
//
|
|
// Release-asset paths ({owner}/{repo}/releases/download/{tag}/{asset}) are
|
|
// version-pinned and cached in the shared artifact cache, so they keep being
|
|
// served when the upstream is unreachable. Every other path is served through
|
|
// the metadata cache: fresh within the metadata TTL, revalidated with the
|
|
// upstream's validators after that, and served stale when the upstream fails
|
|
// or refuses the request. That covers API responses such as
|
|
// api.github.com/repos/{owner}/{repo}/releases/tags/{tag}.
|
|
type GenericHandler struct {
|
|
proxy *Proxy
|
|
repositories map[string]string
|
|
}
|
|
|
|
// NewGenericHandler creates a generic HTTP download proxy handler.
|
|
func NewGenericHandler(proxy *Proxy, repositories map[string]string) *GenericHandler {
|
|
h := &GenericHandler{
|
|
proxy: proxy,
|
|
repositories: make(map[string]string, len(repositories)),
|
|
}
|
|
for name, upstreamURL := range repositories {
|
|
h.repositories[name] = strings.TrimSuffix(upstreamURL, "/")
|
|
}
|
|
return h
|
|
}
|
|
|
|
// Routes returns the HTTP handler for generic download requests.
|
|
// Mount this at /generic on your router.
|
|
func (h *GenericHandler) 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
|
|
}
|
|
|
|
if asset, ok := parseGitHubReleaseAsset(rest); ok {
|
|
h.handleReleaseAsset(w, r, repository, upstreamURL, rest, asset)
|
|
return
|
|
}
|
|
|
|
h.handleMetadata(w, r, repository, upstreamURL, rest)
|
|
})
|
|
}
|
|
|
|
// githubReleaseAsset is the identity of a version-pinned release download.
|
|
type githubReleaseAsset struct {
|
|
owner string
|
|
repo string
|
|
tag string
|
|
filename string
|
|
}
|
|
|
|
// parseGitHubReleaseAsset extracts the release identity from a path shaped
|
|
// like {owner}/{repo}/releases/download/{tag}/{asset}.
|
|
func parseGitHubReleaseAsset(path string) (githubReleaseAsset, bool) {
|
|
matches := githubReleaseAssetPattern.FindStringSubmatch(path)
|
|
if len(matches) != githubReleaseAssetMatchCount {
|
|
return githubReleaseAsset{}, false
|
|
}
|
|
return githubReleaseAsset{
|
|
owner: matches[1],
|
|
repo: matches[2],
|
|
tag: matches[3],
|
|
filename: matches[4],
|
|
}, true
|
|
}
|
|
|
|
// handleReleaseAsset fetches and caches a version-pinned release asset in the
|
|
// artifact cache. The configured upstream name is part of the cache identity
|
|
// so two upstreams serving the same path never share bytes.
|
|
func (h *GenericHandler) handleReleaseAsset(w http.ResponseWriter, r *http.Request, repository, upstreamURL, path string, asset githubReleaseAsset) {
|
|
name := asset.owner + "/" + asset.repo
|
|
downloadURL := upstreamURL + "/" + path
|
|
cacheFilename := repository + "/" + asset.filename
|
|
|
|
h.proxy.Logger.Info("generic release asset download",
|
|
"repository", repository, "name", name, "version", asset.tag, "filename", asset.filename)
|
|
|
|
result, err := h.proxy.GetOrFetchArtifactFromURL(
|
|
r.Context(), genericEcosystem, name, asset.tag, cacheFilename, downloadURL)
|
|
if err != nil {
|
|
h.proxy.serveArtifactError(w, err, "failed to fetch release asset")
|
|
return
|
|
}
|
|
|
|
if result.Artifact.MediaType == "" {
|
|
result.Artifact.MediaType = "application/octet-stream"
|
|
}
|
|
serveArtifact(w, r.Method, result)
|
|
}
|
|
|
|
// handleMetadata serves any other path through the metadata cache. The query
|
|
// string is forwarded and is part of the cache identity. A fixed Accept header
|
|
// keeps all clients on one cached representation.
|
|
func (h *GenericHandler) handleMetadata(w http.ResponseWriter, r *http.Request, repository, upstreamURL, path string) {
|
|
target := upstreamURL + "/" + path
|
|
if r.URL.RawQuery != "" {
|
|
target += "?" + r.URL.RawQuery
|
|
}
|
|
|
|
h.proxy.ProxyCached(w, r, target, genericEcosystem,
|
|
h.metadataCacheKey(repository, upstreamURL, path, r.URL.RawQuery), genericAcceptAny)
|
|
}
|
|
|
|
// metadataCacheKey derives the metadata cache key from the upstream name, its
|
|
// URL, the request path and query. Hashing the identity keeps distinct
|
|
// upstreams from sharing entries and drops cached entries when an upstream is
|
|
// repointed, mirroring APKHandler.metadataCacheKey.
|
|
func (h *GenericHandler) metadataCacheKey(repository, upstreamURL, path, query string) string {
|
|
identity := repository + "\x00" + upstreamURL + "\x00" + path + "\x00" + query
|
|
digest := sha256.Sum256([]byte(identity))
|
|
return hex.EncodeToString(digest[:])
|
|
}
|