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.
177 lines
5.4 KiB
Go
177 lines
5.4 KiB
Go
package handler
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/git-pkgs/proxy/internal/retention"
|
|
"github.com/git-pkgs/registries/fetch"
|
|
)
|
|
|
|
const (
|
|
goUpstream = "https://proxy.golang.org"
|
|
asciiCaseOffset = 32 // difference between lowercase and uppercase ASCII letters
|
|
)
|
|
|
|
// Module zips are stored under pkg:golang/{module path}, which the PURL
|
|
// rules lowercase, so a package rule covers a module whatever case either
|
|
// is written in. A major version suffix (/v2) is part of the module path
|
|
// and so a module of its own. A key in the escaped form proxy URLs and the
|
|
// module cache use (github.com/!azure) does not survive the default
|
|
// mapping's round trip and is refused; the handler stores decoded paths,
|
|
// so such a key could never match.
|
|
func init() {
|
|
retention.Default.Register(retention.Spec{Key: "golang", CanonicalPackage: retention.DefaultCanonical("golang")})
|
|
}
|
|
|
|
// GoHandler handles Go module proxy protocol requests.
|
|
type GoHandler struct {
|
|
proxy *Proxy
|
|
upstreamURL string
|
|
proxyURL string
|
|
sumDBs map[string]string
|
|
sumDBOffline bool
|
|
sumDBProbes goSumDBProbes
|
|
}
|
|
|
|
// NewGoHandler creates a new Go module proxy handler.
|
|
func NewGoHandler(proxy *Proxy, proxyURL string) *GoHandler {
|
|
return &GoHandler{
|
|
proxy: proxy,
|
|
upstreamURL: goUpstream,
|
|
proxyURL: strings.TrimSuffix(proxyURL, "/"),
|
|
sumDBs: map[string]string{defaultGoSumDBName: defaultGoSumDBURL},
|
|
}
|
|
}
|
|
|
|
// NewGoHandlerWithUpstream creates a Go module handler with a custom upstream.
|
|
func NewGoHandlerWithUpstream(proxy *Proxy, proxyURL, upstreamURL string) *GoHandler {
|
|
h := NewGoHandler(proxy, proxyURL)
|
|
h.upstreamURL = configuredUpstreamURL(upstreamURL, goUpstream)
|
|
return h
|
|
}
|
|
|
|
// Routes returns the HTTP handler for Go proxy requests.
|
|
func (h *GoHandler) Routes() http.Handler {
|
|
// Go module paths can contain slashes, so just use the handler directly
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
h.handleRequest(w, r)
|
|
})
|
|
}
|
|
|
|
// handleRequest routes Go proxy requests based on the URL pattern.
|
|
func (h *GoHandler) handleRequest(w http.ResponseWriter, r *http.Request) {
|
|
path := strings.TrimPrefix(r.URL.Path, "/")
|
|
|
|
// Checksum database requests go to the database, not the module upstream
|
|
if rest, ok := strings.CutPrefix(path, "sumdb/"); ok {
|
|
h.handleSumDB(w, r, rest)
|
|
return
|
|
}
|
|
|
|
// Check for @v/ pattern to identify module requests
|
|
if idx := strings.Index(path, "/@v/"); idx >= 0 {
|
|
module := path[:idx]
|
|
rest := path[idx+4:] // after "/@v/"
|
|
|
|
decodedMod := decodeGoModule(module)
|
|
switch {
|
|
case rest == "list":
|
|
// GET /{module}/@v/list - list versions
|
|
h.proxyCached(w, r, decodedMod+"/@v/list")
|
|
|
|
case strings.HasSuffix(rest, ".info"):
|
|
// GET /{module}/@v/{version}.info - version metadata
|
|
h.proxyCached(w, r, decodedMod+"/@v/"+rest)
|
|
|
|
case strings.HasSuffix(rest, ".mod"):
|
|
// GET /{module}/@v/{version}.mod - go.mod file
|
|
h.proxyCached(w, r, decodedMod+"/@v/"+rest)
|
|
|
|
case strings.HasSuffix(rest, ".zip"):
|
|
// GET /{module}/@v/{version}.zip - source archive (cache this)
|
|
version := strings.TrimSuffix(rest, ".zip")
|
|
h.handleDownload(w, r, module, version)
|
|
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Check for @latest
|
|
if strings.HasSuffix(path, "/@latest") {
|
|
module := strings.TrimSuffix(path, "/@latest")
|
|
h.proxyCached(w, r, decodeGoModule(module)+"/@latest")
|
|
return
|
|
}
|
|
|
|
http.NotFound(w, r)
|
|
}
|
|
|
|
// handleDownload serves a module zip, fetching and caching from upstream if needed.
|
|
func (h *GoHandler) handleDownload(w http.ResponseWriter, r *http.Request, module, version string) {
|
|
// Decode module path (! followed by lowercase = uppercase)
|
|
decodedModule := decodeGoModule(module)
|
|
filename := fmt.Sprintf("%s@%s.zip", lastComponent(decodedModule), version)
|
|
|
|
h.proxy.Logger.Info("go module download request",
|
|
"module", decodedModule, "version", version)
|
|
|
|
downloadURL := h.upstreamURL + r.URL.Path
|
|
result, err := h.proxy.GetOrFetchArtifactFromURL(
|
|
r.Context(), "golang", decodedModule, version, filename, downloadURL,
|
|
)
|
|
if err != nil {
|
|
if errors.Is(err, fetch.ErrNotFound) {
|
|
http.Error(w, "not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
if errors.Is(err, ErrArtifactBlocked) || errors.Is(err, ErrVersionDenied) {
|
|
http.Error(w, err.Error(), http.StatusForbidden)
|
|
return
|
|
}
|
|
if writeUpstreamHeld(w, err) {
|
|
return
|
|
}
|
|
h.proxy.Logger.Error("failed to get artifact", "error", err)
|
|
http.Error(w, "failed to fetch module", http.StatusBadGateway)
|
|
return
|
|
}
|
|
|
|
ServeArtifactRequest(w, r, result)
|
|
}
|
|
|
|
// proxyCached forwards a request with metadata caching.
|
|
func (h *GoHandler) proxyCached(w http.ResponseWriter, r *http.Request, cacheKey string) {
|
|
h.proxy.ProxyCached(w, r, h.upstreamURL+r.URL.Path, "golang", cacheKey, "*/*")
|
|
}
|
|
|
|
// decodeGoModule decodes an encoded module path.
|
|
// In the encoding, uppercase letters are represented as "!" followed by lowercase.
|
|
func decodeGoModule(encoded string) string {
|
|
var b strings.Builder
|
|
for i := 0; i < len(encoded); i++ {
|
|
if encoded[i] == '!' && i+1 < len(encoded) {
|
|
b.WriteByte(encoded[i+1] - asciiCaseOffset) // lowercase to uppercase
|
|
i++
|
|
} else {
|
|
b.WriteByte(encoded[i])
|
|
}
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
// lastComponent returns the last path component of a module path.
|
|
func lastComponent(path string) string {
|
|
if idx := strings.LastIndex(path, "/"); idx >= 0 {
|
|
return path[idx+1:]
|
|
}
|
|
return path
|
|
}
|