Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-11-10 08:09:17 -05:00
pkg-proxy/internal/handler/conan.go

209 lines
6.8 KiB
Go
Raw Permalink Normal View History

2026-01-20 21:52:44 +00:00
package handler
import (
"net/http"
"strings"
"github.com/git-pkgs/proxy/internal/retention"
2026-01-20 21:52:44 +00:00
)
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")})
}
2026-01-20 21:52:44 +00:00
// 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
}
2026-01-20 21:52:44 +00:00
// 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)
2026-01-20 21:52:44 +00:00
// Proxy all other endpoints (metadata, search, etc.) with caching
mux.HandleFunc("GET /", h.proxyCached)
2026-01-20 21:52:44 +00:00
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.
2026-01-20 21:52:44 +00:00
if !h.shouldCacheFile(filename) {
h.proxyCached(w, r)
2026-01-20 21:52:44 +00:00
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)
2026-01-20 21:52:44 +00:00
result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "conan", name, version, storageFilename, upstreamURL)
2026-01-20 21:52:44 +00:00
if err != nil {
h.proxy.serveArtifactError(w, err, "failed to fetch file")
2026-01-20 21:52:44 +00:00
return
}
Support HTTP byte-range requests for local file cached artifact downloads (#404) * feat(storage): expose efficient seek for local files Open file-backed artifacts with os.Open so local readers support efficient seeking. Leave cloud storage readers unchanged. Keep the legacy sidecar mapping test focused on fileblob's reader: Blob.Open now reads local files directly and no longer consults the sidecar. Continue checking sidecar cleanup and post-store reads. * feat(handler): preserve efficient seek through integrity checks Keep the cache result's single Reader field and preserve io.Seeker when the underlying storage reader supports efficient seeking. Sequential reads continue through whole-object integrity verification. A successful seek switches subsequent reads to the source for range responses, where full-object verification is not possible. Test both modes and verify the source is closed once. * feat(handler): add request-aware byte range responses Add a request-aware artifact serving helper for bounded, open-ended, and suffix byte ranges, including If-Range handling, 206 responses, and 416 responses for valid unsatisfiable ranges. Ignore malformed and multi-range requests by serving the full artifact. Advertise byte-range support only for seekable readers, preserving HEAD, redirect, and non-seekable behavior. Clarify that net/http recovers ErrAbortHandler per request and keeps the server running. * feat(handler): enable byte ranges across artifact downloads Forward each artifact request to the shared range-aware response helper, including cached generic release assets and OCI blobs. This lets the handler honor ranges only when its reader supports efficient seeking, without changing direct-storage redirect behavior. Add warm-cache endpoint tests that assert partial response headers and bytes without an upstream refetch. Document supported range behavior, unsupported reader fallback, and the integrity limitation of partial reads.
2026-10-04 09:48:41 -07:00
ServeArtifactRequest(w, r, result)
2026-01-20 21:52:44 +00:00
}
// 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.
2026-01-20 21:52:44 +00:00
if !h.shouldCacheFile(filename) {
h.proxyCached(w, r)
2026-01-20 21:52:44 +00:00
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)
2026-01-20 21:52:44 +00:00
result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "conan", name, version, storageFilename, upstreamURL)
2026-01-20 21:52:44 +00:00
if err != nil {
h.proxy.serveArtifactError(w, err, "failed to fetch file")
2026-01-20 21:52:44 +00:00
return
}
Support HTTP byte-range requests for local file cached artifact downloads (#404) * feat(storage): expose efficient seek for local files Open file-backed artifacts with os.Open so local readers support efficient seeking. Leave cloud storage readers unchanged. Keep the legacy sidecar mapping test focused on fileblob's reader: Blob.Open now reads local files directly and no longer consults the sidecar. Continue checking sidecar cleanup and post-store reads. * feat(handler): preserve efficient seek through integrity checks Keep the cache result's single Reader field and preserve io.Seeker when the underlying storage reader supports efficient seeking. Sequential reads continue through whole-object integrity verification. A successful seek switches subsequent reads to the source for range responses, where full-object verification is not possible. Test both modes and verify the source is closed once. * feat(handler): add request-aware byte range responses Add a request-aware artifact serving helper for bounded, open-ended, and suffix byte ranges, including If-Range handling, 206 responses, and 416 responses for valid unsatisfiable ranges. Ignore malformed and multi-range requests by serving the full artifact. Advertise byte-range support only for seekable readers, preserving HEAD, redirect, and non-seekable behavior. Clarify that net/http recovers ErrAbortHandler per request and keeps the server running. * feat(handler): enable byte ranges across artifact downloads Forward each artifact request to the shared range-aware response helper, including cached generic release assets and OCI blobs. This lets the handler honor ranges only when its reader supports efficient seeking, without changing direct-storage redirect behavior. Add warm-cache endpoint tests that assert partial response headers and bytes without an upstream refetch. Document supported range behavior, unsupported reader fallback, and the integrity limitation of partial reads.
2026-10-04 09:48:41 -07:00
ServeArtifactRequest(w, r, result)
2026-01-20 21:52:44 +00:00
}
// 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
}
2026-01-20 21:52:44 +00:00
// 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).
2026-01-20 21:52:44 +00:00
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",
2026-01-20 21:52:44 +00:00
}
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, "*/*")
}
2026-01-20 21:52:44 +00:00
// 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)
2026-01-20 21:52:44 +00:00
if err != nil {
if writeUpstreamHeld(w, err) {
return
}
2026-01-20 21:52:44 +00:00
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)
2026-01-20 21:52:44 +00:00
}