2026-01-20 21:52:44 +00:00
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/json"
|
Fix metadata caching, 404 propagation, mirror progress, and registry stubs
- ProxyCached now stores upstream Last-Modified in the cache and uses it
(along with ETag) for conditional request handling, returning 304 when
client validators match. Adds Content-Length to cached responses.
- Handlers calling FetchOrCacheMetadata (pypi, composer, pub, nuget) now
check for ErrUpstreamNotFound and return 404 instead of 502, matching
the existing npm and cargo behavior.
- Mirror jobs report live progress via a periodic callback while running,
so API polls return real counts instead of zeroed progress.
- Registry mirroring removed from CLI flags, API acceptance, README, and
docs since every enumerator was a stub returning "not yet implemented".
- Added tests for the conditional metadata path (ETag/If-None-Match,
Last-Modified/If-Modified-Since, 304 responses, header omission).
2026-04-01 20:14:11 +01:00
|
|
|
"errors"
|
2026-01-20 21:52:44 +00:00
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
"net/http"
|
2026-04-06 19:06:48 +01:00
|
|
|
"path"
|
2026-01-20 21:52:44 +00:00
|
|
|
"strings"
|
2026-03-04 19:00:31 +00:00
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/git-pkgs/purl"
|
2026-01-20 21:52:44 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
2026-03-18 10:59:29 +00:00
|
|
|
composerUpstream = "https://packagist.org"
|
|
|
|
|
composerRepo = "https://repo.packagist.org"
|
|
|
|
|
vendorPackageParts = 2
|
2026-01-20 21:52:44 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// ComposerHandler handles Composer/Packagist registry protocol requests.
|
|
|
|
|
type ComposerHandler struct {
|
|
|
|
|
proxy *Proxy
|
|
|
|
|
upstreamURL string
|
|
|
|
|
repoURL string
|
|
|
|
|
proxyURL string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewComposerHandler creates a new Composer protocol handler.
|
|
|
|
|
func NewComposerHandler(proxy *Proxy, proxyURL string) *ComposerHandler {
|
|
|
|
|
return &ComposerHandler{
|
|
|
|
|
proxy: proxy,
|
|
|
|
|
upstreamURL: composerUpstream,
|
|
|
|
|
repoURL: composerRepo,
|
|
|
|
|
proxyURL: strings.TrimSuffix(proxyURL, "/"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Routes returns the HTTP handler for Composer requests.
|
|
|
|
|
func (h *ComposerHandler) Routes() http.Handler {
|
|
|
|
|
mux := http.NewServeMux()
|
|
|
|
|
|
|
|
|
|
// Service index
|
|
|
|
|
mux.HandleFunc("GET /packages.json", h.handleServiceIndex)
|
|
|
|
|
|
|
|
|
|
// Package metadata (Composer v2 format) - use prefix since {package}.json isn't allowed
|
|
|
|
|
mux.HandleFunc("GET /p2/", h.handlePackageMetadata)
|
|
|
|
|
|
|
|
|
|
// Package downloads
|
|
|
|
|
mux.HandleFunc("GET /files/{vendor}/{package}/{version}/{filename}", h.handleDownload)
|
|
|
|
|
|
|
|
|
|
// Search and list (proxy without modification)
|
|
|
|
|
mux.HandleFunc("GET /search.json", h.proxyUpstream)
|
|
|
|
|
mux.HandleFunc("GET /packages/list.json", h.proxyUpstream)
|
|
|
|
|
|
|
|
|
|
return mux
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// handleServiceIndex returns the Composer repository service index.
|
|
|
|
|
func (h *ComposerHandler) handleServiceIndex(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
// Return a minimal service index pointing to our proxy
|
|
|
|
|
index := map[string]any{
|
2026-04-18 07:43:22 -04:00
|
|
|
"packages": map[string]any{},
|
|
|
|
|
"metadata-url": h.proxyURL + "/composer/p2/%package%.json",
|
|
|
|
|
"notify-batch": h.upstreamURL + "/downloads/",
|
|
|
|
|
"search": h.proxyURL + "/composer/search.json?q=%query%&type=%type%",
|
2026-01-20 21:52:44 +00:00
|
|
|
"providers-lazy-url": h.proxyURL + "/composer/p2/%package%.json",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
_ = json.NewEncoder(w).Encode(index)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// handlePackageMetadata proxies and rewrites package metadata.
|
|
|
|
|
func (h *ComposerHandler) handlePackageMetadata(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
// Parse path: /p2/{vendor}/{package}.json
|
|
|
|
|
path := strings.TrimPrefix(r.URL.Path, "/p2/")
|
|
|
|
|
path = strings.TrimSuffix(path, ".json")
|
2026-03-18 10:59:29 +00:00
|
|
|
parts := strings.SplitN(path, "/", vendorPackageParts)
|
|
|
|
|
if len(parts) != vendorPackageParts || parts[0] == "" || parts[1] == "" {
|
2026-01-20 21:52:44 +00:00
|
|
|
http.Error(w, "invalid package path", http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
vendor := parts[0]
|
|
|
|
|
pkg := parts[1]
|
|
|
|
|
packageName := vendor + "/" + pkg
|
|
|
|
|
|
|
|
|
|
h.proxy.Logger.Info("composer metadata request", "package", packageName)
|
|
|
|
|
|
|
|
|
|
upstreamURL := fmt.Sprintf("%s/p2/%s/%s.json", h.repoURL, vendor, pkg)
|
|
|
|
|
|
2026-03-19 21:06:02 +00:00
|
|
|
body, _, err := h.proxy.FetchOrCacheMetadata(r.Context(), "composer", packageName, upstreamURL)
|
2026-01-20 21:52:44 +00:00
|
|
|
if err != nil {
|
Fix metadata caching, 404 propagation, mirror progress, and registry stubs
- ProxyCached now stores upstream Last-Modified in the cache and uses it
(along with ETag) for conditional request handling, returning 304 when
client validators match. Adds Content-Length to cached responses.
- Handlers calling FetchOrCacheMetadata (pypi, composer, pub, nuget) now
check for ErrUpstreamNotFound and return 404 instead of 502, matching
the existing npm and cargo behavior.
- Mirror jobs report live progress via a periodic callback while running,
so API polls return real counts instead of zeroed progress.
- Registry mirroring removed from CLI flags, API acceptance, README, and
docs since every enumerator was a stub returning "not yet implemented".
- Added tests for the conditional metadata path (ETag/If-None-Match,
Last-Modified/If-Modified-Since, 304 responses, header omission).
2026-04-01 20:14:11 +01:00
|
|
|
if errors.Is(err, ErrUpstreamNotFound) {
|
|
|
|
|
http.Error(w, "not found", http.StatusNotFound)
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-01-20 21:52:44 +00:00
|
|
|
h.proxy.Logger.Error("upstream request failed", "error", err)
|
|
|
|
|
http.Error(w, "upstream request failed", http.StatusBadGateway)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rewritten, err := h.rewriteMetadata(body)
|
|
|
|
|
if err != nil {
|
|
|
|
|
h.proxy.Logger.Warn("failed to rewrite metadata, proxying original", "error", err)
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
_, _ = w.Write(body)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
_, _ = w.Write(rewritten)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// rewriteMetadata rewrites dist URLs in Composer metadata to point at this proxy.
|
Fix Composer minified metadata expansion and namespaced package routing (#63)
* Fix Composer minified metadata expansion and namespaced package routing
Packagist serves metadata in a minified format where only the first version
entry has all fields and subsequent entries inherit from the previous one.
The proxy was passing this through without expanding it, which meant cooldown
filtering could break the inheritance chain (losing fields like `name`) and
`~dev` sentinel markers were silently dropped.
The proxy now expands the minified format before filtering and rewriting,
ensuring every version entry is self-contained.
Web UI and API routes used single-segment chi URL params for package names,
which broke for Composer's `vendor/name` format. `/package/composer/monolog/monolog`
would match the version show route instead of the package show route.
All `/package/` and related API routes now use wildcard paths with a
`resolvePackageName` helper that tries increasingly longer path prefixes as
package names via DB lookup, correctly handling namespaced packages across
all endpoints (show, version, browse, compare, vulns).
Fixes #61, fixes #62
* Add namespaced package routing tests for all affected ecosystems
Verifies the wildcard routing handles slashes in package names for
npm (@babel/core), Go modules (github.com/stretchr/testify),
OCI images (library/nginx), Conda (conda-forge/numpy), and
Conan (zlib/1.2.13@demo/stable).
* Regenerate swagger docs after route refactor
The swagger annotations for the old per-endpoint handlers were removed
during the wildcard routing refactor. Regenerate to match current state.
2026-04-06 13:07:02 +01:00
|
|
|
// If the metadata uses the minified Composer v2 format, it is expanded first so
|
|
|
|
|
// that every version entry contains all fields. If cooldown is enabled, versions
|
|
|
|
|
// published too recently are filtered out.
|
2026-01-20 21:52:44 +00:00
|
|
|
func (h *ComposerHandler) rewriteMetadata(body []byte) ([]byte, error) {
|
|
|
|
|
var metadata map[string]any
|
|
|
|
|
if err := json.Unmarshal(body, &metadata); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
packages, ok := metadata["packages"].(map[string]any)
|
|
|
|
|
if !ok {
|
|
|
|
|
return body, nil
|
|
|
|
|
}
|
|
|
|
|
|
Fix Composer minified metadata expansion and namespaced package routing (#63)
* Fix Composer minified metadata expansion and namespaced package routing
Packagist serves metadata in a minified format where only the first version
entry has all fields and subsequent entries inherit from the previous one.
The proxy was passing this through without expanding it, which meant cooldown
filtering could break the inheritance chain (losing fields like `name`) and
`~dev` sentinel markers were silently dropped.
The proxy now expands the minified format before filtering and rewriting,
ensuring every version entry is self-contained.
Web UI and API routes used single-segment chi URL params for package names,
which broke for Composer's `vendor/name` format. `/package/composer/monolog/monolog`
would match the version show route instead of the package show route.
All `/package/` and related API routes now use wildcard paths with a
`resolvePackageName` helper that tries increasingly longer path prefixes as
package names via DB lookup, correctly handling namespaced packages across
all endpoints (show, version, browse, compare, vulns).
Fixes #61, fixes #62
* Add namespaced package routing tests for all affected ecosystems
Verifies the wildcard routing handles slashes in package names for
npm (@babel/core), Go modules (github.com/stretchr/testify),
OCI images (library/nginx), Conda (conda-forge/numpy), and
Conan (zlib/1.2.13@demo/stable).
* Regenerate swagger docs after route refactor
The swagger annotations for the old per-endpoint handlers were removed
during the wildcard routing refactor. Regenerate to match current state.
2026-04-06 13:07:02 +01:00
|
|
|
minified := metadata["minified"] == "composer/2.0"
|
|
|
|
|
|
2026-01-20 21:52:44 +00:00
|
|
|
for packageName, versions := range packages {
|
|
|
|
|
versionList, ok := versions.([]any)
|
|
|
|
|
if !ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
Fix Composer minified metadata expansion and namespaced package routing (#63)
* Fix Composer minified metadata expansion and namespaced package routing
Packagist serves metadata in a minified format where only the first version
entry has all fields and subsequent entries inherit from the previous one.
The proxy was passing this through without expanding it, which meant cooldown
filtering could break the inheritance chain (losing fields like `name`) and
`~dev` sentinel markers were silently dropped.
The proxy now expands the minified format before filtering and rewriting,
ensuring every version entry is self-contained.
Web UI and API routes used single-segment chi URL params for package names,
which broke for Composer's `vendor/name` format. `/package/composer/monolog/monolog`
would match the version show route instead of the package show route.
All `/package/` and related API routes now use wildcard paths with a
`resolvePackageName` helper that tries increasingly longer path prefixes as
package names via DB lookup, correctly handling namespaced packages across
all endpoints (show, version, browse, compare, vulns).
Fixes #61, fixes #62
* Add namespaced package routing tests for all affected ecosystems
Verifies the wildcard routing handles slashes in package names for
npm (@babel/core), Go modules (github.com/stretchr/testify),
OCI images (library/nginx), Conda (conda-forge/numpy), and
Conan (zlib/1.2.13@demo/stable).
* Regenerate swagger docs after route refactor
The swagger annotations for the old per-endpoint handlers were removed
during the wildcard routing refactor. Regenerate to match current state.
2026-04-06 13:07:02 +01:00
|
|
|
if minified {
|
|
|
|
|
versionList = expandMinifiedVersions(versionList)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 10:59:29 +00:00
|
|
|
packages[packageName] = h.filterAndRewriteVersions(packageName, versionList)
|
|
|
|
|
}
|
|
|
|
|
|
Fix Composer minified metadata expansion and namespaced package routing (#63)
* Fix Composer minified metadata expansion and namespaced package routing
Packagist serves metadata in a minified format where only the first version
entry has all fields and subsequent entries inherit from the previous one.
The proxy was passing this through without expanding it, which meant cooldown
filtering could break the inheritance chain (losing fields like `name`) and
`~dev` sentinel markers were silently dropped.
The proxy now expands the minified format before filtering and rewriting,
ensuring every version entry is self-contained.
Web UI and API routes used single-segment chi URL params for package names,
which broke for Composer's `vendor/name` format. `/package/composer/monolog/monolog`
would match the version show route instead of the package show route.
All `/package/` and related API routes now use wildcard paths with a
`resolvePackageName` helper that tries increasingly longer path prefixes as
package names via DB lookup, correctly handling namespaced packages across
all endpoints (show, version, browse, compare, vulns).
Fixes #61, fixes #62
* Add namespaced package routing tests for all affected ecosystems
Verifies the wildcard routing handles slashes in package names for
npm (@babel/core), Go modules (github.com/stretchr/testify),
OCI images (library/nginx), Conda (conda-forge/numpy), and
Conan (zlib/1.2.13@demo/stable).
* Regenerate swagger docs after route refactor
The swagger annotations for the old per-endpoint handlers were removed
during the wildcard routing refactor. Regenerate to match current state.
2026-04-06 13:07:02 +01:00
|
|
|
delete(metadata, "minified")
|
|
|
|
|
|
2026-03-18 10:59:29 +00:00
|
|
|
return json.Marshal(metadata)
|
|
|
|
|
}
|
|
|
|
|
|
Fix Composer minified metadata expansion and namespaced package routing (#63)
* Fix Composer minified metadata expansion and namespaced package routing
Packagist serves metadata in a minified format where only the first version
entry has all fields and subsequent entries inherit from the previous one.
The proxy was passing this through without expanding it, which meant cooldown
filtering could break the inheritance chain (losing fields like `name`) and
`~dev` sentinel markers were silently dropped.
The proxy now expands the minified format before filtering and rewriting,
ensuring every version entry is self-contained.
Web UI and API routes used single-segment chi URL params for package names,
which broke for Composer's `vendor/name` format. `/package/composer/monolog/monolog`
would match the version show route instead of the package show route.
All `/package/` and related API routes now use wildcard paths with a
`resolvePackageName` helper that tries increasingly longer path prefixes as
package names via DB lookup, correctly handling namespaced packages across
all endpoints (show, version, browse, compare, vulns).
Fixes #61, fixes #62
* Add namespaced package routing tests for all affected ecosystems
Verifies the wildcard routing handles slashes in package names for
npm (@babel/core), Go modules (github.com/stretchr/testify),
OCI images (library/nginx), Conda (conda-forge/numpy), and
Conan (zlib/1.2.13@demo/stable).
* Regenerate swagger docs after route refactor
The swagger annotations for the old per-endpoint handlers were removed
during the wildcard routing refactor. Regenerate to match current state.
2026-04-06 13:07:02 +01:00
|
|
|
// expandMinifiedVersions expands the Composer v2 minified format where each
|
|
|
|
|
// version entry only contains fields that differ from the previous entry.
|
|
|
|
|
// The "~dev" sentinel string resets the inheritance chain.
|
|
|
|
|
func expandMinifiedVersions(versionList []any) []any {
|
|
|
|
|
expanded := make([]any, 0, len(versionList))
|
|
|
|
|
inherited := map[string]any{}
|
|
|
|
|
|
|
|
|
|
for _, v := range versionList {
|
|
|
|
|
// The "~dev" sentinel resets the inheritance chain for dev versions.
|
|
|
|
|
if s, ok := v.(string); ok && s == "~dev" {
|
|
|
|
|
inherited = map[string]any{}
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
vmap, ok := v.(map[string]any)
|
|
|
|
|
if !ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Merge inherited fields into a new map, then overlay current fields.
|
2026-04-06 16:43:20 +01:00
|
|
|
// Deep copy values to avoid shared references between versions.
|
Fix Composer minified metadata expansion and namespaced package routing (#63)
* Fix Composer minified metadata expansion and namespaced package routing
Packagist serves metadata in a minified format where only the first version
entry has all fields and subsequent entries inherit from the previous one.
The proxy was passing this through without expanding it, which meant cooldown
filtering could break the inheritance chain (losing fields like `name`) and
`~dev` sentinel markers were silently dropped.
The proxy now expands the minified format before filtering and rewriting,
ensuring every version entry is self-contained.
Web UI and API routes used single-segment chi URL params for package names,
which broke for Composer's `vendor/name` format. `/package/composer/monolog/monolog`
would match the version show route instead of the package show route.
All `/package/` and related API routes now use wildcard paths with a
`resolvePackageName` helper that tries increasingly longer path prefixes as
package names via DB lookup, correctly handling namespaced packages across
all endpoints (show, version, browse, compare, vulns).
Fixes #61, fixes #62
* Add namespaced package routing tests for all affected ecosystems
Verifies the wildcard routing handles slashes in package names for
npm (@babel/core), Go modules (github.com/stretchr/testify),
OCI images (library/nginx), Conda (conda-forge/numpy), and
Conan (zlib/1.2.13@demo/stable).
* Regenerate swagger docs after route refactor
The swagger annotations for the old per-endpoint handlers were removed
during the wildcard routing refactor. Regenerate to match current state.
2026-04-06 13:07:02 +01:00
|
|
|
merged := make(map[string]any, len(inherited)+len(vmap))
|
|
|
|
|
for k, val := range inherited {
|
2026-04-06 16:43:20 +01:00
|
|
|
merged[k] = deepCopyValue(val)
|
Fix Composer minified metadata expansion and namespaced package routing (#63)
* Fix Composer minified metadata expansion and namespaced package routing
Packagist serves metadata in a minified format where only the first version
entry has all fields and subsequent entries inherit from the previous one.
The proxy was passing this through without expanding it, which meant cooldown
filtering could break the inheritance chain (losing fields like `name`) and
`~dev` sentinel markers were silently dropped.
The proxy now expands the minified format before filtering and rewriting,
ensuring every version entry is self-contained.
Web UI and API routes used single-segment chi URL params for package names,
which broke for Composer's `vendor/name` format. `/package/composer/monolog/monolog`
would match the version show route instead of the package show route.
All `/package/` and related API routes now use wildcard paths with a
`resolvePackageName` helper that tries increasingly longer path prefixes as
package names via DB lookup, correctly handling namespaced packages across
all endpoints (show, version, browse, compare, vulns).
Fixes #61, fixes #62
* Add namespaced package routing tests for all affected ecosystems
Verifies the wildcard routing handles slashes in package names for
npm (@babel/core), Go modules (github.com/stretchr/testify),
OCI images (library/nginx), Conda (conda-forge/numpy), and
Conan (zlib/1.2.13@demo/stable).
* Regenerate swagger docs after route refactor
The swagger annotations for the old per-endpoint handlers were removed
during the wildcard routing refactor. Regenerate to match current state.
2026-04-06 13:07:02 +01:00
|
|
|
}
|
|
|
|
|
for k, val := range vmap {
|
|
|
|
|
merged[k] = val
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update inherited state for next iteration.
|
|
|
|
|
inherited = merged
|
|
|
|
|
|
|
|
|
|
expanded = append(expanded, merged)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return expanded
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-06 16:43:20 +01:00
|
|
|
// deepCopyValue returns a deep copy of JSON-like values (maps, slices, scalars).
|
|
|
|
|
func deepCopyValue(v any) any {
|
|
|
|
|
switch val := v.(type) {
|
|
|
|
|
case map[string]any:
|
|
|
|
|
m := make(map[string]any, len(val))
|
|
|
|
|
for k, v := range val {
|
|
|
|
|
m[k] = deepCopyValue(v)
|
|
|
|
|
}
|
|
|
|
|
return m
|
|
|
|
|
case []any:
|
|
|
|
|
s := make([]any, len(val))
|
|
|
|
|
for i, v := range val {
|
|
|
|
|
s[i] = deepCopyValue(v)
|
|
|
|
|
}
|
|
|
|
|
return s
|
|
|
|
|
default:
|
|
|
|
|
return v
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 10:59:29 +00:00
|
|
|
// filterAndRewriteVersions applies cooldown filtering and rewrites dist URLs
|
|
|
|
|
// for a single package's version list.
|
|
|
|
|
func (h *ComposerHandler) filterAndRewriteVersions(packageName string, versionList []any) []any {
|
|
|
|
|
packagePURL := purl.MakePURLString("composer", packageName, "")
|
|
|
|
|
|
|
|
|
|
filtered := versionList[:0]
|
|
|
|
|
for _, v := range versionList {
|
|
|
|
|
vmap, ok := v.(map[string]any)
|
|
|
|
|
if !ok {
|
|
|
|
|
continue
|
2026-01-20 21:52:44 +00:00
|
|
|
}
|
2026-03-04 19:00:31 +00:00
|
|
|
|
2026-03-18 10:59:29 +00:00
|
|
|
version, _ := vmap["version"].(string)
|
|
|
|
|
|
|
|
|
|
if h.shouldFilterVersion(packagePURL, packageName, version, vmap) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
h.rewriteDistURL(vmap, packageName, version)
|
|
|
|
|
filtered = append(filtered, v)
|
2026-01-20 21:52:44 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-18 10:59:29 +00:00
|
|
|
return filtered
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// shouldFilterVersion returns true if the version should be excluded due to cooldown.
|
|
|
|
|
func (h *ComposerHandler) shouldFilterVersion(packagePURL, packageName, version string, vmap map[string]any) bool {
|
|
|
|
|
if h.proxy.Cooldown == nil || !h.proxy.Cooldown.Enabled() {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
timeStr, ok := vmap["time"].(string)
|
|
|
|
|
if !ok {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
publishedAt, err := time.Parse(time.RFC3339, timeStr)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !h.proxy.Cooldown.IsAllowed("composer", packagePURL, publishedAt) {
|
|
|
|
|
h.proxy.Logger.Info("cooldown: filtering composer version",
|
|
|
|
|
"package", packageName, "version", version)
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// rewriteDistURL rewrites the dist URL in a version entry to point at this proxy.
|
|
|
|
|
func (h *ComposerHandler) rewriteDistURL(vmap map[string]any, packageName, version string) {
|
|
|
|
|
dist, ok := vmap["dist"].(map[string]any)
|
|
|
|
|
if !ok {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
url, ok := dist["url"].(string)
|
|
|
|
|
if !ok || url == "" {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
filename := "package.zip"
|
|
|
|
|
if idx := strings.LastIndex(url, "/"); idx >= 0 {
|
|
|
|
|
filename = url[idx+1:]
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-06 16:43:20 +01:00
|
|
|
// GitHub zipball URLs end with a bare commit hash (no extension).
|
|
|
|
|
// Append .zip so the archives library can detect the format.
|
2026-04-06 19:06:48 +01:00
|
|
|
if path.Ext(filename) == "" {
|
2026-04-06 16:43:20 +01:00
|
|
|
if distType, _ := dist["type"].(string); distType == "zip" {
|
|
|
|
|
filename += ".zip"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 10:59:29 +00:00
|
|
|
parts := strings.SplitN(packageName, "/", vendorPackageParts)
|
|
|
|
|
if len(parts) == vendorPackageParts {
|
|
|
|
|
newURL := fmt.Sprintf("%s/composer/files/%s/%s/%s/%s",
|
|
|
|
|
h.proxyURL, parts[0], parts[1], version, filename)
|
|
|
|
|
dist["url"] = newURL
|
|
|
|
|
}
|
2026-01-20 21:52:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// handleDownload serves a package file, fetching and caching from upstream if needed.
|
|
|
|
|
func (h *ComposerHandler) handleDownload(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
vendor := r.PathValue("vendor")
|
|
|
|
|
pkg := r.PathValue("package")
|
|
|
|
|
version := r.PathValue("version")
|
|
|
|
|
filename := r.PathValue("filename")
|
|
|
|
|
|
|
|
|
|
packageName := vendor + "/" + pkg
|
|
|
|
|
|
|
|
|
|
h.proxy.Logger.Info("composer download request",
|
|
|
|
|
"package", packageName, "version", version, "filename", filename)
|
|
|
|
|
|
|
|
|
|
// We need to fetch the metadata to get the actual download URL
|
|
|
|
|
// since Packagist URLs include a hash
|
|
|
|
|
metaURL := fmt.Sprintf("%s/p2/%s/%s.json", h.repoURL, vendor, pkg)
|
|
|
|
|
|
|
|
|
|
req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, metaURL, nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
http.Error(w, "failed to create request", http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-13 07:44:11 +00:00
|
|
|
resp, err := h.proxy.HTTPClient.Do(req)
|
2026-01-20 21:52:44 +00:00
|
|
|
if err != nil {
|
|
|
|
|
h.proxy.Logger.Error("failed to fetch metadata", "error", err)
|
|
|
|
|
http.Error(w, "failed to fetch metadata", http.StatusBadGateway)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
|
|
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
|
|
|
http.Error(w, "package not found", http.StatusNotFound)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var metadata map[string]any
|
|
|
|
|
if err := json.NewDecoder(resp.Body).Decode(&metadata); err != nil {
|
|
|
|
|
http.Error(w, "failed to parse metadata", http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Find the download URL for this version
|
|
|
|
|
downloadURL := h.findDownloadURL(metadata, packageName, version)
|
|
|
|
|
if downloadURL == "" {
|
|
|
|
|
http.Error(w, "version not found", http.StatusNotFound)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "composer", packageName, version, filename, downloadURL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
h.proxy.Logger.Error("failed to get artifact", "error", err)
|
|
|
|
|
http.Error(w, "failed to fetch package", http.StatusBadGateway)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ServeArtifact(w, result)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// findDownloadURL finds the dist URL for a specific version in metadata.
|
|
|
|
|
func (h *ComposerHandler) findDownloadURL(metadata map[string]any, packageName, version string) string {
|
|
|
|
|
packages, ok := metadata["packages"].(map[string]any)
|
|
|
|
|
if !ok {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
versions, ok := packages[packageName].([]any)
|
|
|
|
|
if !ok {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, v := range versions {
|
|
|
|
|
vmap, ok := v.(map[string]any)
|
|
|
|
|
if !ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if vmap["version"] == version {
|
|
|
|
|
if dist, ok := vmap["dist"].(map[string]any); ok {
|
|
|
|
|
if url, ok := dist["url"].(string); ok {
|
|
|
|
|
return url
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// proxyUpstream forwards a request to packagist.org without caching.
|
|
|
|
|
func (h *ComposerHandler) proxyUpstream(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
upstreamURL := h.upstreamURL + r.URL.Path
|
|
|
|
|
if r.URL.RawQuery != "" {
|
|
|
|
|
upstreamURL += "?" + r.URL.RawQuery
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
h.proxy.Logger.Debug("proxying to upstream", "url", upstreamURL)
|
|
|
|
|
|
|
|
|
|
req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, upstreamURL, nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
http.Error(w, "failed to create request", http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-13 07:44:11 +00:00
|
|
|
resp, err := h.proxy.HTTPClient.Do(req)
|
2026-01-20 21:52:44 +00:00
|
|
|
if err != nil {
|
|
|
|
|
h.proxy.Logger.Error("upstream request failed", "error", err)
|
|
|
|
|
http.Error(w, "upstream request failed", http.StatusBadGateway)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
|
|
|
|
|
|
for k, vv := range resp.Header {
|
|
|
|
|
for _, v := range vv {
|
|
|
|
|
w.Header().Add(k, v)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
w.WriteHeader(resp.StatusCode)
|
|
|
|
|
_, _ = io.Copy(w, resp.Body)
|
|
|
|
|
}
|