Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-11-09 07:39:19 -05:00
pkg-proxy/internal/server/query_in_path_test.go
Simon Chrzanowski 1fbee102b1 Ignore path parameters when checking for dot segments
Tomcat and Jetty drop ";params" from a path segment before resolving it,
so "..;" passed the dot-segment check and still got the credentials
configured for the upstream path. The check now ignores ";params", and
package routes refuse ';' like '?' and '#', which also stops "1.0;x"
from passing the denylist as a different version. Composer archives
stay exempt, since git branch names may contain ';'.
2026-10-23 09:47:07 +02:00

113 lines
4.4 KiB
Go

package server
import (
"context"
"errors"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"github.com/git-pkgs/proxy/internal/config"
"github.com/git-pkgs/proxy/internal/database"
"github.com/git-pkgs/proxy/internal/handler"
"github.com/git-pkgs/proxy/internal/storage"
"github.com/git-pkgs/registries/fetch"
"github.com/go-chi/chi/v5"
)
// countingTransport records every upstream request and fails it, so a test
// can tell whether a handler reached out at all.
type countingTransport struct{ hits atomic.Int32 }
func (c *countingTransport) RoundTrip(*http.Request) (*http.Response, error) {
c.hits.Add(1)
return nil, errors.New("upstream contacted")
}
// TestProtocolHandlersRejectQueryInPath sends a path with an encoded '?' or
// '#' to every mounted ecosystem. Without the guard, most of these fetch the
// file before the '?' and cache it under the name after it.
func TestProtocolHandlersRejectQueryInPath(t *testing.T) {
db, err := database.Create(t.TempDir() + "/test.db")
if err != nil {
t.Fatalf("creating database: %v", err)
}
t.Cleanup(func() { _ = db.Close() })
store, err := storage.OpenBucket(context.Background(), "file://"+t.TempDir())
if err != nil {
t.Fatalf("opening storage: %v", err)
}
transport := &countingTransport{}
client := &http.Client{Transport: transport}
fetcher := fetch.NewFetcher(fetch.WithHTTPClient(client), fetch.WithMaxRetries(0))
t.Cleanup(func() { _ = fetcher.Close() })
proxy := handler.NewProxy(db, store, fetcher, fetch.NewResolver(), slog.New(slog.NewTextHandler(io.Discard, nil)))
proxy.HTTPClient = client
cfg := config.Default()
cfg.BaseURL = "http://proxy.example"
cfg.Upstream.Generic = map[string]string{"github": "https://github.com"}
s := &Server{cfg: cfg}
r := chi.NewRouter()
s.mountProtocolHandlers(r, proxy)
for _, target := range []string{
"/apk/alpine/v3.22/main/x86_64/busybox-1.37.0-r12.apk%3F/foo-9.9-r0.apk",
"/apk/alpine/v3.22/main/x86_64/busybox-1.37.0-r12.apk%23/foo-9.9-r0.apk",
"/rpm/Packages/n/nginx-1.24.0-1.fc39.x86_64.rpm%3F/foo-9.9-1.fc39.x86_64.rpm",
"/debian/pool/main/c/curl/curl_8.5.0-2_amd64.deb%3F/foo_9.9-1_amd64.deb",
"/generic/github/owner/repo/releases/download/v1/tool.tar.gz%3F/other/repo/releases/download/v2/tool.tar.gz",
"/pypi/packages/ab/cd/ef/requests-2.31.0.tar.gz%3F/ab/cd/ef/foo-9.9.tar.gz",
"/maven/com/google/guava/guava/32.1.3-jre/guava-32.1.3-jre.jar%3F/org/evil/foo/9.9/foo-9.9.jar",
"/conda/conda-forge/linux-64/numpy-1.26.0-py312_0.conda%3F%2Ffoo-9.9-py_0.conda",
"/cran/src/contrib/ggplot2_3.4.0.tar.gz%3F%2Ffoo_9.9.tar.gz",
"/gem/gems/rails-7.0.0.gem%3F%2Ffoo-9.9.gem",
"/hex/tarballs/phoenix-1.7.10.tar%3F%2Ffoo-9.9.tar",
"/pub/packages/http/versions/0.13.0.tar.gz%3F1.2.0.tar.gz",
"/go/golang.org/x/text/@v/v0.3.0.zip%3F/@v/v0.3.8.zip",
"/nuget/v3-flatcontainer/newtonsoft.json/13.0.1/newtonsoft.json.13.0.1.nupkg%3F%2Ffoo.9.9.nupkg",
"/conan/v2/files/zlib/1.3/_/_/rev/recipe/conanfile.py%3F%2Fx",
"/composer/p2/monolog/monolog.json%3Fx",
"/npm/lodash%3F/-/lodash-4.17.21.tgz",
"/cargo/crates/serde%3F/1.0.0/download",
"/julia/registry/23338594-aafe-5451-b93e-139f81909106/abc%3F",
"/swift/apple/swift-nio/2.0.0%3F.zip",
"/homebrew/formula%3F.json",
"/helm/charts/nginx-1.0.0.tgz%3F",
"/v2/library/alpine/manifests/latest%3F",
"/gradle/cache/abc%3F",
} {
w := httptest.NewRecorder()
r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, target, nil))
if w.Code != http.StatusBadRequest {
t.Errorf("%s: status = %d, want 400", target, w.Code)
}
}
if n := transport.hits.Load(); n != 0 {
t.Errorf("upstream was contacted %d times", n)
}
}
// TestComposerFilesAllowHashInVersion keeps Composer archive downloads of git
// branches with '#' or ';' in their name working: the version is escaped in
// the dist URL and the archive is resolved from metadata, not from the
// request path.
func TestComposerFilesAllowHashInVersion(t *testing.T) {
for _, target := range []string{
"/composer/files/acme/lib/dev-fix%2F%2342/abc123.zip",
"/composer/files/acme/lib/dev-fix%3B42/abc123.zip",
} {
var reached atomic.Bool
next := http.HandlerFunc(func(http.ResponseWriter, *http.Request) { reached.Store(true) })
w := httptest.NewRecorder()
rejectQueryInPath(next).ServeHTTP(w, httptest.NewRequest(http.MethodGet, target, nil))
if w.Code == http.StatusBadRequest || !reached.Load() {
t.Errorf("%s: status = %d, reached = %v; want the request passed on", target, w.Code, reached.Load())
}
}
}