Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-11-09 07:39:19 -05:00
pkg-proxy/internal/mirror/versions.go
simon chrzanowski 1979fb098e
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 11:34:05 +00:00

174 lines
5.6 KiB
Go

package mirror
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/git-pkgs/proxy/internal/config"
"github.com/git-pkgs/proxy/internal/handler"
"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)
}
// 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)
}
// 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
}
// ListVersions implements VersionLister.
func (r RegistryVersions) ListVersions(ctx context.Context, ecosystem, name string) ([]string, error) {
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
}
// FindVersion implements VersionFinder.
func (r RegistryVersions) FindVersion(ctx context.Context, ecosystem, name, version string) (registries.Version, error) {
versions, err := r.fetchVersions(ctx, ecosystem, name)
if err != nil {
return registries.Version{}, err
}
for _, v := range versions {
if v.Number == version {
return v, nil
}
}
return registries.Version{}, fmt.Errorf("%w: %s %s@%s", registries.ErrNotFound, ecosystem, name, version)
}
func (r RegistryVersions) fetchVersions(ctx context.Context, ecosystem, name string) ([]registries.Version, error) {
reg, name, err := r.registryFor(ctx, ecosystem, name)
if err != nil {
return nil, err
}
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
}
switch purl.EcosystemToPURLType(ecosystem) {
case "helm":
// The namespace names the repository, which URL resolves to the
// index the chart is listed in.
_, name, _ = strings.Cut(name, "/")
case "swift":
var baseURL string
if baseURL, err = r.baseURL("swift", name); err != nil {
return nil, "", err
}
if name, err = swiftIdentity(ctx, r.client(), baseURL, name); err != nil {
return nil, "", err
}
}
return reg, name, nil
}
// 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
purlType := purl.EcosystemToPURLType(ecosystem)
baseURL, err := r.baseURL(purlType, name)
if err != nil {
return nil, err
}
return registries.New(purlType, baseURL, r.client())
}
func (r RegistryVersions) baseURL(purlType, name string) (string, error) {
if r.URL == nil {
return "", nil
}
return r.URL(purlType, name)
}
func (r RegistryVersions) client() *registries.Client {
if r.Client == nil {
return registries.DefaultClient()
}
return r.Client
}
// 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) {
source := handler.SwiftSourceURL(name)
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
}