diff --git a/CHANGELOG.md b/CHANGELOG.md index 90c5047..7e9a3ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,5 @@ # 更新日志 -4.1.7 - 2025-07-20 ---- -- CHANGE: 更新相关依赖 -- CHANGE: 改进代码结构, 完善处理 - -4.1.7-rc.0 - 2025-07-20 ---- -- PRE-RELEASE: v4.1.7-rc.0是v4.1.7预发布版本,请勿在生产环境中使用; -- CHANGE: 更新相关依赖 -- CHANGE: 改进代码结构, 完善处理 - 4.1.6 - 2025-07-07 --- - CHANGE: 更新[Touka框架](https://github.com/infinite-iroha/touka)版本到`v0.2.9`, 提升`io`相关方式的性能并降低分配 diff --git a/DEV-VERSION b/DEV-VERSION index 0f725cf..b56a9d3 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -4.1.7-rc.0 \ No newline at end of file +4.1.6-rc.0 \ No newline at end of file diff --git a/VERSION b/VERSION index a4428cf..561ad33 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.1.7 \ No newline at end of file +4.1.6 diff --git a/config/config.go b/config/config.go index 0e59f0e..d88daa0 100644 --- a/config/config.go +++ b/config/config.go @@ -60,14 +60,12 @@ type HttpcConfig struct { [gitclone] mode = "bypass" # bypass / cache smartGitAddr = "http://127.0.0.1:8080" -//cacheTimeout = 10 ForceH2C = true */ type GitCloneConfig struct { Mode string `toml:"mode"` SmartGitAddr string `toml:"smartGitAddr"` - //CacheTimeout int `toml:"cacheTimeout"` - ForceH2C bool `toml:"ForceH2C"` + ForceH2C bool `toml:"ForceH2C"` } /* @@ -177,11 +175,10 @@ user1 = "testpass" test = "test123" */ type DockerConfig struct { - Enabled bool `toml:"enabled"` - Target string `toml:"target"` - Auth bool `toml:"auth"` - Credentials map[string]string `toml:"credentials"` - AuthPassThrough bool `toml:"authPassThrough"` + Enabled bool `toml:"enabled"` + Target string `toml:"target"` + Auth bool `toml:"auth"` + Credentials map[string]string `toml:"credentials"` } // LoadConfig 从 TOML 配置文件加载配置 diff --git a/go.mod b/go.mod index 1d5ff59..23f27ac 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,11 @@ module ghproxy -go 1.24.5 +go 1.24.4 require ( github.com/BurntSushi/toml v1.5.0 - github.com/WJQSERVER-STUDIO/httpc v0.8.1 - golang.org/x/net v0.42.0 + github.com/WJQSERVER-STUDIO/httpc v0.8.0 + golang.org/x/net v0.41.0 golang.org/x/time v0.12.0 ) @@ -16,12 +16,12 @@ require ( github.com/fenthope/reco v0.0.3 github.com/fenthope/record v0.0.3 github.com/hashicorp/golang-lru/v2 v2.0.7 - github.com/infinite-iroha/touka v0.3.1 + github.com/infinite-iroha/touka v0.2.9 github.com/wjqserver/modembed v0.0.1 ) require ( github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.6 // indirect - github.com/go-json-experiment/json v0.0.0-20250714165856-be8212f5270d // indirect + github.com/go-json-experiment/json v0.0.0-20250626171732-1a886bd29d1b // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect ) diff --git a/go.sum b/go.sum index 3347216..e93fac2 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.6 h1:/50VJYXd6jcu+p5BnEBDyiX0nAy github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.6/go.mod h1:FZ6XE+4TKy4MOfX1xWKe6Rwsg0ucYFCdNh1KLvyKTfc= github.com/WJQSERVER-STUDIO/go-utils/limitreader v0.0.2 h1:8bBkKk6E2Zr+I5szL7gyc5f0DK8N9agIJCpM1Cqw2NE= github.com/WJQSERVER-STUDIO/go-utils/limitreader v0.0.2/go.mod h1:yPX8xuZH+py7eLJwOYj3VVI/4/Yuy5+x8Mhq8qezcPg= -github.com/WJQSERVER-STUDIO/httpc v0.8.1 h1:/eG8aYKL3WfQILIRbG+cbzQjPkNHEPTqfGUdQS5rtI4= -github.com/WJQSERVER-STUDIO/httpc v0.8.1/go.mod h1:mxXBf2hqbQGNHkVy/7wfU7Xi2s09MyZpbY2hyR+4uD4= +github.com/WJQSERVER-STUDIO/httpc v0.8.0 h1:G7inJ5EEsg5+BkeFiNIo/6+Mj7Ygiq85yMT3Ld7frJY= +github.com/WJQSERVER-STUDIO/httpc v0.8.0/go.mod h1:50297rvgppmgPbZEtWzTWgkomoqPREkGy9T3Y/NqN7o= github.com/fenthope/bauth v0.0.1 h1:+4UIQshGx3mYD4L3f2S4MLZOi5PWU7fU5GK3wsZvwzE= github.com/fenthope/bauth v0.0.1/go.mod h1:1fveTpgfR1p+WXQ8MXm9BfBCeNYi55j23jxCOGOvBSA= github.com/fenthope/ikumi v0.0.2 h1:5oaSTf/Msp7M2O3o/X20omKWEQbFhX4KV0CVF21oCdk= @@ -14,17 +14,17 @@ github.com/fenthope/reco v0.0.3 h1:RmnQ0D9a8PWtwOODawitTe4BztTnS9wYwrDbipISNq4= github.com/fenthope/reco v0.0.3/go.mod h1:mDkGLHte5udWTIcjQTxrABRcf56SSdxBOCLgrRDwI/Y= github.com/fenthope/record v0.0.3 h1:v5urgs5LAkLMlljAT/MjW8fWuRHXPnAraTem5ui7rm4= github.com/fenthope/record v0.0.3/go.mod h1:KFEkSc4TDZ3QIhP/wglD32uYVA6X1OUcripiao1DEE4= -github.com/go-json-experiment/json v0.0.0-20250714165856-be8212f5270d h1:+d6m5Bjvv0/RJct1VcOw2P5bvBOGjENmxORJYnSYDow= -github.com/go-json-experiment/json v0.0.0-20250714165856-be8212f5270d/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= +github.com/go-json-experiment/json v0.0.0-20250626171732-1a886bd29d1b h1:ooF9/NzXkXL3OOLRwtPuQT/D7Kx2S5w/Kl1GnMF9h2s= +github.com/go-json-experiment/json v0.0.0-20250626171732-1a886bd29d1b/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/infinite-iroha/touka v0.3.1 h1:djR9hg5MbVpT1dIz2GWo4MZ/kx3l6bJ4nrpzpvdi3uk= -github.com/infinite-iroha/touka v0.3.1/go.mod h1:pHOYHE4AKoQ1KikHF9JYKIJ4he8um1MzgcddscjCeyg= +github.com/infinite-iroha/touka v0.2.9 h1:Ugu0H3Zdip/ZnDbaCXquxsWnntByCUDBONez1oZANaU= +github.com/infinite-iroha/touka v0.2.9/go.mod h1:Cmok9Xs8yNRNEUSqiZfi3xtdO1UZYw/yP+phf+zjH2Y= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/wjqserver/modembed v0.0.1 h1:8ZDz7t9M5DLrUFlYgBUUmrMzxWsZPmHvOazkr/T2jEs= github.com/wjqserver/modembed v0.0.1/go.mod h1:sYbQJMAjSBsdYQrUsuHY380XXE1CuRh8g9yyCztTXOQ= -golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= -golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= diff --git a/main.go b/main.go index fac8fb4..4fa4019 100644 --- a/main.go +++ b/main.go @@ -337,7 +337,6 @@ func main() { r.Use(touka.Recovery()) // Recovery中间件 r.SetLogger(logger) - r.SetErrorHandler(proxy.UnifiedToukaErrorHandler) r.SetHTTPClient(httpClient) r.Use(record.Middleware()) // log中间件 r.Use(viaHeader()) @@ -368,6 +367,7 @@ func main() { } setupApi(cfg, r, version) setupPages(cfg, r) + //r.RedirectTrailingSlash = false r.SetRedirectTrailingSlash(false) r.GET("/github.com/:user/:repo/releases/*filepath", func(c *touka.Context) { @@ -443,6 +443,12 @@ func main() { proxy.GhcrWithImageRouting(cfg)(c) }) + /* + r.Any("/v2/:target/*filepath", func( c *touka.Context) { + proxy.GhcrRouting(cfg)(c) + }) + */ + r.NoRoute(func(c *touka.Context) { proxy.NoRouteHandler(cfg)(c) }) diff --git a/proxy/docker.go b/proxy/docker.go index 1f707db..cdcff70 100644 --- a/proxy/docker.go +++ b/proxy/docker.go @@ -48,12 +48,12 @@ func GhcrWithImageRouting(cfg *config.Config) touka.HandlerFunc { target := "" if strings.ContainsRune(reqTarget, charToFind) { - switch reqTarget { - case "docker.io": + + if reqTarget == "docker.io" { target = dockerhubTarget - case "ghcr.io": + } else if reqTarget == "ghcr.io" { target = ghcrTarget - default: + } else { target = reqTarget } } else { @@ -132,6 +132,11 @@ func GhcrRequest(ctx context.Context, c *touka.Context, u string, image *imageIn return } + //c.Request.Header.VisitAll(func(key, value []byte) { + // headerKey := string(key) + // headerValue := string(value) + // req.Header.Add(headerKey, headerValue) + //}) copyHeader(c.Request.Header, req.Header) req.Header.Set("Host", target) @@ -149,9 +154,8 @@ func GhcrRequest(ctx context.Context, c *touka.Context, u string, image *imageIn return } - switch resp.StatusCode { - - case 401: + // 处理状态码 + if resp.StatusCode == 401 { // 请求target /v2/路径 if string(c.GetRequestURIPath()) != "/v2/" { resp.Body.Close() @@ -177,7 +181,13 @@ func GhcrRequest(ctx context.Context, c *touka.Context, u string, image *imageIn 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) + }) + */ copyHeader(c.Request.Header, req.Header) req.Header.Set("Host", target) @@ -192,20 +202,9 @@ func GhcrRequest(ctx context.Context, c *touka.Context, u string, image *imageIn } } - case 404: // 错误处理(404) + } else if resp.StatusCode == 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) - return - } } var ( @@ -235,6 +234,14 @@ func GhcrRequest(ctx context.Context, c *touka.Context, u string, image *imageIn } } + // 复制响应头,排除需要移除的 header + /* + for key, values := range resp.Header { + for _, value := range values { + c.Response.Header.Add(key, value) + } + } + */ c.SetHeaders(resp.Header) c.Status(resp.StatusCode) diff --git a/proxy/error.go b/proxy/error.go index 222f382..be4c578 100644 --- a/proxy/error.go +++ b/proxy/error.go @@ -20,19 +20,6 @@ func HandleError(c *touka.Context, message string) { c.Errorf("%s %s %s %s %s Error: %v", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.UserAgent(), c.Request.Proto, message) } -func UnifiedToukaErrorHandler(c *touka.Context, code int, err error) { - - errMsg := "" - if err != nil { - errMsg = err.Error() - } - c.Errorf("%s %s %s %s %s Error: %v", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.UserAgent(), c.Request.Proto, errMsg) - - constructedGHErr := NewErrorWithStatusLookup(code, errMsg) - - ErrorPage(c, constructedGHErr) -} - type GHProxyErrors struct { StatusCode int StatusDesc string @@ -78,25 +65,6 @@ var ( StatusText: "服务器内部错误", HelpInfo: "服务器处理您的请求时发生错误,请稍后重试或联系管理员。", } - // 502 - ErrBadGateway = &GHProxyErrors{ - StatusCode: 502, - StatusDesc: "Bad Gateway", - StatusText: "网关错误", - HelpInfo: "代理服务器从上游服务器接收到无效响应。", - } - ErrServiceUnavailable = &GHProxyErrors{ - StatusCode: 503, - StatusDesc: "Service Unavailable", - StatusText: "服务不可用", - HelpInfo: "服务器目前无法处理请求,通常是由于服务器过载或停机维护。", - } - ErrGatewayTimeout = &GHProxyErrors{ - StatusCode: 504, - StatusDesc: "Gateway Timeout", - StatusText: "网关超时", - HelpInfo: "代理服务器未能及时从上游服务器接收到响应。", - } ) var statusErrorMap map[int]*GHProxyErrors @@ -109,9 +77,6 @@ func init() { ErrNotFound.StatusCode: ErrNotFound, ErrTooManyRequests.StatusCode: ErrTooManyRequests, ErrInternalServerError.StatusCode: ErrInternalServerError, - ErrBadGateway.StatusCode: ErrBadGateway, - ErrServiceUnavailable.StatusCode: ErrServiceUnavailable, - ErrGatewayTimeout.StatusCode: ErrGatewayTimeout, } } @@ -204,11 +169,11 @@ func NewSizedLRUCache(maxBytes int64) (*SizedLRUCache, error) { // 当内部 LRU 缓存因其自身的条目容量限制或 RemoveOldest 方法被调用而逐出条目时, // 此回调函数会被执行,从而更新 currentBytes。 var err error - //c.cache, err = lru.NewWithEvict[string, []byte](10000, func(key string, value []byte) { - c.cache, err = lru.NewWithEvict(10000, func(key string, value []byte) { + c.cache, err = lru.NewWithEvict[string, []byte](10000, func(key string, value []byte) { c.mu.Lock() defer c.mu.Unlock() c.currentBytes -= int64(len(value)) + //logDebug("LRU evicted key: %s, size: %d, current total: %d", key, len(value), c.currentBytes) }) if err != nil { return nil, err @@ -230,6 +195,7 @@ func (c *SizedLRUCache) Add(key string, value []byte) { // 如果待添加的条目本身就大于缓存的最大容量,则不进行缓存。 if itemSize > c.maxBytes { + //c.Warnf("Item key %s (size %d) larger than cache max capacity %d. Not caching.", key, itemSize, c.maxBytes) return } @@ -237,19 +203,23 @@ func (c *SizedLRUCache) Add(key string, value []byte) { if oldVal, ok := c.cache.Get(key); ok { c.currentBytes -= int64(len(oldVal)) c.cache.Remove(key) + //logDebug("Key %s exists, removed old size %d. Current total: %d", key, len(oldVal), c.currentBytes) } // 主动逐出最旧的条目,直到有足够的空间容纳新条目。 for c.currentBytes+itemSize > c.maxBytes && c.cache.Len() > 0 { _, _, existed := c.cache.RemoveOldest() if !existed { + //c.Warnf("Attempted to remove oldest, but item not found.") break } + //logDebug("Proactively evicted item (size %d) to free space. Current total: %d", len(oldVal), c.currentBytes) } // 添加新条目到内部 LRU 缓存。 c.cache.Add(key, value) c.currentBytes += itemSize // 手动增加新条目的大小到 currentBytes。 + //logDebug("Item added: key %s, size: %d, current total: %d", key, itemSize, c.currentBytes) } const maxErrorPageCacheBytes = 512 * 1024 // 错误页面缓存的最大容量:512KB @@ -261,6 +231,7 @@ func init() { var err error errorPageCache, err = NewSizedLRUCache(maxErrorPageCacheBytes) if err != nil { + // logError("Failed to initialize error page LRU cache: %v", err) panic(err) } } @@ -312,16 +283,6 @@ func htmlTemplateRender(data interface{}) ([]byte, error) { } func ErrorPage(c *touka.Context, errInfo *GHProxyErrors) { - - select { - case <-c.Request.Context().Done(): - return - default: - if c.Writer.Written() { - return - } - } - // 将 errInfo 转换为 ErrorPageData 结构体 var err error var cacheKey string diff --git a/proxy/gitreq.go b/proxy/gitreq.go index af8e6bc..f007290 100644 --- a/proxy/gitreq.go +++ b/proxy/gitreq.go @@ -30,11 +30,7 @@ func GitReq(ctx context.Context, c *touka.Context, u string, cfg *config.Config, return } // 构建新url - var paramStr string - if len(queryParams) > 0 { - paramStr = "?" + queryParams.Encode() - } - u = cfg.GitClone.SmartGitAddr + userPath + repoPath + remainingPath + paramStr + u = cfg.GitClone.SmartGitAddr + userPath + repoPath + remainingPath + "?" + queryParams.Encode() } if cfg.GitClone.Mode == "cache" { diff --git a/proxy/match.go b/proxy/match.go index a50d018..8050779 100644 --- a/proxy/match.go +++ b/proxy/match.go @@ -28,6 +28,7 @@ func init() { gistPrefixLen = len(gistPrefix) gistContentPrefixLen = len(gistContentPrefix) apiPrefixLen = len(apiPrefix) + //log.Printf("githubPrefixLen: %d, rawPrefixLen: %d, gistPrefixLen: %d, apiPrefixLen: %d", githubPrefixLen, rawPrefixLen, gistPrefixLen, apiPrefixLen) } // Matcher 从原始URL路径中高效地解析并匹配代理规则. @@ -158,6 +159,105 @@ func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHPro return "", "", "", NewErrorWithStatusLookup(404, "no matcher found for the given path") } +// 原实现 +/* +func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHProxyErrors) { + var ( + user string + repo string + matcher string + ) + // 匹配 "https://github.com"开头的链接 + if strings.HasPrefix(rawPath, "https://github.com") { + remainingPath := strings.TrimPrefix(rawPath, "https://github.com") + + //if strings.HasPrefix(remainingPath, "/") { + // remainingPath = strings.TrimPrefix(remainingPath, "/") + //} + + remainingPath = strings.TrimPrefix(remainingPath, "/") + // 预期格式/user/repo/more... + // 取出user和repo和最后部分 + parts := strings.Split(remainingPath, "/") + if len(parts) <= 2 { + errMsg := "Not enough parts in path after matching 'https://github.com*'" + return "", "", "", NewErrorWithStatusLookup(400, errMsg) + } + user = parts[0] + repo = parts[1] + // 匹配 "https://github.com"开头的链接 + if len(parts) >= 3 { + switch parts[2] { + case "releases", "archive": + matcher = "releases" + case "blob": + matcher = "blob" + case "raw": + matcher = "raw" + case "info", "git-upload-pack": + matcher = "clone" + default: + errMsg := "Url Matched 'https://github.com*', but didn't match the next matcher" + return "", "", "", NewErrorWithStatusLookup(400, errMsg) + } + } + return user, repo, matcher, nil + } + // 匹配 "https://raw"开头的链接 + if strings.HasPrefix(rawPath, "https://raw") { + remainingPath := strings.TrimPrefix(rawPath, "https://") + parts := strings.Split(remainingPath, "/") + if len(parts) <= 3 { + errMsg := "URL after matched 'https://raw*' should have at least 4 parts (user/repo/branch/file)." + return "", "", "", NewErrorWithStatusLookup(400, errMsg) + } + user = parts[1] + repo = parts[2] + matcher = "raw" + + return user, repo, matcher, nil + } + // 匹配 "https://gist"开头的链接 + if strings.HasPrefix(rawPath, "https://gist") { + remainingPath := strings.TrimPrefix(rawPath, "https://") + parts := strings.Split(remainingPath, "/") + if len(parts) <= 3 { + errMsg := "URL after matched 'https://gist*' should have at least 4 parts (user/gist_id)." + return "", "", "", NewErrorWithStatusLookup(400, errMsg) + } + user = parts[1] + repo = "" + matcher = "gist" + return user, repo, matcher, nil + } + // 匹配 "https://api.github.com/"开头的链接 + if strings.HasPrefix(rawPath, "https://api.github.com/") { + matcher = "api" + remainingPath := strings.TrimPrefix(rawPath, "https://api.github.com/") + + parts := strings.Split(remainingPath, "/") + if parts[0] == "repos" { + user = parts[1] + repo = parts[2] + } + if parts[0] == "users" { + user = parts[1] + } + if !cfg.Auth.ForceAllowApi { + if cfg.Auth.Method != "header" || !cfg.Auth.Enabled { + //return "", "", "", ErrAuthHeaderUnavailable + errMsg := "AuthHeader Unavailable, Need to open header auth to enable api proxy" + return "", "", "", NewErrorWithStatusLookup(403, errMsg) + } + } + return user, repo, matcher, nil + } + //return "", "", "", ErrNotFound + errMsg := "Didn't match any matcher" + return "", "", "", NewErrorWithStatusLookup(404, errMsg) +} +*/ + var ( proxyableMatchersMap map[string]struct{} initMatchersOnce sync.Once