mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-11-09 15:49:16 -05:00
A response arriving during a hold was sent before it began, so a late success no longer releases the host; only one after the hold has ended does. Artifact fetches check RateLimitGate.Held before calling the fetcher, so held cache misses fail with the HeldError without counting as failures in the host's circuit breaker. Every handler that answers an upstream fetch error with 502 now answers a held request with 503 and Retry-After instead. OnHold and RecordUpstreamRateLimitHold drop the unused duration.
209 lines
6.8 KiB
Go
209 lines
6.8 KiB
Go
package handler
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/git-pkgs/proxy/internal/retention"
|
|
)
|
|
|
|
const (
|
|
conanUpstream = "https://center.conan.io"
|
|
)
|
|
|
|
// Recipe and package archives are stored under pkg:conan/{name}, with the
|
|
// version as the version and the revisions and any user/channel in the
|
|
// filename, and purl keeps conan names as they are. A package rule therefore
|
|
// covers every version, revision and user/channel of a recipe, and uses the
|
|
// default key mapping.
|
|
func init() {
|
|
retention.Default.Register(retention.Spec{Key: "conan", CanonicalPackage: retention.DefaultCanonical("conan")})
|
|
}
|
|
|
|
// ConanHandler handles Conan registry protocol requests.
|
|
type ConanHandler struct {
|
|
proxy *Proxy
|
|
upstreamURL string
|
|
proxyURL string
|
|
}
|
|
|
|
// NewConanHandler creates a new Conan protocol handler.
|
|
func NewConanHandler(proxy *Proxy, proxyURL string) *ConanHandler {
|
|
return &ConanHandler{
|
|
proxy: proxy,
|
|
upstreamURL: conanUpstream,
|
|
proxyURL: strings.TrimSuffix(proxyURL, "/"),
|
|
}
|
|
}
|
|
|
|
// NewConanHandlerWithUpstream creates a Conan handler with a custom upstream.
|
|
func NewConanHandlerWithUpstream(proxy *Proxy, proxyURL, upstreamURL string) *ConanHandler {
|
|
h := NewConanHandler(proxy, proxyURL)
|
|
h.upstreamURL = configuredUpstreamURL(upstreamURL, conanUpstream)
|
|
return h
|
|
}
|
|
|
|
// Routes returns the HTTP handler for Conan requests.
|
|
func (h *ConanHandler) Routes() http.Handler {
|
|
mux := http.NewServeMux()
|
|
|
|
// Ping endpoint
|
|
mux.HandleFunc("GET /v1/ping", h.handlePing)
|
|
mux.HandleFunc("GET /v2/ping", h.handlePing)
|
|
|
|
// Recipe and package file downloads, as built by the Conan 2 client
|
|
// (ClientV2Router.recipe_file and package_file). The v1 protocol hands out
|
|
// signed absolute URLs on the upstream host, so its downloads never pass
|
|
// through here.
|
|
mux.HandleFunc("GET /v2/conans/{name}/{version}/{user}/{channel}/revisions/{revision}/files/{filename}", h.handleRecipeFile)
|
|
mux.HandleFunc("GET /v2/conans/{name}/{version}/{user}/{channel}/revisions/{revision}/packages/{pkgref}/revisions/{pkgrev}/files/{filename}", h.handlePackageFile)
|
|
|
|
// Proxy all other endpoints (metadata, search, etc.) with caching
|
|
mux.HandleFunc("GET /", h.proxyCached)
|
|
|
|
return mux
|
|
}
|
|
|
|
// handlePing responds to Conan ping requests.
|
|
func (h *ConanHandler) handlePing(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("X-Conan-Server-Capabilities", "revisions")
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
|
|
// handleRecipeFile serves a recipe file, fetching and caching from upstream if needed.
|
|
func (h *ConanHandler) handleRecipeFile(w http.ResponseWriter, r *http.Request) {
|
|
name := r.PathValue("name")
|
|
version := r.PathValue("version")
|
|
user := r.PathValue("user")
|
|
channel := r.PathValue("channel")
|
|
revision := r.PathValue("revision")
|
|
filename := r.PathValue("filename")
|
|
|
|
// Only the archives are artifacts; the rest goes through the metadata cache.
|
|
if !h.shouldCacheFile(filename) {
|
|
h.proxyCached(w, r)
|
|
return
|
|
}
|
|
|
|
h.proxy.Logger.Info("conan recipe download",
|
|
"name", name, "version", version, "user", user, "channel", channel, "filename", filename)
|
|
|
|
upstreamURL := h.upstreamURL + r.URL.Path
|
|
storageFilename := conanStorageFilename(user, channel, revision+"_"+filename)
|
|
|
|
result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "conan", name, version, storageFilename, upstreamURL)
|
|
if err != nil {
|
|
h.proxy.serveArtifactError(w, err, "failed to fetch file")
|
|
return
|
|
}
|
|
|
|
ServeArtifactRequest(w, r, result)
|
|
}
|
|
|
|
// handlePackageFile serves a package file, fetching and caching from upstream if needed.
|
|
func (h *ConanHandler) handlePackageFile(w http.ResponseWriter, r *http.Request) {
|
|
name := r.PathValue("name")
|
|
version := r.PathValue("version")
|
|
user := r.PathValue("user")
|
|
channel := r.PathValue("channel")
|
|
revision := r.PathValue("revision")
|
|
pkgref := r.PathValue("pkgref")
|
|
pkgrev := r.PathValue("pkgrev")
|
|
filename := r.PathValue("filename")
|
|
|
|
// Only the archives are artifacts; the rest goes through the metadata cache.
|
|
if !h.shouldCacheFile(filename) {
|
|
h.proxyCached(w, r)
|
|
return
|
|
}
|
|
|
|
h.proxy.Logger.Info("conan package download",
|
|
"name", name, "version", version, "pkgref", pkgref, "filename", filename)
|
|
|
|
upstreamURL := h.upstreamURL + r.URL.Path
|
|
storageFilename := conanStorageFilename(user, channel, revision+"_"+pkgref+"_"+pkgrev+"_"+filename)
|
|
|
|
result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "conan", name, version, storageFilename, upstreamURL)
|
|
if err != nil {
|
|
h.proxy.serveArtifactError(w, err, "failed to fetch file")
|
|
return
|
|
}
|
|
|
|
ServeArtifactRequest(w, r, result)
|
|
}
|
|
|
|
// conanStorageFilename names a cached archive. Archives are stored under
|
|
// pkg:conan/{name}@{version}, so the file name carries the revisions and, when
|
|
// set, user and channel, to keep every recipe and binary apart.
|
|
func conanStorageFilename(user, channel, file string) string {
|
|
if user == "_" && channel == "_" {
|
|
return file
|
|
}
|
|
return user + "@" + channel + "_" + file
|
|
}
|
|
|
|
// shouldCacheFile returns true if the file should be cached.
|
|
func (h *ConanHandler) shouldCacheFile(filename string) bool {
|
|
// Cache the large archive files. Since Conan 2.25 they can also be xz or
|
|
// zstd (core.upload:compression_format).
|
|
cacheFiles := []string{
|
|
"conan_sources.tgz", "conan_sources.txz", "conan_sources.tzst",
|
|
"conan_export.tgz", "conan_export.txz", "conan_export.tzst",
|
|
"conan_package.tgz", "conan_package.txz", "conan_package.tzst",
|
|
}
|
|
|
|
for _, f := range cacheFiles {
|
|
if filename == f {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// proxyCached forwards a request with metadata caching.
|
|
func (h *ConanHandler) proxyCached(w http.ResponseWriter, r *http.Request) {
|
|
cacheKey := strings.TrimPrefix(r.URL.Path, "/")
|
|
cacheKey = strings.ReplaceAll(cacheKey, "/", "_")
|
|
if r.URL.RawQuery != "" {
|
|
cacheKey += "_" + r.URL.RawQuery
|
|
}
|
|
upstreamURL := h.upstreamURL + r.URL.Path
|
|
if r.URL.RawQuery != "" {
|
|
upstreamURL += "?" + r.URL.RawQuery
|
|
}
|
|
h.proxy.ProxyCached(w, r, upstreamURL, "conan", cacheKey, "*/*")
|
|
}
|
|
|
|
// proxyUpstream forwards a request to conan center without caching.
|
|
func (h *ConanHandler) proxyUpstream(w http.ResponseWriter, r *http.Request) {
|
|
upstreamURL := h.upstreamURL + r.URL.Path
|
|
if r.URL.RawQuery != "" {
|
|
upstreamURL += "?" + r.URL.RawQuery
|
|
}
|
|
|
|
h.proxy.Logger.Debug("proxying to upstream", "url", upstreamURL)
|
|
|
|
req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, upstreamURL, nil)
|
|
if err != nil {
|
|
http.Error(w, "failed to create request", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Copy authorization header for authenticated requests
|
|
if auth := r.Header.Get("Authorization"); auth != "" {
|
|
req.Header.Set("Authorization", auth)
|
|
}
|
|
|
|
resp, err := h.proxy.HTTPClient.Do(req)
|
|
if err != nil {
|
|
if writeUpstreamHeld(w, err) {
|
|
return
|
|
}
|
|
h.proxy.Logger.Error("upstream request failed", "error", err)
|
|
http.Error(w, "upstream request failed", http.StatusBadGateway)
|
|
return
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
h.proxy.relayResponse(w, r, resp, nil)
|
|
}
|