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.
235 lines
6.4 KiB
Go
235 lines
6.4 KiB
Go
package handler
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/git-pkgs/proxy/internal/retention"
|
|
)
|
|
|
|
const (
|
|
cargoUpstream = "https://index.crates.io"
|
|
cargoDownloadBase = "https://static.crates.io/crates"
|
|
|
|
cargoIndexLen1 = 1
|
|
cargoIndexLen2 = 2
|
|
cargoIndexLen3 = 3
|
|
)
|
|
|
|
// Crates are stored under pkg:cargo/{name}, with the name cargo puts in the
|
|
// download URL, which is the name as published. purl keeps cargo names as
|
|
// they are, so package rules use the default key mapping and have to match
|
|
// that name exactly.
|
|
func init() {
|
|
retention.Default.Register(retention.Spec{Key: "cargo", CanonicalPackage: retention.DefaultCanonical("cargo")})
|
|
}
|
|
|
|
// CargoHandler handles cargo registry protocol requests.
|
|
type CargoHandler struct {
|
|
proxy *Proxy
|
|
indexURL string
|
|
downloadURL string
|
|
proxyURL string
|
|
}
|
|
|
|
// NewCargoHandler creates a new cargo protocol handler.
|
|
func NewCargoHandler(proxy *Proxy, proxyURL, indexURL, downloadURL string) *CargoHandler {
|
|
if strings.TrimSpace(indexURL) == "" {
|
|
indexURL = cargoUpstream
|
|
}
|
|
if strings.TrimSpace(downloadURL) == "" {
|
|
downloadURL = cargoDownloadBase
|
|
}
|
|
|
|
return &CargoHandler{
|
|
proxy: proxy,
|
|
indexURL: strings.TrimSuffix(indexURL, "/"),
|
|
downloadURL: strings.TrimSuffix(downloadURL, "/"),
|
|
proxyURL: strings.TrimSuffix(proxyURL, "/"),
|
|
}
|
|
}
|
|
|
|
// Routes returns the HTTP handler for cargo requests.
|
|
// Mount this at /cargo on your router.
|
|
func (h *CargoHandler) Routes() http.Handler {
|
|
mux := http.NewServeMux()
|
|
|
|
// Config endpoint
|
|
mux.HandleFunc("GET /config.json", h.handleConfig)
|
|
|
|
// Sparse index endpoints
|
|
// Crate names 1-2 chars: /1/{name} or /2/{name}
|
|
// Crate names 3 chars: /3/{first_char}/{name}
|
|
// Crate names 4+ chars: /{first_two}/{second_two}/{name}
|
|
mux.HandleFunc("GET /1/{name}", h.handleIndex)
|
|
mux.HandleFunc("GET /2/{name}", h.handleIndex)
|
|
mux.HandleFunc("GET /3/{a}/{name}", h.handleIndex)
|
|
mux.HandleFunc("GET /{a}/{b}/{name}", h.handleIndex)
|
|
|
|
// Download endpoint
|
|
mux.HandleFunc("GET /crates/{name}/{version}/download", h.handleDownload)
|
|
|
|
return mux
|
|
}
|
|
|
|
// CargoConfig is the registry configuration returned by config.json.
|
|
type CargoConfig struct {
|
|
DL string `json:"dl"`
|
|
API string `json:"api,omitempty"`
|
|
}
|
|
|
|
// handleConfig returns the registry configuration.
|
|
func (h *CargoHandler) handleConfig(w http.ResponseWriter, r *http.Request) {
|
|
config := CargoConfig{
|
|
DL: h.proxyURL + "/cargo/crates/{crate}/{version}/download",
|
|
}
|
|
|
|
w.Header().Set(headerContentType, "application/json")
|
|
_ = json.NewEncoder(w).Encode(config)
|
|
}
|
|
|
|
// handleIndex proxies the crate index from upstream.
|
|
func (h *CargoHandler) handleIndex(w http.ResponseWriter, r *http.Request) {
|
|
name := r.PathValue("name")
|
|
if name == "" {
|
|
http.Error(w, "not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
h.proxy.Logger.Info("cargo index request", "crate", name)
|
|
|
|
indexPath := h.buildIndexPath(name)
|
|
upstreamURL := fmt.Sprintf("%s/%s", h.indexURL, indexPath)
|
|
|
|
body, contentType, err := h.proxy.FetchOrCacheMetadata(r.Context(), "cargo", name, upstreamURL, "text/plain")
|
|
if err != nil {
|
|
if errors.Is(err, ErrUpstreamNotFound) {
|
|
http.Error(w, "not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
if writeUpstreamHeld(w, err) {
|
|
return
|
|
}
|
|
h.proxy.Logger.Error("failed to fetch upstream index", "error", err)
|
|
http.Error(w, "failed to fetch from upstream", http.StatusBadGateway)
|
|
return
|
|
}
|
|
|
|
if contentType == "" {
|
|
contentType = "text/plain; charset=utf-8"
|
|
}
|
|
|
|
w.Header().Set(headerContentType, contentType)
|
|
w.WriteHeader(http.StatusOK)
|
|
h.applyCooldownFiltering(w, body)
|
|
}
|
|
|
|
type crateIndexEntry struct {
|
|
Name string `json:"name"`
|
|
Version string `json:"vers"`
|
|
PublishTime string `json:"pubtime,omitempty"`
|
|
}
|
|
|
|
func (h *CargoHandler) applyCooldownFiltering(downstreamResponse http.ResponseWriter, body []byte) {
|
|
cooldownEnabled := h.proxy.Cooldown != nil && h.proxy.Cooldown.Enabled()
|
|
if !cooldownEnabled && h.proxy.Denylist == nil {
|
|
_, _ = downstreamResponse.Write(body)
|
|
return
|
|
}
|
|
|
|
scanner := bufio.NewScanner(strings.NewReader(string(body)))
|
|
// The response is already size-bounded by FetchOrCacheMetadata. Crate
|
|
// entries with many dependencies can exceed Scanner's default token limit.
|
|
scanner.Buffer(nil, len(body)+1)
|
|
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
|
|
var crate crateIndexEntry
|
|
err := json.Unmarshal([]byte(line), &crate)
|
|
|
|
if err != nil {
|
|
h.proxy.Logger.Error("failed to parse json entry in index", "error", err)
|
|
continue
|
|
}
|
|
|
|
if h.proxy.versionDenied("cargo", crate.Name, crate.Version) {
|
|
continue
|
|
}
|
|
publishedAt, err := time.Parse(time.RFC3339, crate.PublishTime)
|
|
|
|
if !cooldownEnabled || crate.PublishTime == "" || err != nil {
|
|
_, _ = downstreamResponse.Write([]byte(line + "\n"))
|
|
continue
|
|
}
|
|
|
|
cratePURL := canonicalPackagePURL("cargo", crate.Name)
|
|
|
|
if !h.proxy.Cooldown.IsAllowed("cargo", cratePURL, publishedAt) {
|
|
h.proxy.Logger.Info("cooldown: filtering cargo version",
|
|
"crate", crate.Name, "version", crate.Version,
|
|
"published", crate.PublishTime)
|
|
continue
|
|
}
|
|
|
|
_, _ = downstreamResponse.Write([]byte(line + "\n"))
|
|
}
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
h.proxy.Logger.Error("error reading index response", "error", err)
|
|
}
|
|
}
|
|
|
|
// buildIndexPath builds the sparse index path for a crate name.
|
|
func (h *CargoHandler) buildIndexPath(name string) string {
|
|
name = strings.ToLower(name)
|
|
|
|
switch len(name) {
|
|
case cargoIndexLen1:
|
|
return fmt.Sprintf("1/%s", name)
|
|
case cargoIndexLen2:
|
|
return fmt.Sprintf("2/%s", name)
|
|
case cargoIndexLen3:
|
|
return fmt.Sprintf("3/%c/%s", name[0], name)
|
|
default:
|
|
return fmt.Sprintf("%s/%s/%s", name[0:2], name[2:4], name)
|
|
}
|
|
}
|
|
|
|
// handleDownload serves a crate file, fetching and caching from upstream if needed.
|
|
func (h *CargoHandler) handleDownload(w http.ResponseWriter, r *http.Request) {
|
|
name := r.PathValue("name")
|
|
version := r.PathValue("version")
|
|
|
|
if name == "" || version == "" {
|
|
http.Error(w, "invalid request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
filename := fmt.Sprintf("%s-%s.crate", name, version)
|
|
|
|
h.proxy.Logger.Info("cargo download request",
|
|
"crate", name, "version", version, "filename", filename)
|
|
|
|
downloadURL := fmt.Sprintf(
|
|
"%s/%s/%s",
|
|
h.downloadURL,
|
|
url.PathEscape(name),
|
|
url.PathEscape(filename),
|
|
)
|
|
result, err := h.proxy.GetOrFetchArtifactFromURL(
|
|
r.Context(), "cargo", name, version, filename, downloadURL,
|
|
)
|
|
if err != nil {
|
|
h.proxy.serveArtifactError(w, err, "failed to fetch crate")
|
|
return
|
|
}
|
|
|
|
ServeArtifactRequest(w, r, result)
|
|
}
|