Compare commits
2 Commits
v0.0.8
...
bbc7204901
| Author | SHA1 | Date | |
|---|---|---|---|
| bbc7204901 | |||
| 40c6f2fcce |
@@ -21,7 +21,6 @@ Type = "docker"
|
||||
Username = ""
|
||||
Password = ""
|
||||
CacheTTL = 43200
|
||||
EnableHeadCheck = true
|
||||
|
||||
[[Hub]]
|
||||
Name = "composer-cache"
|
||||
|
||||
@@ -19,4 +19,3 @@ Module = "legacy"
|
||||
Username = ""
|
||||
Password = ""
|
||||
CacheTTL = 43200
|
||||
EnableHeadCheck = true
|
||||
|
||||
@@ -19,4 +19,3 @@ Module = "legacy"
|
||||
Username = ""
|
||||
Password = ""
|
||||
CacheTTL = 43200
|
||||
EnableHeadCheck = false
|
||||
|
||||
@@ -64,16 +64,15 @@ type GlobalConfig struct {
|
||||
|
||||
// HubConfig 决定单个代理实例如何与下游/上游交互。
|
||||
type HubConfig struct {
|
||||
Name string `mapstructure:"Name"`
|
||||
Domain string `mapstructure:"Domain"`
|
||||
Upstream string `mapstructure:"Upstream"`
|
||||
Proxy string `mapstructure:"Proxy"`
|
||||
Type string `mapstructure:"Type"`
|
||||
Username string `mapstructure:"Username"`
|
||||
Password string `mapstructure:"Password"`
|
||||
CacheTTL Duration `mapstructure:"CacheTTL"`
|
||||
ValidationMode string `mapstructure:"ValidationMode"`
|
||||
EnableHeadCheck bool `mapstructure:"EnableHeadCheck"`
|
||||
Name string `mapstructure:"Name"`
|
||||
Domain string `mapstructure:"Domain"`
|
||||
Upstream string `mapstructure:"Upstream"`
|
||||
Proxy string `mapstructure:"Proxy"`
|
||||
Type string `mapstructure:"Type"`
|
||||
Username string `mapstructure:"Username"`
|
||||
Password string `mapstructure:"Password"`
|
||||
CacheTTL Duration `mapstructure:"CacheTTL"`
|
||||
ValidationMode string `mapstructure:"ValidationMode"`
|
||||
}
|
||||
|
||||
// Config 是 TOML 文件映射的整体结构。
|
||||
@@ -109,11 +108,12 @@ func CredentialModes(hubs []HubConfig) []string {
|
||||
|
||||
// StrategyOverrides 将 hub 层的 TTL/Validation 配置映射为模块策略覆盖项。
|
||||
func (h HubConfig) StrategyOverrides(ttl time.Duration) hubmodule.StrategyOptions {
|
||||
opts := hubmodule.StrategyOptions{
|
||||
TTLOverride: ttl,
|
||||
}
|
||||
opts := hubmodule.StrategyOptions{}
|
||||
if mode := strings.TrimSpace(h.ValidationMode); mode != "" {
|
||||
opts.ValidationOverride = hubmodule.ValidationMode(mode)
|
||||
}
|
||||
if h.CacheTTL.DurationValue() > 0 {
|
||||
opts.TTLOverride = ttl
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
@@ -37,7 +37,14 @@ func cachePolicy(_ *hooks.RequestContext, locatorPath string, current hooks.Cach
|
||||
// 索引类(Release/Packages/Contents)需要 If-None-Match/If-Modified-Since 再验证。
|
||||
if strings.HasSuffix(clean, "/release") ||
|
||||
strings.HasSuffix(clean, "/inrelease") ||
|
||||
strings.HasSuffix(clean, "/release.gpg") {
|
||||
strings.HasSuffix(clean, "/release.gpg") ||
|
||||
strings.HasSuffix(clean, "/packages") ||
|
||||
strings.HasSuffix(clean, "/packages.gz") ||
|
||||
strings.HasSuffix(clean, "/packages.xz") ||
|
||||
strings.HasSuffix(clean, "/sources") ||
|
||||
strings.HasSuffix(clean, "/sources.gz") ||
|
||||
strings.HasSuffix(clean, "/sources.xz") ||
|
||||
strings.Contains(clean, "/contents-") {
|
||||
current.AllowCache = true
|
||||
current.AllowStore = true
|
||||
current.RequireRevalidate = true
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/any-hub/any-hub/internal/proxy/hooks"
|
||||
@@ -15,6 +16,9 @@ func init() {
|
||||
}
|
||||
|
||||
func normalizePath(ctx *hooks.RequestContext, clean string, rawQuery []byte) (string, []byte) {
|
||||
if ctx == nil || !isDockerHubHost(ctx.UpstreamHost) {
|
||||
return clean, rawQuery
|
||||
}
|
||||
repo, rest, ok := splitDockerRepoPath(clean)
|
||||
if !ok || repo == "" || strings.Contains(repo, "/") || repo == "library" {
|
||||
return clean, rawQuery
|
||||
@@ -54,6 +58,9 @@ func contentType(_ *hooks.RequestContext, locatorPath string) string {
|
||||
}
|
||||
|
||||
func isDockerHubHost(host string) bool {
|
||||
if parsedHost, _, err := net.SplitHostPort(host); err == nil {
|
||||
host = parsedHost
|
||||
}
|
||||
switch strings.ToLower(host) {
|
||||
case "registry-1.docker.io", "docker.io", "index.docker.io":
|
||||
return true
|
||||
|
||||
@@ -19,6 +19,14 @@ func TestNormalizePathAddsLibraryForDockerHub(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizePathSkipsLibraryForNonDockerHub(t *testing.T) {
|
||||
ctx := &hooks.RequestContext{UpstreamHost: "registry.k8s.io"}
|
||||
path, _ := normalizePath(ctx, "/v2/kube-apiserver/manifests/v1.35.3", nil)
|
||||
if path != "/v2/kube-apiserver/manifests/v1.35.3" {
|
||||
t.Fatalf("expected non-docker hub path to remain unchanged, got %s", path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitDockerRepoPath(t *testing.T) {
|
||||
repo, rest, ok := splitDockerRepoPath("/v2/library/nginx/manifests/latest")
|
||||
if !ok || repo != "library/nginx" || rest != "/manifests/latest" {
|
||||
|
||||
@@ -11,13 +11,14 @@ type StrategyOptions struct {
|
||||
// ResolveStrategy 将模块的默认策略与 hub 级覆盖合并。
|
||||
func ResolveStrategy(meta ModuleMetadata, opts StrategyOptions) CacheStrategyProfile {
|
||||
strategy := meta.CacheStrategy
|
||||
if strategy.TTLHint > 0 && opts.TTLOverride > 0 {
|
||||
strategy.TTLHint = opts.TTLOverride
|
||||
}
|
||||
if opts.ValidationOverride != "" {
|
||||
strategy.ValidationMode = opts.ValidationOverride
|
||||
}
|
||||
return normalizeStrategy(strategy)
|
||||
strategy = normalizeStrategy(strategy)
|
||||
if opts.TTLOverride > 0 {
|
||||
strategy.TTLHint = opts.TTLOverride
|
||||
}
|
||||
return strategy
|
||||
}
|
||||
|
||||
func normalizeStrategy(profile CacheStrategyProfile) CacheStrategyProfile {
|
||||
|
||||
@@ -3,7 +3,7 @@ package proxy
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
@@ -202,10 +202,25 @@ func (h *Handler) serveCache(
|
||||
c.Status(status)
|
||||
|
||||
if method == http.MethodHead {
|
||||
// 对 HEAD 请求仍向上游发起一次 HEAD,以满足“显式请求 + 再验证”的期望。
|
||||
if route != nil && route.UpstreamURL != nil {
|
||||
if resp, err := h.revalidateRequest(c, route, resolveUpstreamURL(route, route.UpstreamURL, c, hook), result.Entry.Locator, ""); err == nil {
|
||||
resp.Body.Close()
|
||||
shouldRevalidate := true
|
||||
if hook != nil && hook.hasHooks && hook.def.CachePolicy != nil {
|
||||
policy := hook.def.CachePolicy(hook.ctx, result.Entry.Locator.Path, hooks.CachePolicy{
|
||||
AllowCache: true,
|
||||
AllowStore: true,
|
||||
RequireRevalidate: true,
|
||||
})
|
||||
if !policy.AllowCache || !policy.AllowStore {
|
||||
shouldRevalidate = false
|
||||
} else {
|
||||
shouldRevalidate = policy.RequireRevalidate
|
||||
}
|
||||
}
|
||||
|
||||
if shouldRevalidate {
|
||||
if resp, err := h.revalidateRequest(c, route, resolveUpstreamURL(route, route.UpstreamURL, c, hook), result.Entry.Locator, ""); err == nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
result.Reader.Close()
|
||||
@@ -571,7 +586,7 @@ func resolveContentType(route *server.HubRoute, locator cache.Locator, hook *hoo
|
||||
func buildLocator(route *server.HubRoute, c fiber.Ctx, clean string, rawQuery []byte) cache.Locator {
|
||||
query := rawQuery
|
||||
if len(query) > 0 {
|
||||
sum := sha1.Sum(query)
|
||||
sum := sha256.Sum256(query)
|
||||
clean = fmt.Sprintf("%s/__qs/%s", clean, hex.EncodeToString(sum[:]))
|
||||
}
|
||||
loc := cache.Locator{
|
||||
@@ -791,7 +806,7 @@ func (h *Handler) isCacheFresh(
|
||||
return true, nil
|
||||
case http.StatusOK:
|
||||
if resp.Header.Get("Etag") == "" && resp.Header.Get("Docker-Content-Digest") == "" && resp.Header.Get("Last-Modified") == "" {
|
||||
return true, nil
|
||||
return false, nil
|
||||
}
|
||||
h.rememberETag(route, locator, resp)
|
||||
remote := extractModTime(resp.Header)
|
||||
|
||||
@@ -15,11 +15,10 @@ func TestHubRegistryLookupByHost(t *testing.T) {
|
||||
},
|
||||
Hubs: []config.HubConfig{
|
||||
{
|
||||
Name: "docker",
|
||||
Domain: "docker.hub.local",
|
||||
Type: "docker",
|
||||
Upstream: "https://registry-1.docker.io",
|
||||
EnableHeadCheck: true,
|
||||
Name: "docker",
|
||||
Domain: "docker.hub.local",
|
||||
Type: "docker",
|
||||
Upstream: "https://registry-1.docker.io",
|
||||
},
|
||||
{
|
||||
Name: "npm",
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
- `Upstream` (string, required, http/https URL)
|
||||
- `Proxy` (string, optional, URL)
|
||||
- `CacheTTL` (duration, optional, overrides global)
|
||||
- `EnableHeadCheck` (bool, optional, default true)
|
||||
- **Validation Rules**: `Name` 必须唯一;`Domain` + `Port` 组合不得冲突;URL 必须可解析。
|
||||
- **Relationships**: 属于 `Config`,在运行时用于初始化路由、缓存目录 `StoragePath/<Name>`。
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
### HubRoute
|
||||
- **Description**: Host/端口到上游仓库的映射,供 Fiber 路由和 Proxy handler 使用。
|
||||
- **Fields**: `Name` (string, unique), `Domain` (string, FQDN), `Port` (int, 1-65535), `Upstream` (URL), `Proxy` (URL, optional), `CacheTTL` (duration override), `EnableHeadCheck` (bool).
|
||||
- **Fields**: `Name` (string, unique), `Domain` (string, FQDN), `Port` (int, 1-65535), `Upstream` (URL), `Proxy` (URL, optional), `CacheTTL` (duration override).
|
||||
- **Validation**: Name 唯一;Domain 不含协议/路径;Upstream 必须 http/https。
|
||||
- **Relationships**: 由 config 加载到 `HubRegistry`;与 CacheEntry、ProxyRequest 通过 `Name` 关联。
|
||||
|
||||
|
||||
@@ -222,8 +222,8 @@ func TestDockerManifestHeadDoesNotOverwriteCache(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDockerNamespaceFallbackAddsLibrary(t *testing.T) {
|
||||
stub := newCacheFlowStub(t, dockerManifestPath)
|
||||
func TestDockerNonDockerHubUpstreamKeepsOriginalPath(t *testing.T) {
|
||||
stub := newCacheFlowStub(t, dockerManifestNoNamespacePath)
|
||||
defer stub.Close()
|
||||
|
||||
storageDir := t.TempDir()
|
||||
@@ -277,7 +277,7 @@ func TestDockerNamespaceFallbackAddsLibrary(t *testing.T) {
|
||||
}
|
||||
if resp.StatusCode != fiber.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
t.Fatalf("expected 200 when fallback applies, got %d (body=%s)", resp.StatusCode, string(body))
|
||||
t.Fatalf("expected 200 when upstream keeps original path, got %d (body=%s)", resp.StatusCode, string(body))
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
|
||||
@@ -68,8 +68,8 @@ func TestCacheStrategyOverrides(t *testing.T) {
|
||||
}
|
||||
resp2.Body.Close()
|
||||
|
||||
if headCount := countRequests(stub.Requests(), http.MethodHead, "/lodash"); headCount != 0 {
|
||||
t.Fatalf("expected no HEAD before TTL expiry, got %d", headCount)
|
||||
if headCount := countRequests(stub.Requests(), http.MethodHead, "/lodash"); headCount != 1 {
|
||||
t.Fatalf("expected single HEAD before TTL expiry, got %d", headCount)
|
||||
}
|
||||
if getCount := countRequests(stub.Requests(), http.MethodGet, "/lodash"); getCount != 1 {
|
||||
t.Fatalf("upstream should be hit once before TTL expiry, got %d", getCount)
|
||||
@@ -85,8 +85,8 @@ func TestCacheStrategyOverrides(t *testing.T) {
|
||||
}
|
||||
resp3.Body.Close()
|
||||
|
||||
if headCount := countRequests(stub.Requests(), http.MethodHead, "/lodash"); headCount != 1 {
|
||||
t.Fatalf("expected single HEAD after TTL expiry, got %d", headCount)
|
||||
if headCount := countRequests(stub.Requests(), http.MethodHead, "/lodash"); headCount != 2 {
|
||||
t.Fatalf("expected two HEAD requests after TTL expiry, got %d", headCount)
|
||||
}
|
||||
if getCount := countRequests(stub.Requests(), http.MethodGet, "/lodash"); getCount != 1 {
|
||||
t.Fatalf("upstream GET count should remain 1, got %d", getCount)
|
||||
|
||||
@@ -156,6 +156,7 @@ func registerDockerHandlers(mux *http.ServeMux, blob []byte) {
|
||||
func registerNPMHandlers(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/lodash", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat))
|
||||
resp := map[string]any{
|
||||
"name": "lodash",
|
||||
"dist-tags": map[string]string{
|
||||
@@ -174,6 +175,7 @@ func registerNPMHandlers(mux *http.ServeMux) {
|
||||
|
||||
mux.HandleFunc("/lodash/-/lodash-4.17.21.tgz", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
w.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat))
|
||||
_, _ = w.Write([]byte("tarball-bytes"))
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user