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/cmd/proxy/mirror_swift_test.go

186 lines
6.1 KiB
Go
Raw Permalink Normal View History

Accept Swift source repository PURLs in the denylist and retention rules (#641) * Accept Swift source repository PURLs in the denylist and retention rules Closes #613. pkg:swift/github.com/apple/swift-nio, as SBOM tools and GHSA write Swift packages, is resolved to its registry identity through the upstream's /identifiers: - Denylist entries are resolved at startup, and the proxy refuses to start when one can't be, since a skipped entry would let the version through. - Retention keys are accepted at validation and kept pending. The retention loop resolves them before each sweep, and Swift isn't swept while any is pending, so a rule that keeps a package is never skipped. The mirror's source URL helper moves to the handler as SwiftSourceURL. * Resolve Swift source denylist entries in proxy mirror too serve resolved source-form Swift denylist entries through the upstream, but proxy mirror built its denylist directly, so such an entry never matched a mirror download. Both now use server.ConfigureDenylist, and neither runs when an entry can't be resolved. * Apply Swift source policies to every listed identity A Swift registry may list several identities for one source repository, but a denylist entry or retention rule naming the repository only covered the first. Denying pkg:swift/github.com/acme/widget@1.0.0 let acme.widget through when the registry listed other.widget first, and a never-evict rule cleared the cached apple.swift-nio archive when other.swift-nio came first. Both now cover every identity the registry lists. A retention rule that conflicts with another rule for any of them stays pending, with none applied. Mirroring a source PURL still downloads the first identity.
2026-10-31 12:34:05 +01:00
package main
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"sync/atomic"
"testing"
"github.com/git-pkgs/proxy/internal/database"
)
const swiftWidgetArchive = "acme widget archive"
// swiftMirrorUpstream serves acme.widget 1.0.0 the way a Swift package
// registry does, with https://github.com/acme/widget as its source
// repository, and counts archive downloads and identifier lookups. The
// repository's identifiers are ids, or only acme.widget.
func swiftMirrorUpstream(t *testing.T, ids ...string) (srv *httptest.Server, downloads, lookups *atomic.Int64) {
t.Helper()
if len(ids) == 0 {
ids = []string{"acme.widget"}
}
identifiers, err := json.Marshal(map[string][]string{"identifiers": ids})
if err != nil {
t.Fatal(err)
}
downloads, lookups = new(atomic.Int64), new(atomic.Int64)
sum := sha256.Sum256([]byte(swiftWidgetArchive))
srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Version", "1")
switch r.URL.Path {
case "/identifiers":
lookups.Add(1)
if r.URL.Query().Get("url") != "https://github.com/acme/widget" {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(identifiers)
case "/acme/widget":
w.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(w, `{"releases":{"1.0.0":{}}}`)
case "/acme/widget/1.0.0/Package.swift":
w.Header().Set("Content-Type", "text/x-swift")
_, _ = io.WriteString(w, "// swift-tools-version:5.9\n")
case "/acme/widget/1.0.0":
w.Header().Set("Content-Type", "application/json")
_, _ = fmt.Fprintf(w, `{"id":"acme.widget","version":"1.0.0","resources":[{"name":"source-archive","type":"application/zip","checksum":%q}]}`, hex.EncodeToString(sum[:]))
case "/acme/widget/1.0.0.zip":
downloads.Add(1)
w.Header().Set("Content-Type", "application/zip")
_, _ = io.WriteString(w, swiftWidgetArchive)
default:
http.NotFound(w, r)
}
}))
t.Cleanup(srv.Close)
return srv, downloads, lookups
}
// writeSwiftMirrorConfig writes a config using upstream as the Swift
// registry, denying the denied package PURLs.
func writeSwiftMirrorConfig(t *testing.T, dir, upstream string, denied ...string) string {
t.Helper()
cfg := "storage:\n" +
" url: file://" + filepath.Join(dir, "artifacts") + "\n" +
"database:\n" +
" path: " + filepath.Join(dir, "proxy.db") + "\n" +
"upstream:\n" +
" swift: " + upstream + "\n" +
" allow_loopback: true\n"
if len(denied) > 0 {
cfg += "denylist:\n packages:\n"
for _, p := range denied {
cfg += " - " + p + "\n"
}
}
path := filepath.Join(dir, "config.yaml")
if err := os.WriteFile(path, []byte(cfg), 0o600); err != nil {
t.Fatalf("writing config: %v", err)
}
return path
}
func cachedArtifacts(t *testing.T, dir string) int64 {
t.Helper()
db, err := database.Open(filepath.Join(dir, "proxy.db"))
if err != nil {
t.Fatalf("opening database: %v", err)
}
defer func() { _ = db.Close() }()
stats, err := db.GetCacheStats()
if err != nil {
t.Fatalf("GetCacheStats: %v", err)
}
return stats.TotalArtifacts
}
// Without a denylist the fake registry mirrors the release, so the tests
// below fail for the denylist and not for the setup.
func TestMirrorSwiftRelease(t *testing.T) {
upstream, downloads, _ := swiftMirrorUpstream(t)
dir := t.TempDir()
configPath := writeSwiftMirrorConfig(t, dir, upstream.URL)
output := runMirrorProcess(t, 0, "-config", configPath, "pkg:swift/acme/widget@1.0.0")
if !strings.Contains(output, "1 downloaded") {
t.Errorf("output:\n%s", output)
}
if n := downloads.Load(); n != 1 {
t.Errorf("archive downloads = %d, want 1", n)
}
if n := cachedArtifacts(t, dir); n != 1 {
t.Errorf("cached artifacts = %d, want 1", n)
}
}
// A denylist entry naming the package by its source repository blocks the
// mirror download of the registry identity it resolves to.
func TestMirrorDenylistSwiftSourceEntry(t *testing.T) {
upstream, downloads, _ := swiftMirrorUpstream(t)
dir := t.TempDir()
configPath := writeSwiftMirrorConfig(t, dir, upstream.URL, "pkg:swift/github.com/acme/widget@1.0.0")
output := runMirrorProcess(t, 1, "-config", configPath, "pkg:swift/acme/widget@1.0.0")
if !strings.Contains(output, "0 downloaded") || !strings.Contains(output, "1 failed") {
t.Errorf("output:\n%s", output)
}
if n := downloads.Load(); n != 0 {
t.Errorf("archive downloads = %d, want 0", n)
}
if n := cachedArtifacts(t, dir); n != 0 {
t.Errorf("cached artifacts = %d, want 0", n)
}
}
// The registry may list several identities for one repository. A source
// entry denies all of them, not only the first.
func TestMirrorDenylistSwiftSourceEntryEveryIdentity(t *testing.T) {
upstream, downloads, _ := swiftMirrorUpstream(t, "other.widget", "acme.widget")
dir := t.TempDir()
configPath := writeSwiftMirrorConfig(t, dir, upstream.URL, "pkg:swift/github.com/acme/widget@1.0.0")
output := runMirrorProcess(t, 1, "-config", configPath, "pkg:swift/acme/widget@1.0.0")
if !strings.Contains(output, "0 downloaded") || !strings.Contains(output, "1 failed") {
t.Errorf("output:\n%s", output)
}
if n := downloads.Load(); n != 0 {
t.Errorf("archive downloads = %d, want 0", n)
}
}
// A source repository entry the registry can't resolve stops the mirror
// before it downloads anything, as it stops serve: left out, it would let
// the version through.
func TestMirrorDenylistUnresolvedSwiftSource(t *testing.T) {
upstream, downloads, lookups := swiftMirrorUpstream(t)
dir := t.TempDir()
configPath := writeSwiftMirrorConfig(t, dir, upstream.URL, "pkg:swift/github.com/acme/unknown@1.0.0")
output := runMirrorProcess(t, 1, "-config", configPath, "pkg:swift/acme/widget@1.0.0")
if !strings.Contains(output, "configuring denylist") || !strings.Contains(output, "github.com/acme/unknown") {
t.Errorf("output does not report the unresolved entry:\n%s", output)
}
if strings.Contains(output, "Mirror complete") {
t.Errorf("mirror ran:\n%s", output)
}
if lookups.Load() == 0 {
t.Error("the entry was not looked up")
}
if n := downloads.Load(); n != 0 {
t.Errorf("archive downloads = %d, want 0", n)
}
}