This commit is contained in:
@@ -10,7 +10,46 @@ import (
|
|||||||
"github.com/any-hub/any-hub/internal/proxy/hooks"
|
"github.com/any-hub/any-hub/internal/proxy/hooks"
|
||||||
)
|
)
|
||||||
|
|
||||||
var composerDistRegistry sync.Map
|
var composerDists = newDistRegistry()
|
||||||
|
|
||||||
|
type distRegistry struct {
|
||||||
|
sync.RWMutex
|
||||||
|
items map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDistRegistry() *distRegistry {
|
||||||
|
return &distRegistry{items: map[string]string{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *distRegistry) remember(domain, pkg, reference, distType, upstream string) {
|
||||||
|
key := composerDistKey(domain, pkg, reference, distType)
|
||||||
|
if key == "" || strings.TrimSpace(upstream) == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.Lock()
|
||||||
|
r.items[key] = upstream
|
||||||
|
r.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *distRegistry) lookup(domain, pkg, reference, distType string) (string, bool) {
|
||||||
|
key := composerDistKey(domain, pkg, reference, distType)
|
||||||
|
if key == "" {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
r.RLock()
|
||||||
|
val, ok := r.items[key]
|
||||||
|
r.RUnlock()
|
||||||
|
if !ok || strings.TrimSpace(val) == "" {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return val, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *distRegistry) reset() {
|
||||||
|
r.Lock()
|
||||||
|
r.items = map[string]string{}
|
||||||
|
r.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
hooks.MustRegister("composer", hooks.Hooks{
|
hooks.MustRegister("composer", hooks.Hooks{
|
||||||
@@ -120,21 +159,16 @@ func contentType(_ *hooks.RequestContext, locatorPath string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func rewriteComposerRootBody(body []byte, domain string) ([]byte, bool, error) {
|
func rewriteComposerRootBody(body []byte, domain string) ([]byte, bool, error) {
|
||||||
|
domain = strings.TrimSpace(domain)
|
||||||
var root map[string]any
|
var root map[string]any
|
||||||
if err := json.Unmarshal(body, &root); err != nil {
|
if err := json.Unmarshal(body, &root); err != nil {
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
changed := false
|
changed := false
|
||||||
if rewriteComposerRootURLField(root, "metadata-url", domain) {
|
changed = rewriteComposerRootURLField(root, "metadata-url", domain) || changed
|
||||||
changed = true
|
changed = rewriteComposerRootURLField(root, "providers-url", domain) || changed
|
||||||
}
|
changed = ensureComposerMirrors(root, domain) || changed
|
||||||
if rewriteComposerRootURLField(root, "providers-url", domain) {
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
if ensureComposerMirrors(root, domain) {
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if !changed {
|
if !changed {
|
||||||
return body, false, nil
|
return body, false, nil
|
||||||
@@ -152,11 +186,13 @@ func rewriteComposerRootURLField(root map[string]any, key string, domain string)
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
proxied := buildComposerProxyURL(value, domain)
|
proxied := buildComposerProxyURL(value, domain)
|
||||||
if proxied == value {
|
if proxied == "" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
changed := proxied != value
|
||||||
root[key] = proxied
|
root[key] = proxied
|
||||||
return true
|
return changed
|
||||||
}
|
}
|
||||||
|
|
||||||
func ensureComposerMirrors(root map[string]any, domain string) bool {
|
func ensureComposerMirrors(root map[string]any, domain string) bool {
|
||||||
@@ -184,6 +220,10 @@ func ensureComposerMirrors(root map[string]any, domain string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func rewriteComposerMetadata(body []byte, domain string) ([]byte, bool, error) {
|
func rewriteComposerMetadata(body []byte, domain string) ([]byte, bool, error) {
|
||||||
|
domain = strings.TrimSpace(domain)
|
||||||
|
if domain == "" {
|
||||||
|
return body, false, nil
|
||||||
|
}
|
||||||
type packagesRoot struct {
|
type packagesRoot struct {
|
||||||
Packages map[string]json.RawMessage `json:"packages"`
|
Packages map[string]json.RawMessage `json:"packages"`
|
||||||
}
|
}
|
||||||
@@ -268,6 +308,7 @@ func rewriteComposerVersion(entry map[string]any, domain string, packageName str
|
|||||||
if entry == nil {
|
if entry == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
domain = strings.TrimSpace(domain)
|
||||||
changed := false
|
changed := false
|
||||||
if packageName != "" {
|
if packageName != "" {
|
||||||
if name, _ := entry["name"].(string); strings.TrimSpace(name) == "" {
|
if name, _ := entry["name"].(string); strings.TrimSpace(name) == "" {
|
||||||
@@ -286,7 +327,7 @@ func rewriteComposerVersion(entry map[string]any, domain string, packageName str
|
|||||||
reference, _ := distVal["reference"].(string)
|
reference, _ := distVal["reference"].(string)
|
||||||
distType, _ := distVal["type"].(string)
|
distType, _ := distVal["type"].(string)
|
||||||
if packageName != "" && domain != "" && reference != "" && distType != "" {
|
if packageName != "" && domain != "" && reference != "" && distType != "" {
|
||||||
registerComposerDist(domain, packageName, reference, distType, urlValue)
|
composerDists.remember(domain, packageName, reference, distType, urlValue)
|
||||||
}
|
}
|
||||||
rewritten := rewriteComposerLegacyDistURL(urlValue, domain)
|
rewritten := rewriteComposerLegacyDistURL(urlValue, domain)
|
||||||
if rewritten == urlValue {
|
if rewritten == urlValue {
|
||||||
@@ -395,45 +436,64 @@ func parseComposerDistURL(path string, rawQuery string) (*url.URL, bool) {
|
|||||||
return target, true
|
return target, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func stripPackagistHost(raw string) string {
|
|
||||||
raw = strings.TrimSpace(raw)
|
|
||||||
raw = strings.ReplaceAll(raw, "https://repo.packagist.org", "")
|
|
||||||
raw = strings.ReplaceAll(raw, "http://repo.packagist.org", "")
|
|
||||||
return raw
|
|
||||||
}
|
|
||||||
|
|
||||||
func isPackagistHost(host string) bool {
|
func isPackagistHost(host string) bool {
|
||||||
return strings.EqualFold(host, "repo.packagist.org")
|
return strings.EqualFold(host, "repo.packagist.org")
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildComposerProxyURL(raw string, domain string) string {
|
func stripPackagistPrefix(raw string) (string, bool) {
|
||||||
trimmed := stripPackagistHost(strings.TrimSpace(raw))
|
for _, prefix := range []string{
|
||||||
|
"https://repo.packagist.org",
|
||||||
|
"http://repo.packagist.org",
|
||||||
|
} {
|
||||||
|
if strings.HasPrefix(raw, prefix) {
|
||||||
|
trimmed := strings.TrimPrefix(raw, prefix)
|
||||||
if trimmed == "" {
|
if trimmed == "" {
|
||||||
return trimmed
|
return "/", true
|
||||||
}
|
}
|
||||||
if parsed, err := url.Parse(trimmed); err == nil && parsed.Host != "" {
|
|
||||||
if domain != "" && strings.EqualFold(parsed.Host, domain) {
|
|
||||||
return trimmed
|
|
||||||
}
|
|
||||||
if !isPackagistHost(parsed.Host) {
|
|
||||||
return trimmed
|
|
||||||
}
|
|
||||||
if path := parsed.EscapedPath(); path != "" {
|
|
||||||
trimmed = path
|
|
||||||
if parsed.RawQuery != "" {
|
|
||||||
trimmed = trimmed + "?" + parsed.RawQuery
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.HasPrefix(trimmed, "/") {
|
if !strings.HasPrefix(trimmed, "/") {
|
||||||
trimmed = "/" + trimmed
|
trimmed = "/" + trimmed
|
||||||
}
|
}
|
||||||
|
return trimmed, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildComposerProxyURL(raw string, domain string) string {
|
||||||
|
value := strings.TrimSpace(raw)
|
||||||
|
if value == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(value, "http://") || strings.HasPrefix(value, "https://") {
|
||||||
|
parsed, err := url.Parse(value)
|
||||||
|
switch {
|
||||||
|
case err != nil:
|
||||||
|
if trimmed, ok := stripPackagistPrefix(value); ok {
|
||||||
|
value = trimmed
|
||||||
|
} else {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
case domain != "" && strings.EqualFold(parsed.Host, domain):
|
||||||
|
return value
|
||||||
|
case !isPackagistHost(parsed.Host):
|
||||||
|
return value
|
||||||
|
default:
|
||||||
|
value = parsed.EscapedPath()
|
||||||
|
if value == "" {
|
||||||
|
value = "/"
|
||||||
|
}
|
||||||
|
if parsed.RawQuery != "" {
|
||||||
|
value += "?" + parsed.RawQuery
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pathOnly, query := splitPathAndQuery(value)
|
||||||
|
pathOnly = ensureLeadingSlash(pathOnly)
|
||||||
|
|
||||||
if domain == "" {
|
if domain == "" {
|
||||||
return trimmed
|
return pathOnly + query
|
||||||
}
|
}
|
||||||
return "https://" + domain + trimmed
|
return "https://" + domain + pathOnly + query
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveComposerMirrorDist(domain string, locator string) string {
|
func resolveComposerMirrorDist(domain string, locator string) string {
|
||||||
@@ -445,7 +505,7 @@ func resolveComposerMirrorDist(domain string, locator string) string {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
target, ok := lookupComposerDist(domain, pkg, reference, distType)
|
target, ok := composerDists.lookup(domain, pkg, reference, distType)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -483,30 +543,6 @@ func parseComposerMirrorDistLocator(locator string) (string, string, string, boo
|
|||||||
return packageName, reference, distType, true
|
return packageName, reference, distType, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerComposerDist(domain string, packageName string, reference string, distType string, upstream string) {
|
|
||||||
key := composerDistKey(domain, packageName, reference, distType)
|
|
||||||
if key == "" || strings.TrimSpace(upstream) == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
composerDistRegistry.Store(key, upstream)
|
|
||||||
}
|
|
||||||
|
|
||||||
func lookupComposerDist(domain string, packageName string, reference string, distType string) (string, bool) {
|
|
||||||
key := composerDistKey(domain, packageName, reference, distType)
|
|
||||||
if key == "" {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
value, ok := composerDistRegistry.Load(key)
|
|
||||||
if !ok {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
str, _ := value.(string)
|
|
||||||
if strings.TrimSpace(str) == "" {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
return str, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func composerDistKey(domain string, packageName string, reference string, distType string) string {
|
func composerDistKey(domain string, packageName string, reference string, distType string) string {
|
||||||
domain = strings.ToLower(strings.TrimSpace(domain))
|
domain = strings.ToLower(strings.TrimSpace(domain))
|
||||||
pkg := strings.ToLower(strings.TrimSpace(packageName))
|
pkg := strings.ToLower(strings.TrimSpace(packageName))
|
||||||
@@ -519,18 +555,27 @@ func composerDistKey(domain string, packageName string, reference string, distTy
|
|||||||
}
|
}
|
||||||
|
|
||||||
func trimComposerNamespace(p string) string {
|
func trimComposerNamespace(p string) string {
|
||||||
if strings.HasPrefix(p, "/composer/") {
|
return ensureLeadingSlash(p)
|
||||||
return strings.TrimPrefix(p, "/composer")
|
|
||||||
}
|
|
||||||
if p == "/composer" {
|
|
||||||
return "/"
|
|
||||||
}
|
|
||||||
return p
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func resetComposerDistRegistry() {
|
func resetComposerDistRegistry() {
|
||||||
composerDistRegistry.Range(func(key, _ any) bool {
|
composerDists.reset()
|
||||||
composerDistRegistry.Delete(key)
|
}
|
||||||
return true
|
|
||||||
})
|
func splitPathAndQuery(raw string) (string, string) {
|
||||||
|
if idx := strings.IndexByte(raw, '?'); idx >= 0 {
|
||||||
|
return raw[:idx], raw[idx:]
|
||||||
|
}
|
||||||
|
return raw, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureLeadingSlash(p string) string {
|
||||||
|
p = strings.TrimSpace(p)
|
||||||
|
if p == "" {
|
||||||
|
return "/"
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(p, "/") {
|
||||||
|
p = "/" + p
|
||||||
|
}
|
||||||
|
return path.Clean(p)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,9 +26,9 @@ func TestResolveDistUpstream(t *testing.T) {
|
|||||||
|
|
||||||
func TestResolveMirrorDistUpstream(t *testing.T) {
|
func TestResolveMirrorDistUpstream(t *testing.T) {
|
||||||
resetComposerDistRegistry()
|
resetComposerDistRegistry()
|
||||||
registerComposerDist("cache.example", "vendor/pkg", "abc123", "zip", "https://github.com/org/repo.zip")
|
composerDists.remember("cache.example", "vendor/pkg", "abc123", "zip", "https://github.com/org/repo.zip")
|
||||||
ctx := &hooks.RequestContext{Domain: "cache.example"}
|
ctx := &hooks.RequestContext{Domain: "cache.example"}
|
||||||
url := resolveDistUpstream(ctx, "", "/composer/dists/vendor/pkg/abc123.zip", nil)
|
url := resolveDistUpstream(ctx, "", "/dists/vendor/pkg/abc123.zip", nil)
|
||||||
if url != "https://github.com/org/repo.zip" {
|
if url != "https://github.com/org/repo.zip" {
|
||||||
t.Fatalf("unexpected upstream %s", url)
|
t.Fatalf("unexpected upstream %s", url)
|
||||||
}
|
}
|
||||||
@@ -78,10 +78,10 @@ func TestRewritePackagesRoot(t *testing.T) {
|
|||||||
if err := json.Unmarshal(rewritten, &payload); err != nil {
|
if err := json.Unmarshal(rewritten, &payload); err != nil {
|
||||||
t.Fatalf("unmarshal: %v", err)
|
t.Fatalf("unmarshal: %v", err)
|
||||||
}
|
}
|
||||||
if payload["metadata-url"] != "https://cache.example/composer/p2/%package%.json" {
|
if payload["metadata-url"] != "https://cache.example/p2/%package%.json" {
|
||||||
t.Fatalf("metadata URL not rewritten: %v", payload["metadata-url"])
|
t.Fatalf("metadata URL not rewritten: %v", payload["metadata-url"])
|
||||||
}
|
}
|
||||||
if payload["providers-url"] != "https://cache.example/composer/p/%package%$%hash%.json" {
|
if payload["providers-url"] != "https://cache.example/p/%package%$%hash%.json" {
|
||||||
t.Fatalf("providers URL not rewritten: %v", payload["providers-url"])
|
t.Fatalf("providers URL not rewritten: %v", payload["providers-url"])
|
||||||
}
|
}
|
||||||
mirrors, _ := payload["mirrors"].([]any)
|
mirrors, _ := payload["mirrors"].([]any)
|
||||||
@@ -89,7 +89,7 @@ func TestRewritePackagesRoot(t *testing.T) {
|
|||||||
t.Fatalf("mirrors missing")
|
t.Fatalf("mirrors missing")
|
||||||
}
|
}
|
||||||
entry, _ := mirrors[0].(map[string]any)
|
entry, _ := mirrors[0].(map[string]any)
|
||||||
if entry["dist-url"] != "https://cache.example/composer/dists/%package%/%reference%.%type%" {
|
if entry["dist-url"] != "https://cache.example/dists/%package%/%reference%.%type%" {
|
||||||
t.Fatalf("unexpected mirror dist-url: %v", entry["dist-url"])
|
t.Fatalf("unexpected mirror dist-url: %v", entry["dist-url"])
|
||||||
}
|
}
|
||||||
if pref, _ := entry["preferred"].(bool); !pref {
|
if pref, _ := entry["preferred"].(bool); !pref {
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ func TestComposerProxyCachesMetadataAndDists(t *testing.T) {
|
|||||||
t.Fatalf("expected mirrors entry in packages root")
|
t.Fatalf("expected mirrors entry in packages root")
|
||||||
} else {
|
} else {
|
||||||
if entry, ok := mirrors[0].(map[string]any); ok {
|
if entry, ok := mirrors[0].(map[string]any); ok {
|
||||||
if distURL, _ := entry["dist-url"].(string); distURL != "https://composer.hub.local/composer/dists/%package%/%reference%.%type%" {
|
if distURL, _ := entry["dist-url"].(string); distURL != "https://composer.hub.local/dists/%package%/%reference%.%type%" {
|
||||||
t.Fatalf("unexpected mirrors dist-url: %s", distURL)
|
t.Fatalf("unexpected mirrors dist-url: %s", distURL)
|
||||||
}
|
}
|
||||||
if preferred, _ := entry["preferred"].(bool); !preferred {
|
if preferred, _ := entry["preferred"].(bool); !preferred {
|
||||||
@@ -109,7 +109,7 @@ func TestComposerProxyCachesMetadataAndDists(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
metaPath := "/composer/p2/example/package.json"
|
metaPath := "/p2/example/package.json"
|
||||||
resp := doRequest(metaPath)
|
resp := doRequest(metaPath)
|
||||||
if resp.StatusCode != fiber.StatusOK {
|
if resp.StatusCode != fiber.StatusOK {
|
||||||
t.Fatalf("expected 200 for composer metadata, got %d", resp.StatusCode)
|
t.Fatalf("expected 200 for composer metadata, got %d", resp.StatusCode)
|
||||||
|
|||||||
Reference in New Issue
Block a user