mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-11-09 15:49:16 -05:00
A failed .info fetch counted as an unknown time, so a rate-limited or flaky upstream let versions in cooldown through. It now fails the list or download with 502; only a version upstream doesn't know, or whose .info has no time, is let through. Times are stored under the escaped module path, which keeps modules that differ only in case apart. Modules with uppercase letters were not stored before and fetched every .info again on each list request. Only canonical versions are stored, since a query such as a branch name can resolve to a later commit. A .mod or .zip whose version doesn't unescape is answered with 404 instead of being passed upstream unchecked.
260 lines
7.7 KiB
Go
260 lines
7.7 KiB
Go
package handler
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/git-pkgs/cooldown"
|
|
)
|
|
|
|
type goCooldownUpstream struct {
|
|
server *httptest.Server
|
|
mu sync.Mutex
|
|
hits map[string]int
|
|
}
|
|
|
|
func (u *goCooldownUpstream) count(path string) int {
|
|
u.mu.Lock()
|
|
defer u.mu.Unlock()
|
|
return u.hits[path]
|
|
}
|
|
|
|
// newGoCooldownUpstream serves example.com/mod with an old v1.0.0, a
|
|
// recent v1.1.0 and a recent v1.2.0-rc.1, and @latest pointing at v1.1.0.
|
|
func newGoCooldownUpstream(t *testing.T, latest string) *goCooldownUpstream {
|
|
t.Helper()
|
|
now := time.Now()
|
|
times := map[string]time.Time{
|
|
"v1.0.0": now.Add(-10 * 24 * time.Hour),
|
|
"v1.1.0": now.Add(-time.Hour),
|
|
"v1.2.0-rc.1": now.Add(-2 * time.Hour),
|
|
}
|
|
u := &goCooldownUpstream{hits: map[string]int{}}
|
|
u.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
u.mu.Lock()
|
|
u.hits[r.URL.Path]++
|
|
u.mu.Unlock()
|
|
rest, ok := strings.CutPrefix(r.URL.Path, "/example.com/mod/")
|
|
if !ok {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
writeInfo := func(v string) {
|
|
_ = json.NewEncoder(w).Encode(goInfo{Version: v, Time: times[v]})
|
|
}
|
|
switch {
|
|
case rest == "@v/list":
|
|
_, _ = w.Write([]byte("v1.0.0\nv1.1.0\nv1.2.0-rc.1\n"))
|
|
case rest == "@latest":
|
|
writeInfo(latest)
|
|
case rest == "@v/main.info":
|
|
writeInfo("v1.1.0")
|
|
case strings.HasSuffix(rest, ".info"):
|
|
v := strings.TrimSuffix(strings.TrimPrefix(rest, "@v/"), ".info")
|
|
if _, ok := times[v]; !ok {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
writeInfo(v)
|
|
case strings.HasSuffix(rest, ".mod"):
|
|
_, _ = w.Write([]byte("module example.com/mod\n"))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
t.Cleanup(u.server.Close)
|
|
return u
|
|
}
|
|
|
|
func goCooldownHandler(t *testing.T, u *goCooldownUpstream) (http.Handler, *mockFetcher, *Proxy) {
|
|
t.Helper()
|
|
proxy, _, _, fetcher := setupTestProxy(t)
|
|
proxy.Cooldown = &cooldown.Config{Default: "3d"}
|
|
return NewGoHandlerWithUpstream(proxy, "http://localhost:8080", u.server.URL).Routes(), fetcher, proxy
|
|
}
|
|
|
|
func goGet(h http.Handler, path string) *httptest.ResponseRecorder {
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, httptest.NewRequest(http.MethodGet, path, nil))
|
|
return w
|
|
}
|
|
|
|
func TestGoCooldownFiltersList(t *testing.T) {
|
|
u := newGoCooldownUpstream(t, "v1.1.0")
|
|
h, _, _ := goCooldownHandler(t, u)
|
|
|
|
w := goGet(h, "/example.com/mod/@v/list")
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status = %d", w.Code)
|
|
}
|
|
if got := w.Body.String(); got != "v1.0.0\n" {
|
|
t.Fatalf("list = %q, want only v1.0.0", got)
|
|
}
|
|
|
|
// Publish times are stored, so a second list needs no .info requests.
|
|
_ = goGet(h, "/example.com/mod/@v/list")
|
|
if n := u.count("/example.com/mod/@v/v1.0.0.info"); n != 1 {
|
|
t.Errorf("v1.0.0.info fetched %d times, want 1", n)
|
|
}
|
|
}
|
|
|
|
func TestGoCooldownRefusesVersions(t *testing.T) {
|
|
u := newGoCooldownUpstream(t, "v1.1.0")
|
|
h, fetcher, _ := goCooldownHandler(t, u)
|
|
|
|
for _, p := range []string{
|
|
"/example.com/mod/@v/v1.1.0.info",
|
|
"/example.com/mod/@v/v1.1.0.mod",
|
|
"/example.com/mod/@v/v1.1.0.zip",
|
|
"/example.com/mod/@v/main.info",
|
|
} {
|
|
w := goGet(h, p)
|
|
if w.Code != http.StatusForbidden || !strings.Contains(w.Body.String(), errGoCooldown) {
|
|
t.Errorf("%s: status = %d body %q, want 403 cooldown", p, w.Code, w.Body.String())
|
|
}
|
|
}
|
|
if fetcher.fetchCalled {
|
|
t.Error("zip in cooldown was fetched")
|
|
}
|
|
|
|
for _, p := range []string{"/example.com/mod/@v/v1.0.0.info", "/example.com/mod/@v/v1.0.0.mod"} {
|
|
if w := goGet(h, p); w.Code != http.StatusOK {
|
|
t.Errorf("%s: status = %d, want 200", p, w.Code)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGoCooldownLatest(t *testing.T) {
|
|
u := newGoCooldownUpstream(t, "v1.1.0")
|
|
h, _, _ := goCooldownHandler(t, u)
|
|
|
|
w := goGet(h, "/example.com/mod/@latest")
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status = %d: %s", w.Code, w.Body)
|
|
}
|
|
var info goInfo
|
|
if err := json.Unmarshal(w.Body.Bytes(), &info); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if info.Version != "v1.0.0" {
|
|
t.Fatalf("@latest = %s, want v1.0.0", info.Version)
|
|
}
|
|
}
|
|
|
|
func TestGoCooldownLatestAllowed(t *testing.T) {
|
|
u := newGoCooldownUpstream(t, "v1.0.0")
|
|
h, _, _ := goCooldownHandler(t, u)
|
|
|
|
w := goGet(h, "/example.com/mod/@latest")
|
|
var info goInfo
|
|
if err := json.Unmarshal(w.Body.Bytes(), &info); err != nil || info.Version != "v1.0.0" {
|
|
t.Fatalf("@latest = %d %s, want v1.0.0", w.Code, w.Body)
|
|
}
|
|
if n := u.count("/example.com/mod/@v/list"); n != 0 {
|
|
t.Errorf("list fetched %d times for an allowed @latest", n)
|
|
}
|
|
}
|
|
|
|
func TestGoCooldownDisabledPassesThrough(t *testing.T) {
|
|
u := newGoCooldownUpstream(t, "v1.1.0")
|
|
h, _, proxy := goCooldownHandler(t, u)
|
|
proxy.Cooldown = nil
|
|
|
|
if w := goGet(h, "/example.com/mod/@v/list"); w.Body.String() != "v1.0.0\nv1.1.0\nv1.2.0-rc.1\n" {
|
|
t.Fatalf("list = %q", w.Body.String())
|
|
}
|
|
if w := goGet(h, "/example.com/mod/@v/v1.1.0.info"); w.Code != http.StatusOK {
|
|
t.Fatalf("info status = %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestNewestGoVersion(t *testing.T) {
|
|
tests := []struct {
|
|
versions []string
|
|
want string
|
|
}{
|
|
{[]string{"v1.0.0", "v1.10.0", "v1.9.0", "v2.0.0-rc.1"}, "v1.10.0"},
|
|
{[]string{"v1.0.0-alpha", "v1.0.0-beta"}, "v1.0.0-beta"},
|
|
{[]string{"v2.0.0+incompatible", "v1.5.0"}, "v2.0.0+incompatible"},
|
|
{nil, ""},
|
|
}
|
|
for _, tt := range tests {
|
|
if got := newestGoVersion(tt.versions); got != tt.want {
|
|
t.Errorf("newestGoVersion(%v) = %q, want %q", tt.versions, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGoCooldownFailsClosedOnUpstreamErrors(t *testing.T) {
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case strings.HasSuffix(r.URL.Path, "/@v/list"):
|
|
_, _ = w.Write([]byte("v1.0.0\n"))
|
|
case strings.HasSuffix(r.URL.Path, ".info"):
|
|
http.Error(w, "slow down", http.StatusTooManyRequests)
|
|
default:
|
|
_, _ = w.Write([]byte("module example.com/mod\n"))
|
|
}
|
|
}))
|
|
defer upstream.Close()
|
|
|
|
proxy, _, _, fetcher := setupTestProxy(t)
|
|
proxy.Cooldown = &cooldown.Config{Default: "3d"}
|
|
h := NewGoHandlerWithUpstream(proxy, "http://localhost:8080", upstream.URL).Routes()
|
|
|
|
for _, p := range []string{
|
|
"/example.com/mod/@v/list",
|
|
"/example.com/mod/@v/v1.0.0.mod",
|
|
"/example.com/mod/@v/v1.0.0.zip",
|
|
} {
|
|
if w := goGet(h, p); w.Code != http.StatusBadGateway {
|
|
t.Errorf("%s: status = %d, want 502", p, w.Code)
|
|
}
|
|
}
|
|
if fetcher.fetchCalled {
|
|
t.Error("zip was fetched without a known time")
|
|
}
|
|
}
|
|
|
|
func TestGoCooldownStoresTimesPerExactModule(t *testing.T) {
|
|
u := newGoCooldownUpstream(t, "v1.1.0")
|
|
proxy, db, _, _ := setupTestProxy(t)
|
|
proxy.Cooldown = &cooldown.Config{Default: "3d"}
|
|
h := NewGoHandlerWithUpstream(proxy, "http://localhost:8080", u.server.URL).Routes()
|
|
|
|
_ = goGet(h, "/example.com/mod/@v/list")
|
|
ver, err := db.GetVersionByPURL(canonicalVersionPURL("golang", "example.com/mod", "v1.0.0"))
|
|
if err != nil || ver == nil || !ver.PublishedAt.Valid {
|
|
t.Fatalf("time of v1.0.0 not stored: %v %v", ver, err)
|
|
}
|
|
|
|
gh := &GoHandler{proxy: proxy}
|
|
upper, _, ok := gh.goTimeKey("github.com/Azure/sdk", "v1.0.0")
|
|
lower, _, _ := gh.goTimeKey("github.com/azure/sdk", "v1.0.0")
|
|
if !ok || upper == lower {
|
|
t.Errorf("modules differing in case share a key: %q %q", upper, lower)
|
|
}
|
|
for _, query := range []string{"master", "v1.2", "v1.0.0+meta"} {
|
|
if _, _, ok := gh.goTimeKey("example.com/mod", query); ok {
|
|
t.Errorf("time of non-canonical %q would be stored", query)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGoRejectsBadlyEscapedVersions(t *testing.T) {
|
|
u := newGoCooldownUpstream(t, "v1.1.0")
|
|
h, fetcher, _ := goCooldownHandler(t, u)
|
|
for _, p := range []string{"/example.com/mod/@v/V1.0.0.zip", "/example.com/mod/@v/V1.0.0.mod"} {
|
|
if w := goGet(h, p); w.Code != http.StatusNotFound {
|
|
t.Errorf("%s: status = %d, want 404", p, w.Code)
|
|
}
|
|
}
|
|
if fetcher.fetchCalled || u.count("/example.com/mod/@v/V1.0.0.mod") != 0 {
|
|
t.Error("badly escaped version reached upstream")
|
|
}
|
|
}
|