mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-11-09 23:59:16 -05:00
npm metadata rewriting depended on handler.Proxy, which pulls SQL drivers, cloud SDKs and metrics into its import graph. Move the transformation into internal/metadata/npm with explicit policy, evaluation time, public URL and hosted-version inputs, and share the JSON scanner between npm and Composer through internal/jsonscan. Rebased onto main: rewritten tarball URLs are still logged once per document, now through an OnTarballsRewritten callback that reports the count.
194 lines
6.7 KiB
Go
194 lines
6.7 KiB
Go
package handler
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/Masterminds/semver/v3"
|
|
"github.com/git-pkgs/proxy/internal/metadata/npm"
|
|
)
|
|
|
|
type npmLocalEntry = npm.LocalVersion
|
|
|
|
// npmLocalEntries returns the pushed versions of a package as packument
|
|
// entries, or nil when there are none or hosted packages are disabled.
|
|
func (h *NPMHandler) npmLocalEntries(packageName string) ([]npmLocalEntry, error) {
|
|
local, err := h.proxy.localVersions("npm", packageName)
|
|
if err != nil || len(local) == 0 {
|
|
return nil, err
|
|
}
|
|
entries := make([]npmLocalEntry, 0, len(local))
|
|
for _, lv := range local {
|
|
var manifest map[string]any
|
|
if err := json.Unmarshal(lv.Manifest, &manifest); err != nil {
|
|
return nil, fmt.Errorf("decoding manifest of %s@%s: %w", packageName, lv.Version, err)
|
|
}
|
|
manifest["name"] = packageName
|
|
manifest["version"] = lv.Version
|
|
manifest["_id"] = packageName + "@" + lv.Version
|
|
manifest["dist"] = map[string]string{
|
|
"tarball": h.proxyTarballURL(packageName, lv.Version, lv.Filename),
|
|
"integrity": lv.Integrity,
|
|
}
|
|
entry, err := json.Marshal(manifest)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
entries = append(entries, npmLocalEntry{Version: lv.Version, Entry: entry, PublishedAt: lv.PushedAt.Format(time.RFC3339)})
|
|
}
|
|
return entries, nil
|
|
}
|
|
|
|
func findNPMLocalEntry(local []npmLocalEntry, version string) (npmLocalEntry, bool) {
|
|
for _, e := range local {
|
|
if e.Version == version {
|
|
return e, true
|
|
}
|
|
}
|
|
return npmLocalEntry{}, false
|
|
}
|
|
|
|
// npmMetadataRequest is a packument or version metadata request.
|
|
type npmMetadataRequest struct {
|
|
packageName string
|
|
specifier string
|
|
versionLookup bool
|
|
upstreamURL string
|
|
accept string
|
|
metadataCacheKey string
|
|
}
|
|
|
|
// serveLocalMetadata answers the metadata requests that pushed versions
|
|
// decide, and reports whether it did: a request for a pushed version, any
|
|
// request for a package in a hosted-only scope, and a packument of a package
|
|
// with pushed versions. Everything else takes the usual upstream path.
|
|
func (h *NPMHandler) serveLocalMetadata(w http.ResponseWriter, r *http.Request, req npmMetadataRequest) bool {
|
|
local, err := h.npmLocalEntries(req.packageName)
|
|
if err != nil {
|
|
h.proxy.Logger.Error("failed to read pushed npm versions", "package", req.packageName, "error", err)
|
|
JSONError(w, http.StatusInternalServerError, "failed to read pushed versions")
|
|
return true
|
|
}
|
|
switch e, pushed := findNPMLocalEntry(local, req.specifier); {
|
|
case req.versionLookup && pushed:
|
|
w.Header().Set(headerContentType, contentTypeJSON)
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write(e.Entry)
|
|
case h.proxy.hostedOnly("npm", req.packageName):
|
|
h.serveHostedOnlyMetadata(w, r, req.packageName, req.specifier, req.versionLookup, req.metadataCacheKey, local)
|
|
case len(local) > 0 && !req.versionLookup:
|
|
h.serveMetadataWithLocal(w, r, req.packageName, req.upstreamURL, req.accept, req.metadataCacheKey, local)
|
|
default:
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// serveMetadataWithLocal serves a packument with the pushed versions of the
|
|
// package added. It bypasses the rewrite cache, whose entries are keyed by the
|
|
// upstream document alone, so a push or delete shows up on the next request.
|
|
// When the upstream does not know the package, the packument holds only the
|
|
// pushed versions.
|
|
func (h *NPMHandler) serveMetadataWithLocal(w http.ResponseWriter, r *http.Request, packageName, upstreamURL, accept, metadataCacheKey string, local []npmLocalEntry) {
|
|
upstream := h.proxy.fetchMetadataDocument(r.Context(), "npm", metadataCacheKey, upstreamURL, accept, h.packumentValidator())
|
|
var out []byte
|
|
var err error
|
|
switch {
|
|
case errors.Is(upstream.err, ErrUpstreamNotFound):
|
|
out, err = localNPMPackument(packageName, local)
|
|
case upstream.err != nil:
|
|
if writeUpstreamHeld(w, upstream.err) {
|
|
return
|
|
}
|
|
h.proxy.Logger.Error("failed to fetch npm metadata", "error", upstream.err)
|
|
JSONError(w, http.StatusBadGateway, "failed to fetch from upstream")
|
|
return
|
|
default:
|
|
out, _, err = h.rewriteMetadataWithLocal(packageName, upstream.body, local)
|
|
}
|
|
if err != nil {
|
|
if errors.Is(err, ErrVersionDenied) {
|
|
JSONError(w, http.StatusForbidden, err.Error())
|
|
return
|
|
}
|
|
h.proxy.Logger.Error("failed to add pushed versions to npm metadata", "package", packageName, "error", err)
|
|
JSONError(w, http.StatusBadGateway, "failed to rewrite package metadata")
|
|
return
|
|
}
|
|
markStale(w, upstream.stale)
|
|
serveRewrittenMetadata(w, r, h.proxy.uncachedRewrite("npm", h.proxyURL, metadataCacheKey, out))
|
|
}
|
|
|
|
// serveHostedOnlyMetadata answers a metadata request for a package in a
|
|
// hosted-only scope from its pushed versions alone.
|
|
func (h *NPMHandler) serveHostedOnlyMetadata(w http.ResponseWriter, r *http.Request, packageName, specifier string, versionLookup bool, metadataCacheKey string, local []npmLocalEntry) {
|
|
if len(local) == 0 {
|
|
JSONError(w, http.StatusNotFound, ErrPackageNotFound.Error())
|
|
return
|
|
}
|
|
if versionLookup {
|
|
if specifier == "latest" {
|
|
specifier = latestNPMVersion(local)
|
|
}
|
|
e, ok := findNPMLocalEntry(local, specifier)
|
|
if !ok {
|
|
JSONError(w, http.StatusNotFound, ErrVersionNotFound.Error())
|
|
return
|
|
}
|
|
w.Header().Set(headerContentType, contentTypeJSON)
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write(e.Entry)
|
|
return
|
|
}
|
|
out, err := localNPMPackument(packageName, local)
|
|
if err != nil {
|
|
h.proxy.Logger.Error("failed to build npm metadata from pushed versions", "package", packageName, "error", err)
|
|
JSONError(w, http.StatusInternalServerError, "failed to build package metadata")
|
|
return
|
|
}
|
|
serveRewrittenMetadata(w, r, h.proxy.uncachedRewrite("npm", h.proxyURL, metadataCacheKey, out))
|
|
}
|
|
|
|
// localNPMPackument builds a packument from pushed versions alone.
|
|
func localNPMPackument(packageName string, local []npmLocalEntry) ([]byte, error) {
|
|
versions := make(map[string]json.RawMessage, len(local))
|
|
times := make(map[string]string, len(local))
|
|
for _, e := range local {
|
|
versions[e.Version] = e.Entry
|
|
times[e.Version] = e.PublishedAt
|
|
}
|
|
return json.Marshal(map[string]any{
|
|
"name": packageName,
|
|
"dist-tags": map[string]string{"latest": latestNPMVersion(local)},
|
|
"versions": versions,
|
|
"time": times,
|
|
})
|
|
}
|
|
|
|
// latestNPMVersion returns the highest pushed version, or the last pushed
|
|
// when a version is not semver.
|
|
func latestNPMVersion(local []npmLocalEntry) string {
|
|
latest, latestTime := "", ""
|
|
var latestSemver *semver.Version
|
|
semverOnly := true
|
|
for _, e := range local {
|
|
if e.PublishedAt > latestTime {
|
|
latest, latestTime = e.Version, e.PublishedAt
|
|
}
|
|
v, err := semver.NewVersion(e.Version)
|
|
if err != nil {
|
|
semverOnly = false
|
|
continue
|
|
}
|
|
if latestSemver == nil || v.GreaterThan(latestSemver) {
|
|
latestSemver = v
|
|
}
|
|
}
|
|
if semverOnly && latestSemver != nil {
|
|
return latestSemver.Original()
|
|
}
|
|
return latest
|
|
}
|