mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-11-09 07:39:19 -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.
714 lines
22 KiB
Go
714 lines
22 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/git-pkgs/proxy/internal/config"
|
|
"github.com/git-pkgs/proxy/internal/packageurl"
|
|
"github.com/git-pkgs/proxy/internal/retention"
|
|
"github.com/git-pkgs/purl"
|
|
)
|
|
|
|
const (
|
|
swiftAcceptJSON = "application/vnd.swift.registry.v1+json"
|
|
swiftAcceptManifest = "application/vnd.swift.registry.v1+swift"
|
|
swiftAcceptArchive = "application/vnd.swift.registry.v1+zip"
|
|
swiftContentVersion = "1"
|
|
swiftMaxScopeLength = 39
|
|
swiftMaxNameLength = 100
|
|
)
|
|
|
|
// Source archives are stored under the registry identity,
|
|
// pkg:generic/swift-registry/{scope}.{name}, lowercased. Package rules write
|
|
// the scope as the namespace, pkg:swift/apple/swift-nio, or use the stored
|
|
// form. A source repository such as pkg:swift/github.com/apple/swift-nio
|
|
// names no registry identity; it is resolved through the upstream's
|
|
// /identifiers before it applies.
|
|
func init() {
|
|
retention.Default.Register(retention.Spec{
|
|
Key: "swift",
|
|
CanonicalPackage: swiftRetentionKey,
|
|
SourcePackage: func(p *purl.PURL) bool { return SwiftSourcePURL(p) != "" },
|
|
})
|
|
}
|
|
|
|
// SwiftSourceURL returns the repository URL of a pkg:swift name whose
|
|
// namespace starts with a host, as in github.com/apple/swift-nio, or "" for
|
|
// a name whose namespace is a registry scope.
|
|
func SwiftSourceURL(name string) string {
|
|
if host, _, ok := strings.Cut(name, "/"); ok && strings.Contains(host, ".") {
|
|
return "https://" + name
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// SwiftSourcePURL returns the repository URL of a pkg:swift PURL that names
|
|
// a package by its source repository, as SBOM tools and GHSA do, or "".
|
|
func SwiftSourcePURL(p *purl.PURL) string {
|
|
if p == nil || p.Type != "swift" || p.Namespace == "" {
|
|
return ""
|
|
}
|
|
return SwiftSourceURL(p.Namespace + "/" + p.Name)
|
|
}
|
|
|
|
// ResolveSourcePURL returns p with a source repository namespace replaced by
|
|
// each registry scope and name the upstream lists for it, and p itself when
|
|
// it already names a registry identity. A policy for the repository applies
|
|
// to all of them, since a client may use any.
|
|
func (h *SwiftHandler) ResolveSourcePURL(ctx context.Context, p *purl.PURL) ([]*purl.PURL, error) {
|
|
source := SwiftSourcePURL(p)
|
|
if source == "" {
|
|
return []*purl.PURL{p}, nil
|
|
}
|
|
ids, err := h.ResolveIdentities(ctx, source)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resolved := make([]*purl.PURL, 0, len(ids))
|
|
for _, id := range ids {
|
|
resolved = append(resolved, purl.New("swift", id.Scope, id.Name, p.Version, nil))
|
|
}
|
|
return resolved, nil
|
|
}
|
|
|
|
// swiftRetentionKey maps a package rule to the stored package PURL, refusing
|
|
// keys that name no valid registry scope and package. MakeCacheStrings
|
|
// lowercases the identity the way the handler stores it.
|
|
func swiftRetentionKey(p *purl.PURL) (string, bool) {
|
|
if p == nil {
|
|
return "", false
|
|
}
|
|
var scope, name string
|
|
switch {
|
|
case p.Type == "swift":
|
|
scope, name = p.Namespace, p.Name
|
|
case p.Type == "generic" && strings.EqualFold(p.Namespace, "swift-registry"):
|
|
var found bool
|
|
if scope, name, found = strings.Cut(p.Name, "."); !found {
|
|
return "", false
|
|
}
|
|
default:
|
|
return "", false
|
|
}
|
|
if !validSwiftScope(scope) || !validSwiftPackageName(name) {
|
|
return "", false
|
|
}
|
|
packagePURL, _ := packageurl.MakeCacheStrings("swift", scope+"/"+name, "")
|
|
return packagePURL, packagePURL != ""
|
|
}
|
|
|
|
// SwiftHandler handles the read-only Swift Package Registry v1 protocol.
|
|
type SwiftHandler struct {
|
|
proxy *Proxy
|
|
upstreamURL string
|
|
proxyURL string
|
|
}
|
|
|
|
// NewSwiftHandler creates a Swift Package Registry protocol handler.
|
|
func NewSwiftHandler(proxy *Proxy, proxyURL, upstreamURL string) *SwiftHandler {
|
|
if strings.TrimSpace(upstreamURL) == "" {
|
|
upstreamURL = config.DefaultSwiftUpstream
|
|
}
|
|
|
|
return &SwiftHandler{
|
|
proxy: proxy,
|
|
upstreamURL: strings.TrimSuffix(upstreamURL, "/"),
|
|
proxyURL: strings.TrimSuffix(proxyURL, "/"),
|
|
}
|
|
}
|
|
|
|
// Routes returns the HTTP handler for Swift registry requests.
|
|
func (h *SwiftHandler) Routes() http.Handler {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("GET /identifiers", h.handleIdentifiers)
|
|
mux.HandleFunc("GET /{scope}/{name}/{version}/Package.swift", h.handleManifest)
|
|
mux.HandleFunc("GET /{scope}/{name}/{version}", h.handleRelease)
|
|
mux.HandleFunc("PUT /{scope}/{name}/{version}", h.handlePublishingUnsupported)
|
|
mux.HandleFunc("GET /{scope}/{name}", h.handlePackageReleases)
|
|
return mux
|
|
}
|
|
|
|
func (h *SwiftHandler) handlePackageReleases(w http.ResponseWriter, r *http.Request) {
|
|
scope := r.PathValue("scope")
|
|
name := strings.TrimSuffix(r.PathValue("name"), ".json")
|
|
if !validSwiftScope(scope) || !validSwiftPackageName(name) {
|
|
writeSwiftProblem(w, http.StatusBadRequest, "invalid package identifier")
|
|
return
|
|
}
|
|
scope, name = canonicalSwiftPackage(scope, name)
|
|
|
|
upstreamURL := h.buildUpstreamURL(scope, name, "", "", r.URL.RawQuery)
|
|
doc, err := h.fetchDocument(
|
|
r.Context(), swiftReleasesCacheKey(scope, name, r.URL.RawQuery), upstreamURL, requestAccept(r, swiftAcceptJSON),
|
|
)
|
|
if err != nil {
|
|
h.writeMetadataError(w, err)
|
|
return
|
|
}
|
|
|
|
rewritten, err := h.rewriteReleaseURLs(scope, name, doc.body)
|
|
if err != nil {
|
|
h.proxy.Logger.Warn("failed to rewrite Swift release URLs", "error", err)
|
|
rewritten = doc.body
|
|
}
|
|
if doc.link != "" {
|
|
w.Header().Set("Link", h.rewriteLinkHeader(doc.link, upstreamURL))
|
|
}
|
|
writeSwiftMetadata(w, r, rewritten, doc.contentType)
|
|
}
|
|
|
|
func (h *SwiftHandler) handleRelease(w http.ResponseWriter, r *http.Request) {
|
|
scope := r.PathValue("scope")
|
|
name := r.PathValue("name")
|
|
version := r.PathValue("version")
|
|
if strings.HasSuffix(version, ".zip") {
|
|
h.handleSourceArchive(w, r, scope, name, strings.TrimSuffix(version, ".zip"))
|
|
return
|
|
}
|
|
|
|
version = strings.TrimSuffix(version, ".json")
|
|
if !validSwiftPackageReference(scope, name, version) {
|
|
writeSwiftProblem(w, http.StatusBadRequest, "invalid package release")
|
|
return
|
|
}
|
|
scope, name = canonicalSwiftPackage(scope, name)
|
|
|
|
upstreamURL := h.buildUpstreamURL(scope, name, version, "", r.URL.RawQuery)
|
|
body, contentType, err := h.proxy.FetchOrCacheMetadata(
|
|
r.Context(), "swift", swiftReleaseCacheKey(scope, name, version), upstreamURL, requestAccept(r, swiftAcceptJSON),
|
|
)
|
|
if err != nil {
|
|
h.writeMetadataError(w, err)
|
|
return
|
|
}
|
|
writeSwiftMetadata(w, r, body, contentType)
|
|
}
|
|
|
|
func (h *SwiftHandler) handleManifest(w http.ResponseWriter, r *http.Request) {
|
|
scope := r.PathValue("scope")
|
|
name := r.PathValue("name")
|
|
version := r.PathValue("version")
|
|
if !validSwiftPackageReference(scope, name, version) {
|
|
writeSwiftProblem(w, http.StatusBadRequest, "invalid package release")
|
|
return
|
|
}
|
|
scope, name = canonicalSwiftPackage(scope, name)
|
|
|
|
upstreamURL := h.buildUpstreamURL(scope, name, version, "Package.swift", r.URL.RawQuery)
|
|
doc, err := h.fetchDocument(
|
|
r.Context(), swiftManifestCacheKey(scope, name, version, r.URL.RawQuery), upstreamURL, requestAccept(r, swiftAcceptManifest),
|
|
)
|
|
if err != nil {
|
|
h.writeMetadataError(w, err)
|
|
return
|
|
}
|
|
if doc.link != "" {
|
|
w.Header().Set("Link", h.rewriteLinkHeader(doc.link, upstreamURL))
|
|
}
|
|
writeSwiftMetadata(w, r, doc.body, doc.contentType)
|
|
}
|
|
|
|
func (h *SwiftHandler) handleIdentifiers(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Query().Get("url") == "" {
|
|
writeSwiftProblem(w, http.StatusBadRequest, "url query parameter is required")
|
|
return
|
|
}
|
|
|
|
upstreamURL := h.upstreamURL + "/identifiers?" + r.URL.RawQuery
|
|
cacheKey := swiftMetadataCacheKey("identifiers", r.URL.RawQuery)
|
|
body, contentType, err := h.proxy.FetchOrCacheMetadata(
|
|
r.Context(), "swift", cacheKey, upstreamURL, requestAccept(r, swiftAcceptJSON),
|
|
)
|
|
if err != nil {
|
|
h.writeMetadataError(w, err)
|
|
return
|
|
}
|
|
writeSwiftMetadata(w, r, body, contentType)
|
|
}
|
|
|
|
func (h *SwiftHandler) handlePublishingUnsupported(w http.ResponseWriter, _ *http.Request) {
|
|
w.Header().Set("Allow", "GET, HEAD")
|
|
writeSwiftProblem(w, http.StatusMethodNotAllowed, "publishing isn't supported")
|
|
}
|
|
|
|
func (h *SwiftHandler) handleSourceArchive(w http.ResponseWriter, r *http.Request, scope, name, version string) {
|
|
if !validSwiftPackageReference(scope, name, version) {
|
|
writeSwiftProblem(w, http.StatusBadRequest, "invalid package release")
|
|
return
|
|
}
|
|
scope, name = canonicalSwiftPackage(scope, name)
|
|
|
|
key, err := swiftSourceArchiveKey(scope, name, version)
|
|
if err != nil {
|
|
h.writeArtifactError(w, err)
|
|
return
|
|
}
|
|
filename, packagePURL, versionPURL := key.filename, key.packagePURL, key.versionPURL
|
|
upstreamURL := h.buildUpstreamURL(scope, name, version+".zip", "", r.URL.RawQuery)
|
|
archiveInfo, infoErr := h.fetchArchiveInfo(r.Context(), scope, name, version)
|
|
if infoErr != nil {
|
|
h.writeArtifactError(w, fmt.Errorf("fetching release metadata: %w", infoErr))
|
|
return
|
|
}
|
|
|
|
if r.Method == http.MethodHead {
|
|
h.handleSourceArchiveHead(w, r, name, version, filename, packagePURL, versionPURL, upstreamURL, archiveInfo)
|
|
return
|
|
}
|
|
|
|
headers := make(http.Header)
|
|
headers.Set("Accept", requestAccept(r, swiftAcceptArchive))
|
|
result, err := h.fetchSourceArchive(r.Context(), key, version, upstreamURL, headers, archiveInfo)
|
|
if err != nil {
|
|
h.writeArtifactError(w, err)
|
|
return
|
|
}
|
|
|
|
result.Artifact.MediaType = "application/zip"
|
|
setSwiftArchiveHeaders(w.Header(), name, version, result.Artifact.Digest.Encoded(), archiveInfo)
|
|
ServeArtifactRequest(w, r, result)
|
|
}
|
|
|
|
// swiftArchiveKey is the cache key a release's source archive is stored
|
|
// under.
|
|
type swiftArchiveKey struct {
|
|
packageName, filename, packagePURL, versionPURL string
|
|
}
|
|
|
|
// swiftSourceArchiveKey returns the cache key of the source archive of the
|
|
// canonical (lowercased) scope and name at version.
|
|
func swiftSourceArchiveKey(scope, name, version string) (swiftArchiveKey, error) {
|
|
key := swiftArchiveKey{packageName: scope + "/" + name, filename: fmt.Sprintf("%s-%s.zip", name, version)}
|
|
key.packagePURL, key.versionPURL = packageurl.MakeCacheStrings("swift", key.packageName, version)
|
|
if key.packagePURL == "" || key.versionPURL == "" {
|
|
return swiftArchiveKey{}, fmt.Errorf("%w: swift %q", errUnsupportedPackageIdentity, key.packageName)
|
|
}
|
|
return key, nil
|
|
}
|
|
|
|
// fetchSourceArchive returns the source archive under key from the cache, or
|
|
// fetches it from upstreamURL, verified against the checksum in info.
|
|
func (h *SwiftHandler) fetchSourceArchive(ctx context.Context, key swiftArchiveKey, version, upstreamURL string, headers http.Header, info swiftArchiveInfo) (*CacheResult, error) {
|
|
return h.proxy.getOrFetchArtifactFromURLWithCachePURLs(
|
|
ctx, "swift", key.packageName, version, key.filename, key.packagePURL, key.versionPURL,
|
|
upstreamURL, headers, info.checksum, "",
|
|
)
|
|
}
|
|
|
|
func (h *SwiftHandler) handleSourceArchiveHead(
|
|
w http.ResponseWriter,
|
|
r *http.Request,
|
|
name, version, filename, packagePURL, versionPURL, upstreamURL string,
|
|
archiveInfo swiftArchiveInfo,
|
|
) {
|
|
result, err := h.proxy.getCachedArtifactWithUpstreamHash(
|
|
r.Context(), packagePURL, versionPURL, filename, archiveInfo.checksum,
|
|
)
|
|
if err != nil {
|
|
h.writeArtifactError(w, err)
|
|
return
|
|
}
|
|
if result != nil {
|
|
result.Artifact.MediaType = "application/zip"
|
|
setSwiftArchiveHeaders(w.Header(), name, version, result.Artifact.Digest.Encoded(), archiveInfo)
|
|
ServeArtifactRequest(w, r, result)
|
|
return
|
|
}
|
|
|
|
size, err := h.probeSourceArchive(r.Context(), upstreamURL, requestAccept(r, swiftAcceptArchive))
|
|
if err != nil {
|
|
h.writeArtifactError(w, err)
|
|
return
|
|
}
|
|
setSwiftArchiveHeaders(w.Header(), name, version, "", archiveInfo)
|
|
w.Header().Set(headerContentType, "application/zip")
|
|
if size >= 0 {
|
|
w.Header().Set(headerContentLength, strconv.FormatInt(size, 10))
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
|
|
func (h *SwiftHandler) probeSourceArchive(ctx context.Context, upstreamURL, accept string) (int64, error) {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, upstreamURL, nil)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("creating upstream archive request: %w", err)
|
|
}
|
|
req.Header.Set("Accept", accept)
|
|
req.Header.Set("Range", "bytes=0-0")
|
|
|
|
resp, err := h.proxy.HTTPClient.Do(req)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("requesting upstream archive: %w", err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
if resp.StatusCode == http.StatusNotFound {
|
|
return 0, ErrUpstreamNotFound
|
|
}
|
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusPartialContent {
|
|
return 0, fmt.Errorf("upstream archive returned %d", resp.StatusCode)
|
|
}
|
|
|
|
if resp.StatusCode == http.StatusPartialContent {
|
|
_, total, found := strings.Cut(resp.Header.Get("Content-Range"), "/")
|
|
if !found || total == "*" {
|
|
return -1, nil
|
|
}
|
|
if parsed, parseErr := strconv.ParseInt(total, 10, 64); parseErr == nil {
|
|
return parsed, nil
|
|
}
|
|
return -1, nil
|
|
}
|
|
|
|
size := int64(-1)
|
|
if contentLength := resp.Header.Get(headerContentLength); contentLength != "" {
|
|
if parsed, parseErr := strconv.ParseInt(contentLength, 10, 64); parseErr == nil {
|
|
size = parsed
|
|
}
|
|
}
|
|
return size, nil
|
|
}
|
|
|
|
func (h *SwiftHandler) fetchMetadataWithHeaders(
|
|
ctx context.Context,
|
|
upstreamURL, accept string,
|
|
) ([]byte, string, http.Header, error) {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, upstreamURL, nil)
|
|
if err != nil {
|
|
return nil, "", nil, fmt.Errorf("creating upstream metadata request: %w", err)
|
|
}
|
|
req.Header.Set("Accept", accept)
|
|
|
|
resp, err := h.proxy.HTTPClient.Do(req)
|
|
if err != nil {
|
|
return nil, "", nil, fmt.Errorf("requesting upstream metadata: %w", err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
if resp.StatusCode == http.StatusNotFound {
|
|
return nil, "", nil, ErrUpstreamNotFound
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, "", nil, fmt.Errorf("upstream metadata returned %d", resp.StatusCode)
|
|
}
|
|
|
|
body, err := h.proxy.ReadMetadata(resp.Body)
|
|
if err != nil {
|
|
return nil, "", nil, fmt.Errorf("reading upstream metadata: %w", err)
|
|
}
|
|
contentType := resp.Header.Get(headerContentType)
|
|
if contentType == "" {
|
|
contentType = contentTypeJSON
|
|
}
|
|
return body, contentType, resp.Header.Clone(), nil
|
|
}
|
|
|
|
type swiftReleaseMetadata struct {
|
|
Resources []struct {
|
|
Name string `json:"name"`
|
|
Type string `json:"type"`
|
|
Checksum string `json:"checksum"`
|
|
Signing *struct {
|
|
Signature string `json:"signatureBase64Encoded"`
|
|
Format string `json:"signatureFormat"`
|
|
} `json:"signing"`
|
|
} `json:"resources"`
|
|
}
|
|
|
|
type swiftArchiveInfo struct {
|
|
checksum string
|
|
signature string
|
|
signatureFormat string
|
|
}
|
|
|
|
func (h *SwiftHandler) fetchArchiveInfo(ctx context.Context, scope, name, version string) (swiftArchiveInfo, error) {
|
|
upstreamURL := h.buildUpstreamURL(scope, name, version, "", "")
|
|
body, _, err := h.proxy.FetchOrCacheMetadata(
|
|
ctx, "swift", swiftReleaseCacheKey(scope, name, version), upstreamURL, swiftAcceptJSON,
|
|
)
|
|
if err != nil {
|
|
return swiftArchiveInfo{}, err
|
|
}
|
|
|
|
var metadata swiftReleaseMetadata
|
|
if err := json.Unmarshal(body, &metadata); err != nil {
|
|
return swiftArchiveInfo{}, fmt.Errorf("parsing release metadata: %w", err)
|
|
}
|
|
for _, resource := range metadata.Resources {
|
|
if resource.Name != "source-archive" || resource.Type != "application/zip" {
|
|
continue
|
|
}
|
|
checksum, err := normalizeSwiftChecksum(resource.Checksum)
|
|
if err != nil {
|
|
return swiftArchiveInfo{}, err
|
|
}
|
|
info := swiftArchiveInfo{checksum: checksum}
|
|
if resource.Signing != nil {
|
|
if resource.Signing.Signature == "" || resource.Signing.Format == "" {
|
|
return swiftArchiveInfo{}, errors.New("source archive signing metadata is incomplete")
|
|
}
|
|
info.signature = resource.Signing.Signature
|
|
info.signatureFormat = resource.Signing.Format
|
|
}
|
|
return info, nil
|
|
}
|
|
|
|
return swiftArchiveInfo{}, errors.New("source archive is missing from release metadata")
|
|
}
|
|
|
|
func normalizeSwiftChecksum(checksum string) (string, error) {
|
|
digest, err := hex.DecodeString(checksum)
|
|
if err != nil || len(digest) != sha256.Size {
|
|
return "", errors.New("source archive checksum is not a SHA-256 digest")
|
|
}
|
|
return hex.EncodeToString(digest), nil
|
|
}
|
|
|
|
func setSwiftArchiveHeaders(header http.Header, name, version, contentHash string, info swiftArchiveInfo) {
|
|
header.Set("Cache-Control", "public, immutable")
|
|
header.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s-%s.zip"`, name, version))
|
|
header.Set("Content-Version", swiftContentVersion)
|
|
|
|
checksum := info.checksum
|
|
if checksum == "" {
|
|
checksum = contentHash
|
|
}
|
|
if digest := swiftDigestHeader(checksum); digest != "" {
|
|
header.Set("Digest", digest)
|
|
}
|
|
if info.signature != "" && info.signatureFormat != "" {
|
|
header.Set("X-Swift-Package-Signature", info.signature)
|
|
header.Set("X-Swift-Package-Signature-Format", info.signatureFormat)
|
|
}
|
|
}
|
|
|
|
func swiftDigestHeader(checksum string) string {
|
|
digest, err := hex.DecodeString(checksum)
|
|
if err != nil || len(digest) != sha256.Size {
|
|
return ""
|
|
}
|
|
return "sha-256=" + base64.StdEncoding.EncodeToString(digest)
|
|
}
|
|
|
|
func (h *SwiftHandler) rewriteLinkHeader(value, upstreamRequestURL string) string {
|
|
var result strings.Builder
|
|
for len(value) > 0 {
|
|
start := strings.IndexByte(value, '<')
|
|
if start < 0 {
|
|
result.WriteString(value)
|
|
break
|
|
}
|
|
endOffset := strings.IndexByte(value[start+1:], '>')
|
|
if endOffset < 0 {
|
|
result.WriteString(value)
|
|
break
|
|
}
|
|
end := start + 1 + endOffset
|
|
result.WriteString(value[:start+1])
|
|
result.WriteString(h.rewriteRegistryURL(value[start+1:end], upstreamRequestURL))
|
|
result.WriteByte('>')
|
|
value = value[end+1:]
|
|
}
|
|
return result.String()
|
|
}
|
|
|
|
func (h *SwiftHandler) rewriteRegistryURL(rawURL, upstreamRequestURL string) string {
|
|
base, err := url.Parse(h.upstreamURL)
|
|
if err != nil {
|
|
return rawURL
|
|
}
|
|
requestURL, err := url.Parse(upstreamRequestURL)
|
|
if err != nil {
|
|
return rawURL
|
|
}
|
|
reference, err := url.Parse(rawURL)
|
|
if err != nil {
|
|
return rawURL
|
|
}
|
|
absolute := requestURL.ResolveReference(reference)
|
|
if !strings.EqualFold(absolute.Scheme, base.Scheme) || !strings.EqualFold(absolute.Host, base.Host) {
|
|
return rawURL
|
|
}
|
|
|
|
basePath := strings.TrimSuffix(base.EscapedPath(), "/")
|
|
absolutePath := absolute.EscapedPath()
|
|
if absolutePath != basePath && !strings.HasPrefix(absolutePath, basePath+"/") {
|
|
return rawURL
|
|
}
|
|
suffix := strings.TrimPrefix(absolutePath, basePath)
|
|
rewritten := h.proxyURL + "/swift" + suffix
|
|
if absolute.RawQuery != "" {
|
|
rewritten += "?" + absolute.RawQuery
|
|
}
|
|
if absolute.Fragment != "" {
|
|
rewritten += "#" + absolute.Fragment
|
|
}
|
|
return rewritten
|
|
}
|
|
|
|
func (h *SwiftHandler) rewriteReleaseURLs(scope, name string, body []byte) ([]byte, error) {
|
|
var metadata map[string]any
|
|
if err := json.Unmarshal(body, &metadata); err != nil {
|
|
return nil, err
|
|
}
|
|
releases, ok := metadata["releases"].(map[string]any)
|
|
if !ok {
|
|
return body, nil
|
|
}
|
|
|
|
for version, value := range releases {
|
|
release, ok := value.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if _, hasURL := release["url"]; !hasURL {
|
|
continue
|
|
}
|
|
release["url"] = fmt.Sprintf(
|
|
"%s/swift/%s/%s/%s",
|
|
h.proxyURL,
|
|
url.PathEscape(scope),
|
|
url.PathEscape(name),
|
|
url.PathEscape(version),
|
|
)
|
|
}
|
|
return json.Marshal(metadata)
|
|
}
|
|
|
|
func (h *SwiftHandler) buildUpstreamURL(scope, name, version, resource, rawQuery string) string {
|
|
parts := []string{h.upstreamURL, url.PathEscape(scope), url.PathEscape(name)}
|
|
if version != "" {
|
|
parts = append(parts, url.PathEscape(version))
|
|
}
|
|
if resource != "" {
|
|
parts = append(parts, resource)
|
|
}
|
|
result := strings.Join(parts, "/")
|
|
if rawQuery != "" {
|
|
result += "?" + rawQuery
|
|
}
|
|
return result
|
|
}
|
|
|
|
func swiftMetadataCacheKey(parts ...string) string {
|
|
joined := strings.Join(parts, "\x00")
|
|
digest := sha256.Sum256([]byte(joined))
|
|
return hex.EncodeToString(digest[:])
|
|
}
|
|
|
|
func swiftReleaseCacheKey(scope, name, version string) string {
|
|
return swiftMetadataCacheKey("release", scope, name, version)
|
|
}
|
|
|
|
func requestAccept(r *http.Request, fallback string) string {
|
|
if accept := r.Header.Get("Accept"); accept != "" {
|
|
return accept
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func writeSwiftMetadata(w http.ResponseWriter, r *http.Request, body []byte, contentType string) {
|
|
if contentType == "" {
|
|
contentType = "application/json"
|
|
}
|
|
digest := sha256.Sum256(body)
|
|
etag := fmt.Sprintf(`"%x"`, digest)
|
|
w.Header().Set(headerContentType, contentType)
|
|
w.Header().Set("Content-Version", swiftContentVersion)
|
|
w.Header().Set(headerETag, etag)
|
|
if ifNoneMatchHits(r.Header.Get("If-None-Match"), etag) {
|
|
w.WriteHeader(http.StatusNotModified)
|
|
return
|
|
}
|
|
w.Header().Set(headerContentLength, strconv.Itoa(len(body)))
|
|
w.WriteHeader(http.StatusOK)
|
|
if r.Method != http.MethodHead {
|
|
_, _ = w.Write(body)
|
|
}
|
|
}
|
|
|
|
func (h *SwiftHandler) writeMetadataError(w http.ResponseWriter, err error) {
|
|
if errors.Is(err, ErrUpstreamNotFound) {
|
|
writeSwiftProblem(w, http.StatusNotFound, "not found")
|
|
return
|
|
}
|
|
if writeUpstreamHeld(w, err) {
|
|
return
|
|
}
|
|
h.proxy.Logger.Error("Swift metadata request failed", "error", err)
|
|
writeSwiftProblem(w, http.StatusBadGateway, "upstream request failed")
|
|
}
|
|
|
|
func (h *SwiftHandler) writeArtifactError(w http.ResponseWriter, err error) {
|
|
if errors.Is(err, ErrUpstreamNotFound) {
|
|
writeSwiftProblem(w, http.StatusNotFound, "release not found")
|
|
return
|
|
}
|
|
if errors.Is(err, ErrArtifactBlocked) || errors.Is(err, ErrVersionDenied) {
|
|
writeSwiftProblem(w, http.StatusForbidden, err.Error())
|
|
return
|
|
}
|
|
if writeUpstreamHeld(w, err) {
|
|
return
|
|
}
|
|
h.proxy.Logger.Error("Swift archive request failed", "error", err)
|
|
writeSwiftProblem(w, http.StatusBadGateway, "failed to fetch package")
|
|
}
|
|
|
|
func writeSwiftProblem(w http.ResponseWriter, status int, detail string) {
|
|
w.Header().Set(headerContentType, "application/problem+json")
|
|
w.Header().Set("Content-Version", swiftContentVersion)
|
|
w.WriteHeader(status)
|
|
_ = json.NewEncoder(w).Encode(map[string]string{"detail": detail})
|
|
}
|
|
|
|
func validSwiftPackageReference(scope, name, version string) bool {
|
|
return validSwiftScope(scope) && validSwiftPackageName(name) && version != "" && version != "." && version != ".." && !strings.ContainsAny(version, "/\\")
|
|
}
|
|
|
|
func canonicalSwiftPackage(scope, name string) (string, string) {
|
|
return strings.ToLower(scope), strings.ToLower(name)
|
|
}
|
|
|
|
func validSwiftScope(scope string) bool {
|
|
return validSwiftIdentifier(scope, swiftMaxScopeLength, "-")
|
|
}
|
|
|
|
func validSwiftPackageName(name string) bool {
|
|
return validSwiftIdentifier(name, swiftMaxNameLength, "-_")
|
|
}
|
|
|
|
func validSwiftIdentifier(value string, maxLength int, separators string) bool {
|
|
if value == "" || len(value) > maxLength {
|
|
return false
|
|
}
|
|
previousSeparator := false
|
|
for i := 0; i < len(value); i++ {
|
|
character := value[i]
|
|
separator := strings.ContainsRune(separators, rune(character))
|
|
if separator {
|
|
if i == 0 || i == len(value)-1 || previousSeparator {
|
|
return false
|
|
}
|
|
previousSeparator = true
|
|
continue
|
|
}
|
|
if (character < 'a' || character > 'z') &&
|
|
(character < 'A' || character > 'Z') &&
|
|
(character < '0' || character > '9') {
|
|
return false
|
|
}
|
|
previousSeparator = false
|
|
}
|
|
return true
|
|
}
|