2026-10-22 15:38:35 +02:00
|
|
|
package mirror
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
2026-10-26 16:43:42 +01:00
|
|
|
"encoding/json"
|
|
|
|
|
"errors"
|
2026-10-25 12:36:36 +01:00
|
|
|
"fmt"
|
2026-10-26 16:43:42 +01:00
|
|
|
"net/http"
|
|
|
|
|
"net/url"
|
2026-10-25 19:37:22 +01:00
|
|
|
"strings"
|
2026-10-22 15:38:35 +02:00
|
|
|
|
2026-10-26 16:43:42 +01:00
|
|
|
"github.com/git-pkgs/proxy/internal/config"
|
|
|
|
|
"github.com/git-pkgs/proxy/internal/handler"
|
2026-10-22 15:38:35 +02:00
|
|
|
"github.com/git-pkgs/purl"
|
|
|
|
|
"github.com/git-pkgs/registries"
|
|
|
|
|
_ "github.com/git-pkgs/registries/all"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// VersionLister lists the versions of a package, for PURLs without one.
|
|
|
|
|
type VersionLister interface {
|
|
|
|
|
ListVersions(ctx context.Context, ecosystem, name string) ([]string, error)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RegistryVersions lists versions through the registries clients.
|
|
|
|
|
type RegistryVersions struct {
|
|
|
|
|
// Client makes the requests. Nil uses registries.DefaultClient.
|
|
|
|
|
Client *registries.Client
|
|
|
|
|
// URL returns the registry base URL to list a package from, by PURL
|
|
|
|
|
// type. Nil, or an empty URL, uses the public registry.
|
|
|
|
|
URL func(purlType, name string) (string, error)
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-25 14:36:43 +01:00
|
|
|
// VersionFinder finds one version of a package, with the files it
|
|
|
|
|
// publishes, for ecosystems the mirror fetches from the version's metadata.
|
|
|
|
|
type VersionFinder interface {
|
|
|
|
|
FindVersion(ctx context.Context, ecosystem, name, version string) (registries.Version, error)
|
2026-10-25 12:36:36 +01:00
|
|
|
}
|
|
|
|
|
|
2026-10-30 14:59:52 +01:00
|
|
|
// LatestFinder picks the latest version of a package, for unversioned PURLs
|
|
|
|
|
// when only the latest is mirrored. Listers without it fall back to
|
|
|
|
|
// registries.SelectLatestVersion over ListVersions.
|
|
|
|
|
type LatestFinder interface {
|
|
|
|
|
LatestVersion(ctx context.Context, ecosystem, name string) (string, error)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// LatestVersion implements LatestFinder with registries.FetchLatestVersion:
|
|
|
|
|
// the version the registry advertises as latest where it has one, such as
|
|
|
|
|
// npm's dist-tags.latest, and otherwise the newest version by publish time
|
|
|
|
|
// that isn't yanked, retracted or deprecated.
|
|
|
|
|
func (r RegistryVersions) LatestVersion(ctx context.Context, ecosystem, name string) (string, error) {
|
|
|
|
|
reg, name, err := r.registryFor(ctx, ecosystem, name)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
v, err := registries.FetchLatestVersion(ctx, reg, name)
|
|
|
|
|
if err != nil || v == nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
return v.Number, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-22 15:38:35 +02:00
|
|
|
// ListVersions implements VersionLister.
|
|
|
|
|
func (r RegistryVersions) ListVersions(ctx context.Context, ecosystem, name string) ([]string, error) {
|
2026-10-25 12:36:36 +01:00
|
|
|
versions, err := r.fetchVersions(ctx, ecosystem, name)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
result := make([]string, len(versions))
|
|
|
|
|
for i, v := range versions {
|
|
|
|
|
result[i] = v.Number
|
|
|
|
|
}
|
|
|
|
|
return result, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-25 14:36:43 +01:00
|
|
|
// FindVersion implements VersionFinder.
|
|
|
|
|
func (r RegistryVersions) FindVersion(ctx context.Context, ecosystem, name, version string) (registries.Version, error) {
|
2026-10-25 12:36:36 +01:00
|
|
|
versions, err := r.fetchVersions(ctx, ecosystem, name)
|
|
|
|
|
if err != nil {
|
2026-10-25 14:36:43 +01:00
|
|
|
return registries.Version{}, err
|
2026-10-25 12:36:36 +01:00
|
|
|
}
|
|
|
|
|
for _, v := range versions {
|
|
|
|
|
if v.Number == version {
|
2026-10-25 14:36:43 +01:00
|
|
|
return v, nil
|
2026-10-25 12:36:36 +01:00
|
|
|
}
|
|
|
|
|
}
|
2026-10-25 14:36:43 +01:00
|
|
|
return registries.Version{}, fmt.Errorf("%w: %s %s@%s", registries.ErrNotFound, ecosystem, name, version)
|
2026-10-25 12:36:36 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r RegistryVersions) fetchVersions(ctx context.Context, ecosystem, name string) ([]registries.Version, error) {
|
2026-10-30 14:59:52 +01:00
|
|
|
reg, name, err := r.registryFor(ctx, ecosystem, name)
|
2026-10-25 19:37:46 +01:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2026-10-30 14:59:52 +01:00
|
|
|
return reg.FetchVersions(ctx, name)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// registryFor returns the client for the registry name is listed in, and
|
|
|
|
|
// the name the registry knows the package by.
|
|
|
|
|
func (r RegistryVersions) registryFor(ctx context.Context, ecosystem, name string) (registries.Registry, string, error) { //nolint:ireturn // registries.New returns the interface
|
|
|
|
|
reg, err := r.registry(ecosystem, name)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, "", err
|
|
|
|
|
}
|
2026-10-27 11:00:10 +01:00
|
|
|
switch purl.EcosystemToPURLType(ecosystem) {
|
|
|
|
|
case "helm":
|
2026-10-26 10:33:01 +00:00
|
|
|
// The namespace names the repository, which URL resolves to the
|
|
|
|
|
// index the chart is listed in.
|
|
|
|
|
_, name, _ = strings.Cut(name, "/")
|
2026-10-27 11:00:10 +01:00
|
|
|
case "swift":
|
|
|
|
|
var baseURL string
|
|
|
|
|
if baseURL, err = r.baseURL("swift", name); err != nil {
|
2026-10-30 14:59:52 +01:00
|
|
|
return nil, "", err
|
2026-10-22 15:38:35 +02:00
|
|
|
}
|
2026-10-27 11:00:10 +01:00
|
|
|
if name, err = swiftIdentity(ctx, r.client(), baseURL, name); err != nil {
|
2026-10-30 14:59:52 +01:00
|
|
|
return nil, "", err
|
2026-10-26 16:43:42 +01:00
|
|
|
}
|
|
|
|
|
}
|
2026-10-30 14:59:52 +01:00
|
|
|
return reg, name, nil
|
2026-10-25 19:37:46 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// registry returns the client for the registry name is listed in.
|
|
|
|
|
func (r RegistryVersions) registry(ecosystem, name string) (registries.Registry, error) { //nolint:ireturn // registries.New returns the interface
|
2026-10-22 15:38:35 +02:00
|
|
|
purlType := purl.EcosystemToPURLType(ecosystem)
|
2026-10-27 11:00:10 +01:00
|
|
|
baseURL, err := r.baseURL(purlType, name)
|
2026-10-22 15:38:35 +02:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2026-10-27 11:00:10 +01:00
|
|
|
return registries.New(purlType, baseURL, r.client())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r RegistryVersions) baseURL(purlType, name string) (string, error) {
|
|
|
|
|
if r.URL == nil {
|
|
|
|
|
return "", nil
|
2026-10-25 19:37:22 +01:00
|
|
|
}
|
2026-10-27 11:00:10 +01:00
|
|
|
return r.URL(purlType, name)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r RegistryVersions) client() *registries.Client {
|
|
|
|
|
if r.Client == nil {
|
|
|
|
|
return registries.DefaultClient()
|
|
|
|
|
}
|
|
|
|
|
return r.Client
|
2026-10-22 15:38:35 +02:00
|
|
|
}
|
2026-10-26 16:43:42 +01:00
|
|
|
|
|
|
|
|
// swiftIdentity returns the registry identity, scope.name, of the name of a
|
|
|
|
|
// pkg:swift PURL, looking a source repository up on the registry at baseURL.
|
|
|
|
|
func swiftIdentity(ctx context.Context, client *registries.Client, baseURL, name string) (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
|
|
|
source := handler.SwiftSourceURL(name)
|
2026-10-26 16:43:42 +01:00
|
|
|
if source == "" {
|
|
|
|
|
scope, pkg, err := swiftScopeAndName(name)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
return scope + "." + pkg, nil
|
|
|
|
|
}
|
|
|
|
|
if baseURL == "" {
|
|
|
|
|
baseURL = config.DefaultSwiftUpstream
|
|
|
|
|
}
|
|
|
|
|
var body json.RawMessage
|
|
|
|
|
_, err := client.GetJSONWithHeaders(ctx,
|
|
|
|
|
strings.TrimSuffix(baseURL, "/")+"/identifiers?"+url.Values{"url": {source}}.Encode(),
|
|
|
|
|
http.Header{"Accept": {"application/vnd.swift.registry.v1+json"}}, &body)
|
|
|
|
|
if httpErr := (*registries.HTTPError)(nil); errors.As(err, &httpErr) && httpErr.IsNotFound() {
|
|
|
|
|
return "", fmt.Errorf("the Swift registry has no package for %s", source)
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
scope, pkg, err := handler.SwiftIdentityOf(body, source)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
return scope + "." + pkg, nil
|
|
|
|
|
}
|