diff --git a/CHANGELOG.md b/CHANGELOG.md index 979cb00..639dc60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,37 @@ # 更新日志 -3.0.1 -2025-04-08 +3.0.2 - 2025-04-15 +--- +- CHANGE: 避免重复的re编译操作 +- CHANGE: 去除不必要的请求 +- CHANGE: 改进`httpc`相关配置 +- CHANGE: 更新`httpc` 0.4.0 +- CHANGE: 为不遵守`RFC 2616`, `RFC 9112`的客户端带来兼容性改进 + +25w28b - 2025-04-15 +--- +- PRE-RELEASE: 此版本是v3.0.2预发布版本,请勿在生产环境中使用; +- CHANGE: 改进resp关闭 +- CHANGE: 避免重复的re编译操作 + +25w28a - 2025-04-14 +--- +- PRE-RELEASE: 此版本是预发布版本,请勿在生产环境中使用; +- CHANGE: 去除不必要的请求 +- CHANGE: 改进`httpc`相关配置 +- CHANGE: 合入test版本修改 + +25w28t-2 - 2025-04-11 +--- +- TEST: 测试验证版本 +- CHANGE: 为不遵守`RFC 2616`, `RFC 9112`的客户端带来兼容性改进 + +25w28t-1 - 2025-04-11 +--- +- TEST: 测试验证版本 +- CHANGE: 更新httpc 0.4.0 + +3.0.1 - 2025-04-08 --- - CHANGE: 加入`memLimit`指示gc - CHANGE: 加入`hlog`输出路径配置 diff --git a/DEV-VERSION b/DEV-VERSION index 03ab3b3..0d83d4d 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w27a \ No newline at end of file +25w28b \ No newline at end of file diff --git a/README.md b/README.md index a45ced2..0209413 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![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) -使用Go实现的GHProxy,用于加速部分地区Github仓库的拉取,支持速率限制,用户鉴权,支持Docker部署 +支持 Git clone、raw、releases的 Github 加速项目, 支持自托管的同时带来卓越的性能与极低的资源占用(Golang和HertZ带来的优势), 同时支持多种额外功能 ## 项目说明 @@ -16,9 +16,10 @@ - 🚫 **支持自定义黑名单/白名单** - 🗄️ **支持 Git Clone 缓存(配合 [Smart-Git](https://github.com/WJQSERVER-STUDIO/smart-git))** - 🐳 **支持 Docker 部署** +- 🐳 **支持自托管** - ⚡ **支持速率限制** - 🔒 **支持用户鉴权** -- 🐚 **支持 shell 脚本嵌套加速** +- 🐚 **支持 shell 脚本多层嵌套加速** ### 项目相关 diff --git a/VERSION b/VERSION index 13d683c..d9c62ed 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.0.1 \ No newline at end of file +3.0.2 \ No newline at end of file diff --git a/go.mod b/go.mod index 9b03803..2c6514f 100644 --- a/go.mod +++ b/go.mod @@ -5,10 +5,10 @@ go 1.24.2 require ( github.com/BurntSushi/toml v1.5.0 github.com/WJQSERVER-STUDIO/go-utils/logger v1.5.0 - github.com/cloudwego/hertz v0.9.6 + github.com/cloudwego/hertz v0.9.7 github.com/hertz-contrib/http2 v0.1.8 - github.com/satomitouka/touka-httpc v0.3.3 - golang.org/x/net v0.38.0 + github.com/satomitouka/touka-httpc v0.4.0 + golang.org/x/net v0.39.0 golang.org/x/time v0.11.0 ) @@ -31,7 +31,7 @@ require ( github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect golang.org/x/arch v0.16.0 // indirect - golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect + golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect golang.org/x/sys v0.32.0 // indirect golang.org/x/text v0.24.0 // indirect google.golang.org/protobuf v1.36.6 // indirect diff --git a/go.sum b/go.sum index 730be7f..b036c8b 100644 --- a/go.sum +++ b/go.sum @@ -20,8 +20,8 @@ github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCy github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/gopkg v0.1.4 h1:EoQiCG4sTonTPHxOGE0VlQs+sQR+Hsi2uN0qqwu8O50= github.com/cloudwego/gopkg v0.1.4/go.mod h1:FQuXsRWRsSqJLsMVd5SYzp8/Z1y5gXKnVvRrWUOsCMI= -github.com/cloudwego/hertz v0.9.6 h1:Kj5SSPlKBC32NIN7+B/tt8O1pdDz8brMai00rqqjULQ= -github.com/cloudwego/hertz v0.9.6/go.mod h1:X5Ez52XhtszU4t+CTBGIJI4PqmcI1oSf8ULBz0SWfLo= +github.com/cloudwego/hertz v0.9.7 h1:tAVaiO+vTf+ZkQhvNhKbDJ0hmC4oJ7bzwDi1KhvhHy4= +github.com/cloudwego/hertz v0.9.7/go.mod h1:t6d7NcoQxPmETvzPMMIVPHMn5C5QzpqIiFsaavoLJYQ= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/cloudwego/netpoll v0.7.0 h1:bDrxQaNfijRI1zyGgXHQoE/nYegL0nr+ijO1Norelc4= github.com/cloudwego/netpoll v0.7.0/go.mod h1:PI+YrmyS7cIr0+SD4seJz3Eo3ckkXdu2ZVKBLhURLNU= @@ -50,8 +50,8 @@ github.com/nyaruka/phonenumbers v1.6.0 h1:r9ax45fFg+YLUs2X4bNXm5RAxWl00hYjFgNlv3 github.com/nyaruka/phonenumbers v1.6.0/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.3.3 h1:Th0uJ5do3oqqZgdUDtqD1SH11x8TcJmrwHMJQlEIKCg= -github.com/satomitouka/touka-httpc v0.3.3/go.mod h1:sNXyW5XBufkwB9ZJ+PIlgN/6xiJ7aZV1fWGrXR0u3bA= +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/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= @@ -87,8 +87,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= -golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= -golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= +golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= +golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -98,8 +98,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/main.go b/main.go index cd21df2..3cbd127 100644 --- a/main.go +++ b/main.go @@ -26,6 +26,7 @@ import ( "github.com/cloudwego/hertz/pkg/common/adaptor" "github.com/cloudwego/hertz/pkg/common/hlog" + //"github.com/cloudwego/hertz/pkg/network/standard" "github.com/hertz-contrib/http2/factory" ) @@ -146,6 +147,7 @@ func setupHertZLogger(cfg *config.Config) { } else { hlog.SetOutput(hertZfile) } + hlog.SetLevel(hlog.LevelInfo) } } @@ -239,7 +241,6 @@ func setupPages(cfg *config.Config, r *server.Hertz) { r.StaticFile("/style.css", stylesheetsPath) r.StaticFile("/bootstrap.min.css", bootstrapPath) r.StaticFile("/bootstrap.bundle.min.js", bootstrapBundlePath) - //router.StaticFile("/bootstrap.min.css", bootstrapPath) default: // 处理无效的Pages Mode @@ -377,6 +378,8 @@ func main() { r = server.New( server.WithHostPorts(addr), server.WithH2C(true), + // server.WithALPN(true), + // server.WithTransport(standard.NewTransporter), ) r.AddProtocol("h2", factory.NewServerFactory()) } else { diff --git a/proxy/chunkreq.go b/proxy/chunkreq.go index 0212e0d..47f0e59 100644 --- a/proxy/chunkreq.go +++ b/proxy/chunkreq.go @@ -5,7 +5,6 @@ import ( "context" "fmt" "ghproxy/config" - "io" "net/http" "strconv" @@ -15,39 +14,6 @@ import ( func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, cfg *config.Config, matcher string) { method := c.Request.Method - // 发送HEAD请求, 预获取Content-Length - headReq, err := client.NewRequest("HEAD", u, nil) - if err != nil { - HandleError(c, fmt.Sprintf("Failed to create request: %v", err)) - return - } - setRequestHeaders(c, headReq) - removeWSHeader(headReq) // 删除Conection Upgrade头, 避免与HTTP/2冲突(检查是否存在Upgrade头) - AuthPassThrough(c, cfg, headReq) - - headResp, err := client.Do(headReq) - if err != nil { - HandleError(c, fmt.Sprintf("Failed to send request: %v", err)) - return - } - defer func(Body io.ReadCloser) { - if err := Body.Close(); err != nil { - logError("Failed to close response body: %v", err) - } - }(headResp.Body) - - contentLength := headResp.Header.Get("Content-Length") - sizelimit := cfg.Server.SizeLimit * 1024 * 1024 - if contentLength != "" { - size, err := strconv.Atoi(contentLength) - if err == nil && size > sizelimit { - finalURL := headResp.Request.URL.String() - c.Redirect(http.StatusMovedPermanently, []byte(finalURL)) - logWarning("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Method(), c.Path(), c.Request.Header.Get("User-Agent"), c.Request.Header.GetProtocol(), finalURL, size) - return - } - } - body := c.Request.Body() bodyReader := bytes.NewBuffer(body) @@ -69,18 +35,33 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c // 错误处理(404) if resp.StatusCode == 404 { - c.String(http.StatusNotFound, "File Not Found") - //c.Status(http.StatusNotFound) + //c.String(http.StatusNotFound, "File Not Found") + c.Status(http.StatusNotFound) return } + var ( + bodySize int + contentLength string + sizelimit int + ) + sizelimit = cfg.Server.SizeLimit * 1024 * 1024 contentLength = resp.Header.Get("Content-Length") if contentLength != "" { - size, err := strconv.Atoi(contentLength) - if err == nil && size > sizelimit { + 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 { finalURL := resp.Request.URL.String() + err := resp.Body.Close() + if err != nil { + logError("Failed to close response body: %v", err) + } c.Redirect(http.StatusMovedPermanently, []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, size) + 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 } } @@ -132,6 +113,10 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c return } } else { + if contentLength != "" { + c.SetBodyStream(resp.Body, bodySize) + return + } c.SetBodyStream(resp.Body, -1) } diff --git a/proxy/gitreq.go b/proxy/gitreq.go index 0826a58..c4f0cfd 100644 --- a/proxy/gitreq.go +++ b/proxy/gitreq.go @@ -72,6 +72,9 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co if contentLength != "" { size, err := strconv.Atoi(contentLength) sizelimit := cfg.Server.SizeLimit * 1024 * 1024 + 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) + } if err == nil && size > sizelimit { finalURL := []byte(resp.Request.URL.String()) c.Redirect(http.StatusMovedPermanently, finalURL) diff --git a/proxy/handler.go b/proxy/handler.go index 072fd26..f8b998c 100644 --- a/proxy/handler.go +++ b/proxy/handler.go @@ -106,13 +106,15 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra } // 鉴权 - var authcheck bool - authcheck, err = auth.AuthHandler(ctx, c, cfg) - if !authcheck { - //c.AbortWithStatusJSON(401, gin.H{"error": "Unauthorized"}) - c.AbortWithStatusJSON(401, map[string]string{"error": "Unauthorized"}) - logWarning("%s %s %s %s %s Auth-Error: %v", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), err) - return + if cfg.Auth.Enabled { + var authcheck bool + authcheck, err = auth.AuthHandler(ctx, c, cfg) + if !authcheck { + //c.AbortWithStatusJSON(401, gin.H{"error": "Unauthorized"}) + c.AbortWithStatusJSON(401, map[string]string{"error": "Unauthorized"}) + logWarning("%s %s %s %s %s Auth-Error: %v", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), err) + return + } } // IP METHOD URL USERAGENT PROTO MATCHES diff --git a/proxy/httpc.go b/proxy/httpc.go index 117f26b..1f0c6e9 100644 --- a/proxy/httpc.go +++ b/proxy/httpc.go @@ -42,7 +42,6 @@ func initHTTPClient(cfg *config.Config) { if cfg.Httpc.Mode == "auto" { tr = &http.Transport{ - //MaxIdleConns: 160, IdleConnTimeout: 30 * time.Second, WriteBufferSize: 32 * 1024, // 32KB ReadBufferSize: 32 * 1024, // 32KB @@ -64,7 +63,6 @@ func initHTTPClient(cfg *config.Config) { logWarning("use Auto to Run HTTP Client") fmt.Println("use Auto to Run HTTP Client") tr = &http.Transport{ - //MaxIdleConns: 160, IdleConnTimeout: 30 * time.Second, WriteBufferSize: 32 * 1024, // 32KB ReadBufferSize: 32 * 1024, // 32KB @@ -87,23 +85,11 @@ func initHTTPClient(cfg *config.Config) { func initGitHTTPClient(cfg *config.Config) { - var proTolcols = new(http.Protocols) - proTolcols.SetHTTP1(true) - proTolcols.SetHTTP2(true) - proTolcols.SetUnencryptedHTTP2(true) - if cfg.GitClone.ForceH2C { - proTolcols.SetHTTP1(false) - proTolcols.SetHTTP2(false) - proTolcols.SetUnencryptedHTTP2(true) - } if cfg.Httpc.Mode == "auto" { - gittr = &http.Transport{ - //MaxIdleConns: 160, IdleConnTimeout: 30 * time.Second, WriteBufferSize: 32 * 1024, // 32KB ReadBufferSize: 32 * 1024, // 32KB - Protocols: proTolcols, } } else if cfg.Httpc.Mode == "advanced" { gittr = &http.Transport{ @@ -112,7 +98,6 @@ func initGitHTTPClient(cfg *config.Config) { MaxIdleConnsPerHost: cfg.Httpc.MaxIdleConnsPerHost, WriteBufferSize: 32 * 1024, // 32KB ReadBufferSize: 32 * 1024, // 32KB - Protocols: proTolcols, } } else { // 错误的模式 @@ -130,14 +115,39 @@ func initGitHTTPClient(cfg *config.Config) { if cfg.Outbound.Enabled { initTransport(cfg, gittr) } - if cfg.Server.Debug { + if cfg.Server.Debug && cfg.GitClone.ForceH2C { gitclient = httpc.New( httpc.WithTransport(gittr), httpc.WithDumpLog(), + httpc.WithProtocols(httpc.ProtocolsConfig{ + ForceH2C: true, + }), + ) + } else if !cfg.Server.Debug && cfg.GitClone.ForceH2C { + gitclient = httpc.New( + httpc.WithTransport(gittr), + httpc.WithProtocols(httpc.ProtocolsConfig{ + ForceH2C: true, + }), + ) + } else if cfg.Server.Debug && !cfg.GitClone.ForceH2C { + gitclient = httpc.New( + httpc.WithTransport(gittr), + httpc.WithDumpLog(), + httpc.WithProtocols(httpc.ProtocolsConfig{ + Http1: true, + Http2: true, + Http2_Cleartext: true, + }), ) } else { gitclient = httpc.New( httpc.WithTransport(gittr), + httpc.WithProtocols(httpc.ProtocolsConfig{ + Http1: true, + Http2: true, + Http2_Cleartext: true, + }), ) } } diff --git a/proxy/match.go b/proxy/match.go index 71f779a..8e6c0d9 100644 --- a/proxy/match.go +++ b/proxy/match.go @@ -243,6 +243,8 @@ func extractParts(rawURL string) (string, string, string, url.Values, error) { return repoOwner, repoName, remainingPath, queryParams, nil } +var urlPattern = regexp.MustCompile(`https?://[^\s'"]+`) + // processLinks 处理链接,返回包含处理后数据的 io.Reader func processLinks(input io.Reader, compress string, host string, cfg *config.Config) (readerOut io.Reader, written int64, err error) { pipeReader, pipeWriter := io.Pipe() // 创建 io.Pipe @@ -315,7 +317,6 @@ func processLinks(input io.Reader, compress string, host string, cfg *config.Con }() // 使用正则表达式匹配 http 和 https 链接 - urlPattern := regexp.MustCompile(`https?://[^\s'"]+`) for { line, readErr := bufReader.ReadString('\n') if readErr != nil { diff --git a/proxy/reqheader.go b/proxy/reqheader.go index 3421722..01eab46 100644 --- a/proxy/reqheader.go +++ b/proxy/reqheader.go @@ -13,6 +13,9 @@ func setRequestHeaders(c *app.RequestContext, req *http.Request) { }) } +// removeWSHeader removes the "Upgrade" and "Connection" headers from the given +// Request, which are added by the client when it wants to upgrade the +// connection to a WebSocket connection. func removeWSHeader(req *http.Request) { req.Header.Del("Upgrade") req.Header.Del("Connection")