mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-11-09 15:49:16 -05:00
npm: a tarball request for a version the cached packument did not list yet (cached within metadata_ttl before the release) found no publish time and was let through cooldown, and fetched without integrity. When cooldown applies and the cached packument lacks the version, fetch it again and decide on what upstream lists now; if it can't be fetched, fail as when a publish time can't be read. npm: cooldown only moved dist-tags.latest. Other tags kept naming versions in cooldown, while the denylist removes such tags. Cooldown now removes them too, and drops latest when no version is left, in both the in-memory and streamed rewrites. PyPI: the simple JSON rewrite removed filtered files but left their versions in the PEP 700 versions list. Remove them there too. pub: latest was never rewritten, so its archive_url pointed at pub.dev, it kept naming a version in cooldown when no version was left, and the replacement was the last list entry, which may be a prerelease. Point latest's archive_url at the proxy, replace a held back latest with the newest stable version left (or the newest prerelease), and drop it when nothing is left.
110 lines
3.8 KiB
Go
110 lines
3.8 KiB
Go
package handler
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/git-pkgs/proxy/internal/jsonscan"
|
|
)
|
|
|
|
// errNoAttestations answers a request for a version whose packument entry
|
|
// lists no attestations.
|
|
var errNoAttestations = errors.New("attestations not found")
|
|
|
|
// handleAttestations serves /-/npm/v1/attestations/{name}@{version}, which
|
|
// `npm audit signatures` reads to verify provenance. Packuments point
|
|
// dist.attestations.url here (see npm.AttestationsURL).
|
|
func (h *NPMHandler) handleAttestations(w http.ResponseWriter, r *http.Request) {
|
|
packageName, version, ok := parseNPMAttestationsPath(strings.TrimPrefix(r.URL.Path, npmAttestationsPrefix))
|
|
if !ok {
|
|
JSONError(w, http.StatusBadRequest, "invalid package name")
|
|
return
|
|
}
|
|
|
|
if h.proxy.versionDenied("npm", packageName, version) {
|
|
JSONError(w, http.StatusForbidden, ErrVersionDenied.Error()+": "+canonicalVersionPURL("npm", packageName, version))
|
|
return
|
|
}
|
|
local, err := h.npmLocalEntries(packageName)
|
|
if err != nil {
|
|
h.proxy.Logger.Error("failed to read pushed npm versions", "package", packageName, "error", err)
|
|
JSONError(w, http.StatusInternalServerError, "failed to read pushed versions")
|
|
return
|
|
}
|
|
if _, pushed := findNPMLocalEntry(local, version); pushed || h.proxy.hostedOnly("npm", packageName) {
|
|
JSONError(w, http.StatusNotFound, errNoAttestations.Error())
|
|
return
|
|
}
|
|
|
|
registryURL, cacheKey := h.sourceFor(packageName)
|
|
metadata := sync.OnceValues(func() (npmVersionData, error) {
|
|
return h.versionData(r.Context(), packageName, registryURL+"/"+url.PathEscape(packageName), cacheKey, version)
|
|
})
|
|
|
|
inCooldown, err := h.versionInCooldown(packageName, version, metadata)
|
|
if err != nil {
|
|
if writeUpstreamHeld(w, err) {
|
|
return
|
|
}
|
|
h.proxy.Logger.Warn("cooldown: could not read npm publish time",
|
|
"package", packageName, "version", version, "error", err)
|
|
JSONError(w, http.StatusBadGateway, "failed to check cooldown")
|
|
return
|
|
}
|
|
if inCooldown {
|
|
JSONError(w, http.StatusNotFound, ErrVersionNotFound.Error())
|
|
return
|
|
}
|
|
|
|
data, err := metadata()
|
|
if err != nil {
|
|
if errors.Is(err, ErrUpstreamNotFound) {
|
|
JSONError(w, http.StatusNotFound, ErrPackageNotFound.Error())
|
|
return
|
|
}
|
|
if writeUpstreamHeld(w, err) {
|
|
return
|
|
}
|
|
h.proxy.Logger.Error("failed to fetch npm metadata", "error", err)
|
|
JSONError(w, http.StatusBadGateway, "failed to fetch from upstream")
|
|
return
|
|
}
|
|
listed, _, _ := jsonscan.LookupString(data.entry, "dist", "attestations", "url")
|
|
if listed == "" {
|
|
JSONError(w, http.StatusNotFound, errNoAttestations.Error())
|
|
return
|
|
}
|
|
upstreamURL, err := validateRegistryURL("attestations", listed, registryURL)
|
|
if err != nil {
|
|
// Mirrors of npmjs can keep its URLs in packuments. The proxy only
|
|
// ever asks the package's own registry.
|
|
upstreamURL = registryURL + "/-/npm/v1/attestations/" + url.PathEscape(packageName) + "@" + url.PathEscape(version)
|
|
}
|
|
|
|
h.proxy.proxyImmutable(w, r, upstreamURL, "npm", cacheKey+"/-/attestations/"+version, contentTypeJSON)
|
|
}
|
|
|
|
// parseNPMAttestationsPath splits "{name}@{version}" as the attestations
|
|
// endpoint takes it, where name may be "@scope/name" with the slash encoded
|
|
// or not. It rejects anything that is not one package and one version.
|
|
func parseNPMAttestationsPath(spec string) (packageName, version string, ok bool) {
|
|
at := strings.LastIndex(spec, "@")
|
|
if at <= 0 {
|
|
return "", "", false
|
|
}
|
|
packageName, version = spec[:at], spec[at+1:]
|
|
if version == "" || strings.ContainsAny(version, "/\\") || containsPathTraversal(packageName) || containsPathTraversal(version) {
|
|
return "", "", false
|
|
}
|
|
scope, name, scoped := strings.Cut(packageName, "/")
|
|
switch {
|
|
case scoped:
|
|
ok = len(scope) > 1 && scope[0] == '@' && name != "" && !strings.ContainsAny(name, "/@")
|
|
default:
|
|
ok = !strings.ContainsAny(packageName, "@\\")
|
|
}
|
|
return packageName, version, ok
|
|
}
|