update
This commit is contained in:
333
internal/hubmodule/composer/hooks.go
Normal file
333
internal/hubmodule/composer/hooks.go
Normal file
@@ -0,0 +1,333 @@
|
||||
package composer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/any-hub/any-hub/internal/proxy/hooks"
|
||||
)
|
||||
|
||||
func init() {
|
||||
hooks.MustRegister("composer", hooks.Hooks{
|
||||
NormalizePath: normalizePath,
|
||||
ResolveUpstream: resolveDistUpstream,
|
||||
RewriteResponse: rewriteResponse,
|
||||
CachePolicy: cachePolicy,
|
||||
ContentType: contentType,
|
||||
})
|
||||
}
|
||||
|
||||
func normalizePath(_ *hooks.RequestContext, clean string, rawQuery []byte) (string, []byte) {
|
||||
if isComposerDistPath(clean) {
|
||||
return clean, nil
|
||||
}
|
||||
return clean, rawQuery
|
||||
}
|
||||
|
||||
func resolveDistUpstream(_ *hooks.RequestContext, _ string, clean string, rawQuery []byte) string {
|
||||
if !isComposerDistPath(clean) {
|
||||
return ""
|
||||
}
|
||||
target, ok := parseComposerDistURL(clean, string(rawQuery))
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return target.String()
|
||||
}
|
||||
|
||||
func rewriteResponse(
|
||||
ctx *hooks.RequestContext,
|
||||
status int,
|
||||
headers map[string]string,
|
||||
body []byte,
|
||||
path string,
|
||||
) (int, map[string]string, []byte, error) {
|
||||
switch {
|
||||
case path == "/packages.json":
|
||||
data, changed, err := rewriteComposerRootBody(body, ctx.Domain)
|
||||
if err != nil {
|
||||
return status, headers, body, err
|
||||
}
|
||||
if !changed {
|
||||
return status, headers, body, nil
|
||||
}
|
||||
outHeaders := ensureJSONHeaders(headers)
|
||||
return status, outHeaders, data, nil
|
||||
case isComposerMetadataPath(path):
|
||||
data, changed, err := rewriteComposerMetadata(body, ctx.Domain)
|
||||
if err != nil {
|
||||
return status, headers, body, err
|
||||
}
|
||||
if !changed {
|
||||
return status, headers, body, nil
|
||||
}
|
||||
outHeaders := ensureJSONHeaders(headers)
|
||||
return status, outHeaders, data, nil
|
||||
default:
|
||||
return status, headers, body, nil
|
||||
}
|
||||
}
|
||||
|
||||
func ensureJSONHeaders(headers map[string]string) map[string]string {
|
||||
if headers == nil {
|
||||
headers = map[string]string{}
|
||||
}
|
||||
headers["Content-Type"] = "application/json"
|
||||
delete(headers, "Content-Encoding")
|
||||
delete(headers, "Etag")
|
||||
return headers
|
||||
}
|
||||
|
||||
func cachePolicy(_ *hooks.RequestContext, locatorPath string, current hooks.CachePolicy) hooks.CachePolicy {
|
||||
switch {
|
||||
case isComposerDistPath(locatorPath):
|
||||
current.AllowCache = true
|
||||
current.AllowStore = true
|
||||
current.RequireRevalidate = false
|
||||
case isComposerMetadataPath(locatorPath):
|
||||
current.AllowCache = true
|
||||
current.AllowStore = true
|
||||
current.RequireRevalidate = true
|
||||
default:
|
||||
current.AllowCache = false
|
||||
current.AllowStore = false
|
||||
current.RequireRevalidate = false
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
func contentType(_ *hooks.RequestContext, locatorPath string) string {
|
||||
if isComposerMetadataPath(locatorPath) {
|
||||
return "application/json"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func rewriteComposerRootBody(body []byte, domain string) ([]byte, bool, error) {
|
||||
type root struct {
|
||||
Packages map[string]string `json:"packages"`
|
||||
}
|
||||
var payload root
|
||||
if err := json.Unmarshal(body, &payload); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if len(payload.Packages) == 0 {
|
||||
return body, false, nil
|
||||
}
|
||||
changed := false
|
||||
for key, value := range payload.Packages {
|
||||
rewritten := rewriteComposerAbsolute(domain, value)
|
||||
if rewritten != value {
|
||||
payload.Packages[key] = rewritten
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
return body, false, nil
|
||||
}
|
||||
data, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return data, true, nil
|
||||
}
|
||||
|
||||
func rewriteComposerMetadata(body []byte, domain string) ([]byte, bool, error) {
|
||||
type packagesRoot struct {
|
||||
Packages map[string]json.RawMessage `json:"packages"`
|
||||
}
|
||||
var root packagesRoot
|
||||
if err := json.Unmarshal(body, &root); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if len(root.Packages) == 0 {
|
||||
return body, false, nil
|
||||
}
|
||||
|
||||
changed := false
|
||||
for name, raw := range root.Packages {
|
||||
updated, rewritten, err := rewriteComposerPackagesPayload(raw, domain, name)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if rewritten {
|
||||
root.Packages[name] = updated
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
return body, false, nil
|
||||
}
|
||||
data, err := json.Marshal(root)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return data, true, nil
|
||||
}
|
||||
|
||||
func rewriteComposerPackagesPayload(raw json.RawMessage, domain string, packageName string) (json.RawMessage, bool, error) {
|
||||
var asArray []map[string]any
|
||||
if err := json.Unmarshal(raw, &asArray); err == nil {
|
||||
rewrote := rewriteComposerVersionSlice(asArray, domain, packageName)
|
||||
if !rewrote {
|
||||
return raw, false, nil
|
||||
}
|
||||
data, err := json.Marshal(asArray)
|
||||
return data, true, err
|
||||
}
|
||||
|
||||
var asMap map[string]map[string]any
|
||||
if err := json.Unmarshal(raw, &asMap); err == nil {
|
||||
rewrote := rewriteComposerVersionMap(asMap, domain, packageName)
|
||||
if !rewrote {
|
||||
return raw, false, nil
|
||||
}
|
||||
data, err := json.Marshal(asMap)
|
||||
return data, true, err
|
||||
}
|
||||
|
||||
return raw, false, nil
|
||||
}
|
||||
|
||||
func rewriteComposerVersionSlice(items []map[string]any, domain string, packageName string) bool {
|
||||
changed := false
|
||||
for _, entry := range items {
|
||||
if rewriteComposerVersion(entry, domain, packageName) {
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
func rewriteComposerVersionMap(items map[string]map[string]any, domain string, packageName string) bool {
|
||||
changed := false
|
||||
for _, entry := range items {
|
||||
if rewriteComposerVersion(entry, domain, packageName) {
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
func rewriteComposerVersion(entry map[string]any, domain string, packageName string) bool {
|
||||
if entry == nil {
|
||||
return false
|
||||
}
|
||||
changed := false
|
||||
if packageName != "" {
|
||||
if name, _ := entry["name"].(string); strings.TrimSpace(name) == "" {
|
||||
entry["name"] = packageName
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
distVal, ok := entry["dist"].(map[string]any)
|
||||
if !ok {
|
||||
return changed
|
||||
}
|
||||
urlValue, ok := distVal["url"].(string)
|
||||
if !ok || urlValue == "" {
|
||||
return changed
|
||||
}
|
||||
rewritten := rewriteComposerDistURL(domain, urlValue)
|
||||
if rewritten == urlValue {
|
||||
return changed
|
||||
}
|
||||
distVal["url"] = rewritten
|
||||
return true
|
||||
}
|
||||
|
||||
func rewriteComposerDistURL(domain, original string) string {
|
||||
parsed, err := url.Parse(original)
|
||||
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
||||
return original
|
||||
}
|
||||
prefix := "/dist/" + parsed.Scheme + "/" + parsed.Host
|
||||
newURL := url.URL{
|
||||
Scheme: "https",
|
||||
Host: domain,
|
||||
Path: prefix + parsed.Path,
|
||||
RawQuery: parsed.RawQuery,
|
||||
Fragment: parsed.Fragment,
|
||||
}
|
||||
if raw := parsed.RawPath; raw != "" {
|
||||
newURL.RawPath = prefix + raw
|
||||
}
|
||||
return newURL.String()
|
||||
}
|
||||
|
||||
func rewriteComposerAbsolute(domain, raw string) string {
|
||||
if raw == "" {
|
||||
return raw
|
||||
}
|
||||
if strings.HasPrefix(raw, "//") {
|
||||
return "https://" + domain + strings.TrimPrefix(raw, "//")
|
||||
}
|
||||
if strings.HasPrefix(raw, "http://") || strings.HasPrefix(raw, "https://") {
|
||||
parsed, err := url.Parse(raw)
|
||||
if err != nil {
|
||||
return raw
|
||||
}
|
||||
parsed.Host = domain
|
||||
parsed.Scheme = "https"
|
||||
return parsed.String()
|
||||
}
|
||||
pathVal := raw
|
||||
if !strings.HasPrefix(pathVal, "/") {
|
||||
pathVal = "/" + pathVal
|
||||
}
|
||||
return "https://" + domain + pathVal
|
||||
}
|
||||
|
||||
func isComposerMetadataPath(path string) bool {
|
||||
switch {
|
||||
case path == "/packages.json":
|
||||
return true
|
||||
case strings.HasPrefix(path, "/p2/"):
|
||||
return true
|
||||
case strings.HasPrefix(path, "/p/"):
|
||||
return true
|
||||
case strings.HasPrefix(path, "/provider-"):
|
||||
return true
|
||||
case strings.HasPrefix(path, "/providers/"):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isComposerDistPath(path string) bool {
|
||||
return strings.HasPrefix(path, "/dist/")
|
||||
}
|
||||
|
||||
func parseComposerDistURL(path string, rawQuery string) (*url.URL, bool) {
|
||||
if !strings.HasPrefix(path, "/dist/") {
|
||||
return nil, false
|
||||
}
|
||||
trimmed := strings.TrimPrefix(path, "/dist/")
|
||||
parts := strings.SplitN(trimmed, "/", 3)
|
||||
if len(parts) < 3 {
|
||||
return nil, false
|
||||
}
|
||||
scheme := parts[0]
|
||||
host := parts[1]
|
||||
rest := parts[2]
|
||||
if scheme == "" || host == "" {
|
||||
return nil, false
|
||||
}
|
||||
if rest == "" {
|
||||
rest = "/"
|
||||
} else {
|
||||
rest = "/" + rest
|
||||
}
|
||||
target := &url.URL{
|
||||
Scheme: scheme,
|
||||
Host: host,
|
||||
Path: rest,
|
||||
RawPath: rest,
|
||||
}
|
||||
if rawQuery != "" {
|
||||
target.RawQuery = rawQuery
|
||||
}
|
||||
return target, true
|
||||
}
|
||||
43
internal/hubmodule/composer/hooks_test.go
Normal file
43
internal/hubmodule/composer/hooks_test.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package composer
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/any-hub/any-hub/internal/proxy/hooks"
|
||||
)
|
||||
|
||||
func TestNormalizePathDropsDistQuery(t *testing.T) {
|
||||
path, raw := normalizePath(nil, "/dist/https/example.com/file.zip", []byte("token=1"))
|
||||
if raw != nil {
|
||||
t.Fatalf("expected query to be dropped")
|
||||
}
|
||||
if path != "/dist/https/example.com/file.zip" {
|
||||
t.Fatalf("unexpected path %s", path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveDistUpstream(t *testing.T) {
|
||||
url := resolveDistUpstream(nil, "", "/dist/https/example.com/file.zip", []byte("token=1"))
|
||||
if url != "https://example.com/file.zip?token=1" {
|
||||
t.Fatalf("unexpected upstream %s", url)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewriteResponseUpdatesURLs(t *testing.T) {
|
||||
ctx := &hooks.RequestContext{Domain: "cache.example"}
|
||||
body := []byte(`{"packages":{"a/b":{"1.0.0":{"dist":{"url":"https://pkg.example/dist.zip"}}}}}`)
|
||||
_, headers, rewritten, err := rewriteResponse(ctx, 200, map[string]string{}, body, "/p2/a/b.json")
|
||||
if err != nil {
|
||||
t.Fatalf("rewrite failed: %v", err)
|
||||
}
|
||||
if string(rewritten) == string(body) {
|
||||
t.Fatalf("expected rewrite to modify payload")
|
||||
}
|
||||
if headers["Content-Type"] != "application/json" {
|
||||
t.Fatalf("expected json content type")
|
||||
}
|
||||
if !strings.Contains(string(rewritten), "https://cache.example/dist/https/pkg.example/dist.zip") {
|
||||
t.Fatalf("expected rewritten URL, got %s", string(rewritten))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user