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/swift_mirror.go

187 lines
6.8 KiB
Go
Raw Permalink Normal View History

package handler
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
)
// MirrorRelease caches release version of scope.name so that a client can
// resolve and fetch the release without the upstream.
func (h *SwiftHandler) MirrorRelease(ctx context.Context, scope, name, version string) (*CacheResult, error) {
if !validSwiftPackageReference(scope, name, version) {
return nil, fmt.Errorf("invalid Swift registry release %s.%s@%s", scope, name, version)
}
scope, name = canonicalSwiftPackage(scope, name)
key, err := swiftSourceArchiveKey(scope, name, version)
if err != nil {
return nil, err
}
if h.documentsCached() {
if err := h.mirrorReleaseList(ctx, scope, name); err != nil {
return nil, fmt.Errorf("fetching release list: %w", err)
}
if err := h.mirrorManifests(ctx, scope, name, version); err != nil {
return nil, fmt.Errorf("fetching manifest: %w", err)
}
}
info, err := h.fetchArchiveInfo(ctx, scope, name, version)
if err != nil {
return nil, fmt.Errorf("fetching release metadata: %w", err)
}
headers := http.Header{"Accept": {swiftAcceptArchive}}
return h.fetchSourceArchive(ctx, key, version, h.buildUpstreamURL(scope, name, version+".zip", "", ""), headers, info)
}
// maxSwiftReleasePages bounds the release list pages a mirror follows.
const maxSwiftReleasePages = 100
// mirrorReleaseList caches every page of the release list, under the keys
// the release list route reads for the page links it rewrites.
func (h *SwiftHandler) mirrorReleaseList(ctx context.Context, scope, name string) error {
query := ""
seen := map[string]bool{}
for page := 0; page < maxSwiftReleasePages && !seen[query]; page++ {
seen[query] = true
upstreamURL := h.buildUpstreamURL(scope, name, "", "", query)
doc, err := h.fetchDocument(ctx, swiftReleasesCacheKey(scope, name, query), upstreamURL, swiftAcceptJSON)
if err != nil {
return err
}
next := swiftLinkQueries(doc.link, "next", upstreamURL)
if len(next) == 0 {
return nil
}
query = next[0]
}
return nil
}
// mirrorManifests caches Package.swift of a release and the version-specific
// manifests its Link header lists, under the keys the manifest route reads
// for the alternate links it rewrites.
func (h *SwiftHandler) mirrorManifests(ctx context.Context, scope, name, version string) error {
upstreamURL := h.buildUpstreamURL(scope, name, version, "Package.swift", "")
doc, err := h.fetchDocument(ctx, swiftManifestCacheKey(scope, name, version, ""), upstreamURL, swiftAcceptManifest)
if err != nil {
return err
}
for _, query := range swiftLinkQueries(doc.link, "alternate", upstreamURL) {
alternateURL := h.buildUpstreamURL(scope, name, version, "Package.swift", query)
if _, err := h.fetchDocument(ctx, swiftManifestCacheKey(scope, name, version, query), alternateURL, swiftAcceptManifest); err != nil {
return fmt.Errorf("%s: %w", query, err)
}
}
return nil
}
// swiftLinkQueries returns the queries of the links with relation rel in
// link, a Link header, that point at requestURL's own path with a query,
// such as the next page of a release list or a version-specific manifest.
func swiftLinkQueries(link, rel, requestURL string) []string {
base, err := url.Parse(requestURL)
if err != nil {
return nil
}
var queries []string
for _, value := range strings.Split(link, ",") {
target, params, ok := strings.Cut(strings.TrimSpace(value), ";")
if !ok || !strings.HasPrefix(target, "<") || !strings.HasSuffix(target, ">") || !swiftLinkHasRel(params, rel) {
continue
}
ref, err := url.Parse(strings.TrimSpace(target[1 : len(target)-1]))
if err != nil {
continue
}
resolved := base.ResolveReference(ref)
if resolved.Host != base.Host || resolved.EscapedPath() != base.EscapedPath() || resolved.RawQuery == "" {
continue
}
queries = append(queries, resolved.RawQuery)
}
return queries
}
func swiftLinkHasRel(params, rel string) bool {
for _, param := range strings.Split(params, ";") {
key, value, ok := strings.Cut(strings.TrimSpace(param), "=")
if !ok || !strings.EqualFold(strings.TrimSpace(key), "rel") {
continue
}
for _, r := range strings.Fields(strings.Trim(strings.TrimSpace(value), `"`)) {
if strings.EqualFold(r, rel) {
return true
}
}
}
return false
}
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
// SwiftIdentity is a package's identity in a Swift registry, scope.name.
type SwiftIdentity struct {
Scope, Name string
}
// ResolveIdentity returns the first registry identity the upstream lists for
// the source repository at sourceURL, the one a download uses.
func (h *SwiftHandler) ResolveIdentity(ctx context.Context, sourceURL string) (scope, name string, _ error) {
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
ids, err := h.ResolveIdentities(ctx, sourceURL)
if err != nil {
return "", "", err
}
return ids[0].Scope, ids[0].Name, nil
}
// ResolveIdentities returns every registry identity the upstream lists for
// the source repository at sourceURL; the registry protocol allows several.
// The answer is cached as the identifiers route caches it.
func (h *SwiftHandler) ResolveIdentities(ctx context.Context, sourceURL string) ([]SwiftIdentity, error) {
query := url.Values{"url": {sourceURL}}.Encode()
body, _, err := h.proxy.FetchOrCacheMetadata(ctx, "swift", swiftMetadataCacheKey("identifiers", query),
h.upstreamURL+"/identifiers?"+query, swiftAcceptJSON)
if errors.Is(err, ErrUpstreamNotFound) {
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
return nil, fmt.Errorf("the Swift registry has no package for %s", sourceURL)
}
if err != nil {
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
return nil, err
}
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
return SwiftIdentitiesOf(body, sourceURL)
}
// SwiftIdentityOf returns the scope and name of the first identity in body,
// a registry's answer to an identifiers lookup for sourceURL.
func SwiftIdentityOf(body []byte, sourceURL string) (scope, name string, _ error) {
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
ids, err := SwiftIdentitiesOf(body, sourceURL)
if err != nil {
return "", "", err
}
return ids[0].Scope, ids[0].Name, nil
}
// SwiftIdentitiesOf returns every identity in body, a registry's answer to
// an identifiers lookup for sourceURL. One invalid identity fails the whole
// answer, so a policy never covers only some of them.
func SwiftIdentitiesOf(body []byte, sourceURL string) ([]SwiftIdentity, error) {
var answer struct {
Identifiers []string `json:"identifiers"`
}
if err := json.Unmarshal(body, &answer); err != nil {
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
return nil, fmt.Errorf("parsing the Swift registry's identifiers for %s: %w", sourceURL, err)
}
if len(answer.Identifiers) == 0 {
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
return nil, fmt.Errorf("the Swift registry has no package for %s", sourceURL)
}
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
ids := make([]SwiftIdentity, 0, len(answer.Identifiers))
for _, identifier := range answer.Identifiers {
scope, name, ok := strings.Cut(identifier, ".")
if !ok || scope == "" || name == "" {
return nil, fmt.Errorf("the Swift registry answered %s with the invalid identity %q", sourceURL, identifier)
}
ids = append(ids, SwiftIdentity{Scope: scope, Name: name})
}
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
return ids, nil
}