Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-11-09 15:49:16 -05:00
pkg-proxy/internal/handler/maven_repositories_test.go
Simon Chrzanowski 4ae55605b1 Send Maven coordinate files under odd groups to the 404
A file named {artifact}-{version} under a dotted group directory, such as
org.foo/bar/1.0/bar-1.0-linux.exe, was not recognised as a coordinate file
and so was proxied past the checks. isCoordinateFile no longer checks the
group, so handleDownload answers 404 for it. The docs say which files get
the 404, and the Range test covers a stale If-Range.
2026-11-08 22:18:14 +01:00

225 lines
8.8 KiB
Go

package handler
import (
"io"
"net/http"
"net/http/httptest"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/git-pkgs/registries/fetch"
)
// mavenRepoUpstream serves label-specific bytes for every path it knows and
// counts its requests.
func mavenRepoUpstream(t *testing.T, label string, known ...string) (*httptest.Server, *atomic.Int32) {
t.Helper()
var requests atomic.Int32
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requests.Add(1)
for _, p := range known {
if r.URL.Path == p {
_, _ = io.WriteString(w, label+" "+r.URL.Path)
return
}
}
http.NotFound(w, r)
}))
t.Cleanup(srv.Close)
return srv, &requests
}
func newMavenRepositoriesHandler(t *testing.T, central, portal, grails *httptest.Server) http.Handler {
t.Helper()
proxy, _, store, _ := setupTestProxy(t)
store.seekable = true
proxy.HTTPClient = central.Client()
fetcher := fetch.NewFetcher(fetch.WithHTTPClient(central.Client()), fetch.WithMaxRetries(0))
proxy.Fetcher = fetcher
t.Cleanup(func() { _ = fetcher.Close() })
return NewMavenHandler(proxy, "http://localhost", central.URL, portal.URL).
WithRepositories(map[string]string{"grails": grails.URL + "/restricted/"}).Routes()
}
func getMaven(h http.Handler, path string) *httptest.ResponseRecorder {
w := httptest.NewRecorder()
h.ServeHTTP(w, httptest.NewRequest(http.MethodGet, path, nil))
return w
}
const (
mavenJarPath = "/org/grails/grails-core/6.0.0/grails-core-6.0.0.jar"
mavenMetadataPath = "/org/grails/grails-core/maven-metadata.xml"
)
func TestMavenRepositoriesAreServedFromTheirOwnUpstream(t *testing.T) {
central, _ := mavenRepoUpstream(t, "central", mavenJarPath, mavenMetadataPath)
portal, _ := mavenRepoUpstream(t, "portal")
grails, _ := mavenRepoUpstream(t, "grails", "/restricted"+mavenJarPath, "/restricted"+mavenMetadataPath)
h := newMavenRepositoriesHandler(t, central, portal, grails)
// The same coordinates in both repositories are cached apart.
for range 2 {
if w := getMaven(h, "/grails"+mavenJarPath); w.Code != http.StatusOK || w.Body.String() != "grails /restricted"+mavenJarPath {
t.Fatalf("named jar: status = %d, body = %q", w.Code, w.Body)
}
if w := getMaven(h, mavenJarPath); w.Code != http.StatusOK || w.Body.String() != "central "+mavenJarPath {
t.Fatalf("default jar: status = %d, body = %q", w.Code, w.Body)
}
}
if w := getMaven(h, "/grails"+mavenMetadataPath); w.Code != http.StatusOK || !strings.HasPrefix(w.Body.String(), "grails ") {
t.Errorf("named metadata: status = %d, body = %q", w.Code, w.Body)
}
if w := getMaven(h, mavenMetadataPath); w.Code != http.StatusOK || !strings.HasPrefix(w.Body.String(), "central ") {
t.Errorf("default metadata: status = %d, body = %q", w.Code, w.Body)
}
}
func TestMavenRepositoriesDoNotFallBackToThePluginPortal(t *testing.T) {
const pluginPath = "/org/example/plugin/1.0/plugin-1.0.jar"
central, _ := mavenRepoUpstream(t, "central")
portal, portalRequests := mavenRepoUpstream(t, "portal", pluginPath, "/org/example/plugin/maven-metadata.xml")
grails, _ := mavenRepoUpstream(t, "grails")
h := newMavenRepositoriesHandler(t, central, portal, grails)
for _, path := range []string{"/grails" + pluginPath, "/grails/org/example/plugin/maven-metadata.xml"} {
if w := getMaven(h, path); w.Code != http.StatusNotFound {
t.Errorf("GET %s status = %d, want 404", path, w.Code)
}
}
if n := portalRequests.Load(); n != 0 {
t.Errorf("named repository asked the plugin portal %d times", n)
}
if w := getMaven(h, pluginPath); w.Code != http.StatusOK || !strings.HasPrefix(w.Body.String(), "portal ") {
t.Errorf("default repository: status = %d, body = %q, want the plugin portal's copy", w.Code, w.Body)
}
}
func TestMavenRepositoryRootIsNotFound(t *testing.T) {
central, centralRequests := mavenRepoUpstream(t, "central")
portal, _ := mavenRepoUpstream(t, "portal")
grails, _ := mavenRepoUpstream(t, "grails")
h := newMavenRepositoriesHandler(t, central, portal, grails)
for _, path := range []string{"/grails", "/grails/"} {
if w := getMaven(h, path); w.Code != http.StatusNotFound {
t.Errorf("GET %s status = %d, want 404", path, w.Code)
}
}
if n := centralRequests.Load(); n != 0 {
t.Errorf("repository root went to the default upstream %d times", n)
}
}
func TestMavenRepositoryRepointedServesTheNewUpstream(t *testing.T) {
central, _ := mavenRepoUpstream(t, "central")
portal, _ := mavenRepoUpstream(t, "portal")
staging, _ := mavenRepoUpstream(t, "staging", mavenJarPath)
production, _ := mavenRepoUpstream(t, "production", mavenJarPath)
proxy, _, store, _ := setupTestProxy(t)
store.seekable = true
proxy.HTTPClient = central.Client()
fetcher := fetch.NewFetcher(fetch.WithHTTPClient(central.Client()), fetch.WithMaxRetries(0))
proxy.Fetcher = fetcher
t.Cleanup(func() { _ = fetcher.Close() })
for _, upstream := range []struct {
srv *httptest.Server
label string
}{{staging, "staging"}, {production, "production"}} {
h := NewMavenHandler(proxy, "http://localhost", central.URL, portal.URL).
WithRepositories(map[string]string{"grails": upstream.srv.URL}).Routes()
if w := getMaven(h, "/grails"+mavenJarPath); w.Code != http.StatusOK || w.Body.String() != upstream.label+" "+mavenJarPath {
t.Errorf("%s: status = %d, body = %q", upstream.label, w.Code, w.Body)
}
}
}
// TestMavenDottedGroupDirectoryNotCachedAsGroup checks that a file whose
// group directories contain dots or colons, which the Maven layout never
// has, is not served: it would either share the cache entry of the group
// the dots spell or be proxied past the denylist, scan and cooldown.
func TestMavenDottedGroupDirectoryNotCachedAsGroup(t *testing.T) {
const standard = "/org/foo/bar/1.0/bar-1.0.jar"
odd := []string{
"/org.foo/bar/1.0/bar-1.0.jar", "/org.foo/bar/1.0/bar-1.0-linux.exe",
"/org/foo:x/bar/1.0/bar-1.0.jar", "/org/foo/b:ar/1.0/b:ar-1.0.jar",
}
central, requests := mavenRepoUpstream(t, "central", append([]string{standard}, odd...)...)
portal, _ := mavenRepoUpstream(t, "portal")
var restricted []string
for _, p := range odd {
restricted = append(restricted, "/restricted"+p)
}
grails, grailsRequests := mavenRepoUpstream(t, "grails", restricted...)
h := newMavenRepositoriesHandler(t, central, portal, grails)
if w := getMaven(h, standard); w.Code != http.StatusOK || w.Body.String() != "central "+standard {
t.Errorf("GET %s = %d %q", standard, w.Code, w.Body.String())
}
before := requests.Load() + grailsRequests.Load()
for _, p := range odd {
for _, path := range []string{p, "/grails" + p} {
if w := getMaven(h, path); w.Code != http.StatusNotFound {
t.Errorf("GET %s = %d %q, want 404", path, w.Code, w.Body.String())
}
}
}
if n := requests.Load() + grailsRequests.Load() - before; n != 0 {
t.Errorf("%d upstream requests for paths outside the Maven layout", n)
}
}
// TestMavenUncachedPathFallsBackToPluginPortal checks that a path proxied
// without caching, such as a catalog, still tries the Gradle Plugin Portal
// when the primary upstream lacks it.
func TestMavenUncachedPathFallsBackToPluginPortal(t *testing.T) {
const uncached = "/org/foo/archetype-catalog.xml"
central, _ := mavenRepoUpstream(t, "central")
portal, _ := mavenRepoUpstream(t, "portal", uncached)
grails, _ := mavenRepoUpstream(t, "grails")
h := newMavenRepositoriesHandler(t, central, portal, grails)
if w := getMaven(h, uncached); w.Code != http.StatusOK || w.Body.String() != "portal "+uncached {
t.Errorf("GET %s = %d %q, want the Plugin Portal's copy", uncached, w.Code, w.Body.String())
}
}
func TestMavenUncachedPathKeepsByteRanges(t *testing.T) {
const uncached = "/org/foo/archetype-catalog.xml"
ranged := func() *httptest.Server {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("ETag", `"v2"`)
http.ServeContent(w, r, "archetype-catalog.xml", time.Time{}, strings.NewReader("0123456789"))
}))
t.Cleanup(srv.Close)
return srv
}
central, grails := ranged(), ranged()
portal, _ := mavenRepoUpstream(t, "portal")
h := newMavenRepositoriesHandler(t, central, portal, grails)
for _, p := range []string{uncached, "/grails" + uncached} {
req := httptest.NewRequest(http.MethodGet, p, nil)
req.Header.Set("Range", "bytes=2-4")
w := httptest.NewRecorder()
h.ServeHTTP(w, req)
if w.Code != http.StatusPartialContent || w.Body.String() != "234" {
t.Errorf("GET %s with Range = %d %q, want 206 %q", p, w.Code, w.Body.String(), "234")
}
// A Range for an older copy gets the whole current file.
req = httptest.NewRequest(http.MethodGet, p, nil)
req.Header.Set("Range", "bytes=2-4")
req.Header.Set("If-Range", `"v1"`)
w = httptest.NewRecorder()
h.ServeHTTP(w, req)
if w.Code != http.StatusOK || w.Body.String() != "0123456789" {
t.Errorf("GET %s with stale If-Range = %d %q, want 200 with the whole file", p, w.Code, w.Body.String())
}
}
}