diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f96690..05800b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # 更新日志 +3.2.0 - 2025-04-27 +--- +- CHANGE: 加入`ghcr`和`dockerhub`反代功能 +- FIX: 修复在`HertZ`路由匹配器下与用户名相关功能异常的问题 + +25w31a - 2025-04-27 +--- +- PRE-RELEASE: 此版本是v3.2.0预发布版本,请勿在生产环境中使用; +- CHANGE: 加入`ghcr`和`dockerhub`反代功能 +- FIX: 修复在`HertZ`路由匹配器下与用户名相关功能异常的问题 + 3.1.0 - 2025-04-24 --- - CHANGE: 对标准url使用`HertZ`路由匹配器, 而不是自制匹配器, 以提升效率 diff --git a/DEV-VERSION b/DEV-VERSION index 1fecf83..b7b50a1 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w30e \ No newline at end of file +25w31a \ No newline at end of file diff --git a/README.md b/README.md index 0209413..871c644 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,11 @@ # GHProxy -![pull](https://img.shields.io/docker/pulls/wjqserver/ghproxy.svg)![Docker Image Size (tag)](https://img.shields.io/docker/image-size/wjqserver/ghproxy/latest)[![Go Report Card](https://goreportcard.com/badge/github.com/WJQSERVER-STUDIO/ghproxy)](https://goreportcard.com/report/github.com/WJQSERVER-STUDIO/ghproxy) +![GitHub Release](https://img.shields.io/github/v/release/WJQSERVER-STUDIO/ghproxy?display_name=tag&style=flat) +![pull](https://img.shields.io/docker/pulls/wjqserver/ghproxy.svg) +![Docker Image Size (tag)](https://img.shields.io/docker/image-size/wjqserver/ghproxy/latest) +![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/WJQSERVER-STUDIO/ghproxy) +[![Go Report Card](https://goreportcard.com/badge/github.com/WJQSERVER-STUDIO/ghproxy)](https://goreportcard.com/report/github.com/WJQSERVER-STUDIO/ghproxy) + 支持 Git clone、raw、releases的 Github 加速项目, 支持自托管的同时带来卓越的性能与极低的资源占用(Golang和HertZ带来的优势), 同时支持多种额外功能 diff --git a/VERSION b/VERSION index a0cd9f0..a4f52a5 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.1.0 \ No newline at end of file +3.2.0 \ No newline at end of file diff --git a/config/config.go b/config/config.go index 984dc9e..ed62121 100644 --- a/config/config.go +++ b/config/config.go @@ -18,6 +18,7 @@ type Config struct { Whitelist WhitelistConfig RateLimit RateLimitConfig Outbound OutboundConfig + Docker DockerConfig } /* @@ -143,6 +144,16 @@ type OutboundConfig struct { Url string `toml:"url"` } +/* +[docker] +enabled = false +target = "ghcr" # ghcr/dockerhub +*/ +type DockerConfig struct { + Enabled bool `toml:"enabled"` + Target string `toml:"target"` +} + // LoadConfig 从 TOML 配置文件加载配置 func LoadConfig(filePath string) (*Config, error) { if !FileExists(filePath) { @@ -244,5 +255,9 @@ func DefaultConfig() *Config { Enabled: false, Url: "socks5://127.0.0.1:1080", }, + Docker: DockerConfig{ + Enabled: false, + Target: "ghcr", + }, } } diff --git a/config/config.toml b/config/config.toml index ac1ee59..fc47ef1 100644 --- a/config/config.toml +++ b/config/config.toml @@ -58,4 +58,8 @@ burst = 5 [outbound] enabled = false -url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890" \ No newline at end of file +url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890" + +[docker] +enabled = false +target = "ghcr" # ghcr/dockerhub \ No newline at end of file diff --git a/deploy/config.toml b/deploy/config.toml index b88e30b..eb6d7b8 100644 --- a/deploy/config.toml +++ b/deploy/config.toml @@ -58,3 +58,7 @@ burst = 5 [outbound] enabled = false url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890" + +[docker] +enabled = false +target = "ghcr" # ghcr/dockerhub \ No newline at end of file diff --git a/docs/config.md b/docs/config.md index bad8c65..1a91225 100644 --- a/docs/config.md +++ b/docs/config.md @@ -70,6 +70,10 @@ burst = 5 [outbound] enabled = false url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890" + +[docker] +enabled = false +target = "ghcr" # ghcr/dockerhub ``` ### 配置项详细说明 @@ -295,6 +299,21 @@ url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890" * 支持协议: `socks5://` 和 `http://` * 说明: 设置出站代理服务器的 URL。支持 SOCKS5 和 HTTP 代理协议。 +* **`[docker]` - Docker 镜像代理配置** + + * `enabled`: 是否启用 Docker 镜像代理功能。 + * 类型: 布尔值 (`bool`) + * 默认值: `false` (禁用) + * 说明: 当设置为 `true` 时,`ghproxy` 将尝试代理 Docker 镜像的下载请求,以加速从 GitHub Container Registry (GHCR) 或 Docker Hub 下载镜像。 + + * `target`: 代理的目标 Docker 注册表。 + * 类型: 字符串 (`string`) + * 默认值: `"ghcr"` (代理 GHCR) + * 可选值: `"ghcr"` 或 `"dockerhub"` + * 说明: 指定要代理的 Docker 注册表。 + * `"ghcr"`: 代理 GitHub Container Registry (ghcr.io)。 + * `"dockerhub"`: 代理 Docker Hub (docker.io)。 + ## `blacklist.json` - 黑名单配置 `blacklist.json` 文件用于配置黑名单规则,阻止对特定用户或仓库的访问。 diff --git a/go.mod b/go.mod index 1820f16..4144ee8 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/WJQSERVER-STUDIO/go-utils/logger v1.5.0 github.com/cloudwego/hertz v0.9.7 github.com/hertz-contrib/http2 v0.1.8 - github.com/satomitouka/touka-httpc v0.4.0 + github.com/satomitouka/touka-httpc v0.4.1 golang.org/x/net v0.39.0 golang.org/x/time v0.11.0 ) @@ -36,3 +36,5 @@ require ( golang.org/x/text v0.24.0 // indirect google.golang.org/protobuf v1.36.6 // indirect ) + +//replace github.com/satomitouka/touka-httpc v0.4.1 => /data/github/satomitoka/touka-httpc diff --git a/go.sum b/go.sum index b6da4cc..47257b7 100644 --- a/go.sum +++ b/go.sum @@ -50,8 +50,8 @@ github.com/nyaruka/phonenumbers v1.6.1 h1:XAJcTdYow16VrVKfglznMpJZz8KMJoMjx/91sX github.com/nyaruka/phonenumbers v1.6.1/go.mod h1:7gjs+Lchqm49adhAKB5cdcng5ZXgt6x7Jgvi0ZorUtU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/satomitouka/touka-httpc v0.4.0 h1:cnOONdyJHJImMY8L64bvYF+7Ow/5CPf2Yr3RQRRMZOU= -github.com/satomitouka/touka-httpc v0.4.0/go.mod h1:sNXyW5XBufkwB9ZJ+PIlgN/6xiJ7aZV1fWGrXR0u3bA= +github.com/satomitouka/touka-httpc v0.4.1 h1:K1LJwSJJKRPkol6MPOEzc8bReAIUqxVuzdFfTAi/2AI= +github.com/satomitouka/touka-httpc v0.4.1/go.mod h1:E1JeXw81XclzvlqVvSio/GcDmvN8wWLPpbNRN42Uwfc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= diff --git a/main.go b/main.go index 5314f31..fe6039d 100644 --- a/main.go +++ b/main.go @@ -415,50 +415,54 @@ func main() { setupApi(cfg, r, version) setupPages(cfg, r) - r.GET("/github.com/:username/:repo/releases/*filepath", func(ctx context.Context, c *app.RequestContext) { + r.GET("/github.com/:user/:repo/releases/*filepath", func(ctx context.Context, c *app.RequestContext) { c.Set("matcher", "release") proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) }) - r.GET("/github.com/:username/:repo/archive/*filepath", func(ctx context.Context, c *app.RequestContext) { + r.GET("/github.com/:user/:repo/archive/*filepath", func(ctx context.Context, c *app.RequestContext) { c.Set("matcher", "release") proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) }) - r.GET("/github.com/:username/:repo/blob/*filepath", func(ctx context.Context, c *app.RequestContext) { + r.GET("/github.com/:user/:repo/blob/*filepath", func(ctx context.Context, c *app.RequestContext) { c.Set("matcher", "blob") proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) }) - r.GET("/github.com/:username/:repo/raw/*filepath", func(ctx context.Context, c *app.RequestContext) { + r.GET("/github.com/:user/:repo/raw/*filepath", func(ctx context.Context, c *app.RequestContext) { c.Set("matcher", "raw") proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) }) - r.GET("/github.com/:username/:repo/info/*filepath", func(ctx context.Context, c *app.RequestContext) { + r.GET("/github.com/:user/:repo/info/*filepath", func(ctx context.Context, c *app.RequestContext) { c.Set("matcher", "gitclone") proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) }) - r.GET("/github.com/:username/:repo/git-upload-pack", func(ctx context.Context, c *app.RequestContext) { + r.GET("/github.com/:user/:repo/git-upload-pack", func(ctx context.Context, c *app.RequestContext) { c.Set("matcher", "gitclone") proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) }) - r.GET("/raw.githubusercontent.com/:username/:repo/*filepath", func(ctx context.Context, c *app.RequestContext) { + r.GET("/raw.githubusercontent.com/:user/:repo/*filepath", func(ctx context.Context, c *app.RequestContext) { c.Set("matcher", "raw") proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) }) - r.GET("/gist.githubusercontent.com/:username/*filepath", func(ctx context.Context, c *app.RequestContext) { + r.GET("/gist.githubusercontent.com/:user/*filepath", func(ctx context.Context, c *app.RequestContext) { c.Set("matcher", "gist") proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c) }) - r.GET("/api.github.com/repos/:username/:repo/*filepath", func(ctx context.Context, c *app.RequestContext) { + r.GET("/api.github.com/repos/:user/:repo/*filepath", func(ctx context.Context, c *app.RequestContext) { c.Set("matcher", "api") proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) }) + r.Any("/v2/*filepath", func(ctx context.Context, c *app.RequestContext) { + proxy.GhcrRouting(cfg)(ctx, c) + }) + r.NoRoute(func(ctx context.Context, c *app.RequestContext) { proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c) }) diff --git a/proxy/chunkreq.go b/proxy/chunkreq.go index f68b07f..13be45a 100644 --- a/proxy/chunkreq.go +++ b/proxy/chunkreq.go @@ -52,7 +52,6 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c } setRequestHeaders(c, req) - //removeWSHeader(req) // 删除Conection Upgrade头, 避免与HTTP/2冲突(检查是否存在Upgrade头) AuthPassThrough(c, cfg, req) resp, err = client.Do(req) diff --git a/proxy/docker.go b/proxy/docker.go new file mode 100644 index 0000000..6347f24 --- /dev/null +++ b/proxy/docker.go @@ -0,0 +1,115 @@ +package proxy + +import ( + "context" + "fmt" + "ghproxy/config" + "net/http" + "strconv" + + "github.com/cloudwego/hertz/pkg/app" +) + +func GhcrRouting(cfg *config.Config) app.HandlerFunc { + return func(ctx context.Context, c *app.RequestContext) { + if cfg.Docker.Enabled { + if cfg.Docker.Target == "ghcr" { + GhcrRequest(ctx, c, "https://ghcr.io"+string(c.Request.RequestURI()), cfg, "ghcr") + } else if cfg.Docker.Target == "dockerhub" { + GhcrRequest(ctx, c, "https://registry-1.docker.io"+string(c.Request.RequestURI()), cfg, "dockerhub") + } else { + ErrorPage(c, NewErrorWithStatusLookup(403, "Docker Target is not Allowed")) + return + } + } else { + ErrorPage(c, NewErrorWithStatusLookup(403, "Docker is not Allowed")) + return + } + } +} + +func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *config.Config, matcher string) { + + var ( + method []byte + req *http.Request + resp *http.Response + err error + ) + + method = c.Request.Method() + + rb := client.NewRequestBuilder(string(method), u) + rb.NoDefaultHeaders() + rb.SetBody(c.Request.BodyStream()) + + //req, err = client.NewRequest(string(method), u, c.Request.BodyStream()) + req, err = rb.Build() + if err != nil { + HandleError(c, fmt.Sprintf("Failed to create request: %v", err)) + return + } + + c.Request.Header.VisitAll(func(key, value []byte) { + headerKey := string(key) + headerValue := string(value) + req.Header.Add(headerKey, headerValue) + }) + + resp, err = client.Do(req) + if err != nil { + HandleError(c, fmt.Sprintf("Failed to send request: %v", err)) + return + } + + // 错误处理(404) + if resp.StatusCode == 404 { + ErrorPage(c, NewErrorWithStatusLookup(404, "Page Not Found (From Github)")) + return + } + + var ( + bodySize int + contentLength string + sizelimit int + ) + + sizelimit = cfg.Server.SizeLimit * 1024 * 1024 + contentLength = resp.Header.Get("Content-Length") + if contentLength != "" { + var err error + bodySize, err = strconv.Atoi(contentLength) + if err != nil { + logWarning("%s %s %s %s %s Content-Length header is not a valid integer: %v", c.ClientIP(), c.Method(), c.Path(), c.UserAgent(), c.Request.Header.GetProtocol(), err) + bodySize = -1 + } + if err == nil && bodySize > sizelimit { + var finalURL string + finalURL = resp.Request.URL.String() + err = resp.Body.Close() + if err != nil { + logError("Failed to close response body: %v", err) + } + c.Redirect(301, []byte(finalURL)) + logWarning("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Method(), c.Path(), c.UserAgent(), c.Request.Header.GetProtocol(), finalURL, bodySize) + return + } + } + + // 复制响应头,排除需要移除的 header + for key, values := range resp.Header { + for _, value := range values { + //c.Header(key, value) + c.Response.Header.Add(key, value) + } + } + + c.Status(resp.StatusCode) + + if contentLength != "" { + c.SetBodyStream(resp.Body, bodySize) + return + } + c.SetBodyStream(resp.Body, -1) + +}