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/handler/composer_hosted.go
Simon Chrzanowski 5c798069a1 Treat a null notification-url as absent for routed packages
Composer falls back to the default repository's notify-batch when a
version's notification-url is null, which would report installs of a
routed private package to Packagist. Replace null with an empty string
as is already done for pushed versions.

Also drop the pushed-version check's local url variable, which shadowed
net/url, and test that a pushed manifest's own URL is kept.
2026-11-08 10:06:20 +01:00

134 lines
4.9 KiB
Go

package handler
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"time"
)
// composerRootOnlyKeys are composer.json keys that only apply to the root
// project. Repositories leave them out of the version entries they serve.
var composerRootOnlyKeys = []string{"repositories", "config", "minimum-stability", "prefer-stable"}
// composerLocalEntry is a pushed version as Composer metadata lists it.
type composerLocalEntry struct {
version string
entry json.RawMessage
}
// composerLocalEntries returns the pushed versions of a package for one of
// its metadata files: the dev versions for the ~dev file, the tagged ones for
// the other.
func (h *ComposerHandler) composerLocalEntries(packageName string, dev bool) ([]composerLocalEntry, error) {
local, err := h.proxy.localVersions("composer", packageName)
if err != nil || len(local) == 0 {
return nil, err
}
var entries []composerLocalEntry
for _, lv := range local {
if isDevVersion(lv.Version) != dev {
continue
}
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)
}
for _, key := range composerRootOnlyKeys {
delete(manifest, key)
}
manifest["name"] = packageName
manifest["version"] = lv.Version
manifest["time"] = lv.PushedAt.Format(time.RFC3339)
// As for routed packages, keep Composer from reporting installs of
// a private version to the default repository's notify-batch.
// Composer reads a null notification-url as absent too.
if manifest["notification-url"] == nil {
manifest["notification-url"] = ""
}
manifest["dist"] = map[string]string{
"type": "zip",
"url": h.localDistURL(packageName, lv.Version, lv.Filename),
"reference": lv.SHA256,
"shasum": "",
}
entry, err := json.Marshal(manifest)
if err != nil {
return nil, err
}
entries = append(entries, composerLocalEntry{version: lv.Version, entry: entry})
}
return entries, nil
}
func (h *ComposerHandler) localDistURL(packageName, version, filename string) string {
return fmt.Sprintf("%s/composer/files/%s/%s/%s", h.proxyURL, packageName, url.PathEscape(version), filename)
}
// pushedComposerVersion reports whether version is one of the pushed ones.
// Tags are listed with and without a leading v, and both name one version.
func pushedComposerVersion(local []composerLocalEntry, version string) bool {
for _, e := range local {
if strings.TrimPrefix(e.version, "v") == strings.TrimPrefix(version, "v") {
return true
}
}
return false
}
// serveMetadataWithLocal serves a package's metadata with its pushed versions
// added, bypassing the rewrite cache like the npm handler does. When the
// upstream does not know the package, the document holds only the pushed
// versions.
func (h *ComposerHandler) serveMetadataWithLocal(w http.ResponseWriter, r *http.Request, packageName, upstreamURL, cacheKey string, local []composerLocalEntry) {
upstream := h.proxy.fetchMetadataDocument(r.Context(), "composer", cacheKey, upstreamURL, contentTypeJSON, nil)
var out []byte
var err error
switch {
case errors.Is(upstream.err, ErrUpstreamNotFound):
out, err = localComposerDocument(packageName, local)
case upstream.err != nil:
if writeUpstreamHeld(w, upstream.err) {
return
}
h.proxy.Logger.Error("upstream request failed", "error", upstream.err)
http.Error(w, "upstream request failed", http.StatusBadGateway)
return
default:
out, _, err = h.rewriteMetadataWithLocal(upstream.body, packageName, local)
}
if err != nil {
h.proxy.Logger.Error("failed to add pushed versions to composer metadata", "package", packageName, "error", err)
http.Error(w, "failed to rewrite package metadata", http.StatusBadGateway)
return
}
markStale(w, upstream.stale)
serveRewrittenMetadata(w, r, h.proxy.uncachedRewrite("composer", h.proxyURL, cacheKey, out))
}
// serveHostedOnlyMetadata answers a metadata request for a package of a
// hosted-only vendor from its pushed versions alone.
func (h *ComposerHandler) serveHostedOnlyMetadata(w http.ResponseWriter, r *http.Request, packageName, cacheKey string, local []composerLocalEntry) {
if len(local) == 0 {
http.Error(w, "not found", http.StatusNotFound)
return
}
out, err := localComposerDocument(packageName, local)
if err != nil {
h.proxy.Logger.Error("failed to build composer metadata from pushed versions", "package", packageName, "error", err)
http.Error(w, "failed to build package metadata", http.StatusInternalServerError)
return
}
serveRewrittenMetadata(w, r, h.proxy.uncachedRewrite("composer", h.proxyURL, cacheKey, out))
}
func localComposerDocument(packageName string, local []composerLocalEntry) ([]byte, error) {
entries := make([]json.RawMessage, 0, len(local))
for _, e := range local {
entries = append(entries, e.entry)
}
return json.Marshal(map[string]any{"packages": map[string]any{packageName: entries}})
}