mirror of
https://github.com/sky22333/hubproxy.git
synced 2026-08-28 19:46:44 +08:00
降低日志IO + 匿名manifest缓存
This commit is contained in:
+64
-4
@@ -21,10 +21,11 @@ type registryTarget struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
dockerHubName = "docker.io"
|
dockerHubName = "docker.io"
|
||||||
dockerHubUpstream = "https://registry-1.docker.io"
|
dockerHubUpstream = "https://registry-1.docker.io"
|
||||||
dockerHubAuthRealm = "https://auth.docker.io/token"
|
dockerHubAuthRealm = "https://auth.docker.io/token"
|
||||||
dockerHubAuthService = "registry.docker.io"
|
dockerHubAuthService = "registry.docker.io"
|
||||||
|
maxCachedManifestSize = 4 << 20
|
||||||
)
|
)
|
||||||
|
|
||||||
var hopByHopHeaders = map[string]struct{}{
|
var hopByHopHeaders = map[string]struct{}{
|
||||||
@@ -349,6 +350,13 @@ func addLibraryPrefixToScope(scope string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func proxyRegistryHTTP(c *gin.Context, target registryTarget, upstreamPath string) {
|
func proxyRegistryHTTP(c *gin.Context, target registryTarget, upstreamPath string) {
|
||||||
|
if cacheKey, ok := manifestCacheKey(c, target, upstreamPath); ok {
|
||||||
|
if cachedItem := utils.GlobalCache.Get(cacheKey); cachedItem != nil {
|
||||||
|
utils.WriteCachedResponse(c, cachedItem)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
targetURL := target.Upstream + upstreamPath
|
targetURL := target.Upstream + upstreamPath
|
||||||
if c.Request.URL.RawQuery != "" {
|
if c.Request.URL.RawQuery != "" {
|
||||||
targetURL += "?" + c.Request.URL.RawQuery
|
targetURL += "?" + c.Request.URL.RawQuery
|
||||||
@@ -374,11 +382,63 @@ func proxyRegistryHTTP(c *gin.Context, target registryTarget, upstreamPath strin
|
|||||||
if c.Request.Method == http.MethodHead {
|
if c.Request.Method == http.MethodHead {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cacheKey, ok := manifestCacheKey(c, target, upstreamPath); ok && canCacheManifestResponse(resp) {
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Failed to read manifest response: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
contentType := resp.Header.Get("Content-Type")
|
||||||
|
utils.GlobalCache.Set(cacheKey, body, contentType, cacheHeaders(resp.Header), utils.GetManifestTTL(manifestReference(upstreamPath)))
|
||||||
|
c.Data(resp.StatusCode, contentType, body)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if _, err := io.Copy(c.Writer, resp.Body); err != nil {
|
if _, err := io.Copy(c.Writer, resp.Body); err != nil {
|
||||||
fmt.Printf("Failed to stream registry response: %v\n", err)
|
fmt.Printf("Failed to stream registry response: %v\n", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func manifestCacheKey(c *gin.Context, target registryTarget, upstreamPath string) (string, bool) {
|
||||||
|
if c.Request.Method != http.MethodGet || c.GetHeader("Authorization") != "" || !utils.IsCacheEnabled() {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
if !strings.Contains(upstreamPath, "/manifests/") {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
key := strings.Join([]string{
|
||||||
|
target.Name,
|
||||||
|
upstreamPath,
|
||||||
|
c.Request.URL.RawQuery,
|
||||||
|
strings.Join(c.Request.Header.Values("Accept"), ","),
|
||||||
|
}, "|")
|
||||||
|
return utils.BuildCacheKey("manifest", key), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func canCacheManifestResponse(resp *http.Response) bool {
|
||||||
|
return resp.StatusCode == http.StatusOK &&
|
||||||
|
resp.ContentLength > 0 &&
|
||||||
|
resp.ContentLength <= maxCachedManifestSize
|
||||||
|
}
|
||||||
|
|
||||||
|
func manifestReference(upstreamPath string) string {
|
||||||
|
_, _, reference := parseRegistryPath(strings.TrimPrefix(upstreamPath, "/v2/"))
|
||||||
|
return reference
|
||||||
|
}
|
||||||
|
|
||||||
|
func cacheHeaders(headers http.Header) map[string]string {
|
||||||
|
cached := make(map[string]string)
|
||||||
|
for name, values := range headers {
|
||||||
|
if shouldSkipResponseHeader(name) || strings.EqualFold(name, "WWW-Authenticate") || len(values) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cached[name] = values[0]
|
||||||
|
}
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
|
||||||
func forwardSelectedRequestHeaders(dst http.Header, src http.Header) {
|
func forwardSelectedRequestHeaders(dst http.Header, src http.Header) {
|
||||||
for _, name := range forwardedRequestHeaders {
|
for _, name := range forwardedRequestHeaders {
|
||||||
for _, value := range src.Values(name) {
|
for _, value := range src.Values(name) {
|
||||||
|
|||||||
@@ -528,6 +528,99 @@ enabled = true
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestProxyDockerRegistryCachesAnonymousManifestByAccept(t *testing.T) {
|
||||||
|
var hits int32
|
||||||
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
count := atomic.AddInt32(&hits, 1)
|
||||||
|
body := fmt.Sprintf(`{"schemaVersion":2,"hit":%d}`, count)
|
||||||
|
w.Header().Set("Content-Type", r.Header.Get("Accept"))
|
||||||
|
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(body)))
|
||||||
|
_, _ = w.Write([]byte(body))
|
||||||
|
}))
|
||||||
|
defer upstream.Close()
|
||||||
|
|
||||||
|
initDockerProxyTest(t, `
|
||||||
|
[registries."test.local"]
|
||||||
|
upstream = "`+upstream.URL+`"
|
||||||
|
authHost = "https://auth.test.local/token"
|
||||||
|
authType = "anonymous"
|
||||||
|
enabled = true
|
||||||
|
`)
|
||||||
|
utils.GlobalCache = &utils.UniversalCache{}
|
||||||
|
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
router := gin.New()
|
||||||
|
router.Any("/v2/*path", ProxyDockerRegistryGin)
|
||||||
|
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/v2/test.local/team/app/manifests/latest", nil)
|
||||||
|
req.Header.Set("Accept", "application/vnd.docker.distribution.manifest.v2+json")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Fatalf("request %d status = %d; body=%s", i, w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
if !strings.Contains(w.Body.String(), `"hit":1`) {
|
||||||
|
t.Fatalf("request %d body = %q", i, w.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if got := atomic.LoadInt32(&hits); got != 1 {
|
||||||
|
t.Fatalf("hits = %d, want 1", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/v2/test.local/team/app/manifests/latest", nil)
|
||||||
|
req.Header.Set("Accept", "application/vnd.oci.image.index.v1+json")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Fatalf("second accept status = %d; body=%s", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
if !strings.Contains(w.Body.String(), `"hit":2`) {
|
||||||
|
t.Fatalf("second accept body = %q", w.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProxyDockerRegistryDoesNotCacheAuthenticatedManifest(t *testing.T) {
|
||||||
|
var hits int32
|
||||||
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
count := atomic.AddInt32(&hits, 1)
|
||||||
|
body := fmt.Sprintf(`{"schemaVersion":2,"hit":%d}`, count)
|
||||||
|
w.Header().Set("Content-Type", "application/vnd.docker.distribution.manifest.v2+json")
|
||||||
|
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(body)))
|
||||||
|
_, _ = w.Write([]byte(body))
|
||||||
|
}))
|
||||||
|
defer upstream.Close()
|
||||||
|
|
||||||
|
initDockerProxyTest(t, `
|
||||||
|
[registries."test.local"]
|
||||||
|
upstream = "`+upstream.URL+`"
|
||||||
|
authHost = "https://auth.test.local/token"
|
||||||
|
authType = "anonymous"
|
||||||
|
enabled = true
|
||||||
|
`)
|
||||||
|
utils.GlobalCache = &utils.UniversalCache{}
|
||||||
|
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
router := gin.New()
|
||||||
|
router.Any("/v2/*path", ProxyDockerRegistryGin)
|
||||||
|
|
||||||
|
for i := 1; i <= 2; i++ {
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/v2/test.local/team/app/manifests/latest", nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer token")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Fatalf("request %d status = %d; body=%s", i, w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
if !strings.Contains(w.Body.String(), fmt.Sprintf(`"hit":%d`, i)) {
|
||||||
|
t.Fatalf("request %d body = %q", i, w.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if got := atomic.LoadInt32(&hits); got != 2 {
|
||||||
|
t.Fatalf("hits = %d, want 2", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestProxyDockerRegistryUsesNsQueryForContainerd(t *testing.T) {
|
func TestProxyDockerRegistryUsesNsQueryForContainerd(t *testing.T) {
|
||||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.URL.Path != "/v2/team/app/manifests/latest" {
|
if r.URL.Path != "/v2/team/app/manifests/latest" {
|
||||||
|
|||||||
+17
-11
@@ -3,6 +3,7 @@ package utils
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -17,6 +18,8 @@ const (
|
|||||||
MaxIPCacheSize = 10000
|
MaxIPCacheSize = 10000
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var debugRateLimitLog = strings.EqualFold(os.Getenv("DEBUG_RATE_LIMIT_LOG"), "true")
|
||||||
|
|
||||||
// IPRateLimiter IP限流器结构体
|
// IPRateLimiter IP限流器结构体
|
||||||
type IPRateLimiter struct {
|
type IPRateLimiter struct {
|
||||||
ips map[string]*rateLimiterEntry
|
ips map[string]*rateLimiterEntry
|
||||||
@@ -234,17 +237,20 @@ func RateLimitMiddleware(limiter *IPRateLimiter) gin.HandlerFunc {
|
|||||||
|
|
||||||
cleanIP := extractIPFromAddress(ip)
|
cleanIP := extractIPFromAddress(ip)
|
||||||
|
|
||||||
normalizedIP := normalizeIPForRateLimit(cleanIP)
|
if debugRateLimitLog {
|
||||||
if cleanIP != normalizedIP {
|
normalizedIP := normalizeIPForRateLimit(cleanIP)
|
||||||
fmt.Printf("请求IP: %s (提纯后: %s, 限流段: %s), X-Forwarded-For: %s, X-Real-IP: %s\n",
|
if cleanIP != normalizedIP {
|
||||||
ip, cleanIP, normalizedIP,
|
fmt.Printf("请求IP: %s (提纯后: %s, 限流段: %s), X-Forwarded-For: %s, X-Real-IP: %s\n",
|
||||||
c.GetHeader("X-Forwarded-For"),
|
ip, cleanIP, normalizedIP,
|
||||||
c.GetHeader("X-Real-IP"))
|
c.GetHeader("X-Forwarded-For"),
|
||||||
} else {
|
c.GetHeader("X-Real-IP"))
|
||||||
fmt.Printf("请求IP: %s (提纯后: %s), X-Forwarded-For: %s, X-Real-IP: %s\n",
|
} else {
|
||||||
ip, cleanIP,
|
fmt.Printf("请求IP: %s (提纯后: %s), X-Forwarded-For: %s, X-Real-IP: %s\n",
|
||||||
c.GetHeader("X-Forwarded-For"),
|
ip, cleanIP,
|
||||||
c.GetHeader("X-Real-IP"))
|
c.GetHeader("X-Forwarded-For"),
|
||||||
|
c.GetHeader("X-Real-IP"))
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ipLimiter, allowed := limiter.GetLimiter(cleanIP)
|
ipLimiter, allowed := limiter.GetLimiter(cleanIP)
|
||||||
|
|||||||
Reference in New Issue
Block a user