mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-11-09 15:49:16 -05:00
* 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.
187 lines
6.8 KiB
Go
187 lines
6.8 KiB
Go
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
|
|
}
|
|
|
|
// 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) {
|
|
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) {
|
|
return nil, fmt.Errorf("the Swift registry has no package for %s", sourceURL)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
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) {
|
|
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 {
|
|
return nil, fmt.Errorf("parsing the Swift registry's identifiers for %s: %w", sourceURL, err)
|
|
}
|
|
if len(answer.Identifiers) == 0 {
|
|
return nil, fmt.Errorf("the Swift registry has no package for %s", sourceURL)
|
|
}
|
|
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})
|
|
}
|
|
return ids, nil
|
|
}
|