From 387545ab78f5f71eeb71042c5ad2c0e9a74b7710 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Fri, 25 Jul 2025 16:37:20 +0800 Subject: [PATCH 1/6] refactor: oci image proxy --- proxy/docker.go | 328 +++++++++++++++++++++++++++++++----------------- 1 file changed, 213 insertions(+), 115 deletions(-) diff --git a/proxy/docker.go b/proxy/docker.go index 1f707db..e723851 100644 --- a/proxy/docker.go +++ b/proxy/docker.go @@ -2,19 +2,21 @@ package proxy import ( "context" - "encoding/json" + + "github.com/go-json-experiment/json" + "fmt" - - "github.com/infinite-iroha/touka" - - "ghproxy/config" - "ghproxy/weakcache" - "io" "net/http" + "net/url" "strconv" "strings" + "ghproxy/config" + "ghproxy/weakcache" + + "github.com/WJQSERVER-STUDIO/go-utils/iox" "github.com/WJQSERVER-STUDIO/go-utils/limitreader" + "github.com/infinite-iroha/touka" ) var ( @@ -22,83 +24,109 @@ var ( ghcrTarget = "ghcr.io" ) +// cache 用于存储认证令牌, 避免重复获取 var cache *weakcache.Cache[string] +// imageInfo 结构体用于存储镜像的相关信息 type imageInfo struct { User string Repo string Image string } +// InitWeakCache 初始化弱引用缓存 func InitWeakCache() *weakcache.Cache[string] { + // 使用默认过期时间和容量为100创建一个新的弱引用缓存 cache = weakcache.NewCache[string](weakcache.DefaultExpiration, 100) return cache } +// GhcrWithImageRouting 处理带有镜像路由的请求, 根据目标路由到不同的Docker注册表 func GhcrWithImageRouting(cfg *config.Config) touka.HandlerFunc { return func(c *touka.Context) { + reqTarget := c.Param("target") // 请求中指定的目标 (如 docker.io, ghcr.io, gcr.io) + reqImageUser := c.Param("user") // 镜像用户 + reqImageName := c.Param("repo") // 镜像仓库名 + reqFilePath := c.Param("filepath") // 镜像文件路径 - charToFind := '.' - reqTarget := c.Param("target") - reqImageUser := c.Param("user") - reqImageName := c.Param("repo") - reqFilePath := c.Param("filepath") + // 构造完整的镜像路径 + path := fmt.Sprintf("%s/%s%s", reqImageUser, reqImageName, reqFilePath) + var target string - path := fmt.Sprintf("%s/%s/%s", reqImageUser, reqImageName, reqFilePath) - target := "" - - if strings.ContainsRune(reqTarget, charToFind) { - switch reqTarget { - case "docker.io": - target = dockerhubTarget - case "ghcr.io": - target = ghcrTarget - default: - target = reqTarget + // 根据 reqTarget 智能判断实际的目标注册表 + switch { + case reqTarget == "docker.io": + target = dockerhubTarget // Docker Hub + case reqTarget == "ghcr.io": + target = ghcrTarget // GitHub Container Registry + case strings.HasSuffix(reqTarget, ".gcr.io"), reqTarget == "gcr.io": + target = reqTarget // Google Container Registry 及其子域名 + default: + // 如果 reqTarget 包含点, 则假定它是一个完整的域名 + for _, r := range reqTarget { + if r == '.' { + target = reqTarget + break + } } - } else { - path = c.GetRequestURI() - reqImageUser = c.Param("target") - reqImageName = c.Param("user") } + + // 封装镜像信息 image := &imageInfo{ User: reqImageUser, Repo: reqImageName, Image: fmt.Sprintf("%s/%s", reqImageUser, reqImageName), } + // 调用 GhcrToTarget 处理实际的代理请求 GhcrToTarget(c, cfg, target, path, image) - } - } +// GhcrToTarget 根据配置和目标信息将请求代理到上游Docker注册表 func GhcrToTarget(c *touka.Context, cfg *config.Config, target string, path string, image *imageInfo) { - if cfg.Docker.Enabled { - var ctx = c.Request.Context() - if target != "" { - GhcrRequest(ctx, c, "https://"+target+"/v2/"+path+"?"+c.GetReqQueryString(), image, cfg, target) - } else { - if cfg.Docker.Target == "ghcr" { - GhcrRequest(ctx, c, "https://"+ghcrTarget+c.GetRequestURI(), image, cfg, ghcrTarget) - } else if cfg.Docker.Target == "dockerhub" { - GhcrRequest(ctx, c, "https://"+dockerhubTarget+c.GetRequestURI(), image, cfg, dockerhubTarget) - } else if cfg.Docker.Target != "" { - // 自定义taget - GhcrRequest(ctx, c, "https://"+cfg.Docker.Target+c.GetRequestURI(), image, cfg, cfg.Docker.Target) - } else { - // 配置为空 - ErrorPage(c, NewErrorWithStatusLookup(403, "Docker Target is not set")) - return - } - } - - } else { + // 检查Docker代理是否启用 + if !cfg.Docker.Enabled { ErrorPage(c, NewErrorWithStatusLookup(403, "Docker is not Allowed")) return } + + var destUrl string // 最终代理的目标URL + var upstreamTarget string // 实际的上游目标域名 + var ctx = c.Request.Context() + + // 根据是否指定 target 来确定上游目标和目标URL + if target != "" { + upstreamTarget = target + // 构造目标URL, 拼接 v2/ 路径和原始查询参数 + destUrl = "https://" + upstreamTarget + "/v2/" + path + if query := c.GetReqQueryString(); query != "" { + destUrl += "?" + query + } + c.Debugf("Proxying to target %s: %s", upstreamTarget, destUrl) + } else { + // 如果未指定 target, 则根据配置的默认目标进行代理 + switch cfg.Docker.Target { + case "ghcr": + upstreamTarget = ghcrTarget + case "dockerhub": + upstreamTarget = dockerhubTarget + case "": + ErrorPage(c, NewErrorWithStatusLookup(403, "Docker Target is not set")) + return + default: + upstreamTarget = cfg.Docker.Target + } + // 使用原始请求URI构建目标URL + destUrl = "https://" + upstreamTarget + c.GetRequestURI() + c.Debugf("Proxying to default target %s: %s", upstreamTarget, destUrl) + } + + // 执行实际的代理请求 + GhcrRequest(ctx, c, destUrl, image, cfg, upstreamTarget) } +// GhcrRequest 执行对Docker注册表的HTTP请求, 处理认证和重定向 func GhcrRequest(ctx context.Context, c *touka.Context, u string, image *imageInfo, cfg *config.Config, target string) { var ( @@ -108,23 +136,25 @@ func GhcrRequest(ctx context.Context, c *touka.Context, u string, image *imageIn err error ) + // 当请求上下文被取消时, 确保关闭响应和请求体 go func() { <-ctx.Done() if resp != nil && resp.Body != nil { - resp.Body.Close() + _ = resp.Body.Close() } - if req != nil { - req.Body.Close() + if req != nil && req.Body != nil { + _ = req.Body.Close() } }() method = c.Request.Method ghcrclient := c.GetHTTPC() + // 构建初始请求 rb := ghcrclient.NewRequestBuilder(method, u) - rb.NoDefaultHeaders() - rb.SetBody(c.Request.Body) - rb.WithContext(ctx) + rb.NoDefaultHeaders() // 不使用默认头部, 以便完全控制 + rb.SetBody(c.Request.Body) // 设置请求体 + rb.WithContext(ctx) // 设置请求上下文 req, err = rb.Build() if err != nil { @@ -132,80 +162,138 @@ func GhcrRequest(ctx context.Context, c *touka.Context, u string, image *imageIn return } + // 复制客户端请求的头部到代理请求 copyHeader(c.Request.Header, req.Header) + // 确保 Accept 头部被正确设置 + if acceptHeader, ok := c.Request.Header["Accept"]; ok { + req.Header["Accept"] = acceptHeader + } + + // 设置 Host 头部为上游目标 req.Header.Set("Host", target) + + // 尝试从缓存中获取并使用认证令牌 if image != nil { token, exist := cache.Get(image.Image) if exist { - c.Debugf("Use Cache Token: %s", token) req.Header.Set("Authorization", "Bearer "+token) } } + // 发送初始请求 resp, err = ghcrclient.Do(req) if err != nil { HandleError(c, fmt.Sprintf("Failed to send request: %v", err)) return } - switch resp.StatusCode { + // 处理 401 Unauthorized 或 404 Not Found 响应, 尝试重新认证并重试 + if resp.StatusCode == 401 || resp.StatusCode == 404 { + // 对于 /v2/ 的请求不进行重试, 因为它通常用于发现认证端点 + shouldRetry := string(c.GetRequestURIPath()) != "/v2/" + originalStatusCode := resp.StatusCode + c.Debugf("Initial request failed with status %d. Retry eligibility: %t", originalStatusCode, shouldRetry) + _ = resp.Body.Close() // 关闭当前响应体 - case 401: - // 请求target /v2/路径 - if string(c.GetRequestURIPath()) != "/v2/" { - resp.Body.Close() + if shouldRetry { if image == nil { - ErrorPage(c, NewErrorWithStatusLookup(401, "Unauthorized")) + ErrorPage(c, NewErrorWithStatusLookup(originalStatusCode, "Unauthorized")) return } + // 获取新的认证令牌 token := ChallengeReq(target, image, ctx, c) - // 更新kv if token != "" { - c.Debugf("Update Cache Token: %s", token) - cache.Put(image.Image, token) - } + c.Debugf("Successfully obtained auth token. Retrying request.") + // 重新构建并发送请求 + rb_retry := ghcrclient.NewRequestBuilder(method, u) + rb_retry.NoDefaultHeaders() + rb_retry.SetBody(c.Request.Body) + rb_retry.WithContext(ctx) - rb := ghcrclient.NewRequestBuilder(string(method), u) - rb.NoDefaultHeaders() - rb.SetBody(c.Request.Body) - rb.WithContext(ctx) + req_retry, err_retry := rb_retry.Build() + if err_retry != nil { + HandleError(c, fmt.Sprintf("Failed to create retry request: %v", err_retry)) + return + } - req, err = rb.Build() - if err != nil { - HandleError(c, fmt.Sprintf("Failed to create request: %v", err)) - return - } + copyHeader(c.Request.Header, req_retry.Header) // 复制原始头部 + if acceptHeader, ok := c.Request.Header["Accept"]; ok { + req_retry.Header["Accept"] = acceptHeader + } - copyHeader(c.Request.Header, req.Header) + req_retry.Header.Set("Host", target) // 设置 Host 头部 + req_retry.Header.Set("Authorization", "Bearer "+token) // 使用新令牌 - req.Header.Set("Host", target) - if token != "" { - req.Header.Set("Authorization", "Bearer "+token) - } + c.Debugf("Executing retry request. Method: %s, URL: %s", req_retry.Method, req_retry.URL.String()) - resp, err = ghcrclient.Do(req) - if err != nil { - HandleError(c, fmt.Sprintf("Failed to send request: %v", err)) - return + resp_retry, err_retry := ghcrclient.Do(req_retry) + if err_retry != nil { + HandleError(c, fmt.Sprintf("Failed to send retry request: %v", err_retry)) + return + } + c.Debugf("Retry request completed with status code: %d", resp_retry.StatusCode) + resp = resp_retry // 更新响应为重试后的响应 + } else { + c.Warnf("Failed to obtain auth token. Cannot retry.") } } + } - case 404: // 错误处理(404) - ErrorPage(c, NewErrorWithStatusLookup(404, "Page Not Found (From Github)")) - return - case 302, 301: - finalURL := resp.Header.Get("Location") - if finalURL != "" { - err = resp.Body.Close() - if err != nil { - c.Errorf("Failed to close response body: %v", err) - } - c.Infof("Internal Redirecting to %s", finalURL) - GhcrRequest(ctx, c, finalURL, image, cfg, target) + // 透明地处理 302 Found 或 307 Temporary Redirect 重定向 + if resp.StatusCode == http.StatusFound || resp.StatusCode == http.StatusTemporaryRedirect { + location := resp.Header.Get("Location") + if location == "" { + HandleError(c, "Redirect response missing Location header") return } + + redirectURL, err := url.Parse(location) + if err != nil { + HandleError(c, fmt.Sprintf("Failed to parse redirect location: %v", err)) + return + } + + // 如果 Location 是相对路径, 则根据原始请求的 URL 解析为绝对路径 + if !redirectURL.IsAbs() { + originalURL := resp.Request.URL + redirectURL = originalURL.ResolveReference(redirectURL) + c.Debugf("Resolved relative redirect to absolute URL: %s", redirectURL.String()) + } + + c.Debugf("Handling redirect. Status: %d, Final Location: %s", resp.StatusCode, redirectURL.String()) + _ = resp.Body.Close() // 关闭当前响应体 + + // 创建并发送重定向请求, 通常使用 GET 方法 + redirectReq, err := http.NewRequestWithContext(ctx, "GET", redirectURL.String(), nil) + if err != nil { + HandleError(c, fmt.Sprintf("Failed to create redirect request: %v", err)) + return + } + redirectReq.Header.Set("User-Agent", c.Request.UserAgent()) // 复制 User-Agent + + c.Debugf("Executing redirect request to: %s", redirectURL.String()) + redirectResp, err := ghcrclient.Do(redirectReq) + if err != nil { + HandleError(c, fmt.Sprintf("Failed to execute redirect request to %s: %v", redirectURL.String(), err)) + return + } + c.Debugf("Redirect request to %s completed with status %d", redirectURL.String(), redirectResp.StatusCode) + resp = redirectResp // 更新响应为重定向后的响应 + } + + // 如果最终响应是 404, 则读取响应体并返回自定义错误页面 + if resp.StatusCode == 404 { + bodyBytes, err := iox.ReadAll(resp.Body) + if err != nil { + c.Warnf("Failed to read upstream 404 response body: %v", err) + } else { + c.Warnf("Upstream 404 response body: %s", string(bodyBytes)) + } + _ = resp.Body.Close() + ErrorPage(c, NewErrorWithStatusLookup(404, "Page Not Found (From Upstream)")) + return } var ( @@ -214,6 +302,7 @@ func GhcrRequest(ctx context.Context, c *touka.Context, u string, image *imageIn sizelimit int ) + // 获取配置中的大小限制并转换单位 (MB -> Byte) sizelimit = cfg.Server.SizeLimit * 1024 * 1024 contentLength = resp.Header.Get("Content-Length") if contentLength != "" { @@ -221,77 +310,85 @@ func GhcrRequest(ctx context.Context, c *touka.Context, u string, image *imageIn bodySize, err = strconv.Atoi(contentLength) if err != nil { c.Warnf("%s %s %s %s %s Content-Length header is not a valid integer: %v", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.UserAgent(), c.Request.Proto, err) - bodySize = -1 + bodySize = -1 // 无法解析则设置为 -1 } + // 如果内容大小超出限制, 返回 301 重定向到原始上游URL if err == nil && bodySize > sizelimit { finalURL := resp.Request.URL.String() - err = resp.Body.Close() - if err != nil { - c.Errorf("Failed to close response body: %v", err) - } + _ = resp.Body.Close() // 关闭响应体 c.Redirect(301, finalURL) c.Warnf("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.UserAgent(), c.Request.Proto, finalURL, bodySize) return } } + // 将上游响应头部复制到客户端响应 c.SetHeaders(resp.Header) - + // 设置客户端响应状态码 c.Status(resp.StatusCode) - bodyReader := resp.Body + // 如果启用了带宽限制, 则使用限速读取器 if cfg.RateLimit.BandwidthLimit.Enabled { bodyReader = limitreader.NewRateLimitedReader(bodyReader, bandwidthLimit, int(bandwidthBurst), ctx) } + // 根据 Content-Length 设置响应体流 if contentLength != "" { c.SetBodyStream(bodyReader, bodySize) return } - c.SetBodyStream(bodyReader, -1) - + c.SetBodyStream(bodyReader, -1) // Content-Length 未知 } +// AuthToken 用于解析认证响应中的令牌 type AuthToken struct { Token string `json:"token"` } +// ChallengeReq 执行认证挑战流程, 获取新的认证令牌 func ChallengeReq(target string, image *imageInfo, ctx context.Context, c *touka.Context) (token string) { var resp401 *http.Response var req401 *http.Request var err error ghcrclient := c.GetHTTPC() + // 对 /v2/ 端点发送 GET 请求以触发认证挑战 rb401 := ghcrclient.NewRequestBuilder("GET", "https://"+target+"/v2/") rb401.NoDefaultHeaders() rb401.WithContext(ctx) - rb401.AddHeader("User-Agent", "docker/28.1.1 go/go1.23.8 git-commit/01f442b kernel/6.12.25-amd64 os/linux arch/amd64 UpstreamClient(Docker-Client/28.1.1 ") + //rb401.AddHeader("User-Agent", "docker/28.1.1 go/go1.23.8 git-commit/01f442b kernel/6.12.25-amd64 os/linux arch/amd64 UpstreamClient(Docker-Client/28.1.1 ") req401, err = rb401.Build() if err != nil { HandleError(c, fmt.Sprintf("Failed to create request: %v", err)) return } - req401.Header.Set("Host", target) + req401.Header.Set("Host", target) // 设置 Host 头部 resp401, err = ghcrclient.Do(req401) if err != nil { HandleError(c, fmt.Sprintf("Failed to send request: %v", err)) return } - defer resp401.Body.Close() + defer func() { + _ = resp401.Body.Close() // 确保响应体关闭 + }() + + // 解析 Www-Authenticate 头部, 获取认证领域和参数 bearer, err := parseBearerWWWAuthenticateHeader(resp401.Header.Get("Www-Authenticate")) if err != nil { c.Errorf("Failed to parse Www-Authenticate header: %v", err) return } + // 构建认证范围 (scope), 通常是 repository::pull scope := fmt.Sprintf("repository:%s:pull", image.Image) + // 使用解析到的 Realm 和 Service, 以及 scope 请求认证令牌 getAuthRB := ghcrclient.NewRequestBuilder("GET", bearer.Realm). NoDefaultHeaders(). WithContext(ctx). - AddHeader("User-Agent", "docker/28.1.1 go/go1.23.8 git-commit/01f442b kernel/6.12.25-amd64 os/linux arch/amd64 UpstreamClient(Docker-Client/28.1.1 "). + //AddHeader("User-Agent", "docker/28.1.1 go/go1.23.8 git-commit/01f442b kernel/6.12.25-amd64 os/linux arch/amd64 UpstreamClient(Docker-Client/28.1.1 "). SetHeader("Host", bearer.Service). AddQueryParam("service", bearer.Service). AddQueryParam("scope", scope) @@ -307,24 +404,25 @@ func ChallengeReq(target string, image *imageInfo, ctx context.Context, c *touka c.Errorf("Failed to send request: %v", err) return } + defer func() { + _ = authResp.Body.Close() // 确保响应体关闭 + }() - defer authResp.Body.Close() - - bodyBytes, err := io.ReadAll(authResp.Body) + // 读取认证响应体 + bodyBytes, err := iox.ReadAll(authResp.Body) if err != nil { c.Errorf("Failed to read auth response body: %v", err) return } - // 解码json + // 解码 JSON 响应以获取令牌 var authToken AuthToken err = json.Unmarshal(bodyBytes, &authToken) if err != nil { c.Errorf("Failed to decode auth response body: %v", err) return } - token = authToken.Token + token = authToken.Token // 提取令牌 return token - } From 8689738f4ff475d26afa8e0f01f606778cc3bf02 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Fri, 25 Jul 2025 16:39:37 +0800 Subject: [PATCH 2/6] 4.2.2-rc.0 --- CHANGELOG.md | 5 +++++ DEV-VERSION | 2 +- go.mod | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ad0f46..a83a34e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 更新日志 +4.2.2-rc.0 - 2025-07-25 +--- +- PRE-RELEASE: v4.2.2-rc.0是v4.2.2预发布版本,请勿在生产环境中使用; +- CHANGE: 重构OCI镜像代理部分, 完善对`ghcr`,`gcr`,`k8s.gcr`等上游源特殊处理的适配 + 4.2.1 - 2025-07-25 --- - CHANGE: 更新主题样式, 新增`free`主题, `design`与`hub`主题样式更新 diff --git a/DEV-VERSION b/DEV-VERSION index 5cff3f3..c6cdc65 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -4.2.0-rc.0 \ No newline at end of file +4.2.2-rc.0 \ No newline at end of file diff --git a/go.mod b/go.mod index 6a9d201..2e73766 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( ) require ( + github.com/WJQSERVER-STUDIO/go-utils/iox v0.0.2 github.com/WJQSERVER-STUDIO/go-utils/limitreader v0.0.2 github.com/fenthope/bauth v0.0.1 github.com/fenthope/ikumi v0.0.2 @@ -24,6 +25,5 @@ require ( require ( github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.6 // indirect - github.com/WJQSERVER-STUDIO/go-utils/iox v0.0.2 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect ) From 31c26b00fb1241cb3cac6a431ca5c99a9ae2a351 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Fri, 25 Jul 2025 17:07:25 +0800 Subject: [PATCH 3/6] fix retry body --- proxy/docker.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/proxy/docker.go b/proxy/docker.go index e723851..1471171 100644 --- a/proxy/docker.go +++ b/proxy/docker.go @@ -1,6 +1,7 @@ package proxy import ( + "bytes" "context" "github.com/go-json-experiment/json" @@ -149,12 +150,17 @@ func GhcrRequest(ctx context.Context, c *touka.Context, u string, image *imageIn method = c.Request.Method ghcrclient := c.GetHTTPC() + bodyByte, err := c.GetReqBodyFull() + if err != nil { + HandleError(c, fmt.Sprintf("Failed to read request body: %v", err)) + return + } // 构建初始请求 rb := ghcrclient.NewRequestBuilder(method, u) - rb.NoDefaultHeaders() // 不使用默认头部, 以便完全控制 - rb.SetBody(c.Request.Body) // 设置请求体 - rb.WithContext(ctx) // 设置请求上下文 + rb.NoDefaultHeaders() // 不使用默认头部, 以便完全控制 + rb.SetBody(bytes.NewBuffer(bodyByte)) // 设置请求体 + rb.WithContext(ctx) // 设置请求上下文 req, err = rb.Build() if err != nil { @@ -209,7 +215,7 @@ func GhcrRequest(ctx context.Context, c *touka.Context, u string, image *imageIn // 重新构建并发送请求 rb_retry := ghcrclient.NewRequestBuilder(method, u) rb_retry.NoDefaultHeaders() - rb_retry.SetBody(c.Request.Body) + rb_retry.SetBody(bytes.NewBuffer(bodyByte)) rb_retry.WithContext(ctx) req_retry, err_retry := rb_retry.Build() From 596e4098897f73fc1259f34aa63fcda791dbee0a Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Fri, 25 Jul 2025 17:16:26 +0800 Subject: [PATCH 4/6] 4.2.2 --- CHANGELOG.md | 4 ++++ VERSION | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a83a34e..525bb62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 更新日志 +4.2.2 - 2025-07-25 +--- +- CHANGE: 重构OCI镜像代理部分, 完善对`ghcr`,`gcr`,`k8s.gcr`等上游源特殊处理的适配 + 4.2.2-rc.0 - 2025-07-25 --- - PRE-RELEASE: v4.2.2-rc.0是v4.2.2预发布版本,请勿在生产环境中使用; diff --git a/VERSION b/VERSION index d87edbf..078bf8b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.2.1 \ No newline at end of file +4.2.2 \ No newline at end of file From 90c6dd3d79aff2e855af4368a8be158db6b6b10c Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Fri, 25 Jul 2025 17:54:44 +0800 Subject: [PATCH 5/6] update body close 1 --- proxy/docker.go | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/proxy/docker.go b/proxy/docker.go index 1471171..937bdd8 100644 --- a/proxy/docker.go +++ b/proxy/docker.go @@ -137,17 +137,6 @@ func GhcrRequest(ctx context.Context, c *touka.Context, u string, image *imageIn err error ) - // 当请求上下文被取消时, 确保关闭响应和请求体 - go func() { - <-ctx.Done() - if resp != nil && resp.Body != nil { - _ = resp.Body.Close() - } - if req != nil && req.Body != nil { - _ = req.Body.Close() - } - }() - method = c.Request.Method ghcrclient := c.GetHTTPC() bodyByte, err := c.GetReqBodyFull() @@ -247,6 +236,8 @@ func GhcrRequest(ctx context.Context, c *touka.Context, u string, image *imageIn } } + defer resp.Body.Close() + // 透明地处理 302 Found 或 307 Temporary Redirect 重定向 if resp.StatusCode == http.StatusFound || resp.StatusCode == http.StatusTemporaryRedirect { location := resp.Header.Get("Location") @@ -287,6 +278,7 @@ func GhcrRequest(ctx context.Context, c *touka.Context, u string, image *imageIn } c.Debugf("Redirect request to %s completed with status %d", redirectURL.String(), redirectResp.StatusCode) resp = redirectResp // 更新响应为重定向后的响应 + defer resp.Body.Close() } // 如果最终响应是 404, 则读取响应体并返回自定义错误页面 @@ -344,7 +336,7 @@ func GhcrRequest(ctx context.Context, c *touka.Context, u string, image *imageIn c.SetBodyStream(bodyReader, bodySize) return } - c.SetBodyStream(bodyReader, -1) // Content-Length 未知 + c.SetBodyStream(bodyReader, -1) } // AuthToken 用于解析认证响应中的令牌 @@ -376,9 +368,8 @@ func ChallengeReq(target string, image *imageInfo, ctx context.Context, c *touka HandleError(c, fmt.Sprintf("Failed to send request: %v", err)) return } - defer func() { - _ = resp401.Body.Close() // 确保响应体关闭 - }() + + defer resp401.Body.Close() // 确保响应体关闭 // 解析 Www-Authenticate 头部, 获取认证领域和参数 bearer, err := parseBearerWWWAuthenticateHeader(resp401.Header.Get("Www-Authenticate")) From e06e292b1f31065a7d7530198292e5da58e60bbd Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Fri, 25 Jul 2025 18:12:08 +0800 Subject: [PATCH 6/6] update body close && weakcache --- proxy/docker.go | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/proxy/docker.go b/proxy/docker.go index 937bdd8..4712ae3 100644 --- a/proxy/docker.go +++ b/proxy/docker.go @@ -3,9 +3,6 @@ package proxy import ( "bytes" "context" - - "github.com/go-json-experiment/json" - "fmt" "net/http" "net/url" @@ -17,6 +14,7 @@ import ( "github.com/WJQSERVER-STUDIO/go-utils/iox" "github.com/WJQSERVER-STUDIO/go-utils/limitreader" + "github.com/go-json-experiment/json" "github.com/infinite-iroha/touka" ) @@ -129,7 +127,6 @@ func GhcrToTarget(c *touka.Context, cfg *config.Config, target string, path stri // GhcrRequest 执行对Docker注册表的HTTP请求, 处理认证和重定向 func GhcrRequest(ctx context.Context, c *touka.Context, u string, image *imageInfo, cfg *config.Config, target string) { - var ( method string req *http.Request @@ -189,10 +186,10 @@ func GhcrRequest(ctx context.Context, c *touka.Context, u string, image *imageIn shouldRetry := string(c.GetRequestURIPath()) != "/v2/" originalStatusCode := resp.StatusCode c.Debugf("Initial request failed with status %d. Retry eligibility: %t", originalStatusCode, shouldRetry) - _ = resp.Body.Close() // 关闭当前响应体 if shouldRetry { if image == nil { + _ = resp.Body.Close() // 终止流程, 关闭当前响应体 ErrorPage(c, NewErrorWithStatusLookup(originalStatusCode, "Unauthorized")) return } @@ -201,6 +198,12 @@ func GhcrRequest(ctx context.Context, c *touka.Context, u string, image *imageIn if token != "" { c.Debugf("Successfully obtained auth token. Retrying request.") + _ = resp.Body.Close() // 在发起重试请求前, 关闭旧的响应体 + + // 更新kv + c.Debugf("Update Cache Token: %s", token) + cache.Put(image.Image, token) + // 重新构建并发送请求 rb_retry := ghcrclient.NewRequestBuilder(method, u) rb_retry.NoDefaultHeaders() @@ -232,22 +235,23 @@ func GhcrRequest(ctx context.Context, c *touka.Context, u string, image *imageIn resp = resp_retry // 更新响应为重试后的响应 } else { c.Warnf("Failed to obtain auth token. Cannot retry.") + // 获取令牌失败, 将继续处理原始的401/404响应, 其响应体仍然打开 } } } - defer resp.Body.Close() - // 透明地处理 302 Found 或 307 Temporary Redirect 重定向 if resp.StatusCode == http.StatusFound || resp.StatusCode == http.StatusTemporaryRedirect { location := resp.Header.Get("Location") if location == "" { + _ = resp.Body.Close() // 终止流程, 关闭当前响应体 HandleError(c, "Redirect response missing Location header") return } redirectURL, err := url.Parse(location) if err != nil { + _ = resp.Body.Close() // 终止流程, 关闭当前响应体 HandleError(c, fmt.Sprintf("Failed to parse redirect location: %v", err)) return } @@ -260,7 +264,7 @@ func GhcrRequest(ctx context.Context, c *touka.Context, u string, image *imageIn } c.Debugf("Handling redirect. Status: %d, Final Location: %s", resp.StatusCode, redirectURL.String()) - _ = resp.Body.Close() // 关闭当前响应体 + _ = resp.Body.Close() // 明确关闭重定向响应的响应体, 因为我们将发起新请求 // 创建并发送重定向请求, 通常使用 GET 方法 redirectReq, err := http.NewRequestWithContext(ctx, "GET", redirectURL.String(), nil) @@ -278,18 +282,17 @@ func GhcrRequest(ctx context.Context, c *touka.Context, u string, image *imageIn } c.Debugf("Redirect request to %s completed with status %d", redirectURL.String(), redirectResp.StatusCode) resp = redirectResp // 更新响应为重定向后的响应 - defer resp.Body.Close() } // 如果最终响应是 404, 则读取响应体并返回自定义错误页面 if resp.StatusCode == 404 { + defer resp.Body.Close() // 使用defer确保在函数返回前关闭响应体 bodyBytes, err := iox.ReadAll(resp.Body) if err != nil { c.Warnf("Failed to read upstream 404 response body: %v", err) } else { c.Warnf("Upstream 404 response body: %s", string(bodyBytes)) } - _ = resp.Body.Close() ErrorPage(c, NewErrorWithStatusLookup(404, "Page Not Found (From Upstream)")) return } @@ -313,7 +316,7 @@ func GhcrRequest(ctx context.Context, c *touka.Context, u string, image *imageIn // 如果内容大小超出限制, 返回 301 重定向到原始上游URL if err == nil && bodySize > sizelimit { finalURL := resp.Request.URL.String() - _ = resp.Body.Close() // 关闭响应体 + _ = resp.Body.Close() // 明确关闭响应体, 因为我们将重定向而不是流式传输 c.Redirect(301, finalURL) c.Warnf("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.UserAgent(), c.Request.Proto, finalURL, bodySize) return @@ -324,6 +327,7 @@ func GhcrRequest(ctx context.Context, c *touka.Context, u string, image *imageIn c.SetHeaders(resp.Header) // 设置客户端响应状态码 c.Status(resp.StatusCode) + // bodyReader 的所有权将转移给 SetBodyStream, 不再由此函数管理关闭 bodyReader := resp.Body // 如果启用了带宽限制, 则使用限速读取器 @@ -355,7 +359,6 @@ func ChallengeReq(target string, image *imageInfo, ctx context.Context, c *touka rb401 := ghcrclient.NewRequestBuilder("GET", "https://"+target+"/v2/") rb401.NoDefaultHeaders() rb401.WithContext(ctx) - //rb401.AddHeader("User-Agent", "docker/28.1.1 go/go1.23.8 git-commit/01f442b kernel/6.12.25-amd64 os/linux arch/amd64 UpstreamClient(Docker-Client/28.1.1 ") req401, err = rb401.Build() if err != nil { HandleError(c, fmt.Sprintf("Failed to create request: %v", err)) @@ -368,7 +371,6 @@ func ChallengeReq(target string, image *imageInfo, ctx context.Context, c *touka HandleError(c, fmt.Sprintf("Failed to send request: %v", err)) return } - defer resp401.Body.Close() // 确保响应体关闭 // 解析 Www-Authenticate 头部, 获取认证领域和参数 @@ -385,7 +387,6 @@ func ChallengeReq(target string, image *imageInfo, ctx context.Context, c *touka getAuthRB := ghcrclient.NewRequestBuilder("GET", bearer.Realm). NoDefaultHeaders(). WithContext(ctx). - //AddHeader("User-Agent", "docker/28.1.1 go/go1.23.8 git-commit/01f442b kernel/6.12.25-amd64 os/linux arch/amd64 UpstreamClient(Docker-Client/28.1.1 "). SetHeader("Host", bearer.Service). AddQueryParam("service", bearer.Service). AddQueryParam("scope", scope) @@ -401,9 +402,7 @@ func ChallengeReq(target string, image *imageInfo, ctx context.Context, c *touka c.Errorf("Failed to send request: %v", err) return } - defer func() { - _ = authResp.Body.Close() // 确保响应体关闭 - }() + defer authResp.Body.Close() // 确保响应体关闭 // 读取认证响应体 bodyBytes, err := iox.ReadAll(authResp.Body)