Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions internal/handler/cargo.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"time"

Expand All @@ -30,11 +31,18 @@ type CargoHandler struct {
}

// NewCargoHandler creates a new cargo protocol handler.
func NewCargoHandler(proxy *Proxy, proxyURL string) *CargoHandler {
func NewCargoHandler(proxy *Proxy, proxyURL, indexURL, downloadURL string) *CargoHandler {
if strings.TrimSpace(indexURL) == "" {
indexURL = cargoUpstream
}
if strings.TrimSpace(downloadURL) == "" {
downloadURL = cargoDownloadBase
}

return &CargoHandler{
proxy: proxy,
indexURL: cargoUpstream,
downloadURL: cargoDownloadBase,
indexURL: strings.TrimSuffix(indexURL, "/"),
downloadURL: strings.TrimSuffix(downloadURL, "/"),
proxyURL: strings.TrimSuffix(proxyURL, "/"),
}
}
Expand Down Expand Up @@ -191,7 +199,15 @@ func (h *CargoHandler) handleDownload(w http.ResponseWriter, r *http.Request) {
h.proxy.Logger.Info("cargo download request",
"crate", name, "version", version, "filename", filename)

result, err := h.proxy.GetOrFetchArtifact(r.Context(), "cargo", name, version, filename)
downloadURL := fmt.Sprintf(
"%s/%s/%s",
h.downloadURL,
url.PathEscape(name),
url.PathEscape(filename),
)
result, err := h.proxy.GetOrFetchArtifactFromURL(
r.Context(), "cargo", name, version, filename, downloadURL,
)
if err != nil {
h.proxy.Logger.Error("failed to get artifact", "error", err)
http.Error(w, "failed to fetch crate", http.StatusBadGateway)
Expand Down
60 changes: 60 additions & 0 deletions internal/handler/cargo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package handler

import (
"encoding/json"
"io"
"log/slog"
"net/http"
"net/http/httptest"
Expand All @@ -10,6 +11,7 @@ import (
"time"

"github.com/git-pkgs/cooldown"
"github.com/git-pkgs/registries/fetch"
)

func cargoTestProxy() *Proxy {
Expand Down Expand Up @@ -70,6 +72,64 @@ func TestCargoConfigEndpoint(t *testing.T) {
}
}

func TestCargoHandlerUsesConfiguredUpstreams(t *testing.T) {
t.Run("index", func(t *testing.T) {
var requestPath string
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestPath = r.URL.Path
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()
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")
}
})

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"}
Expand Down
26 changes: 23 additions & 3 deletions internal/handler/npm.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,14 @@ type NPMHandler struct {
}

// NewNPMHandler creates a new npm protocol handler.
func NewNPMHandler(proxy *Proxy, proxyURL string) *NPMHandler {
func NewNPMHandler(proxy *Proxy, proxyURL, upstreamURL string) *NPMHandler {
if strings.TrimSpace(upstreamURL) == "" {
upstreamURL = npmUpstream
}

return &NPMHandler{
proxy: proxy,
upstreamURL: npmUpstream,
upstreamURL: strings.TrimSuffix(upstreamURL, "/"),
proxyURL: strings.TrimSuffix(proxyURL, "/"),
}
}
Expand Down Expand Up @@ -263,7 +267,15 @@ func (h *NPMHandler) handleDownload(w http.ResponseWriter, r *http.Request) {
h.proxy.Logger.Info("npm download request",
"package", packageName, "version", version, "filename", filename)

result, err := h.proxy.GetOrFetchArtifact(r.Context(), "npm", packageName, version, filename)
downloadURL := fmt.Sprintf(
"%s/%s/-/%s",
h.upstreamURL,
escapeNPMDownloadPackage(packageName),
url.PathEscape(filename),
)
result, err := h.proxy.GetOrFetchArtifactFromURL(
r.Context(), "npm", packageName, version, filename, downloadURL,
)
if err != nil {
h.proxy.Logger.Error("failed to get artifact", "error", err)
JSONError(w, http.StatusBadGateway, "failed to fetch package")
Expand All @@ -273,6 +285,14 @@ func (h *NPMHandler) handleDownload(w http.ResponseWriter, r *http.Request) {
ServeArtifact(w, result)
}

func escapeNPMDownloadPackage(packageName string) string {
scope, name, scoped := strings.Cut(packageName, "/")
if scoped && strings.HasPrefix(scope, "@") && len(scope) > 1 && name != "" && !strings.Contains(name, "/") {
return url.PathEscape(scope) + "/" + url.PathEscape(name)
}
return url.PathEscape(packageName)
}

// extractPackageName extracts the package name from the request path.
// Handles both scoped (@scope/name) and unscoped (name) packages.
func (h *NPMHandler) extractPackageName(r *http.Request) string {
Expand Down
72 changes: 72 additions & 0 deletions internal/handler/npm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ 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"
)

const testVersion100 = "1.0.0"
Expand Down Expand Up @@ -46,6 +49,75 @@ func TestNPMExtractVersionFromFilename(t *testing.T) {
}
}

func TestNPMHandlerUsesConfiguredUpstream(t *testing.T) {
t.Run("metadata", func(t *testing.T) {
var requestPath string
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestPath = r.URL.Path
w.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(w, `{"versions":{}}`)
}))
defer upstream.Close()

proxy, _, _, _ := setupTestProxy(t)
proxy.HTTPClient = upstream.Client()
h := NewNPMHandler(proxy, "http://proxy.test", upstream.URL+"/root/")

req := httptest.NewRequest(http.MethodGet, "/testpkg", 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 != "/root/testpkg" {
t.Errorf("upstream path = %q, want %q", requestPath, "/root/testpkg")
}
})

t.Run("download", func(t *testing.T) {
proxy, _, _, artifactFetcher := setupTestProxy(t)
artifactFetcher.artifact = &fetch.Artifact{
Body: io.NopCloser(strings.NewReader("package")),
ContentType: "application/gzip",
}
h := NewNPMHandler(proxy, "http://proxy.test", "https://npm.example.test/root/")

req := httptest.NewRequest(http.MethodGet, "/testpkg/-/testpkg-1.0.0.tgz", 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://npm.example.test/root/testpkg/-/testpkg-1.0.0.tgz"
if artifactFetcher.fetchedURL != want {
t.Errorf("fetched URL = %q, want %q", artifactFetcher.fetchedURL, want)
}
})

t.Run("scoped download", func(t *testing.T) {
proxy, _, _, artifactFetcher := setupTestProxy(t)
artifactFetcher.artifact = &fetch.Artifact{
Body: io.NopCloser(strings.NewReader("package")),
ContentType: "application/gzip",
}
h := NewNPMHandler(proxy, "http://proxy.test", "https://npm.example.test/root/")

req := httptest.NewRequest(http.MethodGet, "/@scope/name/-/name-1.0.0.tgz", 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://npm.example.test/root/@scope/name/-/name-1.0.0.tgz"
if artifactFetcher.fetchedURL != want {
t.Errorf("fetched URL = %q, want %q", artifactFetcher.fetchedURL, want)
}
})
}

func TestNPMRewriteMetadata(t *testing.T) {
h := &NPMHandler{
proxy: testProxy(),
Expand Down
9 changes: 7 additions & 2 deletions internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,13 @@ func (s *Server) Start() error {
})

// Mount protocol handlers
npmHandler := handler.NewNPMHandler(proxy, s.cfg.BaseURL)
cargoHandler := handler.NewCargoHandler(proxy, s.cfg.BaseURL)
npmHandler := handler.NewNPMHandler(proxy, s.cfg.BaseURL, s.cfg.Upstream.NPM)
cargoHandler := handler.NewCargoHandler(
proxy,
s.cfg.BaseURL,
s.cfg.Upstream.Cargo,
s.cfg.Upstream.CargoDownload,
)
gemHandler := handler.NewGemHandler(proxy, s.cfg.BaseURL)
goHandler := handler.NewGoHandler(proxy, s.cfg.BaseURL)
hexHandler := handler.NewHexHandler(proxy, s.cfg.BaseURL)
Expand Down
9 changes: 7 additions & 2 deletions internal/server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,13 @@ func newTestServer(t *testing.T) *testServer {
r := chi.NewRouter()

// Mount handlers
npmHandler := handler.NewNPMHandler(proxy, cfg.BaseURL)
cargoHandler := handler.NewCargoHandler(proxy, cfg.BaseURL)
npmHandler := handler.NewNPMHandler(proxy, cfg.BaseURL, cfg.Upstream.NPM)
cargoHandler := handler.NewCargoHandler(
proxy,
cfg.BaseURL,
cfg.Upstream.Cargo,
cfg.Upstream.CargoDownload,
)
gemHandler := handler.NewGemHandler(proxy, cfg.BaseURL)
goHandler := handler.NewGoHandler(proxy, cfg.BaseURL)
pypiHandler := handler.NewPyPIHandler(proxy, cfg.BaseURL)
Expand Down
Loading