mirror of
https://github.com/sky22333/hubproxy.git
synced 2026-08-05 03:24:57 +08:00
标记匿名token区分匿名manifest
This commit is contained in:
@@ -1,11 +1,16 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"hubproxy/config"
|
||||
@@ -26,6 +31,7 @@ const (
|
||||
dockerHubAuthRealm = "https://auth.docker.io/token"
|
||||
dockerHubAuthService = "registry.docker.io"
|
||||
maxCachedManifestSize = 4 << 20
|
||||
maxAnonymousTokens = 4096
|
||||
)
|
||||
|
||||
var hopByHopHeaders = map[string]struct{}{
|
||||
@@ -50,6 +56,13 @@ var forwardedRequestHeaders = []string{
|
||||
"If-Unmodified-Since",
|
||||
}
|
||||
|
||||
type anonymousTokenStore struct {
|
||||
mu sync.Mutex
|
||||
entries map[string]time.Time
|
||||
}
|
||||
|
||||
var anonymousTokens = &anonymousTokenStore{entries: make(map[string]time.Time)}
|
||||
|
||||
// 保留初始化入口,在线代理无状态。
|
||||
func InitDockerProxy() {}
|
||||
|
||||
@@ -276,12 +289,83 @@ func ProxyDockerAuthGin(c *gin.Context) {
|
||||
|
||||
copyResponseHeaders(c, resp.Header, target)
|
||||
if cacheable && resp.StatusCode == http.StatusOK && len(body) > 0 {
|
||||
utils.GlobalCache.SetToken(cacheKey, string(body), utils.ExtractTTLFromResponse(body))
|
||||
ttl := utils.ExtractTTLFromResponse(body)
|
||||
utils.GlobalCache.SetToken(cacheKey, string(body), ttl)
|
||||
anonymousTokens.RememberFromResponse(body, ttl)
|
||||
}
|
||||
|
||||
c.Data(resp.StatusCode, resp.Header.Get("Content-Type"), body)
|
||||
}
|
||||
|
||||
func (s *anonymousTokenStore) RememberFromResponse(body []byte, ttl time.Duration) {
|
||||
token := tokenFromAuthResponse(body)
|
||||
if token == "" || ttl <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
expiresAt := now.Add(ttl)
|
||||
key := tokenHash(token)
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.cleanupLocked(now)
|
||||
if len(s.entries) >= maxAnonymousTokens {
|
||||
return
|
||||
}
|
||||
s.entries[key] = expiresAt
|
||||
}
|
||||
|
||||
func (s *anonymousTokenStore) IsKnown(token string) bool {
|
||||
if token == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
key := tokenHash(token)
|
||||
now := time.Now()
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
expiresAt, ok := s.entries[key]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if now.After(expiresAt) {
|
||||
delete(s.entries, key)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *anonymousTokenStore) cleanupLocked(now time.Time) {
|
||||
for key, expiresAt := range s.entries {
|
||||
if now.After(expiresAt) {
|
||||
delete(s.entries, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func tokenFromAuthResponse(body []byte) string {
|
||||
var tokenResp struct {
|
||||
Token string `json:"token"`
|
||||
AccessToken string `json:"access_token"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &tokenResp); err != nil {
|
||||
return ""
|
||||
}
|
||||
if tokenResp.Token != "" {
|
||||
return tokenResp.Token
|
||||
}
|
||||
return tokenResp.AccessToken
|
||||
}
|
||||
|
||||
func tokenHash(token string) string {
|
||||
sum := sha256.Sum256([]byte(token))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func buildAuthURL(target registryTarget, rawQuery string) (string, error) {
|
||||
authURL, err := url.Parse(target.AuthRealm)
|
||||
if err != nil {
|
||||
@@ -401,7 +485,7 @@ func proxyRegistryHTTP(c *gin.Context, target registryTarget, upstreamPath strin
|
||||
}
|
||||
|
||||
func manifestCacheKey(c *gin.Context, target registryTarget, upstreamPath string) (string, bool) {
|
||||
if c.Request.Method != http.MethodGet || c.GetHeader("Authorization") != "" || !utils.IsCacheEnabled() {
|
||||
if c.Request.Method != http.MethodGet || !utils.IsCacheEnabled() || !isAnonymousManifestRequest(c) {
|
||||
return "", false
|
||||
}
|
||||
if !strings.Contains(upstreamPath, "/manifests/") {
|
||||
@@ -417,6 +501,21 @@ func manifestCacheKey(c *gin.Context, target registryTarget, upstreamPath string
|
||||
return utils.BuildCacheKey("manifest", key), true
|
||||
}
|
||||
|
||||
func isAnonymousManifestRequest(c *gin.Context) bool {
|
||||
authHeader := strings.TrimSpace(c.GetHeader("Authorization"))
|
||||
if authHeader == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
const bearerPrefix = "bearer "
|
||||
if len(authHeader) <= len(bearerPrefix) || !strings.EqualFold(authHeader[:len(bearerPrefix)], bearerPrefix) {
|
||||
return false
|
||||
}
|
||||
|
||||
token := strings.TrimSpace(authHeader[len(bearerPrefix):])
|
||||
return anonymousTokens.IsKnown(token)
|
||||
}
|
||||
|
||||
func canCacheManifestResponse(resp *http.Response) bool {
|
||||
return resp.StatusCode == http.StatusOK &&
|
||||
resp.ContentLength > 0 &&
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"hubproxy/config"
|
||||
@@ -621,6 +622,93 @@ enabled = true
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyDockerRegistryCachesKnownAnonymousBearerManifest(t *testing.T) {
|
||||
anonymousTokens = &anonymousTokenStore{entries: make(map[string]time.Time)}
|
||||
anonymousTokens.RememberFromResponse([]byte(`{"token":"anonymous-token","expires_in":3600}`), time.Hour)
|
||||
|
||||
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 := 0; i < 2; i++ {
|
||||
req := httptest.NewRequest(http.MethodGet, "/v2/test.local/team/app/manifests/latest", nil)
|
||||
req.Header.Set("Authorization", "Bearer anonymous-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(), `"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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyDockerRegistryDoesNotCacheUnknownBearerManifest(t *testing.T) {
|
||||
anonymousTokens = &anonymousTokenStore{entries: make(map[string]time.Time)}
|
||||
|
||||
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 user-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) {
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v2/team/app/manifests/latest" {
|
||||
|
||||
Reference in New Issue
Block a user