2026-01-20 21:52:44 +00:00
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
"net/http"
|
|
|
|
|
"strings"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
conanUpstream = "https://center.conan.io"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// 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, "/"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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 file downloads (cache these)
|
|
|
|
|
mux.HandleFunc("GET /v1/files/{name}/{version}/{user}/{channel}/{revision}/recipe/{filename}", h.handleRecipeFile)
|
|
|
|
|
mux.HandleFunc("GET /v2/files/{name}/{version}/{user}/{channel}/{revision}/recipe/{filename}", h.handleRecipeFile)
|
|
|
|
|
|
|
|
|
|
// Package file downloads (cache these)
|
|
|
|
|
mux.HandleFunc("GET /v1/files/{name}/{version}/{user}/{channel}/{revision}/package/{pkgref}/{pkgrev}/{filename}", h.handlePackageFile)
|
|
|
|
|
mux.HandleFunc("GET /v2/files/{name}/{version}/{user}/{channel}/{revision}/package/{pkgref}/{pkgrev}/{filename}", h.handlePackageFile)
|
|
|
|
|
|
2026-03-19 21:06:02 +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 cache specific files
|
|
|
|
|
if !h.shouldCacheFile(filename) {
|
|
|
|
|
h.proxyUpstream(w, r)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Conan package name format: name/version@user/channel
|
|
|
|
|
packageName := fmt.Sprintf("%s/%s@%s/%s", name, version, user, channel)
|
|
|
|
|
|
|
|
|
|
h.proxy.Logger.Info("conan recipe download",
|
|
|
|
|
"name", name, "version", version, "user", user, "channel", channel, "filename", filename)
|
|
|
|
|
|
|
|
|
|
upstreamURL := h.upstreamURL + r.URL.Path
|
|
|
|
|
|
|
|
|
|
// Use revision as part of version for storage
|
|
|
|
|
storageVersion := fmt.Sprintf("%s_%s", version, revision)
|
|
|
|
|
storageFilename := fmt.Sprintf("recipe_%s", filename)
|
|
|
|
|
|
|
|
|
|
result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "conan", packageName, storageVersion, storageFilename, upstreamURL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
h.proxy.Logger.Error("failed to get artifact", "error", err)
|
|
|
|
|
http.Error(w, "failed to fetch file", http.StatusBadGateway)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ServeArtifact(w, 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 cache specific files
|
|
|
|
|
if !h.shouldCacheFile(filename) {
|
|
|
|
|
h.proxyUpstream(w, r)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
packageName := fmt.Sprintf("%s/%s@%s/%s", name, version, user, channel)
|
|
|
|
|
|
|
|
|
|
h.proxy.Logger.Info("conan package download",
|
|
|
|
|
"name", name, "version", version, "pkgref", pkgref, "filename", filename)
|
|
|
|
|
|
|
|
|
|
upstreamURL := h.upstreamURL + r.URL.Path
|
|
|
|
|
|
|
|
|
|
// Use revision and package ref as part of version for storage
|
|
|
|
|
storageVersion := fmt.Sprintf("%s_%s_%s_%s", version, revision, pkgref, pkgrev)
|
|
|
|
|
storageFilename := fmt.Sprintf("package_%s", filename)
|
|
|
|
|
|
|
|
|
|
result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "conan", packageName, storageVersion, storageFilename, upstreamURL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
h.proxy.Logger.Error("failed to get artifact", "error", err)
|
|
|
|
|
http.Error(w, "failed to fetch file", http.StatusBadGateway)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ServeArtifact(w, result)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// shouldCacheFile returns true if the file should be cached.
|
|
|
|
|
func (h *ConanHandler) shouldCacheFile(filename string) bool {
|
|
|
|
|
// Cache the large archive files
|
|
|
|
|
cacheFiles := []string{
|
|
|
|
|
"conan_sources.tgz",
|
|
|
|
|
"conan_export.tgz",
|
|
|
|
|
"conan_package.tgz",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, f := range cacheFiles {
|
|
|
|
|
if filename == f {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-19 21:06:02 +00:00
|
|
|
// 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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-13 07:44:11 +00:00
|
|
|
resp, err := h.proxy.HTTPClient.Do(req)
|
2026-01-20 21:52:44 +00:00
|
|
|
if err != nil {
|
|
|
|
|
h.proxy.Logger.Error("upstream request failed", "error", err)
|
|
|
|
|
http.Error(w, "upstream request failed", http.StatusBadGateway)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
|
|
|
|
|
|
// Copy response headers
|
|
|
|
|
for k, vv := range resp.Header {
|
|
|
|
|
for _, v := range vv {
|
|
|
|
|
w.Header().Add(k, v)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
w.WriteHeader(resp.StatusCode)
|
|
|
|
|
_, _ = io.Copy(w, resp.Body)
|
|
|
|
|
}
|