Apply cooldown to Go modules
With cooldown on, @v/list leaves out versions whose .info time is
inside the window, and @latest falls back to the newest release out of
cooldown when upstream's answer is too new. Each listed version's
.info is read once and its time is stored in the versions table, as
npm does, except for modules whose path has uppercase letters, since
golang PURLs are lowercased.
A .info, .mod or .zip for a version in cooldown, and a query such as a
branch name that resolves to one, gets 403. The go command moves on to
the next GOPROXY entry after 404 or 410, so a 404 would let
GOPROXY=proxy,direct fetch the version directly.
Go has no publish time: .info carries the commit time of the revision,
so a version tagged on an old commit counts as old. The docs say so.
Part of #132
2026-10-23 12:40:19 +02:00
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"errors"
|
2026-10-23 12:46:39 +02:00
|
|
|
"fmt"
|
Apply cooldown to Go modules
With cooldown on, @v/list leaves out versions whose .info time is
inside the window, and @latest falls back to the newest release out of
cooldown when upstream's answer is too new. Each listed version's
.info is read once and its time is stored in the versions table, as
npm does, except for modules whose path has uppercase letters, since
golang PURLs are lowercased.
A .info, .mod or .zip for a version in cooldown, and a query such as a
branch name that resolves to one, gets 403. The go command moves on to
the next GOPROXY entry after 404 or 410, so a 404 would let
GOPROXY=proxy,direct fetch the version directly.
Go has no publish time: .info carries the commit time of the revision,
so a version tagged on an old commit counts as old. The docs say so.
Part of #132
2026-10-23 12:40:19 +02:00
|
|
|
"net/http"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
gomodule "golang.org/x/mod/module"
|
|
|
|
|
"golang.org/x/mod/semver"
|
|
|
|
|
"golang.org/x/sync/errgroup"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Go cooldown measures a version by the Time in its .info, which the module
|
|
|
|
|
// proxy takes from the commit the version points at. That is when the code
|
|
|
|
|
// was committed, not when the version was tagged or first fetched.
|
|
|
|
|
//
|
|
|
|
|
// Versions in cooldown are left out of @v/list and @latest, and their .info,
|
|
|
|
|
// .mod and .zip are answered with 403. The go command falls back to the next
|
|
|
|
|
// GOPROXY entry on 404 and 410, so with GOPROXY=proxy,direct a 404 would let
|
|
|
|
|
// it fetch the version directly.
|
|
|
|
|
|
|
|
|
|
// goInfoFetchLimit bounds the .info requests made at once to filter a list.
|
|
|
|
|
const goInfoFetchLimit = 8
|
|
|
|
|
|
|
|
|
|
const errGoCooldown = "version is in cooldown"
|
|
|
|
|
|
|
|
|
|
// goInfo is the JSON served for a version's .info and for @latest.
|
|
|
|
|
type goInfo struct {
|
|
|
|
|
Version string `json:"Version"`
|
|
|
|
|
Time time.Time `json:"Time"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *GoHandler) cooldownEnabled() bool {
|
|
|
|
|
return h.proxy.Cooldown != nil && h.proxy.Cooldown.Enabled()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *GoHandler) allowedAt(module string, published time.Time) bool {
|
|
|
|
|
if published.IsZero() {
|
|
|
|
|
return true
|
|
|
|
|
}
|
2026-10-30 15:06:42 +01:00
|
|
|
return h.proxy.cooldown().IsAllowed("golang", canonicalPackagePURL("golang", module), published)
|
Apply cooldown to Go modules
With cooldown on, @v/list leaves out versions whose .info time is
inside the window, and @latest falls back to the newest release out of
cooldown when upstream's answer is too new. Each listed version's
.info is read once and its time is stored in the versions table, as
npm does, except for modules whose path has uppercase letters, since
golang PURLs are lowercased.
A .info, .mod or .zip for a version in cooldown, and a query such as a
branch name that resolves to one, gets 403. The go command moves on to
the next GOPROXY entry after 404 or 410, so a 404 would let
GOPROXY=proxy,direct fetch the version directly.
Go has no publish time: .info carries the commit time of the revision,
so a version tagged on an old commit counts as old. The docs say so.
Part of #132
2026-10-23 12:40:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// writeGoMetadataError answers a failed upstream metadata fetch.
|
|
|
|
|
func (h *GoHandler) writeGoMetadataError(w http.ResponseWriter, err error) {
|
|
|
|
|
if errors.Is(err, ErrUpstreamNotFound) {
|
|
|
|
|
http.Error(w, "not found", http.StatusNotFound)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if writeUpstreamHeld(w, err) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
h.proxy.Logger.Error("go metadata fetch failed", "error", err)
|
|
|
|
|
http.Error(w, "failed to fetch from upstream", http.StatusBadGateway)
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-25 17:29:22 +01:00
|
|
|
// fetchGoMetadata reads a module's @v/list, @latest, or a version's .info.
|
|
|
|
|
// The .info of a canonical version comes from the cache whatever its age,
|
|
|
|
|
// like proxyVersionFile serves it.
|
Apply cooldown to Go modules
With cooldown on, @v/list leaves out versions whose .info time is
inside the window, and @latest falls back to the newest release out of
cooldown when upstream's answer is too new. Each listed version's
.info is read once and its time is stored in the versions table, as
npm does, except for modules whose path has uppercase letters, since
golang PURLs are lowercased.
A .info, .mod or .zip for a version in cooldown, and a query such as a
branch name that resolves to one, gets 403. The go command moves on to
the next GOPROXY entry after 404 or 410, so a 404 would let
GOPROXY=proxy,direct fetch the version directly.
Go has no publish time: .info carries the commit time of the revision,
so a version tagged on an old commit counts as old. The docs say so.
Part of #132
2026-10-23 12:40:19 +02:00
|
|
|
func (h *GoHandler) fetchGoMetadata(ctx context.Context, escapedModule, module, rest string) ([]byte, error) {
|
2026-10-25 17:29:22 +01:00
|
|
|
cacheKey, upstreamURL := module+"/"+rest, h.upstreamURL+"/"+escapedModule+"/"+rest
|
|
|
|
|
if v, ok := strings.CutPrefix(rest, "@v/"); ok && strings.HasSuffix(v, ".info") && goVersionImmutable(strings.TrimSuffix(v, ".info")) {
|
|
|
|
|
res := h.proxy.fetchImmutableMetadata(ctx, "golang", cacheKey, upstreamURL, "*/*")
|
|
|
|
|
return res.body, res.err
|
|
|
|
|
}
|
|
|
|
|
body, _, err := h.proxy.FetchOrCacheMetadata(ctx, "golang", cacheKey, upstreamURL, "*/*")
|
Apply cooldown to Go modules
With cooldown on, @v/list leaves out versions whose .info time is
inside the window, and @latest falls back to the newest release out of
cooldown when upstream's answer is too new. Each listed version's
.info is read once and its time is stored in the versions table, as
npm does, except for modules whose path has uppercase letters, since
golang PURLs are lowercased.
A .info, .mod or .zip for a version in cooldown, and a query such as a
branch name that resolves to one, gets 403. The go command moves on to
the next GOPROXY entry after 404 or 410, so a 404 would let
GOPROXY=proxy,direct fetch the version directly.
Go has no publish time: .info carries the commit time of the revision,
so a version tagged on an old commit counts as old. The docs say so.
Part of #132
2026-10-23 12:40:19 +02:00
|
|
|
return body, err
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-31 09:49:35 +00:00
|
|
|
// Keep escaped module paths in publish-time keys for cache compatibility.
|
|
|
|
|
// Only canonical versions are stored; branch queries can resolve differently.
|
2026-10-23 12:46:39 +02:00
|
|
|
func (h *GoHandler) goTimeKey(module, version string) (versionPURL, packagePURL string, ok bool) {
|
|
|
|
|
if h.proxy.DB == nil || gomodule.CanonicalVersion(version) != version {
|
|
|
|
|
return "", "", false
|
|
|
|
|
}
|
|
|
|
|
escaped, err := gomodule.EscapePath(module)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", "", false
|
|
|
|
|
}
|
|
|
|
|
return canonicalVersionPURL("golang", escaped, version), canonicalPackagePURL("golang", escaped), true
|
Apply cooldown to Go modules
With cooldown on, @v/list leaves out versions whose .info time is
inside the window, and @latest falls back to the newest release out of
cooldown when upstream's answer is too new. Each listed version's
.info is read once and its time is stored in the versions table, as
npm does, except for modules whose path has uppercase letters, since
golang PURLs are lowercased.
A .info, .mod or .zip for a version in cooldown, and a query such as a
branch name that resolves to one, gets 403. The go command moves on to
the next GOPROXY entry after 404 or 410, so a 404 would let
GOPROXY=proxy,direct fetch the version directly.
Go has no publish time: .info carries the commit time of the revision,
so a version tagged on an old commit counts as old. The docs say so.
Part of #132
2026-10-23 12:40:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// publishTime returns a version's time from the versions table, or from its
|
2026-10-23 12:46:39 +02:00
|
|
|
// .info, which is then stored. A version upstream doesn't know, or whose
|
|
|
|
|
// .info has no time, gets the zero time and is let through, as in the other
|
|
|
|
|
// ecosystems. A failed fetch is an error, so a flaky or rate-limited
|
|
|
|
|
// upstream can't let a version through.
|
|
|
|
|
func (h *GoHandler) publishTime(ctx context.Context, escapedModule, module, version string) (time.Time, error) {
|
|
|
|
|
if versionPURL, _, ok := h.goTimeKey(module, version); ok {
|
Apply cooldown to Go modules
With cooldown on, @v/list leaves out versions whose .info time is
inside the window, and @latest falls back to the newest release out of
cooldown when upstream's answer is too new. Each listed version's
.info is read once and its time is stored in the versions table, as
npm does, except for modules whose path has uppercase letters, since
golang PURLs are lowercased.
A .info, .mod or .zip for a version in cooldown, and a query such as a
branch name that resolves to one, gets 403. The go command moves on to
the next GOPROXY entry after 404 or 410, so a 404 would let
GOPROXY=proxy,direct fetch the version directly.
Go has no publish time: .info carries the commit time of the revision,
so a version tagged on an old commit counts as old. The docs say so.
Part of #132
2026-10-23 12:40:19 +02:00
|
|
|
if ver, err := h.proxy.DB.GetVersionByPURL(versionPURL); err == nil && ver != nil && ver.PublishedAt.Valid {
|
2026-10-23 12:46:39 +02:00
|
|
|
return ver.PublishedAt.Time, nil
|
Apply cooldown to Go modules
With cooldown on, @v/list leaves out versions whose .info time is
inside the window, and @latest falls back to the newest release out of
cooldown when upstream's answer is too new. Each listed version's
.info is read once and its time is stored in the versions table, as
npm does, except for modules whose path has uppercase letters, since
golang PURLs are lowercased.
A .info, .mod or .zip for a version in cooldown, and a query such as a
branch name that resolves to one, gets 403. The go command moves on to
the next GOPROXY entry after 404 or 410, so a 404 would let
GOPROXY=proxy,direct fetch the version directly.
Go has no publish time: .info carries the commit time of the revision,
so a version tagged on an old commit counts as old. The docs say so.
Part of #132
2026-10-23 12:40:19 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
escapedVersion, err := gomodule.EscapeVersion(version)
|
|
|
|
|
if err != nil {
|
2026-10-23 12:46:39 +02:00
|
|
|
return time.Time{}, nil
|
Apply cooldown to Go modules
With cooldown on, @v/list leaves out versions whose .info time is
inside the window, and @latest falls back to the newest release out of
cooldown when upstream's answer is too new. Each listed version's
.info is read once and its time is stored in the versions table, as
npm does, except for modules whose path has uppercase letters, since
golang PURLs are lowercased.
A .info, .mod or .zip for a version in cooldown, and a query such as a
branch name that resolves to one, gets 403. The go command moves on to
the next GOPROXY entry after 404 or 410, so a 404 would let
GOPROXY=proxy,direct fetch the version directly.
Go has no publish time: .info carries the commit time of the revision,
so a version tagged on an old commit counts as old. The docs say so.
Part of #132
2026-10-23 12:40:19 +02:00
|
|
|
}
|
|
|
|
|
body, err := h.fetchGoMetadata(ctx, escapedModule, module, "@v/"+escapedVersion+".info")
|
2026-10-23 12:46:39 +02:00
|
|
|
if errors.Is(err, ErrUpstreamNotFound) {
|
|
|
|
|
return time.Time{}, nil
|
|
|
|
|
}
|
Apply cooldown to Go modules
With cooldown on, @v/list leaves out versions whose .info time is
inside the window, and @latest falls back to the newest release out of
cooldown when upstream's answer is too new. Each listed version's
.info is read once and its time is stored in the versions table, as
npm does, except for modules whose path has uppercase letters, since
golang PURLs are lowercased.
A .info, .mod or .zip for a version in cooldown, and a query such as a
branch name that resolves to one, gets 403. The go command moves on to
the next GOPROXY entry after 404 or 410, so a 404 would let
GOPROXY=proxy,direct fetch the version directly.
Go has no publish time: .info carries the commit time of the revision,
so a version tagged on an old commit counts as old. The docs say so.
Part of #132
2026-10-23 12:40:19 +02:00
|
|
|
if err != nil {
|
2026-10-23 12:46:39 +02:00
|
|
|
return time.Time{}, err
|
Apply cooldown to Go modules
With cooldown on, @v/list leaves out versions whose .info time is
inside the window, and @latest falls back to the newest release out of
cooldown when upstream's answer is too new. Each listed version's
.info is read once and its time is stored in the versions table, as
npm does, except for modules whose path has uppercase letters, since
golang PURLs are lowercased.
A .info, .mod or .zip for a version in cooldown, and a query such as a
branch name that resolves to one, gets 403. The go command moves on to
the next GOPROXY entry after 404 or 410, so a 404 would let
GOPROXY=proxy,direct fetch the version directly.
Go has no publish time: .info carries the commit time of the revision,
so a version tagged on an old commit counts as old. The docs say so.
Part of #132
2026-10-23 12:40:19 +02:00
|
|
|
}
|
|
|
|
|
var info goInfo
|
2026-10-23 12:46:39 +02:00
|
|
|
if err := json.Unmarshal(body, &info); err != nil {
|
|
|
|
|
return time.Time{}, err
|
|
|
|
|
}
|
|
|
|
|
if !info.Time.IsZero() {
|
|
|
|
|
h.storePublishTime(module, version, info.Time)
|
Apply cooldown to Go modules
With cooldown on, @v/list leaves out versions whose .info time is
inside the window, and @latest falls back to the newest release out of
cooldown when upstream's answer is too new. Each listed version's
.info is read once and its time is stored in the versions table, as
npm does, except for modules whose path has uppercase letters, since
golang PURLs are lowercased.
A .info, .mod or .zip for a version in cooldown, and a query such as a
branch name that resolves to one, gets 403. The go command moves on to
the next GOPROXY entry after 404 or 410, so a 404 would let
GOPROXY=proxy,direct fetch the version directly.
Go has no publish time: .info carries the commit time of the revision,
so a version tagged on an old commit counts as old. The docs say so.
Part of #132
2026-10-23 12:40:19 +02:00
|
|
|
}
|
2026-10-23 12:46:39 +02:00
|
|
|
return info.Time, nil
|
Apply cooldown to Go modules
With cooldown on, @v/list leaves out versions whose .info time is
inside the window, and @latest falls back to the newest release out of
cooldown when upstream's answer is too new. Each listed version's
.info is read once and its time is stored in the versions table, as
npm does, except for modules whose path has uppercase letters, since
golang PURLs are lowercased.
A .info, .mod or .zip for a version in cooldown, and a query such as a
branch name that resolves to one, gets 403. The go command moves on to
the next GOPROXY entry after 404 or 410, so a 404 would let
GOPROXY=proxy,direct fetch the version directly.
Go has no publish time: .info carries the commit time of the revision,
so a version tagged on an old commit counts as old. The docs say so.
Part of #132
2026-10-23 12:40:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *GoHandler) storePublishTime(module, version string, published time.Time) {
|
2026-10-23 12:46:39 +02:00
|
|
|
versionPURL, packagePURL, ok := h.goTimeKey(module, version)
|
|
|
|
|
if !ok {
|
Apply cooldown to Go modules
With cooldown on, @v/list leaves out versions whose .info time is
inside the window, and @latest falls back to the newest release out of
cooldown when upstream's answer is too new. Each listed version's
.info is read once and its time is stored in the versions table, as
npm does, except for modules whose path has uppercase letters, since
golang PURLs are lowercased.
A .info, .mod or .zip for a version in cooldown, and a query such as a
branch name that resolves to one, gets 403. The go command moves on to
the next GOPROXY entry after 404 or 410, so a 404 would let
GOPROXY=proxy,direct fetch the version directly.
Go has no publish time: .info carries the commit time of the revision,
so a version tagged on an old commit counts as old. The docs say so.
Part of #132
2026-10-23 12:40:19 +02:00
|
|
|
return
|
|
|
|
|
}
|
2026-10-23 12:46:39 +02:00
|
|
|
if err := h.proxy.DB.SetVersionPublishedAt(versionPURL, packagePURL, published); err != nil {
|
Apply cooldown to Go modules
With cooldown on, @v/list leaves out versions whose .info time is
inside the window, and @latest falls back to the newest release out of
cooldown when upstream's answer is too new. Each listed version's
.info is read once and its time is stored in the versions table, as
npm does, except for modules whose path has uppercase letters, since
golang PURLs are lowercased.
A .info, .mod or .zip for a version in cooldown, and a query such as a
branch name that resolves to one, gets 403. The go command moves on to
the next GOPROXY entry after 404 or 410, so a 404 would let
GOPROXY=proxy,direct fetch the version directly.
Go has no publish time: .info carries the commit time of the revision,
so a version tagged on an old commit counts as old. The docs say so.
Part of #132
2026-10-23 12:40:19 +02:00
|
|
|
h.proxy.Logger.Warn("cooldown: could not store go publish time",
|
|
|
|
|
"module", module, "version", version, "error", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-23 12:46:39 +02:00
|
|
|
// checkCooldown answers a request for an exact version in cooldown, or whose
|
|
|
|
|
// time couldn't be read, and reports whether it did.
|
|
|
|
|
func (h *GoHandler) checkCooldown(w http.ResponseWriter, r *http.Request, escapedModule, module, version string) bool {
|
Apply cooldown to Go modules
With cooldown on, @v/list leaves out versions whose .info time is
inside the window, and @latest falls back to the newest release out of
cooldown when upstream's answer is too new. Each listed version's
.info is read once and its time is stored in the versions table, as
npm does, except for modules whose path has uppercase letters, since
golang PURLs are lowercased.
A .info, .mod or .zip for a version in cooldown, and a query such as a
branch name that resolves to one, gets 403. The go command moves on to
the next GOPROXY entry after 404 or 410, so a 404 would let
GOPROXY=proxy,direct fetch the version directly.
Go has no publish time: .info carries the commit time of the revision,
so a version tagged on an old commit counts as old. The docs say so.
Part of #132
2026-10-23 12:40:19 +02:00
|
|
|
if !h.cooldownEnabled() {
|
|
|
|
|
return false
|
|
|
|
|
}
|
2026-10-23 12:46:39 +02:00
|
|
|
published, err := h.publishTime(r.Context(), escapedModule, module, version)
|
|
|
|
|
if err != nil {
|
|
|
|
|
h.proxy.Logger.Warn("cooldown: could not read go version time",
|
|
|
|
|
"module", module, "version", version, "error", err)
|
|
|
|
|
http.Error(w, "failed to check cooldown", http.StatusBadGateway)
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
if !h.allowedAt(module, published) {
|
|
|
|
|
h.refuseCooldown(w, module, version)
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
return false
|
Apply cooldown to Go modules
With cooldown on, @v/list leaves out versions whose .info time is
inside the window, and @latest falls back to the newest release out of
cooldown when upstream's answer is too new. Each listed version's
.info is read once and its time is stored in the versions table, as
npm does, except for modules whose path has uppercase letters, since
golang PURLs are lowercased.
A .info, .mod or .zip for a version in cooldown, and a query such as a
branch name that resolves to one, gets 403. The go command moves on to
the next GOPROXY entry after 404 or 410, so a 404 would let
GOPROXY=proxy,direct fetch the version directly.
Go has no publish time: .info carries the commit time of the revision,
so a version tagged on an old commit counts as old. The docs say so.
Part of #132
2026-10-23 12:40:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *GoHandler) refuseCooldown(w http.ResponseWriter, module, version string) {
|
|
|
|
|
h.proxy.Logger.Info("cooldown: withholding go module version", "module", module, "version", version)
|
|
|
|
|
http.Error(w, errGoCooldown+": "+canonicalVersionPURL("golang", module, version), http.StatusForbidden)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// allowedVersions returns the listed versions of module that are out of
|
|
|
|
|
// cooldown, in their upstream order.
|
|
|
|
|
func (h *GoHandler) allowedVersions(ctx context.Context, escapedModule, module string) ([]string, error) {
|
|
|
|
|
body, err := h.fetchGoMetadata(ctx, escapedModule, module, "@v/list")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
var versions []string
|
|
|
|
|
for line := range strings.SplitSeq(string(body), "\n") {
|
|
|
|
|
if v := strings.TrimSpace(line); v != "" {
|
|
|
|
|
versions = append(versions, v)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
keep := make([]bool, len(versions))
|
|
|
|
|
g, gctx := errgroup.WithContext(ctx)
|
|
|
|
|
g.SetLimit(goInfoFetchLimit)
|
|
|
|
|
for i, v := range versions {
|
|
|
|
|
g.Go(func() error {
|
2026-10-23 12:46:39 +02:00
|
|
|
published, err := h.publishTime(gctx, escapedModule, module, v)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("reading time of %s: %w", v, err)
|
|
|
|
|
}
|
|
|
|
|
keep[i] = h.allowedAt(module, published)
|
Apply cooldown to Go modules
With cooldown on, @v/list leaves out versions whose .info time is
inside the window, and @latest falls back to the newest release out of
cooldown when upstream's answer is too new. Each listed version's
.info is read once and its time is stored in the versions table, as
npm does, except for modules whose path has uppercase letters, since
golang PURLs are lowercased.
A .info, .mod or .zip for a version in cooldown, and a query such as a
branch name that resolves to one, gets 403. The go command moves on to
the next GOPROXY entry after 404 or 410, so a 404 would let
GOPROXY=proxy,direct fetch the version directly.
Go has no publish time: .info carries the commit time of the revision,
so a version tagged on an old commit counts as old. The docs say so.
Part of #132
2026-10-23 12:40:19 +02:00
|
|
|
return nil
|
|
|
|
|
})
|
|
|
|
|
}
|
2026-10-23 12:46:39 +02:00
|
|
|
if err := g.Wait(); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
Apply cooldown to Go modules
With cooldown on, @v/list leaves out versions whose .info time is
inside the window, and @latest falls back to the newest release out of
cooldown when upstream's answer is too new. Each listed version's
.info is read once and its time is stored in the versions table, as
npm does, except for modules whose path has uppercase letters, since
golang PURLs are lowercased.
A .info, .mod or .zip for a version in cooldown, and a query such as a
branch name that resolves to one, gets 403. The go command moves on to
the next GOPROXY entry after 404 or 410, so a 404 would let
GOPROXY=proxy,direct fetch the version directly.
Go has no publish time: .info carries the commit time of the revision,
so a version tagged on an old commit counts as old. The docs say so.
Part of #132
2026-10-23 12:40:19 +02:00
|
|
|
|
|
|
|
|
allowed := versions[:0]
|
|
|
|
|
for i, v := range versions {
|
|
|
|
|
if keep[i] {
|
|
|
|
|
allowed = append(allowed, v)
|
|
|
|
|
} else {
|
|
|
|
|
h.proxy.Logger.Info("cooldown: filtering go module version", "module", module, "version", v)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return allowed, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// serveCooldownList serves @v/list without versions in cooldown.
|
|
|
|
|
func (h *GoHandler) serveCooldownList(w http.ResponseWriter, r *http.Request, escapedModule, module string) {
|
|
|
|
|
allowed, err := h.allowedVersions(r.Context(), escapedModule, module)
|
|
|
|
|
if err != nil {
|
|
|
|
|
h.writeGoMetadataError(w, err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
w.Header().Set(headerContentType, "text/plain; charset=utf-8")
|
|
|
|
|
if len(allowed) > 0 {
|
|
|
|
|
_, _ = w.Write([]byte(strings.Join(allowed, "\n") + "\n"))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// serveCooldownInfo serves a .info, refusing it when the version it names is
|
|
|
|
|
// in cooldown. rest is the request after /@v/: an escaped version or a query
|
|
|
|
|
// such as a branch name, followed by .info.
|
|
|
|
|
func (h *GoHandler) serveCooldownInfo(w http.ResponseWriter, r *http.Request, escapedModule, module, rest string) {
|
|
|
|
|
body, err := h.fetchGoMetadata(r.Context(), escapedModule, module, "@v/"+rest)
|
|
|
|
|
if err != nil {
|
|
|
|
|
h.writeGoMetadataError(w, err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var info goInfo
|
|
|
|
|
if err := json.Unmarshal(body, &info); err == nil && info.Version != "" {
|
|
|
|
|
if !h.allowedAt(module, info.Time) {
|
|
|
|
|
h.refuseCooldown(w, module, info.Version)
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-10-23 12:46:39 +02:00
|
|
|
if !info.Time.IsZero() {
|
Apply cooldown to Go modules
With cooldown on, @v/list leaves out versions whose .info time is
inside the window, and @latest falls back to the newest release out of
cooldown when upstream's answer is too new. Each listed version's
.info is read once and its time is stored in the versions table, as
npm does, except for modules whose path has uppercase letters, since
golang PURLs are lowercased.
A .info, .mod or .zip for a version in cooldown, and a query such as a
branch name that resolves to one, gets 403. The go command moves on to
the next GOPROXY entry after 404 or 410, so a 404 would let
GOPROXY=proxy,direct fetch the version directly.
Go has no publish time: .info carries the commit time of the revision,
so a version tagged on an old commit counts as old. The docs say so.
Part of #132
2026-10-23 12:40:19 +02:00
|
|
|
h.storePublishTime(module, info.Version, info.Time)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
w.Header().Set(headerContentType, contentTypeJSON)
|
|
|
|
|
_, _ = w.Write(body)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// serveCooldownLatest serves @latest. When upstream's answer is in cooldown,
|
|
|
|
|
// it serves the newest listed version out of cooldown instead: the highest
|
|
|
|
|
// release, or else the highest pre-release, which is how the go command
|
|
|
|
|
// picks from a list.
|
|
|
|
|
func (h *GoHandler) serveCooldownLatest(w http.ResponseWriter, r *http.Request, escapedModule, module string) {
|
|
|
|
|
body, err := h.fetchGoMetadata(r.Context(), escapedModule, module, "@latest")
|
|
|
|
|
if err != nil {
|
|
|
|
|
h.writeGoMetadataError(w, err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var info goInfo
|
|
|
|
|
if err := json.Unmarshal(body, &info); err != nil || h.allowedAt(module, info.Time) {
|
|
|
|
|
w.Header().Set(headerContentType, contentTypeJSON)
|
|
|
|
|
_, _ = w.Write(body)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
allowed, err := h.allowedVersions(r.Context(), escapedModule, module)
|
|
|
|
|
if err != nil {
|
|
|
|
|
h.writeGoMetadataError(w, err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
best := newestGoVersion(allowed)
|
|
|
|
|
if best == "" {
|
|
|
|
|
h.refuseCooldown(w, module, info.Version)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
escapedVersion, err := gomodule.EscapeVersion(best)
|
|
|
|
|
if err != nil {
|
|
|
|
|
h.refuseCooldown(w, module, info.Version)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
h.serveCooldownInfo(w, r, escapedModule, module, escapedVersion+".info")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// newestGoVersion returns the highest release in versions, or the highest
|
|
|
|
|
// pre-release when there is no release.
|
|
|
|
|
func newestGoVersion(versions []string) string {
|
|
|
|
|
var release, prerelease string
|
|
|
|
|
for _, v := range versions {
|
|
|
|
|
if !semver.IsValid(v) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if semver.Prerelease(v) == "" {
|
|
|
|
|
if release == "" || semver.Compare(v, release) > 0 {
|
|
|
|
|
release = v
|
|
|
|
|
}
|
|
|
|
|
} else if prerelease == "" || semver.Compare(v, prerelease) > 0 {
|
|
|
|
|
prerelease = v
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if release != "" {
|
|
|
|
|
return release
|
|
|
|
|
}
|
|
|
|
|
return prerelease
|
|
|
|
|
}
|