mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-11-09 15:49:16 -05:00
Mirroring a PyPI version passed each file's sha256 as the integrity GetOrFetchArtifactFromURLWithIntegrity records with the version. Wheels of one version share the name and version they are cached under, so the last file's checksum replaced the others', and serving the other wheels from the cache failed the integrity check. The mirror now passes the sha256 as a digest, which is checked against that file's content hash alone.
141 lines
4.7 KiB
Go
141 lines
4.7 KiB
Go
package server
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/git-pkgs/proxy/internal/config"
|
|
"github.com/git-pkgs/proxy/internal/handler"
|
|
)
|
|
|
|
const (
|
|
mirrorWheelPath = "/packages/ab/cd/example_pkg-1.0-py3-none-any.whl"
|
|
mirrorLinuxWheelPath = "/packages/23/45/example_pkg-1.0-cp312-cp312-manylinux_2_17_x86_64.whl"
|
|
mirrorSdistPath = "/packages/ef/01/example.pkg-1.0.tar.gz"
|
|
)
|
|
|
|
// pypiUpstream serves the JSON API for example-pkg 1.0 with two wheels, which
|
|
// are cached under the same name and version, and an sdist, whose filename
|
|
// spells the name differently. It counts file downloads by path.
|
|
type pypiUpstream struct {
|
|
*httptest.Server
|
|
mu sync.Mutex
|
|
files map[string]string
|
|
hits map[string]int
|
|
}
|
|
|
|
func newPyPIUpstream(t *testing.T, digests map[string]string) *pypiUpstream {
|
|
t.Helper()
|
|
u := &pypiUpstream{
|
|
files: map[string]string{mirrorWheelPath: "wheel", mirrorLinuxWheelPath: "linux wheel", mirrorSdistPath: "sdist"},
|
|
hits: map[string]int{},
|
|
}
|
|
u.Server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/pypi/example-pkg/json" {
|
|
var files []map[string]any
|
|
for _, path := range []string{mirrorWheelPath, mirrorLinuxWheelPath, mirrorSdistPath} {
|
|
sum := sha256.Sum256([]byte(u.files[path]))
|
|
digest := hex.EncodeToString(sum[:])
|
|
if d, ok := digests[path]; ok {
|
|
digest = d
|
|
}
|
|
files = append(files, map[string]any{
|
|
"url": u.URL + path,
|
|
"digests": map[string]string{"sha256": digest},
|
|
"size": len(u.files[path]),
|
|
})
|
|
}
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"info": map[string]any{"name": "example-pkg"},
|
|
"releases": map[string]any{"1.0": files},
|
|
})
|
|
return
|
|
}
|
|
body, ok := u.files[r.URL.Path]
|
|
if !ok {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
u.mu.Lock()
|
|
u.hits[r.URL.Path]++
|
|
u.mu.Unlock()
|
|
_, _ = io.WriteString(w, body)
|
|
}))
|
|
t.Cleanup(u.Close)
|
|
return u
|
|
}
|
|
|
|
func (u *pypiUpstream) hitsFor(path string) int {
|
|
u.mu.Lock()
|
|
defer u.mu.Unlock()
|
|
return u.hits[path]
|
|
}
|
|
|
|
func pypiMirrorConfig(upstream *pypiUpstream) *config.Config {
|
|
cfg := config.Default()
|
|
cfg.Upstream.PyPI = upstream.URL
|
|
cfg.Upstream.PyPIDownload = upstream.URL
|
|
return cfg
|
|
}
|
|
|
|
func TestMirrorPyPIFetchesEveryFile(t *testing.T) {
|
|
upstream := newPyPIUpstream(t, nil)
|
|
cfg := pypiMirrorConfig(upstream)
|
|
|
|
progress, proxy := mirrorWithConfig(t, cfg, "pkg:pypi/example-pkg@1.0")
|
|
|
|
if progress.Completed != 1 || progress.Failed != 0 {
|
|
t.Fatalf("completed = %d, failed = %d, errors = %v", progress.Completed, progress.Failed, progress.Errors)
|
|
}
|
|
for path := range upstream.files {
|
|
if got := upstream.hitsFor(path); got != 1 {
|
|
t.Errorf("%s fetched %d times, want 1", path, got)
|
|
}
|
|
}
|
|
|
|
// pip gets every file from the cache once the upstream is down.
|
|
upstream.Close()
|
|
pypi := handler.NewPyPIHandlerWithUpstreams(proxy, "http://proxy.test", cfg.Upstream.PyPI, cfg.Upstream.PyPIDownload)
|
|
for path, want := range upstream.files {
|
|
rec := httptest.NewRecorder()
|
|
pypi.Routes().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, path, nil))
|
|
if rec.Code != http.StatusOK || rec.Body.String() != want {
|
|
t.Errorf("%s from cache: status = %d, body = %q, want 200 and %q", path, rec.Code, rec.Body, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMirrorPyPIFileNameQualifier(t *testing.T) {
|
|
upstream := newPyPIUpstream(t, nil)
|
|
|
|
progress, _ := mirrorWithConfig(t, pypiMirrorConfig(upstream), "pkg:pypi/example-pkg@1.0?file_name=example.pkg-1.0.tar.gz")
|
|
|
|
if progress.Completed != 1 || upstream.hitsFor(mirrorSdistPath) != 1 {
|
|
t.Errorf("completed = %d, sdist hits = %d, errors = %v; want the sdist", progress.Completed, upstream.hitsFor(mirrorSdistPath), progress.Errors)
|
|
}
|
|
if upstream.hitsFor(mirrorWheelPath) != 0 {
|
|
t.Error("the wheel was mirrored although file_name names the sdist")
|
|
}
|
|
|
|
progress, _ = mirrorWithConfig(t, pypiMirrorConfig(upstream), "pkg:pypi/example-pkg@1.0?file_name=missing-1.0.tar.gz")
|
|
if progress.Failed != 1 || len(progress.Errors) != 1 || !strings.Contains(progress.Errors[0].Error, "missing-1.0.tar.gz") {
|
|
t.Errorf("failed = %d, errors = %v; want a failure naming the missing file", progress.Failed, progress.Errors)
|
|
}
|
|
}
|
|
|
|
func TestMirrorPyPIRefusesDigestMismatch(t *testing.T) {
|
|
upstream := newPyPIUpstream(t, map[string]string{mirrorWheelPath: strings.Repeat("0", 64)})
|
|
|
|
progress, _ := mirrorWithConfig(t, pypiMirrorConfig(upstream), "pkg:pypi/example-pkg@1.0?file_name=example_pkg-1.0-py3-none-any.whl")
|
|
|
|
if progress.Failed != 1 || progress.Completed != 0 {
|
|
t.Errorf("completed = %d, failed = %d, errors = %v; want the mismatching wheel refused", progress.Completed, progress.Failed, progress.Errors)
|
|
}
|
|
}
|