mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-08-01 10:08:08 -04:00
NormalizedPackages runs config keys through purl.Parse().String(), which applies per-type rules like lowercasing pypi names. The handler side was building lookup keys with MakePURLString, which does not, so a config entry for pkg:pypi/Django would be normalized to pkg:pypi/django and never match the runtime key pkg:pypi/Django. Route both sides through the same canonical form: a new canonicalPackagePURL helper calls Normalize() on the constructed PURL before stringifying, and all cooldown IsAllowed call sites use it.
220 lines
5.7 KiB
Go
220 lines
5.7 KiB
Go
package handler
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
pubUpstream = "https://pub.dev"
|
|
pubPathParts = 2 // name + version in path split by /versions/
|
|
)
|
|
|
|
// PubHandler handles pub.dev registry protocol requests.
|
|
type PubHandler struct {
|
|
proxy *Proxy
|
|
upstreamURL string
|
|
proxyURL string
|
|
}
|
|
|
|
// NewPubHandler creates a new pub.dev protocol handler.
|
|
func NewPubHandler(proxy *Proxy, proxyURL string) *PubHandler {
|
|
return &PubHandler{
|
|
proxy: proxy,
|
|
upstreamURL: pubUpstream,
|
|
proxyURL: strings.TrimSuffix(proxyURL, "/"),
|
|
}
|
|
}
|
|
|
|
// Routes returns the HTTP handler for pub requests.
|
|
func (h *PubHandler) Routes() http.Handler {
|
|
mux := http.NewServeMux()
|
|
|
|
// Package downloads (cache these) - use prefix since {version}.tar.gz isn't allowed
|
|
mux.HandleFunc("GET /packages/", h.handleDownload)
|
|
|
|
// API endpoints (proxy with URL rewriting)
|
|
mux.HandleFunc("GET /api/packages/{name}", h.handlePackageMetadata)
|
|
|
|
return mux
|
|
}
|
|
|
|
// handleDownload serves a package tarball, fetching and caching from upstream if needed.
|
|
func (h *PubHandler) handleDownload(w http.ResponseWriter, r *http.Request) {
|
|
// Parse path: /packages/{name}/versions/{version}.tar.gz
|
|
path := strings.TrimPrefix(r.URL.Path, "/packages/")
|
|
parts := strings.Split(path, "/versions/")
|
|
if len(parts) != pubPathParts {
|
|
http.Error(w, "invalid request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
name := parts[0]
|
|
version := strings.TrimSuffix(parts[1], ".tar.gz")
|
|
|
|
if name == "" || version == "" {
|
|
http.Error(w, "invalid request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
filename := fmt.Sprintf("%s-%s.tar.gz", name, version)
|
|
|
|
h.proxy.Logger.Info("pub download request",
|
|
"name", name, "version", version)
|
|
|
|
result, err := h.proxy.GetOrFetchArtifact(r.Context(), "pub", name, version, filename)
|
|
if err != nil {
|
|
h.proxy.Logger.Error("failed to get artifact", "error", err)
|
|
http.Error(w, "failed to fetch package", http.StatusBadGateway)
|
|
return
|
|
}
|
|
|
|
ServeArtifact(w, result)
|
|
}
|
|
|
|
// handlePackageMetadata proxies package metadata and rewrites archive URLs.
|
|
func (h *PubHandler) handlePackageMetadata(w http.ResponseWriter, r *http.Request) {
|
|
name := r.PathValue("name")
|
|
if name == "" {
|
|
http.Error(w, "invalid package name", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
h.proxy.Logger.Info("pub metadata request", "package", name)
|
|
|
|
upstreamURL := fmt.Sprintf("%s/api/packages/%s", h.upstreamURL, name)
|
|
|
|
body, _, err := h.proxy.FetchOrCacheMetadata(r.Context(), "pub", name, upstreamURL)
|
|
if err != nil {
|
|
if errors.Is(err, ErrUpstreamNotFound) {
|
|
http.Error(w, "not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
h.proxy.Logger.Error("upstream request failed", "error", err)
|
|
http.Error(w, "upstream request failed", http.StatusBadGateway)
|
|
return
|
|
}
|
|
|
|
rewritten, err := h.rewriteMetadata(name, body)
|
|
if err != nil {
|
|
h.proxy.Logger.Warn("failed to rewrite metadata, proxying original", "error", err)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write(body)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write(rewritten)
|
|
}
|
|
|
|
// rewriteMetadata rewrites archive_url fields to point at this proxy.
|
|
// If cooldown is enabled, versions published too recently are filtered out.
|
|
func (h *PubHandler) rewriteMetadata(name string, body []byte) ([]byte, error) {
|
|
var metadata map[string]any
|
|
if err := json.Unmarshal(body, &metadata); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
versions, ok := metadata["versions"].([]any)
|
|
if !ok {
|
|
return body, nil
|
|
}
|
|
|
|
packagePURL := canonicalPackagePURL("pub", name)
|
|
filtered := h.filterAndRewriteVersions(name, packagePURL, versions)
|
|
metadata["versions"] = filtered
|
|
|
|
h.updateLatestVersion(metadata, filtered)
|
|
|
|
return json.Marshal(metadata)
|
|
}
|
|
|
|
// filterAndRewriteVersions applies cooldown filtering and rewrites archive URLs
|
|
// for a package's version list.
|
|
func (h *PubHandler) filterAndRewriteVersions(name, packagePURL string, versions []any) []any {
|
|
filtered := versions[:0]
|
|
for _, vdata := range versions {
|
|
vmap, ok := vdata.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
version, _ := vmap["version"].(string)
|
|
if version == "" {
|
|
continue
|
|
}
|
|
|
|
if h.shouldFilterVersion(packagePURL, name, version, vmap) {
|
|
continue
|
|
}
|
|
|
|
newURL := fmt.Sprintf("%s/pub/packages/%s/versions/%s.tar.gz", h.proxyURL, name, version)
|
|
vmap["archive_url"] = newURL
|
|
filtered = append(filtered, vdata)
|
|
|
|
h.proxy.Logger.Debug("rewrote archive URL",
|
|
"package", name, "version", version, "new", newURL)
|
|
}
|
|
|
|
return filtered
|
|
}
|
|
|
|
// shouldFilterVersion returns true if the version should be excluded due to cooldown.
|
|
func (h *PubHandler) shouldFilterVersion(packagePURL, name, version string, vmap map[string]any) bool {
|
|
if h.proxy.Cooldown == nil || !h.proxy.Cooldown.Enabled() {
|
|
return false
|
|
}
|
|
|
|
publishedStr, ok := vmap["published"].(string)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
publishedAt, err := time.Parse(time.RFC3339, publishedStr)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
if !h.proxy.Cooldown.IsAllowed("pub", packagePURL, publishedAt) {
|
|
h.proxy.Logger.Info("cooldown: filtering pub version",
|
|
"package", name, "version", version)
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// updateLatestVersion updates the latest field if the current latest version
|
|
// was removed by cooldown filtering.
|
|
func (h *PubHandler) updateLatestVersion(metadata map[string]any, filtered []any) {
|
|
if h.proxy.Cooldown == nil || !h.proxy.Cooldown.Enabled() {
|
|
return
|
|
}
|
|
|
|
latest, ok := metadata["latest"].(map[string]any)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
latestVer, ok := latest["version"].(string)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
for _, vdata := range filtered {
|
|
if vmap, ok := vdata.(map[string]any); ok {
|
|
if vmap["version"] == latestVer {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(filtered) > 0 {
|
|
metadata["latest"] = filtered[len(filtered)-1]
|
|
}
|
|
}
|