Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-11-09 23:59:16 -05:00
pkg-proxy/internal/handler/npm_hosted.go

193 lines
6.7 KiB
Go
Raw Permalink Normal View History

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)
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
}
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
}