mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-11-09 15:49:16 -05:00
Handlers set the configured upstream auth header on the request itself before handing it to the client. When upstream redirected to another host, Go's client copied that header to the next hop: it strips only Authorization and Cookie, so a "header" type credential such as X-JFrog-Art-Api reached the redirect target. The auth transport already applies credentials per hop, scoped to each hop's URL. - Remove Proxy.applyUpstreamAuth and Proxy.AuthForURL; auth comes only from the transport - Add a server test that redirects a header-authenticated upstream to another host and asserts the header stays with the upstream - Wire handler tests' auth through the transport, as the server does
275 lines
7.1 KiB
Go
275 lines
7.1 KiB
Go
package handler
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/git-pkgs/cooldown"
|
|
"github.com/git-pkgs/registries/fetch"
|
|
)
|
|
|
|
func cargoTestProxy() *Proxy {
|
|
return &Proxy{
|
|
Logger: slog.Default(),
|
|
HTTPClient: http.DefaultClient,
|
|
}
|
|
}
|
|
|
|
func TestCargoBuildIndexPath(t *testing.T) {
|
|
h := &CargoHandler{}
|
|
|
|
tests := []struct {
|
|
name string
|
|
want string
|
|
}{
|
|
{"a", "1/a"},
|
|
{"ab", "2/ab"},
|
|
{"abc", "3/a/abc"},
|
|
{"abcd", "ab/cd/abcd"},
|
|
{"serde", "se/rd/serde"},
|
|
{"tokio", "to/ki/tokio"},
|
|
{"A", "1/a"}, // lowercase
|
|
{"SERDE", "se/rd/serde"}, // lowercase
|
|
{"rand_core", "ra/nd/rand_core"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
got := h.buildIndexPath(tt.name)
|
|
if got != tt.want {
|
|
t.Errorf("buildIndexPath(%q) = %q, want %q", tt.name, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCargoConfigEndpoint(t *testing.T) {
|
|
h := &CargoHandler{
|
|
proxyURL: "http://localhost:8080",
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/config.json", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
h.handleConfig(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
var config CargoConfig
|
|
if err := json.Unmarshal(w.Body.Bytes(), &config); err != nil {
|
|
t.Fatalf("failed to parse config: %v", err)
|
|
}
|
|
|
|
expectedDL := "http://localhost:8080/cargo/crates/{crate}/{version}/download"
|
|
if config.DL != expectedDL {
|
|
t.Errorf("DL = %q, want %q", config.DL, expectedDL)
|
|
}
|
|
}
|
|
|
|
func TestCargoHandlerUsesConfiguredUpstreams(t *testing.T) {
|
|
t.Run("index", func(t *testing.T) {
|
|
var requestPath, authHeader string
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
requestPath = r.URL.Path
|
|
authHeader = r.Header.Get("Authorization")
|
|
if authHeader != "Bearer cargo-token" {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
_, _ = io.WriteString(w, `{"name":"serde","vers":"1.0.0"}`)
|
|
}))
|
|
defer upstream.Close()
|
|
|
|
proxy, _, _, _ := setupTestProxy(t)
|
|
proxy.HTTPClient = upstream.Client()
|
|
authorizeUpstream(proxy, "Bearer cargo-token")
|
|
h := NewCargoHandler(
|
|
proxy,
|
|
"http://proxy.test",
|
|
upstream.URL+"/index/",
|
|
"https://crates.example.test/files/",
|
|
)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/se/rd/serde", nil)
|
|
w := httptest.NewRecorder()
|
|
h.Routes().ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d; body: %s", w.Code, http.StatusOK, w.Body.String())
|
|
}
|
|
if requestPath != "/index/se/rd/serde" {
|
|
t.Errorf("upstream path = %q, want %q", requestPath, "/index/se/rd/serde")
|
|
}
|
|
if authHeader != "Bearer cargo-token" {
|
|
t.Errorf("Authorization = %q, want %q", authHeader, "Bearer cargo-token")
|
|
}
|
|
})
|
|
|
|
t.Run("download", func(t *testing.T) {
|
|
proxy, _, _, artifactFetcher := setupTestProxy(t)
|
|
artifactFetcher.artifact = &fetch.Artifact{
|
|
Body: io.NopCloser(strings.NewReader("crate")),
|
|
ContentType: "application/gzip",
|
|
}
|
|
h := NewCargoHandler(
|
|
proxy,
|
|
"http://proxy.test",
|
|
"https://index.example.test/root/",
|
|
"https://crates.example.test/files/",
|
|
)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/crates/serde/1.0.0/download", nil)
|
|
w := httptest.NewRecorder()
|
|
h.Routes().ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d; body: %s", w.Code, http.StatusOK, w.Body.String())
|
|
}
|
|
want := "https://crates.example.test/files/serde/serde-1.0.0.crate"
|
|
if artifactFetcher.fetchedURL != want {
|
|
t.Errorf("fetched URL = %q, want %q", artifactFetcher.fetchedURL, want)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCargoIndexProxy(t *testing.T) {
|
|
// Create a mock upstream index server
|
|
indexContent := `{"name":"serde","vers":"1.0.0","deps":[],"cksum":"abc123"}
|
|
{"name":"serde","vers":"1.0.1","deps":[],"cksum":"def456"}`
|
|
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/se/rd/serde" {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
_, _ = w.Write([]byte(indexContent))
|
|
}))
|
|
defer upstream.Close()
|
|
|
|
h := &CargoHandler{
|
|
proxy: cargoTestProxy(),
|
|
indexURL: upstream.URL,
|
|
proxyURL: "http://proxy.local",
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/se/rd/serde", nil)
|
|
req.SetPathValue("a", "se")
|
|
req.SetPathValue("b", "rd")
|
|
req.SetPathValue("name", "serde")
|
|
|
|
w := httptest.NewRecorder()
|
|
h.handleIndex(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
|
|
if w.Body.String() != indexContent {
|
|
t.Errorf("body = %q, want %q", w.Body.String(), indexContent)
|
|
}
|
|
}
|
|
|
|
func TestCargoIndexNotFound(t *testing.T) {
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}))
|
|
defer upstream.Close()
|
|
|
|
h := &CargoHandler{
|
|
proxy: cargoTestProxy(),
|
|
indexURL: upstream.URL,
|
|
proxyURL: "http://proxy.local",
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/no/ne/nonexistent", nil)
|
|
req.SetPathValue("a", "no")
|
|
req.SetPathValue("b", "ne")
|
|
req.SetPathValue("name", "nonexistent")
|
|
|
|
w := httptest.NewRecorder()
|
|
h.handleIndex(w, req)
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("status = %d, want %d", w.Code, http.StatusNotFound)
|
|
}
|
|
}
|
|
|
|
func TestCargoRoutes(t *testing.T) {
|
|
h := &CargoHandler{
|
|
proxy: cargoTestProxy(),
|
|
proxyURL: "http://proxy.local",
|
|
}
|
|
|
|
routes := h.Routes()
|
|
|
|
// Test that config.json route exists
|
|
req := httptest.NewRequest(http.MethodGet, "/config.json", nil)
|
|
w := httptest.NewRecorder()
|
|
routes.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("config.json status = %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
}
|
|
|
|
type filterTestCase struct {
|
|
line string
|
|
expected bool
|
|
}
|
|
|
|
func TestCargoCooldown(t *testing.T) {
|
|
now := time.Now()
|
|
|
|
createCase := func(name string, version string, age time.Duration, expected bool) filterTestCase {
|
|
return filterTestCase{line: `{"name":"` + name + `","vers":"` + version + `","cksum":"abcd","features":{},"yanked":false,"pubtime":"` + now.Add(-1*age).Format(time.RFC3339) + `"}`, expected: expected}
|
|
}
|
|
|
|
testCases := []filterTestCase{
|
|
// one week ago
|
|
createCase("serde", "1.0.0", 168*time.Hour, true),
|
|
// one hour ago
|
|
createCase("serde", "1.0.1", 1*time.Hour, false),
|
|
// two hours ago with custom filter (1h)
|
|
createCase("tokio", "1.0.0", 2*time.Hour, true),
|
|
// one hour ago with custom filter (1h)
|
|
createCase("tokio", "1.0.0", 1*time.Minute, false),
|
|
}
|
|
|
|
var testInput strings.Builder
|
|
var expectedOutput strings.Builder
|
|
|
|
for _, testCase := range testCases {
|
|
testInput.WriteString(testCase.line + "\n")
|
|
if testCase.expected {
|
|
expectedOutput.WriteString(testCase.line + "\n")
|
|
}
|
|
}
|
|
|
|
proxy := testProxy()
|
|
proxy.Cooldown = &cooldown.Config{
|
|
Default: "3d",
|
|
Packages: map[string]string{"pkg:cargo/tokio": "1h"},
|
|
}
|
|
|
|
h := &CargoHandler{
|
|
proxy: proxy,
|
|
proxyURL: "http://localhost:8080",
|
|
}
|
|
|
|
recorder := httptest.NewRecorder()
|
|
h.applyCooldownFiltering(recorder, []byte(testInput.String()))
|
|
output := recorder.Body.String()
|
|
|
|
if output != expectedOutput.String() {
|
|
t.Errorf("output = %q, want %q", output, expectedOutput.String())
|
|
}
|
|
|
|
}
|