mirror of
https://github.com/WJQSERVER-STUDIO/ghproxy.git
synced 2026-02-04 00:31:10 +08:00
Compare commits
No commits in common. "596e4098897f73fc1259f34aa63fcda791dbee0a" and "3f802a0ed357f90ec0a37e4687414e2137e6f462" have entirely different histories.
596e409889
...
3f802a0ed3
6 changed files with 119 additions and 238 deletions
13
CHANGELOG.md
13
CHANGELOG.md
|
|
@ -1,18 +1,5 @@
|
||||||
# 更新日志
|
# 更新日志
|
||||||
|
|
||||||
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预发布版本,请勿在生产环境中使用;
|
|
||||||
- CHANGE: 重构OCI镜像代理部分, 完善对`ghcr`,`gcr`,`k8s.gcr`等上游源特殊处理的适配
|
|
||||||
|
|
||||||
4.2.1 - 2025-07-25
|
|
||||||
---
|
|
||||||
- CHANGE: 更新主题样式, 新增`free`主题, `design`与`hub`主题样式更新
|
|
||||||
|
|
||||||
4.2.0 - 2025-07-22
|
4.2.0 - 2025-07-22
|
||||||
---
|
---
|
||||||
- CHANGE: 支持根据IP(CDIR)进行白名单与屏蔽
|
- CHANGE: 支持根据IP(CDIR)进行白名单与屏蔽
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
4.2.2-rc.0
|
4.2.0-rc.0
|
||||||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
||||||
4.2.2
|
4.2.0
|
||||||
2
go.mod
2
go.mod
|
|
@ -10,7 +10,6 @@ require (
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/WJQSERVER-STUDIO/go-utils/iox v0.0.2
|
|
||||||
github.com/WJQSERVER-STUDIO/go-utils/limitreader v0.0.2
|
github.com/WJQSERVER-STUDIO/go-utils/limitreader v0.0.2
|
||||||
github.com/fenthope/bauth v0.0.1
|
github.com/fenthope/bauth v0.0.1
|
||||||
github.com/fenthope/ikumi v0.0.2
|
github.com/fenthope/ikumi v0.0.2
|
||||||
|
|
@ -25,5 +24,6 @@ require (
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.6 // indirect
|
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
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
|
||||||
2
main.go
2
main.go
|
|
@ -195,8 +195,6 @@ func loadEmbeddedPages(cfg *config.Config) (fs.FS, fs.FS, error) {
|
||||||
pages, err = fs.Sub(pageFS, "pages/mino")
|
pages, err = fs.Sub(pageFS, "pages/mino")
|
||||||
case "hub":
|
case "hub":
|
||||||
pages, err = fs.Sub(pageFS, "pages/hub")
|
pages, err = fs.Sub(pageFS, "pages/hub")
|
||||||
case "free":
|
|
||||||
pages, err = fs.Sub(pageFS, "pages/free")
|
|
||||||
default:
|
default:
|
||||||
pages, err = fs.Sub(pageFS, "pages/design") // 默认主题
|
pages, err = fs.Sub(pageFS, "pages/design") // 默认主题
|
||||||
logWarning("Invalid Pages Theme: %s, using default theme 'design'", cfg.Pages.Theme)
|
logWarning("Invalid Pages Theme: %s, using default theme 'design'", cfg.Pages.Theme)
|
||||||
|
|
|
||||||
322
proxy/docker.go
322
proxy/docker.go
|
|
@ -1,23 +1,20 @@
|
||||||
package proxy
|
package proxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"github.com/go-json-experiment/json"
|
|
||||||
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
"net/url"
|
"github.com/infinite-iroha/touka"
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"ghproxy/config"
|
"ghproxy/config"
|
||||||
"ghproxy/weakcache"
|
"ghproxy/weakcache"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/WJQSERVER-STUDIO/go-utils/iox"
|
|
||||||
"github.com/WJQSERVER-STUDIO/go-utils/limitreader"
|
"github.com/WJQSERVER-STUDIO/go-utils/limitreader"
|
||||||
"github.com/infinite-iroha/touka"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -25,109 +22,83 @@ var (
|
||||||
ghcrTarget = "ghcr.io"
|
ghcrTarget = "ghcr.io"
|
||||||
)
|
)
|
||||||
|
|
||||||
// cache 用于存储认证令牌, 避免重复获取
|
|
||||||
var cache *weakcache.Cache[string]
|
var cache *weakcache.Cache[string]
|
||||||
|
|
||||||
// imageInfo 结构体用于存储镜像的相关信息
|
|
||||||
type imageInfo struct {
|
type imageInfo struct {
|
||||||
User string
|
User string
|
||||||
Repo string
|
Repo string
|
||||||
Image string
|
Image string
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitWeakCache 初始化弱引用缓存
|
|
||||||
func InitWeakCache() *weakcache.Cache[string] {
|
func InitWeakCache() *weakcache.Cache[string] {
|
||||||
// 使用默认过期时间和容量为100创建一个新的弱引用缓存
|
|
||||||
cache = weakcache.NewCache[string](weakcache.DefaultExpiration, 100)
|
cache = weakcache.NewCache[string](weakcache.DefaultExpiration, 100)
|
||||||
return cache
|
return cache
|
||||||
}
|
}
|
||||||
|
|
||||||
// GhcrWithImageRouting 处理带有镜像路由的请求, 根据目标路由到不同的Docker注册表
|
|
||||||
func GhcrWithImageRouting(cfg *config.Config) touka.HandlerFunc {
|
func GhcrWithImageRouting(cfg *config.Config) touka.HandlerFunc {
|
||||||
return func(c *touka.Context) {
|
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 := '.'
|
||||||
path := fmt.Sprintf("%s/%s%s", reqImageUser, reqImageName, reqFilePath)
|
reqTarget := c.Param("target")
|
||||||
var target string
|
reqImageUser := c.Param("user")
|
||||||
|
reqImageName := c.Param("repo")
|
||||||
|
reqFilePath := c.Param("filepath")
|
||||||
|
|
||||||
// 根据 reqTarget 智能判断实际的目标注册表
|
path := fmt.Sprintf("%s/%s/%s", reqImageUser, reqImageName, reqFilePath)
|
||||||
switch {
|
target := ""
|
||||||
case reqTarget == "docker.io":
|
|
||||||
target = dockerhubTarget // Docker Hub
|
if strings.ContainsRune(reqTarget, charToFind) {
|
||||||
case reqTarget == "ghcr.io":
|
switch reqTarget {
|
||||||
target = ghcrTarget // GitHub Container Registry
|
case "docker.io":
|
||||||
case strings.HasSuffix(reqTarget, ".gcr.io"), reqTarget == "gcr.io":
|
target = dockerhubTarget
|
||||||
target = reqTarget // Google Container Registry 及其子域名
|
case "ghcr.io":
|
||||||
|
target = ghcrTarget
|
||||||
default:
|
default:
|
||||||
// 如果 reqTarget 包含点, 则假定它是一个完整的域名
|
|
||||||
for _, r := range reqTarget {
|
|
||||||
if r == '.' {
|
|
||||||
target = reqTarget
|
target = reqTarget
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
path = c.GetRequestURI()
|
||||||
|
reqImageUser = c.Param("target")
|
||||||
|
reqImageName = c.Param("user")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 封装镜像信息
|
|
||||||
image := &imageInfo{
|
image := &imageInfo{
|
||||||
User: reqImageUser,
|
User: reqImageUser,
|
||||||
Repo: reqImageName,
|
Repo: reqImageName,
|
||||||
Image: fmt.Sprintf("%s/%s", reqImageUser, reqImageName),
|
Image: fmt.Sprintf("%s/%s", reqImageUser, reqImageName),
|
||||||
}
|
}
|
||||||
|
|
||||||
// 调用 GhcrToTarget 处理实际的代理请求
|
|
||||||
GhcrToTarget(c, cfg, target, path, image)
|
GhcrToTarget(c, cfg, target, path, image)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GhcrToTarget 根据配置和目标信息将请求代理到上游Docker注册表
|
} else {
|
||||||
func GhcrToTarget(c *touka.Context, cfg *config.Config, target string, path string, image *imageInfo) {
|
|
||||||
// 检查Docker代理是否启用
|
|
||||||
if !cfg.Docker.Enabled {
|
|
||||||
ErrorPage(c, NewErrorWithStatusLookup(403, "Docker is not Allowed"))
|
ErrorPage(c, NewErrorWithStatusLookup(403, "Docker is not Allowed"))
|
||||||
return
|
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) {
|
func GhcrRequest(ctx context.Context, c *touka.Context, u string, image *imageInfo, cfg *config.Config, target string) {
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -137,30 +108,23 @@ func GhcrRequest(ctx context.Context, c *touka.Context, u string, image *imageIn
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
// 当请求上下文被取消时, 确保关闭响应和请求体
|
|
||||||
go func() {
|
go func() {
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
if resp != nil && resp.Body != nil {
|
if resp != nil && resp.Body != nil {
|
||||||
_ = resp.Body.Close()
|
resp.Body.Close()
|
||||||
}
|
}
|
||||||
if req != nil && req.Body != nil {
|
if req != nil {
|
||||||
_ = req.Body.Close()
|
req.Body.Close()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
method = c.Request.Method
|
method = c.Request.Method
|
||||||
ghcrclient := c.GetHTTPC()
|
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 := ghcrclient.NewRequestBuilder(method, u)
|
||||||
rb.NoDefaultHeaders() // 不使用默认头部, 以便完全控制
|
rb.NoDefaultHeaders()
|
||||||
rb.SetBody(bytes.NewBuffer(bodyByte)) // 设置请求体
|
rb.SetBody(c.Request.Body)
|
||||||
rb.WithContext(ctx) // 设置请求上下文
|
rb.WithContext(ctx)
|
||||||
|
|
||||||
req, err = rb.Build()
|
req, err = rb.Build()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -168,138 +132,80 @@ func GhcrRequest(ctx context.Context, c *touka.Context, u string, image *imageIn
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 复制客户端请求的头部到代理请求
|
|
||||||
copyHeader(c.Request.Header, req.Header)
|
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)
|
req.Header.Set("Host", target)
|
||||||
|
|
||||||
// 尝试从缓存中获取并使用认证令牌
|
|
||||||
if image != nil {
|
if image != nil {
|
||||||
token, exist := cache.Get(image.Image)
|
token, exist := cache.Get(image.Image)
|
||||||
if exist {
|
if exist {
|
||||||
|
c.Debugf("Use Cache Token: %s", token)
|
||||||
req.Header.Set("Authorization", "Bearer "+token)
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送初始请求
|
|
||||||
resp, err = ghcrclient.Do(req)
|
resp, err = ghcrclient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
|
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理 401 Unauthorized 或 404 Not Found 响应, 尝试重新认证并重试
|
switch resp.StatusCode {
|
||||||
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() // 关闭当前响应体
|
|
||||||
|
|
||||||
if shouldRetry {
|
case 401:
|
||||||
|
// 请求target /v2/路径
|
||||||
|
if string(c.GetRequestURIPath()) != "/v2/" {
|
||||||
|
resp.Body.Close()
|
||||||
if image == nil {
|
if image == nil {
|
||||||
ErrorPage(c, NewErrorWithStatusLookup(originalStatusCode, "Unauthorized"))
|
ErrorPage(c, NewErrorWithStatusLookup(401, "Unauthorized"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 获取新的认证令牌
|
|
||||||
token := ChallengeReq(target, image, ctx, c)
|
token := ChallengeReq(target, image, ctx, c)
|
||||||
|
|
||||||
|
// 更新kv
|
||||||
if token != "" {
|
if token != "" {
|
||||||
c.Debugf("Successfully obtained auth token. Retrying request.")
|
c.Debugf("Update Cache Token: %s", token)
|
||||||
// 重新构建并发送请求
|
cache.Put(image.Image, token)
|
||||||
rb_retry := ghcrclient.NewRequestBuilder(method, u)
|
|
||||||
rb_retry.NoDefaultHeaders()
|
|
||||||
rb_retry.SetBody(bytes.NewBuffer(bodyByte))
|
|
||||||
rb_retry.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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
copyHeader(c.Request.Header, req_retry.Header) // 复制原始头部
|
rb := ghcrclient.NewRequestBuilder(string(method), u)
|
||||||
if acceptHeader, ok := c.Request.Header["Accept"]; ok {
|
rb.NoDefaultHeaders()
|
||||||
req_retry.Header["Accept"] = acceptHeader
|
rb.SetBody(c.Request.Body)
|
||||||
}
|
rb.WithContext(ctx)
|
||||||
|
|
||||||
req_retry.Header.Set("Host", target) // 设置 Host 头部
|
req, err = rb.Build()
|
||||||
req_retry.Header.Set("Authorization", "Bearer "+token) // 使用新令牌
|
|
||||||
|
|
||||||
c.Debugf("Executing retry request. Method: %s, URL: %s", req_retry.Method, req_retry.URL.String())
|
|
||||||
|
|
||||||
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.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 透明地处理 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 {
|
if err != nil {
|
||||||
HandleError(c, fmt.Sprintf("Failed to parse redirect location: %v", err))
|
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果 Location 是相对路径, 则根据原始请求的 URL 解析为绝对路径
|
copyHeader(c.Request.Header, req.Header)
|
||||||
if !redirectURL.IsAbs() {
|
|
||||||
originalURL := resp.Request.URL
|
req.Header.Set("Host", target)
|
||||||
redirectURL = originalURL.ResolveReference(redirectURL)
|
if token != "" {
|
||||||
c.Debugf("Resolved relative redirect to absolute URL: %s", redirectURL.String())
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Debugf("Handling redirect. Status: %d, Final Location: %s", resp.StatusCode, redirectURL.String())
|
resp, err = ghcrclient.Do(req)
|
||||||
_ = resp.Body.Close() // 关闭当前响应体
|
|
||||||
|
|
||||||
// 创建并发送重定向请求, 通常使用 GET 方法
|
|
||||||
redirectReq, err := http.NewRequestWithContext(ctx, "GET", redirectURL.String(), nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
HandleError(c, fmt.Sprintf("Failed to create redirect request: %v", err))
|
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
|
||||||
return
|
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, 则读取响应体并返回自定义错误页面
|
case 404: // 错误处理(404)
|
||||||
if resp.StatusCode == 404 {
|
ErrorPage(c, NewErrorWithStatusLookup(404, "Page Not Found (From Github)"))
|
||||||
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
|
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)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -308,7 +214,6 @@ func GhcrRequest(ctx context.Context, c *touka.Context, u string, image *imageIn
|
||||||
sizelimit int
|
sizelimit int
|
||||||
)
|
)
|
||||||
|
|
||||||
// 获取配置中的大小限制并转换单位 (MB -> Byte)
|
|
||||||
sizelimit = cfg.Server.SizeLimit * 1024 * 1024
|
sizelimit = cfg.Server.SizeLimit * 1024 * 1024
|
||||||
contentLength = resp.Header.Get("Content-Length")
|
contentLength = resp.Header.Get("Content-Length")
|
||||||
if contentLength != "" {
|
if contentLength != "" {
|
||||||
|
|
@ -316,85 +221,77 @@ func GhcrRequest(ctx context.Context, c *touka.Context, u string, image *imageIn
|
||||||
bodySize, err = strconv.Atoi(contentLength)
|
bodySize, err = strconv.Atoi(contentLength)
|
||||||
if err != nil {
|
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)
|
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 // 无法解析则设置为 -1
|
bodySize = -1
|
||||||
}
|
}
|
||||||
// 如果内容大小超出限制, 返回 301 重定向到原始上游URL
|
|
||||||
if err == nil && bodySize > sizelimit {
|
if err == nil && bodySize > sizelimit {
|
||||||
finalURL := resp.Request.URL.String()
|
finalURL := resp.Request.URL.String()
|
||||||
_ = resp.Body.Close() // 关闭响应体
|
err = resp.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
c.Errorf("Failed to close response body: %v", err)
|
||||||
|
}
|
||||||
c.Redirect(301, finalURL)
|
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)
|
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
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将上游响应头部复制到客户端响应
|
|
||||||
c.SetHeaders(resp.Header)
|
c.SetHeaders(resp.Header)
|
||||||
// 设置客户端响应状态码
|
|
||||||
c.Status(resp.StatusCode)
|
c.Status(resp.StatusCode)
|
||||||
|
|
||||||
bodyReader := resp.Body
|
bodyReader := resp.Body
|
||||||
|
|
||||||
// 如果启用了带宽限制, 则使用限速读取器
|
|
||||||
if cfg.RateLimit.BandwidthLimit.Enabled {
|
if cfg.RateLimit.BandwidthLimit.Enabled {
|
||||||
bodyReader = limitreader.NewRateLimitedReader(bodyReader, bandwidthLimit, int(bandwidthBurst), ctx)
|
bodyReader = limitreader.NewRateLimitedReader(bodyReader, bandwidthLimit, int(bandwidthBurst), ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据 Content-Length 设置响应体流
|
|
||||||
if contentLength != "" {
|
if contentLength != "" {
|
||||||
c.SetBodyStream(bodyReader, bodySize)
|
c.SetBodyStream(bodyReader, bodySize)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.SetBodyStream(bodyReader, -1) // Content-Length 未知
|
c.SetBodyStream(bodyReader, -1)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthToken 用于解析认证响应中的令牌
|
|
||||||
type AuthToken struct {
|
type AuthToken struct {
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChallengeReq 执行认证挑战流程, 获取新的认证令牌
|
|
||||||
func ChallengeReq(target string, image *imageInfo, ctx context.Context, c *touka.Context) (token string) {
|
func ChallengeReq(target string, image *imageInfo, ctx context.Context, c *touka.Context) (token string) {
|
||||||
var resp401 *http.Response
|
var resp401 *http.Response
|
||||||
var req401 *http.Request
|
var req401 *http.Request
|
||||||
var err error
|
var err error
|
||||||
ghcrclient := c.GetHTTPC()
|
ghcrclient := c.GetHTTPC()
|
||||||
|
|
||||||
// 对 /v2/ 端点发送 GET 请求以触发认证挑战
|
|
||||||
rb401 := ghcrclient.NewRequestBuilder("GET", "https://"+target+"/v2/")
|
rb401 := ghcrclient.NewRequestBuilder("GET", "https://"+target+"/v2/")
|
||||||
rb401.NoDefaultHeaders()
|
rb401.NoDefaultHeaders()
|
||||||
rb401.WithContext(ctx)
|
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()
|
req401, err = rb401.Build()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
|
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
req401.Header.Set("Host", target) // 设置 Host 头部
|
req401.Header.Set("Host", target)
|
||||||
|
|
||||||
resp401, err = ghcrclient.Do(req401)
|
resp401, err = ghcrclient.Do(req401)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
|
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer func() {
|
defer resp401.Body.Close()
|
||||||
_ = resp401.Body.Close() // 确保响应体关闭
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 解析 Www-Authenticate 头部, 获取认证领域和参数
|
|
||||||
bearer, err := parseBearerWWWAuthenticateHeader(resp401.Header.Get("Www-Authenticate"))
|
bearer, err := parseBearerWWWAuthenticateHeader(resp401.Header.Get("Www-Authenticate"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Errorf("Failed to parse Www-Authenticate header: %v", err)
|
c.Errorf("Failed to parse Www-Authenticate header: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构建认证范围 (scope), 通常是 repository:<image_name>:pull
|
|
||||||
scope := fmt.Sprintf("repository:%s:pull", image.Image)
|
scope := fmt.Sprintf("repository:%s:pull", image.Image)
|
||||||
|
|
||||||
// 使用解析到的 Realm 和 Service, 以及 scope 请求认证令牌
|
|
||||||
getAuthRB := ghcrclient.NewRequestBuilder("GET", bearer.Realm).
|
getAuthRB := ghcrclient.NewRequestBuilder("GET", bearer.Realm).
|
||||||
NoDefaultHeaders().
|
NoDefaultHeaders().
|
||||||
WithContext(ctx).
|
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).
|
SetHeader("Host", bearer.Service).
|
||||||
AddQueryParam("service", bearer.Service).
|
AddQueryParam("service", bearer.Service).
|
||||||
AddQueryParam("scope", scope)
|
AddQueryParam("scope", scope)
|
||||||
|
|
@ -410,25 +307,24 @@ func ChallengeReq(target string, image *imageInfo, ctx context.Context, c *touka
|
||||||
c.Errorf("Failed to send request: %v", err)
|
c.Errorf("Failed to send request: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer func() {
|
|
||||||
_ = authResp.Body.Close() // 确保响应体关闭
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 读取认证响应体
|
defer authResp.Body.Close()
|
||||||
bodyBytes, err := iox.ReadAll(authResp.Body)
|
|
||||||
|
bodyBytes, err := io.ReadAll(authResp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Errorf("Failed to read auth response body: %v", err)
|
c.Errorf("Failed to read auth response body: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解码 JSON 响应以获取令牌
|
// 解码json
|
||||||
var authToken AuthToken
|
var authToken AuthToken
|
||||||
err = json.Unmarshal(bodyBytes, &authToken)
|
err = json.Unmarshal(bodyBytes, &authToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Errorf("Failed to decode auth response body: %v", err)
|
c.Errorf("Failed to decode auth response body: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
token = authToken.Token // 提取令牌
|
token = authToken.Token
|
||||||
|
|
||||||
return token
|
return token
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue