From 85e47b7ce5872c3a01ef3456127b8a5cc45290f6 Mon Sep 17 00:00:00 2001 From: sky22333 Date: Sat, 16 May 2026 04:29:36 +0800 Subject: [PATCH] fix --- src/handlers/docker.go | 78 ++++++++++++++++++++++++++---- src/handlers/docker_test.go | 94 +++++++++++++++++++++++++++++++++++++ 2 files changed, 164 insertions(+), 8 deletions(-) diff --git a/src/handlers/docker.go b/src/handlers/docker.go index f5723b1..30d3f0c 100644 --- a/src/handlers/docker.go +++ b/src/handlers/docker.go @@ -119,12 +119,8 @@ func resolveRegistryTarget(c *gin.Context, pathWithoutV2 string) (registryTarget func resolveTokenTarget(c *gin.Context) (registryTarget, bool) { name := strings.Trim(strings.TrimSpace(c.Param("path")), "/") - if name == "" { - return defaultRegistryTarget(), true - } - - if name == dockerHubName || name == "dockerhub" || name == "registry-1.docker.io" { - return defaultRegistryTarget(), true + if name == "" || isDockerHubAlias(name) { + return inferTokenTargetFromScope(c.Query("scope")) } cfg := config.GetConfig() @@ -135,6 +131,44 @@ func resolveTokenTarget(c *gin.Context) (registryTarget, bool) { return registryTarget{}, false } +func inferTokenTargetFromScope(scope string) (registryTarget, bool) { + if registryName, ok := registryNameFromScope(scope); ok { + if isDockerHubAlias(registryName) { + return defaultRegistryTarget(), true + } + + cfg := config.GetConfig() + if mapping, exists := cfg.Registries[registryName]; exists && mapping.Enabled { + return registryTargetFromMapping(registryName, mapping), true + } + } + + return defaultRegistryTarget(), true +} + +func registryNameFromScope(scope string) (string, bool) { + parts := strings.Split(scope, ":") + if len(parts) != 3 || parts[0] != "repository" { + return "", false + } + + repo := parts[1] + slash := strings.Index(repo, "/") + if slash == -1 { + return "", false + } + + registryName := repo[:slash] + if strings.Contains(registryName, ".") || strings.Contains(registryName, ":") || registryName == "localhost" { + return registryName, true + } + return "", false +} + +func isDockerHubAlias(name string) bool { + return name == dockerHubName || name == "dockerhub" || name == "registry-1.docker.io" +} + // 透明代理 Docker Registry API v2 请求。 func ProxyDockerRegistryGin(c *gin.Context) { path := c.Request.URL.Path @@ -266,8 +300,8 @@ func buildAuthURL(target registryTarget, rawQuery string) (string, error) { continue } for _, value := range values { - if strings.EqualFold(key, "scope") && target.AutoLibraryPrefix { - value = addLibraryPrefixToScope(value) + if strings.EqualFold(key, "scope") { + value = normalizeScopeForTarget(value, target) } query.Add(key, value) } @@ -278,6 +312,34 @@ func buildAuthURL(target registryTarget, rawQuery string) (string, error) { return authURL.String(), nil } +func normalizeScopeForTarget(scope string, target registryTarget) string { + scope = stripTargetRegistryFromScope(scope, target) + if target.AutoLibraryPrefix { + return addLibraryPrefixToScope(scope) + } + return scope +} + +func stripTargetRegistryFromScope(scope string, target registryTarget) string { + parts := strings.Split(scope, ":") + if len(parts) != 3 || parts[0] != "repository" { + return scope + } + + prefixes := []string{target.Name + "/"} + if target.Name == dockerHubName { + prefixes = append(prefixes, "dockerhub/", "registry-1.docker.io/") + } + + for _, prefix := range prefixes { + if strings.HasPrefix(parts[1], prefix) { + parts[1] = strings.TrimPrefix(parts[1], prefix) + return strings.Join(parts, ":") + } + } + return scope +} + func addLibraryPrefixToScope(scope string) string { parts := strings.Split(scope, ":") if len(parts) != 3 || parts[0] != "repository" || strings.Contains(parts[1], "/") { diff --git a/src/handlers/docker_test.go b/src/handlers/docker_test.go index 4faa350..4f69479 100644 --- a/src/handlers/docker_test.go +++ b/src/handlers/docker_test.go @@ -139,6 +139,60 @@ func TestBuildAuthURLForDockerHubAddsLibraryScopeAndService(t *testing.T) { } } +func TestTokenTargetIsInferredFromPathBasedRegistryScope(t *testing.T) { + initDockerProxyTest(t, ` +[registries."ghcr.io"] +upstream = "ghcr.io" +authHost = "ghcr.io/token" +authType = "github" +enabled = true +`) + + gin.SetMode(gin.TestMode) + req := httptest.NewRequest(http.MethodGet, "/token/docker.io?scope=repository:ghcr.io/jeessy2/ddns-go:pull&service=registry.docker.io", nil) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = req + c.Params = gin.Params{{Key: "path", Value: "/docker.io"}} + + target, ok := resolveTokenTarget(c) + if !ok { + t.Fatal("resolveTokenTarget returned false") + } + if target.Name != "ghcr.io" { + t.Fatalf("target.Name = %q, want ghcr.io", target.Name) + } + if target.AuthService != "ghcr.io" { + t.Fatalf("AuthService = %q, want ghcr.io", target.AuthService) + } +} + +func TestBuildAuthURLStripsPathBasedRegistryPrefixForGHCR(t *testing.T) { + target := registryTarget{ + Name: "ghcr.io", + AuthRealm: "https://ghcr.io/token", + AuthService: "ghcr.io", + } + + got, err := buildAuthURL( + target, + "scope=repository%3Aghcr.io%2Fjeessy2%2Fddns-go%3Apull&service=registry.docker.io", + ) + if err != nil { + t.Fatal(err) + } + + if !strings.Contains(got, "service=ghcr.io") { + t.Fatalf("auth URL missing ghcr service: %q", got) + } + if !strings.Contains(got, "scope=repository%3Ajeessy2%2Fddns-go%3Apull") { + t.Fatalf("auth URL missing stripped scope: %q", got) + } + if strings.Contains(got, "registry.docker.io") { + t.Fatalf("auth URL leaked Docker Hub service: %q", got) + } +} + func TestDockerIODefaultTargetUsesBuiltInWhenUnconfigured(t *testing.T) { initDockerProxyTest(t, "") @@ -327,6 +381,46 @@ enabled = true } } +func TestProxyDockerAuthRoutesPathBasedGHCRScopeToGHCRAuth(t *testing.T) { + authServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if got := r.URL.Query().Get("service"); got != "ghcr.io" { + t.Fatalf("service = %q, want ghcr.io", got) + } + if got := r.URL.Query().Get("scope"); got != "repository:jeessy2/ddns-go:pull" { + t.Fatalf("scope = %q, want repository:jeessy2/ddns-go:pull", got) + } + + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"token":"ghcr-token","expires_in":3600}`)) + })) + defer authServer.Close() + + initDockerProxyTest(t, ` +[registries."ghcr.io"] +upstream = "ghcr.io" +authHost = "`+authServer.URL+`" +authType = "github" +enabled = true +`) + utils.GlobalCache = &utils.UniversalCache{} + + gin.SetMode(gin.TestMode) + router := gin.New() + router.Any("/token/*path", ProxyDockerAuthGin) + + req := httptest.NewRequest(http.MethodGet, "/token/docker.io?scope=repository%3Aghcr.io%2Fjeessy2%2Fddns-go%3Apull&service=registry.docker.io", nil) + w := httptest.NewRecorder() + + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("status = %d, want 200; body=%s", w.Code, w.Body.String()) + } + if got := w.Body.String(); !strings.Contains(got, `"token":"ghcr-token"`) { + t.Fatalf("body = %q", got) + } +} + func TestDockerHubShortNameIsProxiedWithLibraryPrefix(t *testing.T) { upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/v2/library/nginx/manifests/latest" {