- Add proxy.go with upstreamCache + fetchUpstreamJSON - Proxy /packages.json, /p2/*, and /metadata/changes.json to wp-packages.org - Rewrite notify-batch and metadata-changes-url in packages.json - Refactor handleDetail to fetch version list from upstream p2 JSON - Remove obsolete composer.go and changes.go (local DB-backed handlers)
167 lines
4.9 KiB
Go
167 lines
4.9 KiB
Go
package http
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/roots/wp-packages/internal/app"
|
|
)
|
|
|
|
// upstreamCache holds cached upstream JSON responses.
|
|
type upstreamCache struct {
|
|
mu sync.RWMutex
|
|
entries map[string]cacheEntry
|
|
}
|
|
|
|
type cacheEntry struct {
|
|
data []byte
|
|
expiresAt time.Time
|
|
}
|
|
|
|
var p2Cache = &upstreamCache{entries: make(map[string]cacheEntry)}
|
|
|
|
func (c *upstreamCache) get(key string) ([]byte, bool) {
|
|
c.mu.RLock()
|
|
ent, ok := c.entries[key]
|
|
c.mu.RUnlock()
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
if time.Now().After(ent.expiresAt) {
|
|
c.mu.Lock()
|
|
delete(c.entries, key)
|
|
c.mu.Unlock()
|
|
return nil, false
|
|
}
|
|
return ent.data, true
|
|
}
|
|
|
|
func (c *upstreamCache) set(key string, data []byte, ttl time.Duration) {
|
|
c.mu.Lock()
|
|
c.entries[key] = cacheEntry{data: data, expiresAt: time.Now().Add(ttl)}
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
func fetchUpstreamJSON(url string, maxAge time.Duration) ([]byte, error) {
|
|
if data, ok := p2Cache.get(url); ok {
|
|
return data, nil
|
|
}
|
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("User-Agent", "packages.wenpai.net/1.0 (mirror)")
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode >= 400 {
|
|
return nil, http.ErrNoLocation
|
|
}
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
p2Cache.set(url, body, maxAge)
|
|
return body, nil
|
|
}
|
|
|
|
func rewritePackagesJSON(body []byte, appURL string) []byte {
|
|
s := string(body)
|
|
s = strings.ReplaceAll(s, "\"notify-batch\": \"https://wp-packages.org/downloads\"", "\"notify-batch\": \""+appURL+"/downloads\"")
|
|
s = strings.ReplaceAll(s, "\"notify-batch\":\"https://wp-packages.org/downloads\"", "\"notify-batch\":\""+appURL+"/downloads\"")
|
|
s = strings.ReplaceAll(s, "\"metadata-changes-url\": \"https://wp-packages.org/metadata/changes.json\"", "\"metadata-changes-url\": \""+appURL+"/metadata/changes.json\"")
|
|
s = strings.ReplaceAll(s, "\"metadata-changes-url\":\"https://wp-packages.org/metadata/changes.json\"", "\"metadata-changes-url\":\""+appURL+"/metadata/changes.json\"")
|
|
return []byte(s)
|
|
}
|
|
|
|
// handlePackagesJSON serves the root Composer repository descriptor by proxying upstream.
|
|
func handlePackagesJSON(a *app.App) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
body, err := fetchUpstreamJSON("https://wp-packages.org/packages.json", 5*time.Minute)
|
|
if err != nil {
|
|
a.Logger.Error("proxying packages.json", "error", err)
|
|
http.Error(w, "upstream unavailable", http.StatusBadGateway)
|
|
return
|
|
}
|
|
body = rewritePackagesJSON(body, a.Config.AppURL)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Header().Set("Cache-Control", "public, max-age=300")
|
|
_, _ = w.Write(body)
|
|
}
|
|
}
|
|
|
|
// handleP2Package serves Composer p2 metadata by proxying upstream.
|
|
func handleP2Package(a *app.App) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
vendor := r.PathValue("vendor")
|
|
file := r.PathValue("file")
|
|
|
|
if file == "" || !strings.HasSuffix(file, ".json") {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
upstreamURL := "https://wp-packages.org/p2/" + vendor + "/" + file
|
|
body, err := fetchUpstreamJSON(upstreamURL, 15*time.Minute)
|
|
if err != nil {
|
|
// If upstream 404, pass it through
|
|
a.Logger.Error("proxying p2", "url", upstreamURL, "error", err)
|
|
http.Error(w, "upstream unavailable", http.StatusBadGateway)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Header().Set("Cache-Control", "public, max-age=900")
|
|
_, _ = w.Write(body)
|
|
}
|
|
}
|
|
|
|
// fetchUpstreamVersions returns the version strings for a package by requesting upstream p2 metadata.
|
|
func fetchUpstreamVersions(pkgType, name string) ([]string, error) {
|
|
url := "https://wp-packages.org/p2/wp-" + pkgType + "/" + name + ".json"
|
|
body, err := fetchUpstreamJSON(url, 15*time.Minute)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var resp struct {
|
|
Packages map[string]map[string]json.RawMessage `json:"packages"`
|
|
}
|
|
if err := json.Unmarshal(body, &resp); err != nil {
|
|
return nil, err
|
|
}
|
|
pkgKey := "wp-" + pkgType + "/" + name
|
|
versions := make([]string, 0, len(resp.Packages[pkgKey]))
|
|
for v := range resp.Packages[pkgKey] {
|
|
versions = append(versions, v)
|
|
}
|
|
return versions, nil
|
|
}
|
|
|
|
// handleMetadataChanges proxies the Composer v2 metadata changes endpoint.
|
|
func handleMetadataChanges(a *app.App) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
query := r.URL.RawQuery
|
|
upstreamURL := "https://wp-packages.org/metadata/changes.json"
|
|
if query != "" {
|
|
upstreamURL += "?" + query
|
|
}
|
|
body, err := fetchUpstreamJSON(upstreamURL, 5*time.Minute)
|
|
if err != nil {
|
|
a.Logger.Error("proxying metadata changes", "error", err)
|
|
http.Error(w, "upstream unavailable", http.StatusBadGateway)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Header().Set("Cache-Control", "public, max-age=300")
|
|
_, _ = w.Write(body)
|
|
}
|
|
}
|