2026-01-20 21:52:44 +00:00
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
|
|
|
|
"net/http"
|
|
|
|
|
"strings"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
2026-03-18 10:59:29 +00:00
|
|
|
goUpstream = "https://proxy.golang.org"
|
|
|
|
|
asciiCaseOffset = 32 // difference between lowercase and uppercase ASCII letters
|
2026-01-20 21:52:44 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// GoHandler handles Go module proxy protocol requests.
|
|
|
|
|
type GoHandler struct {
|
|
|
|
|
proxy *Proxy
|
|
|
|
|
upstreamURL string
|
|
|
|
|
proxyURL string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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, "/"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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, "/")
|
|
|
|
|
|
|
|
|
|
// Sumdb requests - proxy through
|
|
|
|
|
if strings.HasPrefix(path, "sumdb/") {
|
|
|
|
|
h.proxyUpstream(w, r)
|
|
|
|
|
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/"
|
|
|
|
|
|
2026-03-19 21:06:02 +00:00
|
|
|
decodedMod := decodeGoModule(module)
|
2026-01-20 21:52:44 +00:00
|
|
|
switch {
|
|
|
|
|
case rest == "list":
|
|
|
|
|
// GET /{module}/@v/list - list versions
|
2026-03-19 21:06:02 +00:00
|
|
|
h.proxyCached(w, r, decodedMod+"/@v/list")
|
2026-01-20 21:52:44 +00:00
|
|
|
|
|
|
|
|
case strings.HasSuffix(rest, ".info"):
|
|
|
|
|
// GET /{module}/@v/{version}.info - version metadata
|
2026-03-19 21:06:02 +00:00
|
|
|
h.proxyCached(w, r, decodedMod+"/@v/"+rest)
|
2026-01-20 21:52:44 +00:00
|
|
|
|
|
|
|
|
case strings.HasSuffix(rest, ".mod"):
|
|
|
|
|
// GET /{module}/@v/{version}.mod - go.mod file
|
2026-03-19 21:06:02 +00:00
|
|
|
h.proxyCached(w, r, decodedMod+"/@v/"+rest)
|
2026-01-20 21:52:44 +00:00
|
|
|
|
|
|
|
|
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") {
|
2026-03-19 21:06:02 +00:00
|
|
|
module := strings.TrimSuffix(path, "/@latest")
|
|
|
|
|
h.proxyCached(w, r, decodeGoModule(module)+"/@latest")
|
2026-01-20 21:52:44 +00:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
result, err := h.proxy.GetOrFetchArtifact(r.Context(), "golang", decodedModule, version, filename)
|
|
|
|
|
if err != nil {
|
|
|
|
|
h.proxy.Logger.Error("failed to get artifact", "error", err)
|
|
|
|
|
http.Error(w, "failed to fetch module", http.StatusBadGateway)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ServeArtifact(w, result)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// proxyUpstream forwards a request to proxy.golang.org without caching.
|
|
|
|
|
func (h *GoHandler) proxyUpstream(w http.ResponseWriter, r *http.Request) {
|
2026-03-18 10:59:29 +00:00
|
|
|
h.proxy.ProxyUpstream(w, r, h.upstreamURL+r.URL.Path, nil)
|
2026-01-20 21:52:44 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-19 21:06:02 +00:00
|
|
|
// 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, "*/*")
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-20 21:52:44 +00:00
|
|
|
// 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) {
|
2026-03-18 10:59:29 +00:00
|
|
|
b.WriteByte(encoded[i+1] - asciiCaseOffset) // lowercase to uppercase
|
2026-01-20 21:52:44 +00:00
|
|
|
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
|
|
|
|
|
}
|