From 11099176bfd226df48c6e608eeb0cff8f7076dcd Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Wed, 21 May 2025 09:03:00 +0800 Subject: [PATCH 01/99] add support for multi-target docker image(oci) proxy --- main.go | 53 +++------ proxy/authparse.go | 62 ++++++++++ proxy/docker.go | 247 ++++++++++++++++++++++++++++++++++++--- proxy/httpc.go | 61 +++++++++- proxy/match.go | 9 +- proxy/utils.go | 6 +- weakcache/weakcache.go | 258 +++++++++++++++++++++++++++++++++++++++++ 7 files changed, 633 insertions(+), 63 deletions(-) create mode 100644 proxy/authparse.go create mode 100644 weakcache/weakcache.go diff --git a/main.go b/main.go index 484d59f..2236e2a 100644 --- a/main.go +++ b/main.go @@ -17,6 +17,7 @@ import ( "ghproxy/middleware/loggin" "ghproxy/proxy" "ghproxy/rate" + "ghproxy/weakcache" "github.com/WJQSERVER-STUDIO/logger" "github.com/hertz-contrib/http2/factory" @@ -50,6 +51,10 @@ var ( pagesFS embed.FS ) +var ( + wcache *weakcache.Cache[string] // docker token缓存 +) + var ( logw = logger.Logw logDump = logger.LogDump @@ -360,6 +365,9 @@ func init() { setMemLimit(cfg) loadlist(cfg) setupRateLimit(cfg) + if cfg.Docker.Enabled { + wcache = proxy.InitWeakCache() + } if cfg.Server.Debug { runMode = "dev" @@ -468,23 +476,7 @@ func main() { proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) }) - // for 3.4.0 - - /* - r.GET("/v2/", func(ctx context.Context, c *app.RequestContext) { - proxy.GhcrRouting(cfg)(ctx, c) - - /* - //proxy.GhcrRouting(cfg)(ctx, c) - // 返回200与空json - //c.JSON(200, map[string]interface{}{}) - emptyJSON := "{}" - //emptyJSON := `{"name":"disable-list-tags","tags":[]}` - c.Header("Content-Type", "application/json") - c.Header("Content-Length", fmt.Sprint(len(emptyJSON))) - c.String(200, emptyJSON) - */ - /* + r.GET("/v2/", func(ctx context.Context, c *app.RequestContext) { emptyJSON := "{}" c.Header("Content-Type", "application/json") c.Header("Content-Length", fmt.Sprint(len(emptyJSON))) @@ -493,30 +485,18 @@ func main() { c.Status(200) c.Write([]byte(emptyJSON)) - */ + }) + + r.Any("/v2/:target/:user/:repo/*filepath", func(ctx context.Context, c *app.RequestContext) { + proxy.GhcrWithImageRouting(cfg)(ctx, c) + }) /* - w := adaptor.GetCompatResponseWriter(&c.Response) - - const emptyJSON = "{}" - w.Header().Set("Content-Type", "application/json") - w.Header().Set("Content-Length", fmt.Sprint(len(emptyJSON))) - w.Header().Del("Server") - - fmt.Fprint(w, emptyJSON) - */ - /* - }) - r.Any("/v2/:target/*filepath", func(ctx context.Context, c *app.RequestContext) { proxy.GhcrRouting(cfg)(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) }) @@ -531,7 +511,7 @@ func main() { }() } - r.Spin() + defer wcache.StopCleanup() defer logger.Close() defer func() { if hertZfile != nil { @@ -541,5 +521,8 @@ func main() { } } }() + + r.Spin() + fmt.Println("Program Exit") } diff --git a/proxy/authparse.go b/proxy/authparse.go new file mode 100644 index 0000000..f353db9 --- /dev/null +++ b/proxy/authparse.go @@ -0,0 +1,62 @@ +package proxy + +import ( + "fmt" + "strings" +) + +// BearerAuthParams 用于存放解析出的 Bearer 认证参数 +type BearerAuthParams struct { + Realm string + Service string + Scope string +} + +// parseBearerWWWAuthenticateHeader 解析 Bearer 方案的 Www-Authenticate Header。 +// 它期望格式为 'Bearer key1="value1",key2="value2",...' +// 并尝试将已知参数解析到 BearerAuthParams struct 中。 +func parseBearerWWWAuthenticateHeader(headerValue string) (*BearerAuthParams, error) { + if headerValue == "" { + return nil, fmt.Errorf("header value is empty") + } + + // 检查 Scheme 是否是 "Bearer" + parts := strings.SplitN(headerValue, " ", 2) + if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" { + return nil, fmt.Errorf("invalid or non-bearer header format: got '%s'", headerValue) + } + paramsStr := parts[1] + + paramPairs := strings.Split(paramsStr, ",") + tempMap := make(map[string]string) + + for _, pair := range paramPairs { + trimmedPair := strings.TrimSpace(pair) + keyValue := strings.SplitN(trimmedPair, "=", 2) + if len(keyValue) != 2 { + logWarning("Skipping malformed parameter '%s' in Www-Authenticate header: %s", pair, headerValue) + continue + } + key := strings.TrimSpace(keyValue[0]) + value := strings.TrimSpace(keyValue[1]) + if strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"") { + value = value[1 : len(value)-1] + } + tempMap[key] = value + } + + //从 map 中提取值并填充到 struct + authParams := &BearerAuthParams{} + + if realm, ok := tempMap["realm"]; ok { + authParams.Realm = realm + } + if service, ok := tempMap["service"]; ok { + authParams.Service = service + } + if scope, ok := tempMap["scope"]; ok { + authParams.Scope = scope + } + + return authParams, nil +} diff --git a/proxy/docker.go b/proxy/docker.go index cdfc743..d4db75e 100644 --- a/proxy/docker.go +++ b/proxy/docker.go @@ -2,39 +2,132 @@ package proxy import ( "context" + "encoding/json" "fmt" + "ghproxy/config" + "ghproxy/weakcache" + "io" "net/http" "strconv" + "strings" "github.com/WJQSERVER-STUDIO/go-utils/limitreader" "github.com/cloudwego/hertz/pkg/app" ) +var ( + dockerhubTarget = "registry-1.docker.io" + ghcrTarget = "ghcr.io" +) + +var cache *weakcache.Cache[string] + +type imageInfo struct { + User string + Repo string + Image string +} + +func InitWeakCache() *weakcache.Cache[string] { + cache = weakcache.NewCache[string](weakcache.DefaultExpiration, 100) + return cache +} + +/* func GhcrRouting(cfg *config.Config) app.HandlerFunc { return func(ctx context.Context, c *app.RequestContext) { - if cfg.Docker.Enabled { + + charToFind := '.' + reqTarget := c.Param("target") + path := "" + target := "" + + if strings.ContainsRune(reqTarget, charToFind) { + + path = c.Param("filepath") + if reqTarget == "docker.io" { + target = dockerhubTarget + } else if reqTarget == "ghcr.io" { + target = ghcrTarget + } else { + target = reqTarget + } + } else { + path = string(c.Request.RequestURI()) + } + + GhcrToTarget(ctx, c, cfg, target, path, nil) + + } +} +*/ + +func GhcrWithImageRouting(cfg *config.Config) app.HandlerFunc { + return func(ctx context.Context, c *app.RequestContext) { + + 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) + target := "" + + if strings.ContainsRune(reqTarget, charToFind) { + + if reqTarget == "docker.io" { + target = dockerhubTarget + } else if reqTarget == "ghcr.io" { + target = ghcrTarget + } else { + target = reqTarget + } + } else { + path = string(c.Request.RequestURI()) + reqImageUser = c.Param("target") + reqImageName = c.Param("user") + } + image := &imageInfo{ + User: reqImageUser, + Repo: reqImageName, + Image: fmt.Sprintf("%s/%s", reqImageUser, reqImageName), + } + + GhcrToTarget(ctx, c, cfg, target, path, image) + + } + +} + +func GhcrToTarget(ctx context.Context, c *app.RequestContext, cfg *config.Config, target string, path string, image *imageInfo) { + if cfg.Docker.Enabled { + if target != "" { + GhcrRequest(ctx, c, "https://"+target+"/v2/"+path+string(c.Request.QueryString()), image, cfg, target) + + } else { if cfg.Docker.Target == "ghcr" { - GhcrRequest(ctx, c, "https://ghcr.io"+string(c.Request.RequestURI()), cfg, "ghcr") + GhcrRequest(ctx, c, "https://"+ghcrTarget+string(c.Request.RequestURI()), image, cfg, ghcrTarget) } else if cfg.Docker.Target == "dockerhub" { - GhcrRequest(ctx, c, "https://registry-1.docker.io"+string(c.Request.RequestURI()), cfg, "dockerhub") + GhcrRequest(ctx, c, "https://"+dockerhubTarget+string(c.Request.RequestURI()), image, cfg, dockerhubTarget) } else if cfg.Docker.Target != "" { // 自定义taget - GhcrRequest(ctx, c, "https://"+cfg.Docker.Target+string(c.Request.RequestURI()), cfg, "custom") + GhcrRequest(ctx, c, "https://"+cfg.Docker.Target+string(c.Request.RequestURI()), image, cfg, cfg.Docker.Target) } else { // 配置为空 ErrorPage(c, NewErrorWithStatusLookup(403, "Docker Target is not set")) return } - - } else { - ErrorPage(c, NewErrorWithStatusLookup(403, "Docker 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) { +func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, image *imageInfo, cfg *config.Config, target string) { var ( method []byte @@ -55,12 +148,11 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *conf method = c.Request.Method() - rb := client.NewRequestBuilder(string(method), u) + rb := ghcrclient.NewRequestBuilder(string(method), u) rb.NoDefaultHeaders() rb.SetBody(c.Request.BodyStream()) rb.WithContext(ctx) - //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)) @@ -73,14 +165,62 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *conf req.Header.Add(headerKey, headerValue) }) - resp, err = client.Do(req) + req.Header.Set("Host", target) + token, exist := cache.Get(image.Image) + if exist { + logDebug("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 } - // 错误处理(404) - if resp.StatusCode == 404 { + // 处理状态码 + if resp.StatusCode == 401 { + // 请求target /v2/路径 + if string(c.Request.URI().Path()) != "/v2/" { + resp.Body.Close() + token := ChallengeReq(target, image, ctx, c) + + // 更新kv + if token != "" { + logDump("Update Cache Token: %s", token) + cache.Put(image.Image, token) + } + + rb := ghcrclient.NewRequestBuilder(string(method), u) + rb.NoDefaultHeaders() + rb.SetBody(c.Request.BodyStream()) + rb.WithContext(ctx) + + 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) + }) + + req.Header.Set("Host", target) + if 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 + } + } + + } else if resp.StatusCode == 404 { // 错误处理(404) ErrorPage(c, NewErrorWithStatusLookup(404, "Page Not Found (From Github)")) return } @@ -101,8 +241,7 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *conf bodySize = -1 } if err == nil && bodySize > sizelimit { - var finalURL string - finalURL = resp.Request.URL.String() + finalURL := resp.Request.URL.String() err = resp.Body.Close() if err != nil { logError("Failed to close response body: %v", err) @@ -116,7 +255,6 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *conf // 复制响应头,排除需要移除的 header for key, values := range resp.Header { for _, value := range values { - //c.Header(key, value) c.Response.Header.Add(key, value) } } @@ -136,3 +274,78 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *conf c.SetBodyStream(bodyReader, -1) } + +type AuthToken struct { + Token string `json:"token"` +} + +func ChallengeReq(target string, image *imageInfo, ctx context.Context, c *app.RequestContext) (token string) { + var resp401 *http.Response + var req401 *http.Request + var err error + + 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)) + return + } + req401.Header.Set("Host", target) + + resp401, err = ghcrclient.Do(req401) + if err != nil { + HandleError(c, fmt.Sprintf("Failed to send request: %v", err)) + return + } + defer resp401.Body.Close() + bearer, err := parseBearerWWWAuthenticateHeader(resp401.Header.Get("Www-Authenticate")) + if err != nil { + logError("Failed to parse Www-Authenticate header: %v", err) + return + } + + scope := fmt.Sprintf("repository:%s:pull", image.Image) + + 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) + + getAuthReq, err := getAuthRB.Build() + if err != nil { + logError("Failed to create request: %v", err) + return + } + + authResp, err := ghcrclient.Do(getAuthReq) + if err != nil { + logError("Failed to send request: %v", err) + return + } + + defer authResp.Body.Close() + + bodyBytes, err := io.ReadAll(authResp.Body) + if err != nil { + logError("Failed to read auth response body: %v", err) + return + } + + // 解码json + var authToken AuthToken + err = json.Unmarshal(bodyBytes, &authToken) + if err != nil { + logError("Failed to decode auth response body: %v", err) + return + } + token = authToken.Token + + return token + +} diff --git a/proxy/httpc.go b/proxy/httpc.go index 83de29b..4d8bc4c 100644 --- a/proxy/httpc.go +++ b/proxy/httpc.go @@ -12,10 +12,12 @@ import ( var BufferSize int = 32 * 1024 // 32KB var ( - tr *http.Transport - gittr *http.Transport - client *httpc.Client - gitclient *httpc.Client + tr *http.Transport + gittr *http.Transport + client *httpc.Client + gitclient *httpc.Client + ghcrtr *http.Transport + ghcrclient *httpc.Client ) func InitReq(cfg *config.Config) error { @@ -23,11 +25,13 @@ func InitReq(cfg *config.Config) error { if cfg.GitClone.Mode == "cache" { initGitHTTPClient(cfg) } + initGhcrHTTPClient(cfg) err := SetGlobalRateLimit(cfg) if err != nil { return err } return nil + } func initHTTPClient(cfg *config.Config) { @@ -77,6 +81,7 @@ func initHTTPClient(cfg *config.Config) { httpc.WithTransport(tr), ) } + } func initGitHTTPClient(cfg *config.Config) { @@ -147,3 +152,51 @@ func initGitHTTPClient(cfg *config.Config) { ) } } + +func initGhcrHTTPClient(cfg *config.Config) { + var proTolcols = new(http.Protocols) + proTolcols.SetHTTP1(true) + proTolcols.SetHTTP2(true) + if cfg.Httpc.Mode == "auto" { + + ghcrtr = &http.Transport{ + IdleConnTimeout: 30 * time.Second, + WriteBufferSize: 32 * 1024, // 32KB + ReadBufferSize: 32 * 1024, // 32KB + Protocols: proTolcols, + } + } else if cfg.Httpc.Mode == "advanced" { + ghcrtr = &http.Transport{ + MaxIdleConns: cfg.Httpc.MaxIdleConns, + MaxConnsPerHost: cfg.Httpc.MaxConnsPerHost, + MaxIdleConnsPerHost: cfg.Httpc.MaxIdleConnsPerHost, + WriteBufferSize: 32 * 1024, // 32KB + ReadBufferSize: 32 * 1024, // 32KB + Protocols: proTolcols, + } + } else { + // 错误的模式 + logError("unknown httpc mode: %s", cfg.Httpc.Mode) + fmt.Println("unknown httpc mode: ", cfg.Httpc.Mode) + logWarning("use Auto to Run HTTP Client") + fmt.Println("use Auto to Run HTTP Client") + ghcrtr = &http.Transport{ + IdleConnTimeout: 30 * time.Second, + WriteBufferSize: 32 * 1024, // 32KB + ReadBufferSize: 32 * 1024, // 32KB + } + } + if cfg.Outbound.Enabled { + initTransport(cfg, ghcrtr) + } + if cfg.Server.Debug { + ghcrclient = httpc.New( + httpc.WithTransport(ghcrtr), + httpc.WithDumpLog(), + ) + } else { + ghcrclient = httpc.New( + httpc.WithTransport(ghcrtr), + ) + } +} diff --git a/proxy/match.go b/proxy/match.go index 9db0c23..cee4eaa 100644 --- a/proxy/match.go +++ b/proxy/match.go @@ -17,9 +17,12 @@ func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHPro // 匹配 "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, "/") - } + /* + if strings.HasPrefix(remainingPath, "/") { + remainingPath = strings.TrimPrefix(remainingPath, "/") + } + */ + remainingPath = strings.TrimPrefix(remainingPath, "/") // 预期格式/user/repo/more... // 取出user和repo和最后部分 parts := strings.Split(remainingPath, "/") diff --git a/proxy/utils.go b/proxy/utils.go index 21e7886..029dc26 100644 --- a/proxy/utils.go +++ b/proxy/utils.go @@ -13,8 +13,7 @@ func listCheck(cfg *config.Config, c *app.RequestContext, user string, repo stri // 白名单检查 if cfg.Whitelist.Enabled { - var whitelist bool - whitelist = auth.CheckWhitelist(user, repo) + whitelist := auth.CheckWhitelist(user, repo) if !whitelist { ErrorPage(c, NewErrorWithStatusLookup(403, fmt.Sprintf("Whitelist Blocked repo: %s/%s", user, repo))) logInfo("%s %s %s %s %s Whitelist Blocked repo: %s/%s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo) @@ -24,8 +23,7 @@ func listCheck(cfg *config.Config, c *app.RequestContext, user string, repo stri // 黑名单检查 if cfg.Blacklist.Enabled { - var blacklist bool - blacklist = auth.CheckBlacklist(user, repo) + blacklist := auth.CheckBlacklist(user, repo) if blacklist { ErrorPage(c, NewErrorWithStatusLookup(403, fmt.Sprintf("Blacklist Blocked repo: %s/%s", user, repo))) logInfo("%s %s %s %s %s Blacklist Blocked repo: %s/%s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo) diff --git a/weakcache/weakcache.go b/weakcache/weakcache.go new file mode 100644 index 0000000..32d897c --- /dev/null +++ b/weakcache/weakcache.go @@ -0,0 +1,258 @@ +package weakcache + +import ( + "container/list" + "sync" + "time" + "weak" // Go 1.24 引入的 weak 包 +) + +// DefaultExpiration 默认过期时间,这里设置为 15 分钟。 +// 这是一个导出的常量,方便用户使用包时引用默认值。 +const DefaultExpiration = 5 * time.Minute + +// cleanupInterval 是后台清理 Go routine 的扫描间隔,这里设置为 5 分钟。 +// 这是一个内部常量,不导出。 +const cleanupInterval = 2 * time.Minute + +// cacheEntry 缓存项的内部结构。不导出。 +type cacheEntry[T any] struct { + Value T + Expiration time.Time + key string // 存储key,方便在list.Element中引用 +} + +// Cache 是一个基于 weak.Pointer, 带有过期和大小上限 (FIFO) 的泛型缓存。 +// 这是一个导出的类型。 +type Cache[T any] struct { + mu sync.RWMutex + + // 修正:缓存存储:key -> weak.Pointer 到 cacheEntry 结构体 (而不是指向结构体的指针) + // weak.Make(*cacheEntry[T]) 返回 weak.Pointer[cacheEntry[T]] + data map[string]weak.Pointer[cacheEntry[T]] + + // FIFO 链表:存储 key 的 list.Element + // 链表头部是最近放入的,尾部是最早放入的(最老的) + fifoList *list.List + // FIFO 元素的映射:key -> *list.Element + fifoMap map[string]*list.Element + + defaultExpiration time.Duration + maxSize int // 缓存最大容量,0 表示无限制 + + stopCleanup chan struct{} + wg sync.WaitGroup // 用于等待清理 Go routine 退出 +} + +// NewCache 创建一个新的缓存实例。 +// expiration: 新添加项的默认过期时间。如果为 0,则使用 DefaultExpiration。 +// maxSize: 缓存的最大容量,0 表示无限制。当达到上限时,采用 FIFO 策略淘汰。 +// 这是一个导出的构造函数。 +func NewCache[T any](expiration time.Duration, maxSize int) *Cache[T] { + if expiration <= 0 { + expiration = DefaultExpiration + } + + c := &Cache[T]{ + // 修正:初始化 map,值类型已修正 + data: make(map[string]weak.Pointer[cacheEntry[T]]), + fifoList: list.New(), + fifoMap: make(map[string]*list.Element), + defaultExpiration: expiration, + maxSize: maxSize, + stopCleanup: make(chan struct{}), + } + // 启动后台清理 Go routine + c.wg.Add(1) + go c.cleanupLoop() + return c +} + +// Put 将值放入缓存。如果 key 已存在,会更新其值和过期时间。 +// 这是导出的方法。 +func (c *Cache[T]) Put(key string, value T) { + c.mu.Lock() + defer c.mu.Unlock() + + now := time.Now() + expiration := now.Add(c.defaultExpiration) + + // 如果 key 已经存在,更新其值和过期时间。 + // 在 FIFO 策略中, Put 更新不改变其在链表中的位置,除非旧的 entry 已经被 GC。 + if elem, ok := c.fifoMap[key]; ok { + // 从 data map 中获取弱引用,wp 的类型现在是 weak.Pointer[cacheEntry[T]] + if wp, dataOk := c.data[key]; dataOk { + // wp.Value() 返回 *cacheEntry[T], entry 的类型现在是 *cacheEntry[T] + entry := wp.Value() + if entry != nil { + // 旧的 cacheEntry 仍在内存中,直接更新 + entry.Value = value + entry.Expiration = expiration + // 在严格 FIFO 中,更新不移动位置 + return + } + // 如果 weak.Pointer.Value() 为 nil,说明之前的 cacheEntry 已经被 GC 了 + // 此时需要创建一个新的 entry,并将其从旧位置移除,再重新添加 + c.fifoList.Remove(elem) + delete(c.fifoMap, key) + } else { + c.fifoList.Remove(elem) + delete(c.fifoMap, key) + } + } + + // 新建缓存项 (注意这里是结构体值,而不是指针) + // weak.Make 接收的是指针 *T + entry := &cacheEntry[T]{ // 创建结构体指针 + Value: value, + Expiration: expiration, + key: key, // 存储 key + } + + // 将新的 *cacheEntry[T] 包装成 weak.Pointer[cacheEntry[T]] 存入 data map + // weak.Make(entry) 现在返回 weak.Pointer[cacheEntry[T]],类型匹配 data map 的值类型 + c.data[key] = weak.Make(entry) + + // 添加到 FIFO 链表头部 (最近放入/更新的在头部) + // PushFront 返回新的 list.Element + c.fifoMap[key] = c.fifoList.PushFront(key) + + // 检查大小上限并进行淘汰 (淘汰尾部的最老项) + c.evictIfNeeded() +} + +// Get 从缓存中获取值。返回获取到的值和是否存在/是否有效。 +// 这是导出的方法。 +func (c *Cache[T]) Get(key string) (T, bool) { + c.mu.RLock() // 先读锁 + // 从 data map 中获取弱引用,wp 的类型现在是 weak.Pointer[cacheEntry[T]] + wp, ok := c.data[key] + c.mu.RUnlock() // 立即释放读锁,如果需要写操作(removeEntry)可以获得锁 + + var zero T // 零值 + + if !ok { + return zero, false + } + + // 尝试获取实际的 cacheEntry 指针 + // wp.Value() 返回 *cacheEntry[T], entry 的类型现在是 *cacheEntry[T] + entry := wp.Value() + + if entry == nil { + // 对象已被GC回收,需要清理此弱引用 + c.removeEntry(key) // 内部会加写锁 + return zero, false + } + + // 检查过期时间 (通过 entry 指针访问字段) + if time.Now().After(entry.Expiration) { + // 逻辑上已过期 + c.removeEntry(key) // 内部会加写锁 + return zero, false + } + + // 在 FIFO 缓存中,Get 操作不改变项在链表中的位置 + return entry.Value, true // 通过 entry 指针访问值字段 +} + +// removeEntry 从缓存中移除项。 +// 这个方法是内部使用的,不导出。需要被调用者确保持有写锁,或者内部自己加锁。 +// 考虑到 Get 和 cleanupLoop 可能会调用,让其内部自己加锁更安全。 +func (c *Cache[T]) removeEntry(key string) { + c.mu.Lock() + defer c.mu.Unlock() + + // 从 data map 中删除 + delete(c.data, key) + + // 从 FIFO 链表和 fifoMap 中删除 + if elem, ok := c.fifoMap[key]; ok { + c.fifoList.Remove(elem) + delete(c.fifoMap, key) + } +} + +// evictIfNeeded 检查是否需要淘汰最老(FIFO 链表尾部)的项。 +// 这个方法是内部使用的,不导出。必须在持有写锁的情况下调用。 +func (c *Cache[T]) evictIfNeeded() { + if c.maxSize > 0 && c.fifoList.Len() > c.maxSize { + // 淘汰 FIFO 链表尾部的元素 (最老的) + oldest := c.fifoList.Back() + if oldest != nil { + keyToEvict := oldest.Value.(string) // 链表元素存储的是 key + c.fifoList.Remove(oldest) + delete(c.fifoMap, keyToEvict) + delete(c.data, keyToEvict) // 移除弱引用 + } + } +} + +// Size 返回当前缓存中的弱引用项数量。 +// 注意:这个数量可能包含已被 GC 回收但尚未清理的项。 +// 这是一个导出的方法。 +func (c *Cache[T]) Size() int { + c.mu.RLock() + defer c.mu.RUnlock() + return len(c.data) +} + +// cleanupLoop 后台清理 Go routine。不导出。 +func (c *Cache[T]) cleanupLoop() { + defer c.wg.Done() + // 使用内部常量 cleanupInterval + ticker := time.NewTicker(cleanupInterval) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + c.cleanupExpiredAndGCed() + case <-c.stopCleanup: + return + } + } +} + +// cleanupExpiredAndGCed 扫描并清理已过期或已被 GC 回收的项。不导出。 +func (c *Cache[T]) cleanupExpiredAndGCed() { + c.mu.Lock() // 清理时需要写锁 + defer c.mu.Unlock() + + now := time.Now() + keysToRemove := make([]string, 0, len(c.data)) // 预估容量 + + // 遍历 data map 查找需要清理的键 + for key, wp := range c.data { + // wp 的类型是 weak.Pointer[cacheEntry[T]] + // wp.Value() 返回 *cacheEntry[T], entry 的类型是 *cacheEntry[T] + entry := wp.Value() // 尝试获取强引用 + + if entry == nil { + // 已被 GC 回收 + keysToRemove = append(keysToRemove, key) + } else if now.After(entry.Expiration) { + // 逻辑过期 (通过 entry 指针访问字段) + keysToRemove = append(keysToRemove, key) + } + } + + // 执行删除操作 + for _, key := range keysToRemove { + // 从 data map 中删除 + delete(c.data, key) + // 从 FIFO 链表和 fifoMap 中删除 + // 需要再次检查 fifoMap,因为在持有锁期间,evictIfNeeded 可能已经移除了这个 key + if elem, ok := c.fifoMap[key]; ok { + c.fifoList.Remove(elem) + delete(c.fifoMap, key) + } + } +} + +// StopCleanup 停止后台清理 Go routine。 +// 这是一个导出的方法。 +func (c *Cache[T]) StopCleanup() { + close(c.stopCleanup) + c.wg.Wait() // 等待 Go routine 退出 +} From c7954ae91aba1eb947f0fd0b0df1f6c977637083 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Wed, 21 May 2025 09:03:14 +0800 Subject: [PATCH 02/99] 25w40a --- CHANGELOG.md | 6 ++++++ DEV-VERSION | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 427ec1b..aa7ba43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # 更新日志 +25w40a - 2025-05-21 +--- +- PRE-RELEASE: 此版本是v3.4.0预发布版本,请勿在生产环境中使用; +- ADD: 初步实现多`target` Docker代理 +- ADD: 加入`weakcache`用于处理短期令牌 + 3.3.3 - 2025-05-20 --- - CHANGE: 加入`senseClientDisconnection`与`async`配置项 diff --git a/DEV-VERSION b/DEV-VERSION index fdef6bc..2114cc7 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w39a \ No newline at end of file +25w40a \ No newline at end of file From 6ff23f639ebc9143e11b6a6bcea29657292ebabd Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Wed, 21 May 2025 11:54:43 +0800 Subject: [PATCH 03/99] add hub theme & add more check for wcache close --- main.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/main.go b/main.go index 2236e2a..6b84afe 100644 --- a/main.go +++ b/main.go @@ -213,6 +213,8 @@ func loadEmbeddedPages(cfg *config.Config) (fs.FS, fs.FS, error) { pages, err = fs.Sub(pagesFS, "pages/classic") case "mino": pages, err = fs.Sub(pagesFS, "pages/mino") + case "hub": + pages, err = fs.Sub(pagesFS, "pages/hub") default: pages, err = fs.Sub(pagesFS, "pages/design") // 默认主题 logWarning("Invalid Pages Theme: %s, using default theme 'design'", cfg.Pages.Theme) @@ -294,7 +296,7 @@ func setInternalRoute(cfg *config.Config, r *server.Hertz) error { staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req) }) r.GET("/favicon.ico", func(ctx context.Context, c *app.RequestContext) { - staticServer := http.FileServer(http.FS(pages)) + staticServer := http.FileServer(http.FS(assets)) req, err := adaptor.GetCompatRequest(&c.Request) if err != nil { logError("%s", err) @@ -510,8 +512,9 @@ func main() { http.ListenAndServe("localhost:6060", nil) }() } - - defer wcache.StopCleanup() + if wcache != nil { + defer wcache.StopCleanup() + } defer logger.Close() defer func() { if hertZfile != nil { From 31d435bfa0acafc21eb89ad6270535f35b290b84 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Wed, 21 May 2025 11:55:04 +0800 Subject: [PATCH 04/99] add oci proxy & nest shell api --- api/api.go | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/api/api.go b/api/api.go index 0c34ac0..e8ca10f 100644 --- a/api/api.go +++ b/api/api.go @@ -49,14 +49,18 @@ func InitHandleRouter(cfg *config.Config, r *server.Hertz, version string) { apiRouter.GET("/smartgit/status", func(ctx context.Context, c *app.RequestContext) { SmartGitStatusHandler(cfg, c, ctx) }) - + apiRouter.GET("/shell_nest/status", func(ctx context.Context, c *app.RequestContext) { + shellNestStatusHandler(cfg, c, ctx) + }) + apiRouter.GET("/oci_proxy/status", func(ctx context.Context, c *app.RequestContext) { + ociProxyStatusHandler(cfg, c, ctx) + }) } logInfo("API router Init success") } func SizeLimitHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) { sizeLimit := cfg.Server.SizeLimit - logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol()) c.Response.Header.Set("Content-Type", "application/json") c.JSON(200, (map[string]interface{}{ "MaxResponseBodySize": sizeLimit, @@ -64,7 +68,6 @@ func SizeLimitHandler(cfg *config.Config, c *app.RequestContext, ctx context.Con } func WhiteListStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) { - logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol()) c.Response.Header.Set("Content-Type", "application/json") c.JSON(200, (map[string]interface{}{ "Whitelist": cfg.Whitelist.Enabled, @@ -72,7 +75,6 @@ func WhiteListStatusHandler(cfg *config.Config, c *app.RequestContext, ctx conte } func BlackListStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) { - logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol()) c.Response.Header.Set("Content-Type", "application/json") c.JSON(200, (map[string]interface{}{ "Blacklist": cfg.Blacklist.Enabled, @@ -80,7 +82,6 @@ func BlackListStatusHandler(cfg *config.Config, c *app.RequestContext, ctx conte } func CorsStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) { - logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol()) c.Response.Header.Set("Content-Type", "application/json") c.JSON(200, (map[string]interface{}{ "Cors": cfg.Server.Cors, @@ -88,7 +89,6 @@ func CorsStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Co } func HealthcheckHandler(c *app.RequestContext, ctx context.Context) { - logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol()) c.Response.Header.Set("Content-Type", "application/json") c.JSON(200, (map[string]interface{}{ "Status": "OK", @@ -96,7 +96,6 @@ func HealthcheckHandler(c *app.RequestContext, ctx context.Context) { } func VersionHandler(c *app.RequestContext, ctx context.Context, version string) { - logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol()) c.Response.Header.Set("Content-Type", "application/json") c.JSON(200, (map[string]interface{}{ "Version": version, @@ -104,7 +103,6 @@ func VersionHandler(c *app.RequestContext, ctx context.Context, version string) } func RateLimitStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) { - logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol()) c.Response.Header.Set("Content-Type", "application/json") c.JSON(200, (map[string]interface{}{ "RateLimit": cfg.RateLimit.Enabled, @@ -112,7 +110,6 @@ func RateLimitStatusHandler(cfg *config.Config, c *app.RequestContext, ctx conte } func RateLimitLimitHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) { - logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol()) c.Response.Header.Set("Content-Type", "application/json") c.JSON(200, (map[string]interface{}{ "RatePerMinute": cfg.RateLimit.RatePerMinute, @@ -120,9 +117,23 @@ func RateLimitLimitHandler(cfg *config.Config, c *app.RequestContext, ctx contex } func SmartGitStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) { - logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol()) c.Response.Header.Set("Content-Type", "application/json") c.JSON(200, (map[string]interface{}{ "enabled": cfg.GitClone.Mode == "cache", })) } + +func shellNestStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) { + c.Response.Header.Set("Content-Type", "application/json") + c.JSON(200, (map[string]interface{}{ + "enabled": cfg.Shell.Editor, + })) +} + +func ociProxyStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) { + c.Response.Header.Set("Content-Type", "application/json") + c.JSON(200, (map[string]interface{}{ + "enabled": cfg.Docker.Enabled, + "target": cfg.Docker.Target, + })) +} From 430e313d4739b374ac03425bd47d47c44ead0508 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Wed, 21 May 2025 12:08:17 +0800 Subject: [PATCH 05/99] avoid nil *ptr & fix path --- proxy/docker.go | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/proxy/docker.go b/proxy/docker.go index d4db75e..3f4c43a 100644 --- a/proxy/docker.go +++ b/proxy/docker.go @@ -104,8 +104,7 @@ func GhcrWithImageRouting(cfg *config.Config) app.HandlerFunc { func GhcrToTarget(ctx context.Context, c *app.RequestContext, cfg *config.Config, target string, path string, image *imageInfo) { if cfg.Docker.Enabled { if target != "" { - GhcrRequest(ctx, c, "https://"+target+"/v2/"+path+string(c.Request.QueryString()), image, cfg, target) - + GhcrRequest(ctx, c, "https://"+target+"/v2/"+path+"?"+string(c.Request.QueryString()), image, cfg, target) } else { if cfg.Docker.Target == "ghcr" { GhcrRequest(ctx, c, "https://"+ghcrTarget+string(c.Request.RequestURI()), image, cfg, ghcrTarget) @@ -166,10 +165,12 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, image *im }) req.Header.Set("Host", target) - token, exist := cache.Get(image.Image) - if exist { - logDebug("Use Cache Token: %s", token) - req.Header.Set("Authorization", "Bearer "+token) + if image != nil { + token, exist := cache.Get(image.Image) + if exist { + logDebug("Use Cache Token: %s", token) + req.Header.Set("Authorization", "Bearer "+token) + } } resp, err = ghcrclient.Do(req) @@ -183,6 +184,10 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, image *im // 请求target /v2/路径 if string(c.Request.URI().Path()) != "/v2/" { resp.Body.Close() + if image == nil { + ErrorPage(c, NewErrorWithStatusLookup(401, "Unauthorized")) + return + } token := ChallengeReq(target, image, ctx, c) // 更新kv From 1afb3521942a952b57291086889b2d0db2af4017 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Wed, 21 May 2025 15:24:05 +0800 Subject: [PATCH 06/99] 3.4.0 --- CHANGELOG.md | 14 ++++++++++++++ DEV-VERSION | 2 +- VERSION | 2 +- main.go | 1 + 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa7ba43..1666a28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # 更新日志 +3.4.0 - 2025-05-21 +--- +- ADD: 初步实现多`target` Docker代理 +- ADD: 加入`weakcache`用于处理短期令牌 +- ADD: 新增`hub`主题 +- ADD: 新增`/api/shell_nest/status`与`/api/oci_proxy/status` API + +25w40b - 2025-05-21 +--- +- PRE-RELEASE: 此版本是v3.4.0预发布版本,请勿在生产环境中使用; +- ADD: 新增`hub`主题 +- ADD: 新增`/api/shell_nest/status`与`/api/oci_proxy/status` API +- CHANGE: 对细节进行优化 + 25w40a - 2025-05-21 --- - PRE-RELEASE: 此版本是v3.4.0预发布版本,请勿在生产环境中使用; diff --git a/DEV-VERSION b/DEV-VERSION index 2114cc7..9c87980 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w40a \ No newline at end of file +25w40b \ No newline at end of file diff --git a/VERSION b/VERSION index 3f09e91..fbcbf73 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.3.3 \ No newline at end of file +3.4.0 \ No newline at end of file diff --git a/main.go b/main.go index 6b84afe..2cbc5db 100644 --- a/main.go +++ b/main.go @@ -515,6 +515,7 @@ func main() { if wcache != nil { defer wcache.StopCleanup() } + defer logger.Close() defer func() { if hertZfile != nil { From 4598257faa9ce5a0b043dc0626a00291585a9734 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Wed, 21 May 2025 16:07:07 +0800 Subject: [PATCH 07/99] update readme.md --- README.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9df6f2b..42815a0 100644 --- a/README.md +++ b/README.md @@ -37,11 +37,11 @@ [GHProxy项目文档](https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/docs/menu.md) -[GHProxy项目文档Next(仍在建设中)](https://ghproxy-docs.pages.dev/) +[GHProxy项目文档Next(仍在建设中)](https://wjqserver.pages.dev/docs/category/ghproxy) 感谢 [@redbunnys](https://github.com/redbunnys)的维护 ### 使用示例 -``` +```bash # 下载文件 https://ghproxy.1888866.xyz/raw.githubusercontent.com/WJQSERVER-STUDIO/tools-stable/main/tools-stable-ghproxy.sh https://ghproxy.1888866.xyz/https://raw.githubusercontent.com/WJQSERVER-STUDIO/tools-stable/main/tools-stable-ghproxy.sh @@ -49,6 +49,15 @@ https://ghproxy.1888866.xyz/https://raw.githubusercontent.com/WJQSERVER-STUDIO/t # 克隆仓库 git clone https://ghproxy.1888866.xyz/github.com/WJQSERVER-STUDIO/ghproxy.git git clone https://ghproxy.1888866.xyz/https://github.com/WJQSERVER-STUDIO/ghproxy.git + +# Docker(OCI) 代理 +docker pull gh.example.com/wjqserver/ghproxy +docker pull gh.example.com/adguard/adguardhome + +docker pull gh.example.com/docker.io/wjqserver/ghproxy +docker pull gh.example.com/docker.io/adguard/adguardhome + +docker pull gh.example.com/ghcr.io/openfaas/queue-worker ``` ## 部署说明 From 16b6b05fb83146d7218db0aad63b00e8e6373534 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Wed, 28 May 2025 20:17:04 +0800 Subject: [PATCH 08/99] 25w41a --- CHANGELOG.md | 6 ++ DEV-VERSION | 2 +- auth/blacklist.go | 3 +- auth/whitelist.go | 3 +- go.mod | 10 ++- go.sum | 12 ++- proxy/docker.go | 3 +- proxy/error.go | 189 ++++++++++++++++++++++++++++++++++++++++++---- 8 files changed, 201 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1666a28..75b1830 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # 更新日志 +25w41a - 2025-05-28 +--- +- PRE-RELEASE: 此版本是v3.4.1预发布版本,请勿在生产环境中使用; +- ADD: 为`errorpage`部分增加lru缓存, 避免重复渲染 +- CHANGE: 替换到实验性的`encode/json/v2` + 3.4.0 - 2025-05-21 --- - ADD: 初步实现多`target` Docker代理 diff --git a/DEV-VERSION b/DEV-VERSION index 9c87980..cd3e78b 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w40b \ No newline at end of file +25w41a \ No newline at end of file diff --git a/auth/blacklist.go b/auth/blacklist.go index ba2091b..014a41d 100644 --- a/auth/blacklist.go +++ b/auth/blacklist.go @@ -1,12 +1,13 @@ package auth import ( - "encoding/json" "fmt" "ghproxy/config" "os" "strings" "sync" + + "github.com/go-json-experiment/json" ) type Blacklist struct { diff --git a/auth/whitelist.go b/auth/whitelist.go index ee93c20..1218307 100644 --- a/auth/whitelist.go +++ b/auth/whitelist.go @@ -1,12 +1,13 @@ package auth import ( - "encoding/json" "fmt" "ghproxy/config" "os" "strings" "sync" + + "github.com/go-json-experiment/json" ) // Whitelist 用于存储白名单信息 diff --git a/go.mod b/go.mod index d4a930a..0c5cb9b 100644 --- a/go.mod +++ b/go.mod @@ -5,14 +5,18 @@ go 1.24.3 require ( github.com/BurntSushi/toml v1.5.0 github.com/WJQSERVER-STUDIO/httpc v0.5.1 - github.com/WJQSERVER-STUDIO/logger v1.7.1 + github.com/WJQSERVER-STUDIO/logger v1.7.2 github.com/cloudwego/hertz v0.10.0 github.com/hertz-contrib/http2 v0.1.8 golang.org/x/net v0.40.0 golang.org/x/time v0.11.0 ) -require github.com/WJQSERVER-STUDIO/go-utils/limitreader v0.0.2 +require ( + github.com/WJQSERVER-STUDIO/go-utils/limitreader v0.0.2 + github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8 + github.com/hashicorp/golang-lru/v2 v2.0.7 +) require ( github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4 // indirect @@ -26,7 +30,7 @@ require ( github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect - github.com/nyaruka/phonenumbers v1.6.1 // indirect + github.com/nyaruka/phonenumbers v1.6.3 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect diff --git a/go.sum b/go.sum index a614fc3..1d2dadf 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,8 @@ github.com/WJQSERVER-STUDIO/go-utils/log v0.0.3 h1:t6nyLhmo9pSfVHm1Wu1WyLsTpXFSj github.com/WJQSERVER-STUDIO/go-utils/log v0.0.3/go.mod h1:j9Q+xnwpOfve7/uJnZ2izRQw6NNoXjvJHz7vUQAaLZE= github.com/WJQSERVER-STUDIO/httpc v0.5.1 h1:+TKCPYBuj7PAHuiduGCGAqsHAa4QtsUfoVwRN777q64= github.com/WJQSERVER-STUDIO/httpc v0.5.1/go.mod h1:M7KNUZjjhCkzzcg9lBPs9YfkImI+7vqjAyjdA19+joE= -github.com/WJQSERVER-STUDIO/logger v1.7.1 h1:sAFsF3umimY0Vmue5WnGf1Qxvm/vlhK2srZakWVtlFU= -github.com/WJQSERVER-STUDIO/logger v1.7.1/go.mod h1:cvP0XdFIMLtDWOZeKhklshzipkVU1zufsU4rKNfoM24= +github.com/WJQSERVER-STUDIO/logger v1.7.2 h1:Tu9WICwlrY+BMQmY7k4llDB1ziFtZ9VmK7/85VIPN+M= +github.com/WJQSERVER-STUDIO/logger v1.7.2/go.mod h1:yzXPtot0OvR1gzx4+rlFrv/sccUpz0gIXVBwUx3H7fM= github.com/bytedance/gopkg v0.1.1/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/gopkg v0.1.2 h1:8o2feYuxknDpN+O7kPwvSXfMEKfYvJYiA2K7aonoMEQ= github.com/bytedance/gopkg v0.1.2/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= @@ -35,10 +35,14 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8 h1:o8UqXPI6SVwQt04RGsqKp3qqmbOfTNMqDrWsc4O47kk= +github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +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/hertz-contrib/http2 v0.1.8 h1:kjfCGkUxJZHgfPsnRjx1FLJBG55KvtvSQD214guBQLw= github.com/hertz-contrib/http2 v0.1.8/go.mod h1:m42hrl8fiTwE4p8c7JdRUZpkePEthvV89q3elL2GeD0= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= @@ -50,8 +54,8 @@ github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgSh github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/nyaruka/phonenumbers v1.6.1 h1:XAJcTdYow16VrVKfglznMpJZz8KMJoMjx/91sX+K940= -github.com/nyaruka/phonenumbers v1.6.1/go.mod h1:7gjs+Lchqm49adhAKB5cdcng5ZXgt6x7Jgvi0ZorUtU= +github.com/nyaruka/phonenumbers v1.6.3 h1:JU7Q30+UM/03/vto6Q4EiZfEuRpTVyXMqImIbI942Qw= +github.com/nyaruka/phonenumbers v1.6.3/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/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= diff --git a/proxy/docker.go b/proxy/docker.go index 3f4c43a..08b23c3 100644 --- a/proxy/docker.go +++ b/proxy/docker.go @@ -2,9 +2,10 @@ package proxy import ( "context" - "encoding/json" "fmt" + "github.com/go-json-experiment/json" + "ghproxy/config" "ghproxy/weakcache" "io" diff --git a/proxy/error.go b/proxy/error.go index 756c881..b684193 100644 --- a/proxy/error.go +++ b/proxy/error.go @@ -2,12 +2,18 @@ package proxy import ( "bytes" + "crypto/sha256" + "encoding/gob" + "encoding/hex" + "sync" + "fmt" "html/template" "io/fs" "github.com/WJQSERVER-STUDIO/logger" "github.com/cloudwego/hertz/pkg/app" + lru "github.com/hashicorp/golang-lru/v2" ) // 日志模块 @@ -22,7 +28,7 @@ var ( func HandleError(c *app.RequestContext, message string) { ErrorPage(c, NewErrorWithStatusLookup(500, message)) - logError(message) + logError("Error handled: %s", message) } type GHProxyErrors struct { @@ -123,6 +129,22 @@ type ErrorPageData struct { ErrorMessage string } +// ToCacheKey 为 ErrorPageData 生成一个唯一的 SHA256 字符串键。 +// 使用 gob 序列化来确保结构体内容到字节序列的顺序一致性,然后计算哈希。 +func (d ErrorPageData) ToCacheKey() string { + var buf bytes.Buffer + enc := gob.NewEncoder(&buf) + err := enc.Encode(d) + if err != nil { + logError("Failed to gob encode ErrorPageData for cache key: %v", err) + panic(fmt.Errorf("failed to gob encode ErrorPageData: %w", err)) + } + + hasher := sha256.New() + hasher.Write(buf.Bytes()) + return hex.EncodeToString(hasher.Sum(nil)) +} + func ErrPageUnwarper(errInfo *GHProxyErrors) ErrorPageData { return ErrorPageData{ StatusCode: errInfo.StatusCode, @@ -133,25 +155,130 @@ func ErrPageUnwarper(errInfo *GHProxyErrors) ErrorPageData { } } -func ErrorPage(c *app.RequestContext, errInfo *GHProxyErrors) { - pageData, err := htmlTemplateRender(errPagesFs, ErrPageUnwarper(errInfo)) - if err != nil { - c.JSON(errInfo.StatusCode, map[string]string{"error": errInfo.ErrorMessage}) - logDebug("Error reading page.tmpl: %v", err) - return - } - c.Data(errInfo.StatusCode, "text/html; charset=utf-8", pageData) - return +// SizedLRUCache 实现了基于字节大小限制的 LRU 缓存。 +// 它包装了 hashicorp/golang-lru/v2.Cache,并额外管理缓存的总字节大小。 +type SizedLRUCache struct { + cache *lru.Cache[string, []byte] + mu sync.Mutex // 保护 currentBytes 字段 + maxBytes int64 // 缓存的最大字节容量 + currentBytes int64 // 缓存当前占用的字节数 } -func htmlTemplateRender(fsys fs.FS, data interface{}) ([]byte, error) { - tmplPath := "page.tmpl" - tmpl, err := template.ParseFS(fsys, tmplPath) +// NewSizedLRUCache 创建一个新的 SizedLRUCache 实例。 +// 内部的 lru.Cache 的条目容量被设置为一个较大的值 (例如 10000), +// 因为主要的逐出逻辑将由字节大小限制来控制。 +func NewSizedLRUCache(maxBytes int64) (*SizedLRUCache, error) { + if maxBytes <= 0 { + return nil, fmt.Errorf("maxBytes must be positive") + } + + c := &SizedLRUCache{ + maxBytes: maxBytes, + } + + // 创建内部 LRU 缓存,并提供一个 OnEvictedFunc 回调函数。 + // 当内部 LRU 缓存因其自身的条目容量限制或 RemoveOldest 方法被调用而逐出条目时, + // 此回调函数会被执行,从而更新 currentBytes。 + var err error + 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, fmt.Errorf("error parsing template: %w", err) + return nil, err + } + return c, nil +} + +// Get 从缓存中检索值。 +func (c *SizedLRUCache) Get(key string) ([]byte, bool) { + return c.cache.Get(key) +} + +// Add 向缓存中添加或更新一个键值对,并在必要时执行逐出以满足字节限制。 +func (c *SizedLRUCache) Add(key string, value []byte) { + c.mu.Lock() // 保护 currentBytes 和逐出逻辑 + defer c.mu.Unlock() + + itemSize := int64(len(value)) + + // 如果待添加的条目本身就大于缓存的最大容量,则不进行缓存。 + if itemSize > c.maxBytes { + logWarning("Item key %s (size %d) larger than cache max capacity %d. Not caching.", key, itemSize, c.maxBytes) + return + } + + // 如果键已存在,则首先从 currentBytes 中减去旧值的大小,并从内部 LRU 中移除旧条目。 + 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 { + _, oldVal, existed := c.cache.RemoveOldest() + if !existed { + logWarning("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 + +var errorPageCache *SizedLRUCache + +func init() { + // 初始化 SizedLRUCache。 + var err error + errorPageCache, err = NewSizedLRUCache(maxErrorPageCacheBytes) + if err != nil { + logError("Failed to initialize error page LRU cache: %v", err) + panic(err) + } +} + +// parsedTemplateOnce 用于确保 HTML 模板只被解析一次。 +var ( + parsedTemplateOnce sync.Once + parsedTemplate *template.Template + parsedTemplateErr error +) + +// getParsedTemplate 用于获取缓存的解析后的 HTML 模板。 +func getParsedTemplate() (*template.Template, error) { + parsedTemplateOnce.Do(func() { + tmplPath := "page.tmpl" + // 确保 errPagesFs 已初始化。这要求在任何 ErrorPage 调用之前调用 InitErrPagesFS。 + if errPagesFs == nil { + parsedTemplateErr = fmt.Errorf("errPagesFs not initialized. Call InitErrPagesFS first") + return + } + parsedTemplate, parsedTemplateErr = template.ParseFS(errPagesFs, tmplPath) + if parsedTemplateErr != nil { + parsedTemplate = nil + } + }) + return parsedTemplate, parsedTemplateErr +} + +// htmlTemplateRender 修改为使用缓存的模板。 +func htmlTemplateRender(data interface{}) ([]byte, error) { + tmpl, err := getParsedTemplate() + if err != nil { + return nil, fmt.Errorf("failed to get parsed template: %w", err) } if tmpl == nil { - return nil, fmt.Errorf("template is nil") + return nil, fmt.Errorf("template is nil after parsing") } // 创建一个 bytes.Buffer 用于存储渲染结果 @@ -159,9 +286,39 @@ func htmlTemplateRender(fsys fs.FS, data interface{}) ([]byte, error) { err = tmpl.Execute(&buf, data) if err != nil { - return nil, fmt.Errorf("error executing template: %w", err) + return nil, fmt.Errorf("failed to execute template: %w", err) } // 返回 buffer 的内容作为 []byte return buf.Bytes(), nil } + +func ErrorPage(c *app.RequestContext, errInfo *GHProxyErrors) { + // 将 errInfo 转换为 ErrorPageData 结构体 + pageDataStruct := ErrPageUnwarper(errInfo) + // 使用 ErrorPageData 生成一个唯一的 SHA256 缓存键 + cacheKey := pageDataStruct.ToCacheKey() + + var pageData []byte + var err error + + // 尝试从缓存中获取页面数据 + if cachedPage, found := errorPageCache.Get(cacheKey); found { + pageData = cachedPage + logDebug("Serving error page from cache (Key: %s)", cacheKey) + } else { + // 如果不在缓存中,则渲染页面 + pageData, err = htmlTemplateRender(pageDataStruct) + if err != nil { + c.JSON(errInfo.StatusCode, map[string]string{"error": errInfo.ErrorMessage}) + logDebug("Failed to render error page for status %d (Key: %s): %v", errInfo.StatusCode, cacheKey, err) + return + } + + // 将渲染结果存入缓存 + errorPageCache.Add(cacheKey, pageData) + logDebug("Cached error page (Key: %s, Size: %d bytes)", cacheKey, len(pageData)) + } + + c.Data(errInfo.StatusCode, "text/html; charset=utf-8", pageData) +} From 68bf51aaedf7ba2f89957c13f03f316033594e0b Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Wed, 28 May 2025 21:35:52 +0800 Subject: [PATCH 09/99] 25w41b --- CHANGELOG.md | 7 ++++++- DEV-VERSION | 2 +- auth/blacklist.go | 2 +- auth/whitelist.go | 2 +- go.mod | 3 +-- go.sum | 2 -- proxy/docker.go | 2 +- 7 files changed, 11 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75b1830..b292d3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,15 @@ # 更新日志 +25w41b - 2025-05-28 +--- +- PRE-RELEASE: 此版本是v3.4.1预发布版本,请勿在生产环境中使用; +- CHANGE: 把json库替换到[sonic](github.com/bytedance/sonic) + 25w41a - 2025-05-28 --- - PRE-RELEASE: 此版本是v3.4.1预发布版本,请勿在生产环境中使用; - ADD: 为`errorpage`部分增加lru缓存, 避免重复渲染 -- CHANGE: 替换到实验性的`encode/json/v2` +- CHANGE: 替换到实验性的`encoding/json/v2` 3.4.0 - 2025-05-21 --- diff --git a/DEV-VERSION b/DEV-VERSION index cd3e78b..82272f8 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w41a \ No newline at end of file +25w41b \ No newline at end of file diff --git a/auth/blacklist.go b/auth/blacklist.go index 014a41d..fd6b276 100644 --- a/auth/blacklist.go +++ b/auth/blacklist.go @@ -7,7 +7,7 @@ import ( "strings" "sync" - "github.com/go-json-experiment/json" + json "github.com/bytedance/sonic" ) type Blacklist struct { diff --git a/auth/whitelist.go b/auth/whitelist.go index 1218307..b4071d1 100644 --- a/auth/whitelist.go +++ b/auth/whitelist.go @@ -7,7 +7,7 @@ import ( "strings" "sync" - "github.com/go-json-experiment/json" + json "github.com/bytedance/sonic" ) // Whitelist 用于存储白名单信息 diff --git a/go.mod b/go.mod index 0c5cb9b..9f79dc7 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( require ( github.com/WJQSERVER-STUDIO/go-utils/limitreader v0.0.2 - github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8 + github.com/bytedance/sonic v1.13.2 github.com/hashicorp/golang-lru/v2 v2.0.7 ) @@ -22,7 +22,6 @@ require ( github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4 // indirect github.com/WJQSERVER-STUDIO/go-utils/log v0.0.3 // indirect github.com/bytedance/gopkg v0.1.2 // indirect - github.com/bytedance/sonic v1.13.2 // indirect github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/cloudwego/base64x v0.1.5 // indirect github.com/cloudwego/gopkg v0.1.4 // indirect diff --git a/go.sum b/go.sum index 1d2dadf..36131ff 100644 --- a/go.sum +++ b/go.sum @@ -35,8 +35,6 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8 h1:o8UqXPI6SVwQt04RGsqKp3qqmbOfTNMqDrWsc4O47kk= -github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= diff --git a/proxy/docker.go b/proxy/docker.go index 08b23c3..eb5fadf 100644 --- a/proxy/docker.go +++ b/proxy/docker.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/go-json-experiment/json" + json "github.com/bytedance/sonic" "ghproxy/config" "ghproxy/weakcache" From b7ce929db8c3700826c3def70e290f7a94f6ca91 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Thu, 29 May 2025 15:01:44 +0800 Subject: [PATCH 10/99] 3.4.1 --- CHANGELOG.md | 5 +++++ VERSION | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b292d3e..68201d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 更新日志 +3.4.1 - 2025-05-29 +--- +- ADD: 为`errorpage`部分增加lru缓存, 避免重复渲染 +- CHANGE: 把json库替换到[sonic](github.com/bytedance/sonic) + 25w41b - 2025-05-28 --- - PRE-RELEASE: 此版本是v3.4.1预发布版本,请勿在生产环境中使用; diff --git a/VERSION b/VERSION index fbcbf73..8cf6caf 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.4.0 \ No newline at end of file +3.4.1 \ No newline at end of file From 82943428d340e8b85d9171c6271f44d1a233d619 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Thu, 29 May 2025 15:13:28 +0800 Subject: [PATCH 11/99] optimize cache & remove panic change to fallback error json --- proxy/error.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/proxy/error.go b/proxy/error.go index b684193..1820259 100644 --- a/proxy/error.go +++ b/proxy/error.go @@ -137,7 +137,7 @@ func (d ErrorPageData) ToCacheKey() string { err := enc.Encode(d) if err != nil { logError("Failed to gob encode ErrorPageData for cache key: %v", err) - panic(fmt.Errorf("failed to gob encode ErrorPageData: %w", err)) + return "" } hasher := sha256.New() @@ -298,6 +298,11 @@ func ErrorPage(c *app.RequestContext, errInfo *GHProxyErrors) { pageDataStruct := ErrPageUnwarper(errInfo) // 使用 ErrorPageData 生成一个唯一的 SHA256 缓存键 cacheKey := pageDataStruct.ToCacheKey() + if cacheKey == "" { + c.JSON(errInfo.StatusCode, map[string]string{"error": errInfo.ErrorMessage}) + logWarning("Failed to generate cache key for error page: %v", errInfo) + return + } var pageData []byte var err error @@ -311,7 +316,7 @@ func ErrorPage(c *app.RequestContext, errInfo *GHProxyErrors) { pageData, err = htmlTemplateRender(pageDataStruct) if err != nil { c.JSON(errInfo.StatusCode, map[string]string{"error": errInfo.ErrorMessage}) - logDebug("Failed to render error page for status %d (Key: %s): %v", errInfo.StatusCode, cacheKey, err) + logWarning("Failed to render error page for status %d (Key: %s): %v", errInfo.StatusCode, cacheKey, err) return } From 5b17e1f0b6fba4daf77da8700da891c00a99462e Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Fri, 30 May 2025 17:31:35 +0800 Subject: [PATCH 12/99] update docs link --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 42815a0..829de33 100644 --- a/README.md +++ b/README.md @@ -35,9 +35,9 @@ [相关文章](https://blog.wjqserver.com/categories/my-program/) -[GHProxy项目文档](https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/docs/menu.md) +[GHProxy项目文档Next](https://wjqserver-docs.pages.dev/docs/ghproxy/) 感谢 [@redbunnys](https://github.com/redbunnys)的维护 -[GHProxy项目文档Next(仍在建设中)](https://wjqserver.pages.dev/docs/category/ghproxy) 感谢 [@redbunnys](https://github.com/redbunnys)的维护 +[GHProxy项目文档](https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/docs/menu.md) ### 使用示例 From 171fe613426677bd5bcb032eec57657e006f1c13 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Mon, 2 Jun 2025 21:00:49 +0800 Subject: [PATCH 13/99] 25w42a --- CHANGELOG.md | 5 +++++ DEV-VERSION | 2 +- go.mod | 4 +++- go.sum | 8 ++++---- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68201d4..9c2b605 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 更新日志 +25w42a - 2025-06-02 +--- +- PRE-RELEASE: 此版本是v3.4.2预发布版本,请勿在生产环境中使用; +- DEP: 回滚 github.com/nyaruka/phonenumbers 版本到 v1.6.1, v1.6.3观测到了一些反射造成的内存占用异常 + 3.4.1 - 2025-05-29 --- - ADD: 为`errorpage`部分增加lru缓存, 避免重复渲染 diff --git a/DEV-VERSION b/DEV-VERSION index 82272f8..0b20fcc 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w41b \ No newline at end of file +25w42a \ No newline at end of file diff --git a/go.mod b/go.mod index 9f79dc7..437f381 100644 --- a/go.mod +++ b/go.mod @@ -36,11 +36,13 @@ require ( github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect golang.org/x/arch v0.17.0 // indirect - golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect + golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b // indirect golang.org/x/sys v0.33.0 // indirect golang.org/x/text v0.25.0 // indirect google.golang.org/protobuf v1.36.6 // indirect ) +replace github.com/nyaruka/phonenumbers => github.com/nyaruka/phonenumbers v1.6.1 // 1.6.3 has reflect leaking + //replace github.com/WJQSERVER-STUDIO/httpc v0.5.1 => /data/github/WJQSERVER-STUDIO/httpc //replace github.com/WJQSERVER-STUDIO/logger v1.6.0 => /data/github/WJQSERVER-STUDIO/logger diff --git a/go.sum b/go.sum index 36131ff..5e3a90e 100644 --- a/go.sum +++ b/go.sum @@ -52,8 +52,8 @@ github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgSh github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/nyaruka/phonenumbers v1.6.3 h1:JU7Q30+UM/03/vto6Q4EiZfEuRpTVyXMqImIbI942Qw= -github.com/nyaruka/phonenumbers v1.6.3/go.mod h1:7gjs+Lchqm49adhAKB5cdcng5ZXgt6x7Jgvi0ZorUtU= +github.com/nyaruka/phonenumbers v1.6.1 h1:XAJcTdYow16VrVKfglznMpJZz8KMJoMjx/91sX+K940= +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/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= @@ -91,8 +91,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-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI= -golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= +golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b h1:QoALfVG9rhQ/M7vYDScfPdWjGL9dlsVVM5VGh7aKoAA= +golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= 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= From 2bab0a977423480df81cf4fc83b494b29a1f2f3b Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Tue, 3 Jun 2025 16:47:00 +0800 Subject: [PATCH 14/99] 3.4.2 --- CHANGELOG.md | 4 ++++ VERSION | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c2b605..c2820fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 更新日志 +3.4.2 - 2025-06-03 +--- +- DEP: 回滚 github.com/nyaruka/phonenumbers 版本到 v1.6.1, v1.6.3观测到了一些反射造成的内存占用异常 + 25w42a - 2025-06-02 --- - PRE-RELEASE: 此版本是v3.4.2预发布版本,请勿在生产环境中使用; diff --git a/VERSION b/VERSION index 8cf6caf..a423d42 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.4.1 \ No newline at end of file +3.4.2 \ No newline at end of file From e829c2baff44e927919a42161b4570aab3a22c53 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Thu, 5 Jun 2025 00:24:21 +0800 Subject: [PATCH 15/99] 25w43a --- CHANGELOG.md | 7 +++ DEV-VERSION | 2 +- go.mod | 5 +- go.sum | 10 ++-- main.go | 149 +++++++++++++++++++++++++++++---------------------- 5 files changed, 101 insertions(+), 72 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2820fb..49187fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # 更新日志 +25w43a - 2025-06-05 +--- +- PRE-RELEASE: 此版本是v3.4.3预发布版本,请勿在生产环境中使用; +- CHANGE: 弃用`adaptor.GetCompatRequest`, 切换到`adaptor.HertzHandler` +- CHANGE: 为`embedFS`使用包装器, 使其支持`Last-Modified` +- CHANGE: 为静态资源增加`Cache-Control: public, max-age=3600, must-revalidate` + 3.4.2 - 2025-06-03 --- - DEP: 回滚 github.com/nyaruka/phonenumbers 版本到 v1.6.1, v1.6.3观测到了一些反射造成的内存占用异常 diff --git a/DEV-VERSION b/DEV-VERSION index 0b20fcc..e303d5b 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w42a \ No newline at end of file +25w43a \ No newline at end of file diff --git a/go.mod b/go.mod index 437f381..90b925e 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.24.3 require ( github.com/BurntSushi/toml v1.5.0 github.com/WJQSERVER-STUDIO/httpc v0.5.1 - github.com/WJQSERVER-STUDIO/logger v1.7.2 + github.com/WJQSERVER-STUDIO/logger v1.7.3 github.com/cloudwego/hertz v0.10.0 github.com/hertz-contrib/http2 v0.1.8 golang.org/x/net v0.40.0 @@ -14,8 +14,9 @@ require ( require ( github.com/WJQSERVER-STUDIO/go-utils/limitreader v0.0.2 - github.com/bytedance/sonic v1.13.2 + github.com/bytedance/sonic v1.13.3 github.com/hashicorp/golang-lru/v2 v2.0.7 + github.com/wjqserver/modembed v0.0.1 ) require ( diff --git a/go.sum b/go.sum index 5e3a90e..834e6c7 100644 --- a/go.sum +++ b/go.sum @@ -8,15 +8,15 @@ github.com/WJQSERVER-STUDIO/go-utils/log v0.0.3 h1:t6nyLhmo9pSfVHm1Wu1WyLsTpXFSj github.com/WJQSERVER-STUDIO/go-utils/log v0.0.3/go.mod h1:j9Q+xnwpOfve7/uJnZ2izRQw6NNoXjvJHz7vUQAaLZE= github.com/WJQSERVER-STUDIO/httpc v0.5.1 h1:+TKCPYBuj7PAHuiduGCGAqsHAa4QtsUfoVwRN777q64= github.com/WJQSERVER-STUDIO/httpc v0.5.1/go.mod h1:M7KNUZjjhCkzzcg9lBPs9YfkImI+7vqjAyjdA19+joE= -github.com/WJQSERVER-STUDIO/logger v1.7.2 h1:Tu9WICwlrY+BMQmY7k4llDB1ziFtZ9VmK7/85VIPN+M= -github.com/WJQSERVER-STUDIO/logger v1.7.2/go.mod h1:yzXPtot0OvR1gzx4+rlFrv/sccUpz0gIXVBwUx3H7fM= +github.com/WJQSERVER-STUDIO/logger v1.7.3 h1:XoFJ1nBcZKyMvP4v0MZv5jL2q7IkAF7yfXgwyB3MLP4= +github.com/WJQSERVER-STUDIO/logger v1.7.3/go.mod h1:yzXPtot0OvR1gzx4+rlFrv/sccUpz0gIXVBwUx3H7fM= github.com/bytedance/gopkg v0.1.1/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/gopkg v0.1.2 h1:8o2feYuxknDpN+O7kPwvSXfMEKfYvJYiA2K7aonoMEQ= github.com/bytedance/gopkg v0.1.2/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/mockey v1.2.12 h1:aeszOmGw8CPX8CRx1DZ/Glzb1yXvhjDh6jdFBNZjsU4= github.com/bytedance/mockey v1.2.12/go.mod h1:3ZA4MQasmqC87Tw0w7Ygdy7eHIc2xgpZ8Pona5rsYIk= -github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ= -github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= +github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0= +github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= @@ -84,6 +84,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 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= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/arch v0.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU= golang.org/x/arch v0.17.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= diff --git a/main.go b/main.go index 2cbc5db..a441752 100644 --- a/main.go +++ b/main.go @@ -21,6 +21,7 @@ import ( "github.com/WJQSERVER-STUDIO/logger" "github.com/hertz-contrib/http2/factory" + "github.com/wjqserver/modembed" "github.com/cloudwego/hertz/pkg/app" "github.com/cloudwego/hertz/pkg/app/middlewares/server/recovery" @@ -198,25 +199,26 @@ func InitReq(cfg *config.Config) { // loadEmbeddedPages 加载嵌入式页面资源 func loadEmbeddedPages(cfg *config.Config) (fs.FS, fs.FS, error) { + pageFS := modembed.NewModTimeFS(pagesFS, time.Now()) var pages fs.FS var err error switch cfg.Pages.Theme { case "bootstrap": - pages, err = fs.Sub(pagesFS, "pages/bootstrap") + pages, err = fs.Sub(pageFS, "pages/bootstrap") case "nebula": - pages, err = fs.Sub(pagesFS, "pages/nebula") + pages, err = fs.Sub(pageFS, "pages/nebula") case "design": - pages, err = fs.Sub(pagesFS, "pages/design") + pages, err = fs.Sub(pageFS, "pages/design") case "metro": - pages, err = fs.Sub(pagesFS, "pages/metro") + pages, err = fs.Sub(pageFS, "pages/metro") case "classic": - pages, err = fs.Sub(pagesFS, "pages/classic") + pages, err = fs.Sub(pageFS, "pages/classic") case "mino": - pages, err = fs.Sub(pagesFS, "pages/mino") + pages, err = fs.Sub(pageFS, "pages/mino") case "hub": - pages, err = fs.Sub(pagesFS, "pages/hub") + pages, err = fs.Sub(pageFS, "pages/hub") default: - pages, err = fs.Sub(pagesFS, "pages/design") // 默认主题 + pages, err = fs.Sub(pageFS, "pages/design") // 默认主题 logWarning("Invalid Pages Theme: %s, using default theme 'design'", cfg.Pages.Theme) } @@ -225,13 +227,16 @@ func loadEmbeddedPages(cfg *config.Config) (fs.FS, fs.FS, error) { } // 初始化errPagesFs - errPagesInitErr := proxy.InitErrPagesFS(pagesFS) + errPagesInitErr := proxy.InitErrPagesFS(pageFS) if errPagesInitErr != nil { logWarning("errPagesInitErr: %s", errPagesInitErr) } var assets fs.FS - assets, err = fs.Sub(pagesFS, "pages/assets") + assets, err = fs.Sub(pageFS, "pages/assets") + if err != nil { + return nil, nil, fmt.Errorf("failed to load embedded assets: %w", err) + } return pages, assets, nil } @@ -277,6 +282,12 @@ func setupPages(cfg *config.Config, r *server.Hertz) { } } +func pageCacheHeader() func(ctx context.Context, c *app.RequestContext) { + return func(ctx context.Context, c *app.RequestContext) { + c.Header("Cache-Control", "public, max-age=3600, must-revalidate") + } +} + func setInternalRoute(cfg *config.Config, r *server.Hertz) error { // 加载嵌入式资源 @@ -285,61 +296,69 @@ func setInternalRoute(cfg *config.Config, r *server.Hertz) error { logError("Failed when processing pages: %s", err) return err } - // 设置嵌入式资源路由 - r.GET("/", func(ctx context.Context, c *app.RequestContext) { - staticServer := http.FileServer(http.FS(pages)) - req, err := adaptor.GetCompatRequest(&c.Request) - if err != nil { - logError("%s", err) - return - } - staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req) - }) - r.GET("/favicon.ico", func(ctx context.Context, c *app.RequestContext) { - staticServer := http.FileServer(http.FS(assets)) - req, err := adaptor.GetCompatRequest(&c.Request) - if err != nil { - logError("%s", err) - return - } - staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req) - }) - r.GET("/script.js", func(ctx context.Context, c *app.RequestContext) { - staticServer := http.FileServer(http.FS(pages)) - req, err := adaptor.GetCompatRequest(&c.Request) - if err != nil { - logError("%s", err) - return - } - staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req) - }) - r.GET("/style.css", func(ctx context.Context, c *app.RequestContext) { - staticServer := http.FileServer(http.FS(pages)) - req, err := adaptor.GetCompatRequest(&c.Request) - if err != nil { - logError("%s", err) - return - } - staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req) - }) - r.GET("/bootstrap.min.css", func(ctx context.Context, c *app.RequestContext) { - staticServer := http.FileServer(http.FS(assets)) - req, err := adaptor.GetCompatRequest(&c.Request) - if err != nil { - logError("%s", err) - return - } - staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req) - }) - r.GET("/bootstrap.bundle.min.js", func(ctx context.Context, c *app.RequestContext) { - staticServer := http.FileServer(http.FS(assets)) - req, err := adaptor.GetCompatRequest(&c.Request) - if err != nil { - logError("%s", err) - return - } - staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req) - }) + /* + // 设置嵌入式资源路由 + r.GET("/", func(ctx context.Context, c *app.RequestContext) { + staticServer := http.FileServer(http.FS(pages)) + req, err := adaptor.GetCompatRequest(&c.Request) + if err != nil { + logError("%s", err) + return + } + staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req) + }) + r.GET("/favicon.ico", func(ctx context.Context, c *app.RequestContext) { + staticServer := http.FileServer(http.FS(assets)) + req, err := adaptor.GetCompatRequest(&c.Request) + if err != nil { + logError("%s", err) + return + } + staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req) + }) + r.GET("/script.js", func(ctx context.Context, c *app.RequestContext) { + staticServer := http.FileServer(http.FS(pages)) + req, err := adaptor.GetCompatRequest(&c.Request) + if err != nil { + logError("%s", err) + return + } + staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req) + }) + r.GET("/style.css", func(ctx context.Context, c *app.RequestContext) { + staticServer := http.FileServer(http.FS(pages)) + req, err := adaptor.GetCompatRequest(&c.Request) + if err != nil { + logError("%s", err) + return + } + staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req) + }) + r.GET("/bootstrap.min.css", func(ctx context.Context, c *app.RequestContext) { + staticServer := http.FileServer(http.FS(assets)) + req, err := adaptor.GetCompatRequest(&c.Request) + if err != nil { + logError("%s", err) + return + } + staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req) + }) + r.GET("/bootstrap.bundle.min.js", func(ctx context.Context, c *app.RequestContext) { + staticServer := http.FileServer(http.FS(assets)) + req, err := adaptor.GetCompatRequest(&c.Request) + if err != nil { + logError("%s", err) + return + } + staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req) + }) + */ + r.GET("/", pageCacheHeader(), adaptor.HertzHandler(http.FileServer(http.FS(pages)))) + r.GET("/favicon.ico", pageCacheHeader(), adaptor.HertzHandler(http.FileServer(http.FS(assets)))) + r.GET("/script.js", pageCacheHeader(), adaptor.HertzHandler(http.FileServer(http.FS(pages)))) + r.GET("/style.css", pageCacheHeader(), adaptor.HertzHandler(http.FileServer(http.FS(pages)))) + r.GET("/bootstrap.min.css", pageCacheHeader(), adaptor.HertzHandler(http.FileServer(http.FS(assets)))) + r.GET("/bootstrap.bundle.min.js", pageCacheHeader(), adaptor.HertzHandler(http.FileServer(http.FS(assets)))) return nil } From 1370617f5ba91dbba72e738abba8a826db8178a1 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Thu, 5 Jun 2025 17:29:18 +0800 Subject: [PATCH 16/99] 3.4.3 --- CHANGELOG.md | 6 ++++++ VERSION | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49187fe..92b291b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # 更新日志 +3.4.3 - 2025-06-05 +--- +- CHANGE: 弃用`adaptor.GetCompatRequest`, 切换到`adaptor.HertzHandler` +- CHANGE: 为`embedFS`使用包装器, 使其支持`Last-Modified` +- CHANGE: 为静态资源增加`Cache-Control: public, max-age=3600, must-revalidate` + 25w43a - 2025-06-05 --- - PRE-RELEASE: 此版本是v3.4.3预发布版本,请勿在生产环境中使用; diff --git a/VERSION b/VERSION index a423d42..8a0feb9 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.4.2 \ No newline at end of file +3.4.3 \ No newline at end of file From 6be6e1ba2cce564707c7349b30f784ab1f099233 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Thu, 5 Jun 2025 20:14:58 +0800 Subject: [PATCH 17/99] 25w44a --- .github/workflows/build-dev.yml | 2 +- .github/workflows/build.yml | 2 +- .gitignore | 3 +-- CHANGELOG.md | 7 +++++++ DEV-VERSION | 2 +- LICENSE | 20 +++++++++++--------- README.md | 4 +--- config/config.go | 29 ++++++++++++++++------------- config/config.toml | 1 + docs/config.md | 2 ++ docs/flag.md | 2 ++ docs/menu.md | 2 ++ proxy/docker.go | 29 ----------------------------- proxy/utils.go | 4 +++- 14 files changed, 49 insertions(+), 60 deletions(-) diff --git a/.github/workflows/build-dev.yml b/.github/workflows/build-dev.yml index ecee468..c1cb9d1 100644 --- a/.github/workflows/build-dev.yml +++ b/.github/workflows/build-dev.yml @@ -61,7 +61,7 @@ jobs: fi - name: 拉取前端 run: | - sudo git clone https://github.com/WJQSERVER-STUDIO/GHPrxoy-Frontend.git pages + sudo git clone https://github.com/WJQSERVER-STUDIO/GHProxy-Frontend.git pages sudo rm -rf pages/.git/ - name: 安装 Go diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4a02dfb..e0306c0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -62,7 +62,7 @@ jobs: fi - name: 拉取前端 run: | - sudo git clone https://github.com/WJQSERVER-STUDIO/GHPrxoy-Frontend.git pages + sudo git clone https://github.com/WJQSERVER-STUDIO/GHProxy-Frontend.git pages sudo rm -rf pages/.git/ - name: 安装 Go diff --git a/.gitignore b/.gitignore index 1834d15..0ad54a9 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,4 @@ demo.toml list.json repos pages -*_test -.* \ No newline at end of file +*_test \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 92b291b..fa06781 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # 更新日志 +25w44a - 2025-06-05 +--- +- PRE-RELEASE: 此版本是v3.5.0预发布版本,请勿在生产环境中使用; +- CHANGE: 更新许可证 v2.0 => v2.1 +- CHANGE: 修正工作流的一些问题 +- ADD: 增加`ForceAllowApiPassList`, 实现 #114 + 3.4.3 - 2025-06-05 --- - CHANGE: 弃用`adaptor.GetCompatRequest`, 切换到`adaptor.HertzHandler` diff --git a/DEV-VERSION b/DEV-VERSION index e303d5b..c4bec82 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w43a \ No newline at end of file +25w44a \ No newline at end of file diff --git a/LICENSE b/LICENSE index de09e5d..fbdd300 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ WJQserver Studio 开源许可证 -版本 v2.0 +版本 v2.1 版权所有 © WJQserver Studio 2024 @@ -31,8 +31,7 @@ WJQserver Studio 开源许可证 * 1.2 商业使用: 您可以在商业环境中使用本软件,无需获得额外授权,但您的商业使用行为必须遵守以下条款: - * 1.2.1 保持声明: 您在进行商业使用时,不得移除或修改软件中包含的原始版权声明、许可证声明以及来源声明。 - * 1.2.2 开源继承 (Copyleft) 与互惠共享: 如果您或您的组织希望将本软件或其衍生作品用于任何商业用途,包括但不限于: + * 1.2.1 开源继承 (Copyleft) 与互惠共享: 如果您或您的组织希望将本软件或其衍生作品用于任何商业用途,包括但不限于: * 盈利性分发: 销售、出租、许可分发本软件或其衍生作品。 * 盈利性服务: 基于本软件或其衍生作品提供商业服务,例如 SaaS 服务、咨询服务、定制开发服务、收费技术支持服务等。 @@ -44,6 +43,8 @@ WJQserver Studio 开源许可证 * i) 继承本许可证并开源: 您必须以本许可证或兼容的开源许可证分发您的衍生作品,并公开您的衍生作品的全部源代码,使得您的衍生作品的接收者也享有与您相同的权利,包括进一步修改和商业使用的权利。 本选项旨在促进社区的共同发展和知识共享,确保基于本软件的商业创新成果也能回馈社区。 * ii) 获得授权方明确授权: 如果您不希望以开源方式发布您的衍生作品,或者希望使用其他许可证进行分发,或者您希望在商业运营中使用修改后的版本但不开源,您必须事先获得 WJQserver Studio 的明确书面授权。 授权的具体条款和条件将由 WJQserver Studio 另行协商确定。 +* 1.3 保持声明: 公开发布服务时,不得移除或修改软件中包含的原始版权声明、许可证声明以及来源声明。 + 2. 复制与分发 * 2.1 原始版本复制与分发: 您可以复制和分发本软件的原始版本,前提是必须满足以下条件: @@ -51,13 +52,13 @@ WJQserver Studio 开源许可证 * 保留所有声明: 完整保留所有原始版权声明、许可证声明、来源声明以及其他所有权声明。 * 附带许可证: 在分发软件时,必须同时附带本许可证的完整文本,确保接收者知悉并理解本许可证的全部条款。 -* 2.2 衍生作品复制与分发: 您可以复制和分发基于本软件的衍生作品,您对衍生作品的分发行为将受到本许可证第 1.2.2 条(开源继承与互惠共享)的约束。 +* 2.2 衍生作品复制与分发: 您可以复制和分发基于本软件的衍生作品,您对衍生作品的分发行为将受到本许可证第 1.3 条(开源继承与互惠共享)的约束。 3. 修改权限 * 3.1 自由修改: 您被授予自由修改本软件的权限,无论修改目的是非营利性使用还是商业用途。 -* 3.2 修改后使用与分发约束: 当您将修改后的版本用于商业用途或分发修改后的版本时,您需要遵守本许可证第 1.2.2 条(开源继承与互惠共享)以及第 2 条(复制与分发)的规定。 即使您不分发修改后的版本,只要您将其用于商业目的,也需要遵守开源继承条款或获得授权。 +* 3.2 修改后使用与分发约束: 当您将修改后的版本用于商业用途或分发修改后的版本时,您需要遵守本许可证第 1.3 条(开源继承与互惠共享)以及第 2 条(复制与分发)的规定。 即使您不分发修改后的版本,只要您将其用于商业目的,也需要遵守开源继承条款或获得授权。 * 3.3 贡献接受: WJQserver Studio 鼓励社区贡献代码。如果您向本项目贡献代码,您需要同意您的贡献代码按照本许可证条款进行许可。 @@ -130,8 +131,7 @@ License Terms * 1.2 Commercial Use: You may use the Software in a commercial environment without additional authorization, but your commercial use must comply with the following terms: - * 1.2.1 Maintain Statements: When conducting commercial use, you must not remove or modify the original copyright notices, license notices, and source statements contained in the Software. - * 1.2.2 Open Source Inheritance (Copyleft) and Reciprocal Sharing: If you or your organization wish to use the Software or its Derivative Works for any commercial purpose, including but not limited to: + * 1.2.1 Open Source Inheritance (Copyleft) and Reciprocal Sharing: If you or your organization wish to use the Software or its Derivative Works for any commercial purpose, including but not limited to: * Profit-generating Distribution: Selling, renting, licensing, or distributing the Software or its Derivative Works. * Profit-generating Services: Providing commercial services based on the Software or its Derivative Works, such as SaaS services, consulting services, custom development services, and paid technical support services. @@ -143,6 +143,8 @@ License Terms * i) Inherit this License and Open Source: You must distribute your Derivative Works under this License or a compatible open-source license and publicly disclose the entire source code of your Derivative Works, so that recipients of your Derivative Works also enjoy the same rights as you, including the right to further modify and use commercially. This option aims to promote the common development and knowledge sharing of the community, ensuring that commercial innovation achievements based on this Software can also contribute back to the community. * ii) Obtain Explicit Authorization from the Licensor: If you do not wish to release your Derivative Works in an open-source manner, or wish to distribute them under another license, or you wish to use a modified version in commercial operations without open-sourcing it, you must obtain explicit written authorization from WJQserver Studio in advance. The specific terms and conditions of authorization will be determined separately by WJQserver Studio through negotiation. +* 1.3 Maintain Statements: When publish services to public, you must not remove or modify the original copyright notices, license notices, and source statements contained in the Software. + 2. Reproduction and Distribution * 2.1 Reproduction and Distribution of Original Version: You may reproduce and distribute the original version of the Software, provided that the following conditions are met: @@ -150,13 +152,13 @@ License Terms * Retain All Statements: Completely retain all original copyright notices, license notices, source statements, and other proprietary notices. * Accompany with License: When distributing the Software, you must also include the full text of this License to ensure that recipients are aware of and understand all terms of this License. -* 2.2 Reproduction and Distribution of Derivative Works: You may reproduce and distribute Derivative Works based on the Software. Your distribution of Derivative Works will be subject to the constraints of Clause 1.2.2 of this License (Open Source Inheritance and Reciprocal Sharing). +* 2.2 Reproduction and Distribution of Derivative Works: You may reproduce and distribute Derivative Works based on the Software. Your distribution of Derivative Works will be subject to the constraints of Clause 1.3 of this License (Open Source Inheritance and Reciprocal Sharing). 3. Modification Permissions * 3.1 Free Modification: You are granted permission to freely modify the Software, regardless of whether the purpose of modification is for non-profit use or commercial use. -* 3.2 Constraints on Use and Distribution after Modification: When you use a modified version for commercial purposes or distribute a modified version, you need to comply with the provisions of Clause 1.2.2 of this License (Open Source Inheritance and Reciprocal Sharing) and Clause 2 (Reproduction and Distribution). Even if you do not distribute the modified version, as long as you use it for commercial purposes, you also need to comply with the open-source inheritance clause or obtain authorization. +* 3.2 Constraints on Use and Distribution after Modification: When you use a modified version for commercial purposes or distribute a modified version, you need to comply with the provisions of Clause 1.3 of this License (Open Source Inheritance and Reciprocal Sharing) and Clause 2 (Reproduction and Distribution). Even if you do not distribute the modified version, as long as you use it for commercial purposes, you also need to comply with the open-source inheritance clause or obtain authorization. * 3.3 Contribution Acceptance: WJQserver Studio encourages community contribution of code. If you contribute code to this project, you need to agree that your contributed code is licensed under the terms of this License. diff --git a/README.md b/README.md index 829de33..b0d49f9 100644 --- a/README.md +++ b/README.md @@ -35,9 +35,7 @@ [相关文章](https://blog.wjqserver.com/categories/my-program/) -[GHProxy项目文档Next](https://wjqserver-docs.pages.dev/docs/ghproxy/) 感谢 [@redbunnys](https://github.com/redbunnys)的维护 - -[GHProxy项目文档](https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/docs/menu.md) +[GHProxy项目文档](https://wjqserver-docs.pages.dev/docs/ghproxy/) 感谢 [@redbunnys](https://github.com/redbunnys)的维护 ### 使用示例 diff --git a/config/config.go b/config/config.go index c1300dc..0060b63 100644 --- a/config/config.go +++ b/config/config.go @@ -110,15 +110,17 @@ Key = "" Token = "token" enabled = false passThrough = false -ForceAllowApi = true +ForceAllowApi = false +ForceAllowApiPassList = false */ type AuthConfig struct { - Enabled bool `toml:"enabled"` - Method string `toml:"method"` - Key string `toml:"key"` - Token string `toml:"token"` - PassThrough bool `toml:"passThrough"` - ForceAllowApi bool `toml:"ForceAllowApi"` + Enabled bool `toml:"enabled"` + Method string `toml:"method"` + Key string `toml:"key"` + Token string `toml:"token"` + PassThrough bool `toml:"passThrough"` + ForceAllowApi bool `toml:"ForceAllowApi"` + ForceAllowApiPassList bool `toml:"ForceAllowApiPassList"` } type BlacklistConfig struct { @@ -258,12 +260,13 @@ func DefaultConfig() *Config { HertZLogPath: "/data/ghproxy/log/hertz.log", }, Auth: AuthConfig{ - Enabled: false, - Method: "parameters", - Key: "", - Token: "token", - PassThrough: false, - ForceAllowApi: false, + Enabled: false, + Method: "parameters", + Key: "", + Token: "token", + PassThrough: false, + ForceAllowApi: false, + ForceAllowApiPassList: false, }, Blacklist: BlacklistConfig{ Enabled: false, diff --git a/config/config.toml b/config/config.toml index c8ea1f5..9ef2662 100644 --- a/config/config.toml +++ b/config/config.toml @@ -44,6 +44,7 @@ key = "" enabled = false passThrough = false ForceAllowApi = false +ForceAllowApiPassList = false [blacklist] blacklistFile = "/data/ghproxy/config/blacklist.json" diff --git a/docs/config.md b/docs/config.md index e5de161..a01f762 100644 --- a/docs/config.md +++ b/docs/config.md @@ -1,5 +1,7 @@ # ghproxy 用户配置文档 +> 弃用, 请转到 [GHProxy项目文档](https://wjqserver-docs.pages.dev/docs/ghproxy/) + `ghproxy` 的配置主要通过修改 `config` 目录下的 `config.toml`、`blacklist.json` 和 `whitelist.json` 文件来实现。本文档将详细介绍这些配置文件的作用以及用户可以自定义的配置选项。 ## `config.toml` - 主配置文件 diff --git a/docs/flag.md b/docs/flag.md index cc09c4c..c3aa0b7 100644 --- a/docs/flag.md +++ b/docs/flag.md @@ -1,5 +1,7 @@ # Flag +> 弃用, 请转到 [GHProxy项目文档](https://wjqserver-docs.pages.dev/docs/ghproxy/) + GHProxy接受以下flag传入 ```bash diff --git a/docs/menu.md b/docs/menu.md index 369e4b5..7e2c0ed 100644 --- a/docs/menu.md +++ b/docs/menu.md @@ -1,5 +1,7 @@ ## GHProxy 文档 +> 弃用, 请转到 [GHProxy项目文档](https://wjqserver-docs.pages.dev/docs/ghproxy/) + ### 配置文件 https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/docs/config.md diff --git a/proxy/docker.go b/proxy/docker.go index eb5fadf..615041e 100644 --- a/proxy/docker.go +++ b/proxy/docker.go @@ -35,35 +35,6 @@ func InitWeakCache() *weakcache.Cache[string] { return cache } -/* -func GhcrRouting(cfg *config.Config) app.HandlerFunc { - return func(ctx context.Context, c *app.RequestContext) { - - charToFind := '.' - reqTarget := c.Param("target") - path := "" - target := "" - - if strings.ContainsRune(reqTarget, charToFind) { - - path = c.Param("filepath") - if reqTarget == "docker.io" { - target = dockerhubTarget - } else if reqTarget == "ghcr.io" { - target = ghcrTarget - } else { - target = reqTarget - } - } else { - path = string(c.Request.RequestURI()) - } - - GhcrToTarget(ctx, c, cfg, target, path, nil) - - } -} -*/ - func GhcrWithImageRouting(cfg *config.Config) app.HandlerFunc { return func(ctx context.Context, c *app.RequestContext) { diff --git a/proxy/utils.go b/proxy/utils.go index 029dc26..e770c2b 100644 --- a/proxy/utils.go +++ b/proxy/utils.go @@ -10,7 +10,9 @@ import ( ) func listCheck(cfg *config.Config, c *app.RequestContext, user string, repo string, rawPath string) bool { - + if cfg.Auth.ForceAllowApi && cfg.Auth.ForceAllowApiPassList { + return false + } // 白名单检查 if cfg.Whitelist.Enabled { whitelist := auth.CheckWhitelist(user, repo) From 185522133b55f23550fc8ec58d7a7a40ee04d266 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Thu, 5 Jun 2025 22:55:24 +0800 Subject: [PATCH 18/99] 3.5.0 --- CHANGELOG.md | 6 ++++++ VERSION | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa06781..7922247 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # 更新日志 +3.5.0 - 2025-06-05 +--- +- CHANGE: 更新许可证 v2.0 => v2.1 +- CHANGE: 修正工作流的一些问题 +- ADD: 增加`ForceAllowApiPassList`, 实现 #114 + 25w44a - 2025-06-05 --- - PRE-RELEASE: 此版本是v3.5.0预发布版本,请勿在生产环境中使用; diff --git a/VERSION b/VERSION index 8a0feb9..e5b8203 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.4.3 \ No newline at end of file +3.5.0 \ No newline at end of file From f706615d87b755acdfb7f9e47510b917b413c65c Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sat, 7 Jun 2025 23:44:47 +0800 Subject: [PATCH 19/99] update deps --- go.mod | 10 +++++----- go.sum | 10 ++++++++++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 90b925e..1ccfe73 100644 --- a/go.mod +++ b/go.mod @@ -8,8 +8,8 @@ require ( github.com/WJQSERVER-STUDIO/logger v1.7.3 github.com/cloudwego/hertz v0.10.0 github.com/hertz-contrib/http2 v0.1.8 - golang.org/x/net v0.40.0 - golang.org/x/time v0.11.0 + golang.org/x/net v0.41.0 + golang.org/x/time v0.12.0 ) require ( @@ -36,10 +36,10 @@ require ( github.com/tidwall/pretty v1.2.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - golang.org/x/arch v0.17.0 // indirect - golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b // indirect + golang.org/x/arch v0.18.0 // indirect + golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 // indirect golang.org/x/sys v0.33.0 // indirect - golang.org/x/text v0.25.0 // indirect + golang.org/x/text v0.26.0 // indirect google.golang.org/protobuf v1.36.6 // indirect ) diff --git a/go.sum b/go.sum index 834e6c7..4881472 100644 --- a/go.sum +++ b/go.sum @@ -89,12 +89,16 @@ github.com/wjqserver/modembed v0.0.1/go.mod h1:sYbQJMAjSBsdYQrUsuHY380XXE1CuRh8g github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/arch v0.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU= golang.org/x/arch v0.17.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= +golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc= +golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 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-20250531010427-b6e5de432a8b h1:QoALfVG9rhQ/M7vYDScfPdWjGL9dlsVVM5VGh7aKoAA= golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= +golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 h1:bsqhLWFR6G6xiQcb+JoGqdKdRU6WzPWmK8E0jxTjzo4= +golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= 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= @@ -106,6 +110,8 @@ 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.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +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/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= @@ -135,8 +141,12 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= From 5dde21a403320031b43cd4b38189542e66ac11f0 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Mon, 9 Jun 2025 06:58:21 +0800 Subject: [PATCH 20/99] optimize matcher performance --- CHANGELOG.md | 5 + DEV-VERSION | 2 +- proxy/chunkreq.go | 2 +- proxy/match.go | 339 ++++++++++++++++++++++++++++-------------- proxy/matcher_test.go | 303 +++++++++++++++++++++++++++++++++++++ 5 files changed, 536 insertions(+), 115 deletions(-) create mode 100644 proxy/matcher_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 7922247..4df6a10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 更新日志 +25w45a - 2025-06-09 +--- +- PRE-RELEASE: 此版本是v3.5.1预发布版本,请勿在生产环境中使用; +- CHANGE: 大幅优化`Matcher`的性能, 实现零分配, 大幅提升性能; 单次操作时间: `254.3 ns/op` => `29.59 ns/op` + 3.5.0 - 2025-06-05 --- - CHANGE: 更新许可证 v2.0 => v2.1 diff --git a/DEV-VERSION b/DEV-VERSION index c4bec82..ebe0357 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w44a \ No newline at end of file +25w45a \ No newline at end of file diff --git a/proxy/chunkreq.go b/proxy/chunkreq.go index c760b9a..c50b7fb 100644 --- a/proxy/chunkreq.go +++ b/proxy/chunkreq.go @@ -110,7 +110,7 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c bodyReader = limitreader.NewRateLimitedReader(bodyReader, bandwidthLimit, int(bandwidthBurst), ctx) } - if MatcherShell(u) && matchString(matcher, matchedMatchers) && cfg.Shell.Editor { + if MatcherShell(u) && matchString(matcher) && cfg.Shell.Editor { // 判断body是不是gzip var compress string if resp.Header.Get("Content-Encoding") == "gzip" { diff --git a/proxy/match.go b/proxy/match.go index cee4eaa..a28520e 100644 --- a/proxy/match.go +++ b/proxy/match.go @@ -6,152 +6,265 @@ import ( "net/url" "regexp" "strings" + "sync" ) +// Matcher 从原始URL路径中高效地解析并匹配代理规则. 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 + if len(rawPath) < 18 { + return "", "", "", NewErrorWithStatusLookup(404, "path too short") } - // 匹配 "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) + // 匹配 "https://github.com/" + if strings.HasPrefix(rawPath, "https://github.com/") { + remaining := rawPath[19:] + i := strings.IndexByte(remaining, '/') + if i <= 0 { + return "", "", "", NewErrorWithStatusLookup(400, "malformed github path: missing user") + } + user := remaining[:i] + remaining = remaining[i+1:] + i = strings.IndexByte(remaining, '/') + if i <= 0 { + return "", "", "", NewErrorWithStatusLookup(400, "malformed github path: missing repo") + } + repo := remaining[:i] + remaining = remaining[i+1:] + if len(remaining) == 0 { + return "", "", "", NewErrorWithStatusLookup(400, "malformed github path: missing action") + } + i = strings.IndexByte(remaining, '/') + action := remaining + if i != -1 { + action = remaining[:i] + } + var matcher string + switch action { + case "releases", "archive": + matcher = "releases" + case "blob": + matcher = "blob" + case "raw": + matcher = "raw" + case "info", "git-upload-pack": + matcher = "clone" + default: + return "", "", "", NewErrorWithStatusLookup(400, fmt.Sprintf("unsupported github action: %s", action)) } - user = parts[1] - repo = "" - matcher = "gist" return user, repo, matcher, nil } - // 匹配 "https://api.github.com/"开头的链接 + + // 匹配 "https://raw.githubusercontent.com/" + if strings.HasPrefix(rawPath, "https://raw.githubusercontent.com/") { + remaining := rawPath[34:] + // 这里的逻辑与 github.com 的类似, 需要提取 user, repo, branch, file... + // 我们只需要 user 和 repo + i := strings.IndexByte(remaining, '/') + if i <= 0 { + return "", "", "", NewErrorWithStatusLookup(400, "malformed raw url: missing user") + } + user := remaining[:i] + remaining = remaining[i+1:] + i = strings.IndexByte(remaining, '/') + if i <= 0 { + return "", "", "", NewErrorWithStatusLookup(400, "malformed raw url: missing repo") + } + repo := remaining[:i] + // raw 链接至少需要 user/repo/branch 三部分 + remaining = remaining[i+1:] + if len(remaining) == 0 { + return "", "", "", NewErrorWithStatusLookup(400, "malformed raw url: missing branch/commit") + } + return user, repo, "raw", nil + } + + // 匹配 "https://gist.github.com/" + if strings.HasPrefix(rawPath, "https://gist.github.com/") { + remaining := rawPath[24:] + i := strings.IndexByte(remaining, '/') + if i <= 0 { + // case: https://gist.github.com/user + // 这种情况下, gist_id 缺失, 但我们仍然可以认为 user 是有效的 + if len(remaining) > 0 { + return remaining, "", "gist", nil + } + return "", "", "", NewErrorWithStatusLookup(400, "malformed gist url: missing user") + } + // case: https://gist.github.com/user/gist_id... + user := remaining[:i] + return user, "", "gist", 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 !cfg.Auth.ForceAllowApi && (cfg.Auth.Method != "header" || !cfg.Auth.Enabled) { + return "", "", "", NewErrorWithStatusLookup(403, "API proxy requires header authentication") } - 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) + remaining := rawPath[23:] + var user, repo string + if strings.HasPrefix(remaining, "repos/") { + parts := strings.SplitN(remaining[6:], "/", 3) + if len(parts) >= 2 { + user = parts[0] + repo = parts[1] + } + } else if strings.HasPrefix(remaining, "users/") { + parts := strings.SplitN(remaining[6:], "/", 2) + if len(parts) >= 1 { + user = parts[0] } } - return user, repo, matcher, nil + return user, repo, "api", nil } - //return "", "", "", ErrNotFound - errMsg := "Didn't match any matcher" - return "", "", "", NewErrorWithStatusLookup(404, errMsg) + + return "", "", "", NewErrorWithStatusLookup(404, "no matcher found for the given path") } -var ( - matchedMatchers = []string{ - "blob", - "raw", - "gist", +/* + 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 ) -// matchString 检查目标字符串是否在给定的字符串集合中 -func matchString(target string, stringsToMatch []string) bool { - matchMap := make(map[string]struct{}, len(stringsToMatch)) - for _, str := range stringsToMatch { - matchMap[str] = struct{}{} - } - _, exists := matchMap[target] +func initMatchers() { + initMatchersOnce.Do(func() { + matchers := []string{"blob", "raw", "gist"} + proxyableMatchersMap = make(map[string]struct{}, len(matchers)) + for _, m := range matchers { + proxyableMatchersMap[m] = struct{}{} + } + }) +} + +// matchString 与原始版本签名兼容 +func matchString(target string) bool { + initMatchers() + _, exists := proxyableMatchersMap[target] return exists } -// extractParts 从给定的 URL 中提取所需的部分 +// extractParts 与原始版本签名兼容 func extractParts(rawURL string) (string, string, string, url.Values, error) { - // 解析 URL parsedURL, err := url.Parse(rawURL) if err != nil { return "", "", "", nil, err } - // 获取路径部分并分割 - pathParts := strings.Split(parsedURL.Path, "/") + path := parsedURL.Path + if len(path) > 0 && path[0] == '/' { + path = path[1:] + } - // 提取所需的部分 - if len(pathParts) < 3 { + parts := strings.SplitN(path, "/", 3) + + if len(parts) < 2 { return "", "", "", nil, fmt.Errorf("URL path is too short") } - // 提取 /WJQSERVER-STUDIO 和 /go-utils.git - repoOwner := "/" + pathParts[1] - repoName := "/" + pathParts[2] - - // 剩余部分 - remainingPath := strings.Join(pathParts[3:], "/") - if remainingPath != "" { - remainingPath = "/" + remainingPath + repoOwner := "/" + parts[0] + repoName := "/" + parts[1] + var remainingPath string + if len(parts) > 2 { + remainingPath = "/" + parts[2] } - // 查询参数 - queryParams := parsedURL.Query() - - return repoOwner, repoName, remainingPath, queryParams, nil + return repoOwner, repoName, remainingPath, parsedURL.Query(), nil } var urlPattern = regexp.MustCompile(`https?://[^\s'"]+`) diff --git a/proxy/matcher_test.go b/proxy/matcher_test.go new file mode 100644 index 0000000..21329c3 --- /dev/null +++ b/proxy/matcher_test.go @@ -0,0 +1,303 @@ +package proxy + +import ( + "ghproxy/config" + "net/url" + "reflect" + "testing" +) + +func TestMatcher_Compatibility(t *testing.T) { + // --- 准备各种配置用于测试 --- + cfgWithAuth := &config.Config{ + Auth: config.AuthConfig{Enabled: true, Method: "header", ForceAllowApi: false}, + } + cfgNoAuth := &config.Config{ + Auth: config.AuthConfig{Enabled: false}, + } + cfgApiForceAllowed := &config.Config{ + Auth: config.AuthConfig{ForceAllowApi: true}, + } + cfgWrongAuthMethod := &config.Config{ + Auth: config.AuthConfig{Enabled: true, Method: "none"}, + } + + testCases := []struct { + name string + rawPath string + config *config.Config + expectedUser string + expectedRepo string + expectedMatcher string + expectError bool + expectedErrCode int + }{ + { + name: "GH Releases Path", + rawPath: "https://github.com/owner/repo/releases/download/v1.0/asset.zip", + config: cfgWithAuth, + expectedUser: "owner", expectedRepo: "repo", expectedMatcher: "releases", + }, + { + name: "GH Archive Path", + rawPath: "https://github.com/owner/repo.git/archive/main.zip", + config: cfgWithAuth, + expectedUser: "owner", expectedRepo: "repo.git", expectedMatcher: "releases", + }, + { + name: "GH Blob Path", + rawPath: "https://github.com/owner/repo/blob/main/path/to/file.go", + config: cfgWithAuth, + expectedUser: "owner", expectedRepo: "repo", expectedMatcher: "blob", + }, + { + name: "GH Raw Path", + rawPath: "https://github.com/owner/repo/raw/main/image.png", + config: cfgWithAuth, + expectedUser: "owner", expectedRepo: "repo", expectedMatcher: "raw", + }, + { + name: "GH Clone Info Refs", + rawPath: "https://github.com/owner/repo.git/info/refs?service=git-upload-pack", + config: cfgWithAuth, + expectedUser: "owner", expectedRepo: "repo.git", expectedMatcher: "clone", + }, + { + name: "GH Clone Git Upload Pack", + rawPath: "https://github.com/owner/repo/git-upload-pack", + config: cfgWithAuth, + expectedUser: "owner", expectedRepo: "repo", expectedMatcher: "clone", + }, + + { + name: "RawGHUserContent Path", + rawPath: "https://raw.githubusercontent.com/owner/repo/branch/file.sh", + config: cfgWithAuth, + expectedUser: "owner", expectedRepo: "repo", expectedMatcher: "raw", + }, + { + name: "Gist Path", + rawPath: "https://gist.github.com/user/abcdef1234567890", + config: cfgWithAuth, + expectedUser: "user", expectedRepo: "", expectedMatcher: "gist", + }, + { + name: "API Repos Path (with Auth)", + rawPath: "https://api.github.com/repos/owner/repo/pulls", + config: cfgWithAuth, + expectedUser: "owner", expectedRepo: "repo", expectedMatcher: "api", + }, + { + name: "API Users Path (with Auth)", + rawPath: "https://api.github.com/users/someuser/repos", + config: cfgWithAuth, + expectedUser: "someuser", expectedRepo: "", expectedMatcher: "api", + }, + { + name: "API Other Path (with Auth)", + rawPath: "https://api.github.com/octocat", + config: cfgWithAuth, + expectedUser: "", expectedRepo: "", expectedMatcher: "api", + }, + { + name: "API Path (Force Allowed)", + rawPath: "https://api.github.com/repos/owner/repo", + config: cfgApiForceAllowed, // Auth disabled, but force allowed + expectedUser: "owner", expectedRepo: "repo", expectedMatcher: "api", + }, + { + name: "Malformed GH Path (no repo)", + rawPath: "https://github.com/owner/", + config: cfgWithAuth, + expectError: true, expectedErrCode: 400, + }, + { + name: "Malformed GH Path (no action)", + rawPath: "https://github.com/owner/repo", + config: cfgWithAuth, + expectError: true, expectedErrCode: 400, + }, + { + name: "Malformed GH Path (empty user)", + rawPath: "https://github.com//repo/blob/main/file.go", + config: cfgWithAuth, + expectError: true, expectedErrCode: 400, + }, + { + name: "Malformed Raw Path (no repo)", + rawPath: "https://raw.githubusercontent.com/owner/", + config: cfgWithAuth, + expectError: true, expectedErrCode: 400, + }, + { + name: "Malformed Gist Path (no user)", + rawPath: "https://gist.github.com/", + config: cfgWithAuth, + expectError: true, expectedErrCode: 400, + }, + { + name: "Unsupported GH Action", + rawPath: "https://github.com/owner/repo/issues/123", + config: cfgWithAuth, + expectError: true, expectedErrCode: 400, + }, + { + name: "API Path (No Auth)", + rawPath: "https://api.github.com/user", + config: cfgNoAuth, + expectError: true, expectedErrCode: 403, + }, + { + name: "API Path (Wrong Auth Method)", + rawPath: "https://api.github.com/user", + config: cfgWrongAuthMethod, + expectError: true, expectedErrCode: 403, + }, + { + name: "No Matcher Found (other domain)", + rawPath: "https://bitbucket.org/owner/repo", + config: cfgWithAuth, + expectError: true, expectedErrCode: 404, + }, + { + name: "No Matcher Found (path too short)", + rawPath: "https://a.co", + config: cfgWithAuth, + expectError: true, expectedErrCode: 404, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + user, repo, matcher, ghErr := Matcher(tc.rawPath, tc.config) + + if tc.expectError { + if ghErr == nil { + t.Fatalf("Expected a GHProxyErrors error, but got nil") + } + if ghErr.StatusCode != tc.expectedErrCode { + t.Errorf("Expected error code %d, but got %d (msg: %s)", + tc.expectedErrCode, ghErr.StatusCode, ghErr.ErrorMessage) + } + } else { + if ghErr != nil { + t.Fatalf("Expected no error, but got: %s", ghErr.ErrorMessage) + } + if user != tc.expectedUser { + t.Errorf("user: got %q, want %q", user, tc.expectedUser) + } + if repo != tc.expectedRepo { + t.Errorf("repo: got %q, want %q", repo, tc.expectedRepo) + } + if matcher != tc.expectedMatcher { + t.Errorf("matcher: got %q, want %q", matcher, tc.expectedMatcher) + } + } + }) + } +} + +func TestExtractParts_Compatibility(t *testing.T) { + testCases := []struct { + name string + rawURL string + expectedOwner string + expectedRepo string + expectedRem string + expectedQuery url.Values + expectError bool + }{ + { + name: "Standard git clone URL", + rawURL: "https://github.com/WJQSERVER-STUDIO/go-utils.git/info/refs?service=git-upload-pack", + expectedOwner: "/WJQSERVER-STUDIO", + expectedRepo: "/go-utils.git", + expectedRem: "/info/refs", + expectedQuery: url.Values{"service": []string{"git-upload-pack"}}, + }, + { + name: "No remaining path", + rawURL: "https://example.com/owner/repo", + expectedOwner: "/owner", + expectedRepo: "/repo", + expectedRem: "", + expectedQuery: url.Values{}, + }, + { + name: "Root path only", + rawURL: "https://example.com/", + expectError: true, // Path is too short + }, + { + name: "One level path", + rawURL: "https://example.com/owner", + expectError: true, // Path is too short + }, + { + name: "Empty path segments", + rawURL: "https://example.com//repo/a", // Will be treated as /repo/a + expectedOwner: "", // First part is empty + expectedRepo: "/repo", + expectedRem: "/a", + }, + { + name: "Invalid URL format", + rawURL: "://invalid", + expectError: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + owner, repo, rem, query, err := extractParts(tc.rawURL) + + if (err != nil) != tc.expectError { + t.Fatalf("extractParts() error = %v, expectError %v", err, tc.expectError) + } + + if !tc.expectError { + if owner != tc.expectedOwner { + t.Errorf("owner: got %q, want %q", owner, tc.expectedOwner) + } + if repo != tc.expectedRepo { + t.Errorf("repo: got %q, want %q", repo, tc.expectedRepo) + } + if rem != tc.expectedRem { + t.Errorf("remaining path: got %q, want %q", rem, tc.expectedRem) + } + if !reflect.DeepEqual(query, tc.expectedQuery) { + t.Errorf("query: got %v, want %v", query, tc.expectedQuery) + } + } + }) + } +} + +func TestMatchString_Compatibility(t *testing.T) { + testCases := []struct { + target string + expected bool + }{ + {"blob", true}, {"raw", true}, {"gist", true}, + {"clone", false}, {"releases", false}, + } + for _, tc := range testCases { + t.Run(tc.target, func(t *testing.T) { + if got := matchString(tc.target); got != tc.expected { + t.Errorf("matchString('%s') = %v; want %v", tc.target, got, tc.expected) + } + }) + } +} + +func BenchmarkMatcher(b *testing.B) { + cfg := &config.Config{} + path := "https://github.com/WJQSERVER/speedtest-ex/releases/download/v1.2.0/speedtest-linux-amd64.tar.gz" + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _, _, _, _ = Matcher(path, cfg) + } +} From 8d5b764ec742808456ea377e7075095cc6d961db Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Mon, 9 Jun 2025 23:30:39 +0800 Subject: [PATCH 21/99] optimize matcher --- proxy/match.go | 201 +++++++++++++++++++++++------------------- proxy/matcher_test.go | 6 ++ 2 files changed, 117 insertions(+), 90 deletions(-) diff --git a/proxy/match.go b/proxy/match.go index a28520e..896a99b 100644 --- a/proxy/match.go +++ b/proxy/match.go @@ -9,6 +9,25 @@ import ( "sync" ) +var ( + githubPrefix = "https://github.com/" + rawPrefix = "https://raw.githubusercontent.com/" + gistPrefix = "https://gist.github.com/" + apiPrefix = "https://api.github.com/" + githubPrefixLen int + rawPrefixLen int + gistPrefixLen int + apiPrefixLen int +) + +func init() { + githubPrefixLen = len(githubPrefix) + rawPrefixLen = len(rawPrefix) + gistPrefixLen = len(gistPrefix) + apiPrefixLen = len(apiPrefix) + //log.Printf("githubPrefixLen: %d, rawPrefixLen: %d, gistPrefixLen: %d, apiPrefixLen: %d", githubPrefixLen, rawPrefixLen, gistPrefixLen, apiPrefixLen) +} + // Matcher 从原始URL路径中高效地解析并匹配代理规则. func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHProxyErrors) { if len(rawPath) < 18 { @@ -16,8 +35,8 @@ func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHPro } // 匹配 "https://github.com/" - if strings.HasPrefix(rawPath, "https://github.com/") { - remaining := rawPath[19:] + if strings.HasPrefix(rawPath, githubPrefix) { + remaining := rawPath[githubPrefixLen:] i := strings.IndexByte(remaining, '/') if i <= 0 { return "", "", "", NewErrorWithStatusLookup(400, "malformed github path: missing user") @@ -55,8 +74,8 @@ func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHPro } // 匹配 "https://raw.githubusercontent.com/" - if strings.HasPrefix(rawPath, "https://raw.githubusercontent.com/") { - remaining := rawPath[34:] + if strings.HasPrefix(rawPath, rawPrefix) { + remaining := rawPath[rawPrefixLen:] // 这里的逻辑与 github.com 的类似, 需要提取 user, repo, branch, file... // 我们只需要 user 和 repo i := strings.IndexByte(remaining, '/') @@ -79,8 +98,8 @@ func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHPro } // 匹配 "https://gist.github.com/" - if strings.HasPrefix(rawPath, "https://gist.github.com/") { - remaining := rawPath[24:] + if strings.HasPrefix(rawPath, gistPrefix) { + remaining := rawPath[gistPrefixLen:] i := strings.IndexByte(remaining, '/') if i <= 0 { // case: https://gist.github.com/user @@ -96,11 +115,11 @@ func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHPro } // 匹配 "https://api.github.com/" - if strings.HasPrefix(rawPath, "https://api.github.com/") { + if strings.HasPrefix(rawPath, apiPrefix) { if !cfg.Auth.ForceAllowApi && (cfg.Auth.Method != "header" || !cfg.Auth.Enabled) { return "", "", "", NewErrorWithStatusLookup(403, "API proxy requires header authentication") } - remaining := rawPath[23:] + remaining := rawPath[apiPrefixLen:] var user, repo string if strings.HasPrefix(remaining, "repos/") { parts := strings.SplitN(remaining[6:], "/", 3) @@ -120,103 +139,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") +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, "/") - //} + //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 + 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) } - // 匹配 "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)." + 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] - 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) - } + if parts[0] == "users" { 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 !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) } - 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) + 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 diff --git a/proxy/matcher_test.go b/proxy/matcher_test.go index 21329c3..3293817 100644 --- a/proxy/matcher_test.go +++ b/proxy/matcher_test.go @@ -68,6 +68,12 @@ func TestMatcher_Compatibility(t *testing.T) { config: cfgWithAuth, expectedUser: "owner", expectedRepo: "repo", expectedMatcher: "clone", }, + { + name: "Girhub Broken Path", + rawPath: "https://github.com/owner", + config: cfgWithAuth, + expectError: true, expectedErrCode: 400, + }, { name: "RawGHUserContent Path", From 5731418822fd37e3e6c81da6992797a2868b790b Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Mon, 9 Jun 2025 23:31:37 +0800 Subject: [PATCH 22/99] 3.5.1 --- CHANGELOG.md | 4 ++++ VERSION | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4df6a10..a36f5a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 更新日志 +3.5.1 - 2025-06-09 +--- +- CHANGE: 大幅优化`Matcher`的性能, 实现零分配, 大幅提升性能; 单次操作时间: `254.3 ns/op` => `29.59 ns/op` + 25w45a - 2025-06-09 --- - PRE-RELEASE: 此版本是v3.5.1预发布版本,请勿在生产环境中使用; diff --git a/VERSION b/VERSION index e5b8203..3c8ff8c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.5.0 \ No newline at end of file +3.5.1 \ No newline at end of file From 5b05588375dcd169e149b8840bce6ba79ad3be22 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Tue, 10 Jun 2025 22:11:52 +0800 Subject: [PATCH 23/99] update deps && LICENSE --- LICENSE | 516 +++++++++++++++++++++++++++++++++----------------- LICENSE-WSL | 199 +++++++++++++++++++ README.md | 4 +- go.mod | 2 +- go.sum | 4 + proxy/nest.go | 4 - 6 files changed, 552 insertions(+), 177 deletions(-) create mode 100644 LICENSE-WSL diff --git a/LICENSE b/LICENSE index fbdd300..dbdb0fa 100644 --- a/LICENSE +++ b/LICENSE @@ -1,199 +1,373 @@ -WJQserver Studio 开源许可证 -版本 v2.1 +Mozilla Public License Version 2.0 +================================== -版权所有 © WJQserver Studio 2024 +1. Definitions +-------------- -定义 +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. -* 许可 (License): 指的是在本许可证内定义的使用、复制、分发与修改软件的条款与要求。 -* 授权方 (Licensor): 指的是拥有版权的个人或组织,亦或是拥有版权的个人或组织所指派的实体,在本许可证中特指 WJQserver Studio。 -* 贡献者 (Contributor): 指的是授权方以及根据本许可证授予贡献代码或软件的个人或实体。 -* 您 (You): 指的是行使本许可授予的权限的个人或法律实体。 -* 衍生作品 (Derivative Works): 指的是基于本软件或本软件任何部分的修改作品,无论修改程度如何。这包括但不限于基于本软件或其任何部分的修改、修订、改编、翻译或其他形式的创作,以及包含本软件或其部分的集合作品。 -* 非营利性使用 (Non-profit Use): 指的是不以直接商业盈利为主要目的的使用方式,包括但不限于: - * 个人用途: 由个人为了个人学习、研究、实验、非商业项目、个人网站搭建、毕业设计、家庭内部娱乐等非直接商业目的使用软件。 - * 教育用途: 在教育机构(如学校、大学、培训机构)内部用于教学、研究、学术交流等活动。 - * 科研用途: 在科研院所、实验室等机构内部用于科学研究、实验开发等活动。 - * 慈善与公益用途: 由慈善机构、公益组织等非营利性组织为了其公益使命或慈善事业内部运营使用,或对外提供不直接产生商业利润的公益服务。 - * 内部运营用途 (非营利组织): 非营利性组织在其内部运营中使用软件,例如用于行政管理、会员管理、内部沟通、项目管理等非直接营利性活动。 +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. -开源与自由软件 +1.3. "Contribution" + means Covered Software of a particular Contributor. -本项目为开源软件,允许用户在遵循本许可证的前提下访问和使用源代码。 -本项目旨在向用户提供尽可能广泛的非商业使用自由,同时保障社区的共同发展和良性生态,并为商业创新提供清晰的路径。 -强调版权所有,所有权利由 WJQserver Studio 及贡献者共同保留。 +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. -许可证条款 +1.5. "Incompatible With Secondary Licenses" + means -1. 使用权限 + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or -* 1.1 非营利性使用: 您被授予在非营利性使用场景下,为了任何目的,自由使用本软件的权限。 非营利性使用的具体场景包括但不限于定义部分所列举的各种情况。 + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. -* 1.2 商业使用: 您可以在商业环境中使用本软件,无需获得额外授权,但您的商业使用行为必须遵守以下条款: +1.6. "Executable Form" + means any form of the work other than Source Code Form. - * 1.2.1 开源继承 (Copyleft) 与互惠共享: 如果您或您的组织希望将本软件或其衍生作品用于任何商业用途,包括但不限于: +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. - * 盈利性分发: 销售、出租、许可分发本软件或其衍生作品。 - * 盈利性服务: 基于本软件或其衍生作品提供商业服务,例如 SaaS 服务、咨询服务、定制开发服务、收费技术支持服务等。 - * 嵌入式商业应用: 将本软件或其衍生作品嵌入到商业产品或解决方案中进行销售。 - * 组织内部商业运营: 在营利性组织的内部运营中使用修改后的版本以直接支持其商业活动,例如定制化内部系统,通过例如但不限于在软件或相关服务中投放广告 (例如 Google Ads 等),应用内购买 (内购), 会员订阅, 增值功能收费等方式直接或间接产生商业收入。 +1.8. "License" + means this document. - 您必须选择以下两种方式之一: +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. - * i) 继承本许可证并开源: 您必须以本许可证或兼容的开源许可证分发您的衍生作品,并公开您的衍生作品的全部源代码,使得您的衍生作品的接收者也享有与您相同的权利,包括进一步修改和商业使用的权利。 本选项旨在促进社区的共同发展和知识共享,确保基于本软件的商业创新成果也能回馈社区。 - * ii) 获得授权方明确授权: 如果您不希望以开源方式发布您的衍生作品,或者希望使用其他许可证进行分发,或者您希望在商业运营中使用修改后的版本但不开源,您必须事先获得 WJQserver Studio 的明确书面授权。 授权的具体条款和条件将由 WJQserver Studio 另行协商确定。 +1.10. "Modifications" + means any of the following: -* 1.3 保持声明: 公开发布服务时,不得移除或修改软件中包含的原始版权声明、许可证声明以及来源声明。 + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or -2. 复制与分发 + (b) any new file in Source Code Form that contains any Covered + Software. -* 2.1 原始版本复制与分发: 您可以复制和分发本软件的原始版本,前提是必须满足以下条件: +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. - * 保留所有声明: 完整保留所有原始版权声明、许可证声明、来源声明以及其他所有权声明。 - * 附带许可证: 在分发软件时,必须同时附带本许可证的完整文本,确保接收者知悉并理解本许可证的全部条款。 +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. -* 2.2 衍生作品复制与分发: 您可以复制和分发基于本软件的衍生作品,您对衍生作品的分发行为将受到本许可证第 1.3 条(开源继承与互惠共享)的约束。 +1.13. "Source Code Form" + means the form of the work preferred for making modifications. -3. 修改权限 +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. -* 3.1 自由修改: 您被授予自由修改本软件的权限,无论修改目的是非营利性使用还是商业用途。 +2. License Grants and Conditions +-------------------------------- -* 3.2 修改后使用与分发约束: 当您将修改后的版本用于商业用途或分发修改后的版本时,您需要遵守本许可证第 1.3 条(开源继承与互惠共享)以及第 2 条(复制与分发)的规定。 即使您不分发修改后的版本,只要您将其用于商业目的,也需要遵守开源继承条款或获得授权。 +2.1. Grants -* 3.3 贡献接受: WJQserver Studio 鼓励社区贡献代码。如果您向本项目贡献代码,您需要同意您的贡献代码按照本许可证条款进行许可。 +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: -4. 专利权 +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and -* 4.1 无专利担保,风险自担: 本软件以“现状”提供,授权方及贡献者明确声明,不对本软件的专利侵权问题做任何形式的担保,亦不承担任何因专利侵权可能产生的责任与后果。 用户理解并同意,使用本软件的专利风险完全由用户自行承担。 +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. -* 4.2 专利纠纷应对: 如因用户使用本软件而引发任何专利侵权指控、诉讼或索赔,用户应自行负责处理并承担全部法律责任。 授权方及贡献者无义务参与任何相关法律程序,亦不承担任何由此产生的费用或赔偿。 +2.2. Effective Date -5. 免责声明 +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. -* 5.1 “现状”提供,无任何保证: 本软件按“现状”提供,不提供任何明示或暗示的保证,包括但不限于适销性、特定用途适用性及非侵权性。 +2.3. Limitations on Grant Scope -* 5.2 责任限制: 在适用法律允许的最大范围内,在任何情况下,授权方或任何贡献者均不对因使用或无法使用本软件而产生的任何直接、间接、偶然、特殊、惩罚性或后果性损害(包括但不限于采购替代商品或服务;损失使用、数据或利润;或业务中断)负责,无论其是如何造成的,也无论依据何种责任理论,即使已被告知可能发生此类损害。 - -* 5.3 用户法律责任: 用户需根据当地法律对待本项目,确保遵守所有适用法规。 - -6. 许可证期限与终止 - -* 6.1 许可证期限: 除版权所有人主动宣布放弃本软件版权外,本许可证无限期生效。 - -* 6.2 许可证终止: 如果您未能遵守本许可证的任何条款或条件,授权方有权终止本许可证。 您的许可证将在您违反本许可证条款时自动终止。 - -* 6.3 终止后的效力: 许可证终止后,您根据本许可证所享有的所有权利将立即终止,但您在许可证终止前已合法分发的软件副本,其接收者所获得的许可及权利将不受影响,继续有效。 免责声明(第 5 条)和责任限制(第 5.2 条)在本许可证终止后仍然有效。 - -7. 条款修订 - -* 7.1 修订权利保留: 授权方保留随时修改本许可证条款的权利,以便更好地适应法律、技术发展以及社区需求。 - -* 7.2 修订生效与接受: 修订后的条款将在发布时生效,除非另行声明,否则继续使用、复制、分发或修改本软件即表示您接受修订后的条款。授权方鼓励用户定期查阅本许可证的最新版本。 - -8. 其他 - -* 8.1 法定权利: 本许可证不影响您作为最终用户在适用法律下的法定权利。 - -* 8.2 条款可分割性: 若本许可证的某些条款被认定为不可执行,其余条款仍然完全有效。 - -* 8.3 版本更新: 授权方可能会发布本许可证的修订版本或新版本。您可以选择是继续使用本许可证的旧版本还是选择适用新版本。 - -WJQserver Studio Open Source License -Version v2.0 - -Copyright © WJQserver Studio 2024 - -Definitions - -* License: Refers to the terms and requirements for use, reproduction, distribution, and modification defined within this license. -* Licensor: Refers to the individual or organization that holds the copyright, or the entity designated by the copyright holder, specifically WJQserver Studio in this license. -* Contributor: Refers to the Licensor and individuals or entities who contribute code or software under this License. -* You: Refers to the individual or legal entity exercising permissions granted by this License. -* Derivative Works: Refers to works modified based on the Software or any part thereof, regardless of the extent of modification. This includes but is not limited to modifications, revisions, adaptations, translations, or other forms of creation based on the Software or any part thereof, as well as collective works containing the Software or parts thereof. -* Non-profit Use: Refers to uses not primarily intended for direct commercial profit, including but not limited to: - * Personal Use: Use by an individual for personal learning, research, experimentation, non-commercial projects, personal website development, graduation projects, home entertainment, and other non-directly commercial purposes. - * Educational Use: Use within educational institutions (such as schools, universities, training organizations) for activities such as teaching, research, and academic exchange. - * Scientific Research Use: Use within scientific research institutions, laboratories, and similar organizations for activities such as scientific research and experimental development. - * Charitable and Public Welfare Use: Use by charitable organizations, public welfare organizations, and similar non-profit entities for their public missions or internal operation of charitable activities, or to provide public services that do not directly generate commercial profit. - * Internal Operational Use (Non-profit Organizations): Use within the internal operations of non-profit organizations, such as for administrative management, membership management, internal communication, project management, and other non-directly profit-generating activities. - -Open Source and Free Software - -This project is open-source software, allowing users to access and use the source code under the premise of complying with this License. -This project aims to provide users with the broadest possible freedom for non-commercial use while ensuring the common development and healthy ecosystem of the community, and providing a clear path for commercial innovation. -Copyright is emphasized; all rights are jointly reserved by WJQserver Studio and Contributors. - -License Terms - -1. Permissions for Use - -* 1.1 Non-profit Use: You are granted permission to freely use the Software for any purpose in non-profit use scenarios. Specific non-profit use scenarios include but are not limited to the various situations listed in the Definition section. - -* 1.2 Commercial Use: You may use the Software in a commercial environment without additional authorization, but your commercial use must comply with the following terms: - - * 1.2.1 Open Source Inheritance (Copyleft) and Reciprocal Sharing: If you or your organization wish to use the Software or its Derivative Works for any commercial purpose, including but not limited to: - - * Profit-generating Distribution: Selling, renting, licensing, or distributing the Software or its Derivative Works. - * Profit-generating Services: Providing commercial services based on the Software or its Derivative Works, such as SaaS services, consulting services, custom development services, and paid technical support services. - * Embedded Commercial Applications: Embedding the Software or its Derivative Works into commercial products or solutions for sale. - * Internal Commercial Operations: Using modified versions within the internal operations of for-profit organizations to directly support their commercial activities, such as customized internal systems, generating commercial revenue directly or indirectly through means including but not limited to placing advertisements in the software or related services (e.g., Google Ads), in-app purchases, membership subscriptions, and charging for value-added features. - - You must choose one of the following two options: - - * i) Inherit this License and Open Source: You must distribute your Derivative Works under this License or a compatible open-source license and publicly disclose the entire source code of your Derivative Works, so that recipients of your Derivative Works also enjoy the same rights as you, including the right to further modify and use commercially. This option aims to promote the common development and knowledge sharing of the community, ensuring that commercial innovation achievements based on this Software can also contribute back to the community. - * ii) Obtain Explicit Authorization from the Licensor: If you do not wish to release your Derivative Works in an open-source manner, or wish to distribute them under another license, or you wish to use a modified version in commercial operations without open-sourcing it, you must obtain explicit written authorization from WJQserver Studio in advance. The specific terms and conditions of authorization will be determined separately by WJQserver Studio through negotiation. - -* 1.3 Maintain Statements: When publish services to public, you must not remove or modify the original copyright notices, license notices, and source statements contained in the Software. - -2. Reproduction and Distribution - -* 2.1 Reproduction and Distribution of Original Version: You may reproduce and distribute the original version of the Software, provided that the following conditions are met: - - * Retain All Statements: Completely retain all original copyright notices, license notices, source statements, and other proprietary notices. - * Accompany with License: When distributing the Software, you must also include the full text of this License to ensure that recipients are aware of and understand all terms of this License. - -* 2.2 Reproduction and Distribution of Derivative Works: You may reproduce and distribute Derivative Works based on the Software. Your distribution of Derivative Works will be subject to the constraints of Clause 1.3 of this License (Open Source Inheritance and Reciprocal Sharing). - -3. Modification Permissions - -* 3.1 Free Modification: You are granted permission to freely modify the Software, regardless of whether the purpose of modification is for non-profit use or commercial use. - -* 3.2 Constraints on Use and Distribution after Modification: When you use a modified version for commercial purposes or distribute a modified version, you need to comply with the provisions of Clause 1.3 of this License (Open Source Inheritance and Reciprocal Sharing) and Clause 2 (Reproduction and Distribution). Even if you do not distribute the modified version, as long as you use it for commercial purposes, you also need to comply with the open-source inheritance clause or obtain authorization. - -* 3.3 Contribution Acceptance: WJQserver Studio encourages community contribution of code. If you contribute code to this project, you need to agree that your contributed code is licensed under the terms of this License. - -4. Patent Rights - -* 4.1 No Patent Warranty, Risk Self-Bearing: The software is provided “AS IS”, and the Licensor and Contributors explicitly declare that they do not provide any form of warranty regarding patent infringement issues of this software, nor do they assume any responsibility and consequences arising from patent infringement. Users understand and agree that the patent risk of using this software is entirely borne by the users themselves. - -* 4.2 Handling of Patent Disputes: If any patent infringement allegations, lawsuits, or claims arise due to the user's use of this Software, the user shall be solely responsible for handling and bear all legal liabilities. The Licensor and Contributors are under no obligation to participate in any related legal proceedings, nor do they bear any costs or compensation arising therefrom. - -5. Disclaimer of Warranty - -* 5.1 “AS IS” Provision, No Warranty: The software is provided “AS IS” without any express or implied warranties, including but not limited to warranties of merchantability, fitness for a particular purpose, and non-infringement. - -* 5.2 Limitation of Liability: To the maximum extent permitted by applicable law, in no event shall the Licensor or any Contributor be liable for any direct, indirect, incidental, special, punitive, or consequential damages (including but not limited to procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this software, even if advised of the possibility of such damage. - -* 5.3 User Legal Responsibility: Users shall treat this project in accordance with local laws and regulations to ensure compliance with all applicable laws and regulations. - -6. License Term and Termination - -* 6.1 License Term: Unless the copyright holder proactively announces the abandonment of the copyright of this software, this License shall be effective indefinitely from the date of your acceptance. - -* 6.2 License Termination: If you fail to comply with any terms or conditions of this License, the Licensor has the right to terminate this License. Your License will automatically terminate upon your violation of the terms of this License. - -* 6.3 Effect after Termination: Upon termination of the License, all rights granted to you under this License will terminate immediately, but the licenses and rights obtained by recipients of software copies you have legally distributed before the termination of the License will not be affected and will remain valid. The Disclaimer of Warranty (Clause 5) and Limitation of Liability (Clause 5.2) shall remain in effect after the termination of this License. - -7. Revision of Terms - -* 7.1 Reservation of Revision Rights: The Licensor reserves the right to modify the terms of this License at any time to better adapt to legal, technological developments, and community needs. - -* 7.2 Effectiveness and Acceptance of Revisions: Revised terms will take effect upon publication, and unless otherwise stated, continued use, reproduction, distribution, or modification of the Software indicates your acceptance of the revised terms. The Licensor encourages users to periodically review the latest version of this License. - -8. Other - -* 8.1 Statutory Rights: This License does not affect your statutory rights as an end-user under applicable laws. - -* 8.2 Severability of Terms: If certain terms of this License are deemed unenforceable, the remaining terms shall remain in full force and effect. - -* 8.3 Version Updates: The Licensor may publish revised versions or new versions of this License. You may choose to continue using the old version of this License or choose to apply the new version. +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at https://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. \ No newline at end of file diff --git a/LICENSE-WSL b/LICENSE-WSL new file mode 100644 index 0000000..fbdd300 --- /dev/null +++ b/LICENSE-WSL @@ -0,0 +1,199 @@ +WJQserver Studio 开源许可证 +版本 v2.1 + +版权所有 © WJQserver Studio 2024 + +定义 + +* 许可 (License): 指的是在本许可证内定义的使用、复制、分发与修改软件的条款与要求。 +* 授权方 (Licensor): 指的是拥有版权的个人或组织,亦或是拥有版权的个人或组织所指派的实体,在本许可证中特指 WJQserver Studio。 +* 贡献者 (Contributor): 指的是授权方以及根据本许可证授予贡献代码或软件的个人或实体。 +* 您 (You): 指的是行使本许可授予的权限的个人或法律实体。 +* 衍生作品 (Derivative Works): 指的是基于本软件或本软件任何部分的修改作品,无论修改程度如何。这包括但不限于基于本软件或其任何部分的修改、修订、改编、翻译或其他形式的创作,以及包含本软件或其部分的集合作品。 +* 非营利性使用 (Non-profit Use): 指的是不以直接商业盈利为主要目的的使用方式,包括但不限于: + * 个人用途: 由个人为了个人学习、研究、实验、非商业项目、个人网站搭建、毕业设计、家庭内部娱乐等非直接商业目的使用软件。 + * 教育用途: 在教育机构(如学校、大学、培训机构)内部用于教学、研究、学术交流等活动。 + * 科研用途: 在科研院所、实验室等机构内部用于科学研究、实验开发等活动。 + * 慈善与公益用途: 由慈善机构、公益组织等非营利性组织为了其公益使命或慈善事业内部运营使用,或对外提供不直接产生商业利润的公益服务。 + * 内部运营用途 (非营利组织): 非营利性组织在其内部运营中使用软件,例如用于行政管理、会员管理、内部沟通、项目管理等非直接营利性活动。 + +开源与自由软件 + +本项目为开源软件,允许用户在遵循本许可证的前提下访问和使用源代码。 +本项目旨在向用户提供尽可能广泛的非商业使用自由,同时保障社区的共同发展和良性生态,并为商业创新提供清晰的路径。 +强调版权所有,所有权利由 WJQserver Studio 及贡献者共同保留。 + +许可证条款 + +1. 使用权限 + +* 1.1 非营利性使用: 您被授予在非营利性使用场景下,为了任何目的,自由使用本软件的权限。 非营利性使用的具体场景包括但不限于定义部分所列举的各种情况。 + +* 1.2 商业使用: 您可以在商业环境中使用本软件,无需获得额外授权,但您的商业使用行为必须遵守以下条款: + + * 1.2.1 开源继承 (Copyleft) 与互惠共享: 如果您或您的组织希望将本软件或其衍生作品用于任何商业用途,包括但不限于: + + * 盈利性分发: 销售、出租、许可分发本软件或其衍生作品。 + * 盈利性服务: 基于本软件或其衍生作品提供商业服务,例如 SaaS 服务、咨询服务、定制开发服务、收费技术支持服务等。 + * 嵌入式商业应用: 将本软件或其衍生作品嵌入到商业产品或解决方案中进行销售。 + * 组织内部商业运营: 在营利性组织的内部运营中使用修改后的版本以直接支持其商业活动,例如定制化内部系统,通过例如但不限于在软件或相关服务中投放广告 (例如 Google Ads 等),应用内购买 (内购), 会员订阅, 增值功能收费等方式直接或间接产生商业收入。 + + 您必须选择以下两种方式之一: + + * i) 继承本许可证并开源: 您必须以本许可证或兼容的开源许可证分发您的衍生作品,并公开您的衍生作品的全部源代码,使得您的衍生作品的接收者也享有与您相同的权利,包括进一步修改和商业使用的权利。 本选项旨在促进社区的共同发展和知识共享,确保基于本软件的商业创新成果也能回馈社区。 + * ii) 获得授权方明确授权: 如果您不希望以开源方式发布您的衍生作品,或者希望使用其他许可证进行分发,或者您希望在商业运营中使用修改后的版本但不开源,您必须事先获得 WJQserver Studio 的明确书面授权。 授权的具体条款和条件将由 WJQserver Studio 另行协商确定。 + +* 1.3 保持声明: 公开发布服务时,不得移除或修改软件中包含的原始版权声明、许可证声明以及来源声明。 + +2. 复制与分发 + +* 2.1 原始版本复制与分发: 您可以复制和分发本软件的原始版本,前提是必须满足以下条件: + + * 保留所有声明: 完整保留所有原始版权声明、许可证声明、来源声明以及其他所有权声明。 + * 附带许可证: 在分发软件时,必须同时附带本许可证的完整文本,确保接收者知悉并理解本许可证的全部条款。 + +* 2.2 衍生作品复制与分发: 您可以复制和分发基于本软件的衍生作品,您对衍生作品的分发行为将受到本许可证第 1.3 条(开源继承与互惠共享)的约束。 + +3. 修改权限 + +* 3.1 自由修改: 您被授予自由修改本软件的权限,无论修改目的是非营利性使用还是商业用途。 + +* 3.2 修改后使用与分发约束: 当您将修改后的版本用于商业用途或分发修改后的版本时,您需要遵守本许可证第 1.3 条(开源继承与互惠共享)以及第 2 条(复制与分发)的规定。 即使您不分发修改后的版本,只要您将其用于商业目的,也需要遵守开源继承条款或获得授权。 + +* 3.3 贡献接受: WJQserver Studio 鼓励社区贡献代码。如果您向本项目贡献代码,您需要同意您的贡献代码按照本许可证条款进行许可。 + +4. 专利权 + +* 4.1 无专利担保,风险自担: 本软件以“现状”提供,授权方及贡献者明确声明,不对本软件的专利侵权问题做任何形式的担保,亦不承担任何因专利侵权可能产生的责任与后果。 用户理解并同意,使用本软件的专利风险完全由用户自行承担。 + +* 4.2 专利纠纷应对: 如因用户使用本软件而引发任何专利侵权指控、诉讼或索赔,用户应自行负责处理并承担全部法律责任。 授权方及贡献者无义务参与任何相关法律程序,亦不承担任何由此产生的费用或赔偿。 + +5. 免责声明 + +* 5.1 “现状”提供,无任何保证: 本软件按“现状”提供,不提供任何明示或暗示的保证,包括但不限于适销性、特定用途适用性及非侵权性。 + +* 5.2 责任限制: 在适用法律允许的最大范围内,在任何情况下,授权方或任何贡献者均不对因使用或无法使用本软件而产生的任何直接、间接、偶然、特殊、惩罚性或后果性损害(包括但不限于采购替代商品或服务;损失使用、数据或利润;或业务中断)负责,无论其是如何造成的,也无论依据何种责任理论,即使已被告知可能发生此类损害。 + +* 5.3 用户法律责任: 用户需根据当地法律对待本项目,确保遵守所有适用法规。 + +6. 许可证期限与终止 + +* 6.1 许可证期限: 除版权所有人主动宣布放弃本软件版权外,本许可证无限期生效。 + +* 6.2 许可证终止: 如果您未能遵守本许可证的任何条款或条件,授权方有权终止本许可证。 您的许可证将在您违反本许可证条款时自动终止。 + +* 6.3 终止后的效力: 许可证终止后,您根据本许可证所享有的所有权利将立即终止,但您在许可证终止前已合法分发的软件副本,其接收者所获得的许可及权利将不受影响,继续有效。 免责声明(第 5 条)和责任限制(第 5.2 条)在本许可证终止后仍然有效。 + +7. 条款修订 + +* 7.1 修订权利保留: 授权方保留随时修改本许可证条款的权利,以便更好地适应法律、技术发展以及社区需求。 + +* 7.2 修订生效与接受: 修订后的条款将在发布时生效,除非另行声明,否则继续使用、复制、分发或修改本软件即表示您接受修订后的条款。授权方鼓励用户定期查阅本许可证的最新版本。 + +8. 其他 + +* 8.1 法定权利: 本许可证不影响您作为最终用户在适用法律下的法定权利。 + +* 8.2 条款可分割性: 若本许可证的某些条款被认定为不可执行,其余条款仍然完全有效。 + +* 8.3 版本更新: 授权方可能会发布本许可证的修订版本或新版本。您可以选择是继续使用本许可证的旧版本还是选择适用新版本。 + +WJQserver Studio Open Source License +Version v2.0 + +Copyright © WJQserver Studio 2024 + +Definitions + +* License: Refers to the terms and requirements for use, reproduction, distribution, and modification defined within this license. +* Licensor: Refers to the individual or organization that holds the copyright, or the entity designated by the copyright holder, specifically WJQserver Studio in this license. +* Contributor: Refers to the Licensor and individuals or entities who contribute code or software under this License. +* You: Refers to the individual or legal entity exercising permissions granted by this License. +* Derivative Works: Refers to works modified based on the Software or any part thereof, regardless of the extent of modification. This includes but is not limited to modifications, revisions, adaptations, translations, or other forms of creation based on the Software or any part thereof, as well as collective works containing the Software or parts thereof. +* Non-profit Use: Refers to uses not primarily intended for direct commercial profit, including but not limited to: + * Personal Use: Use by an individual for personal learning, research, experimentation, non-commercial projects, personal website development, graduation projects, home entertainment, and other non-directly commercial purposes. + * Educational Use: Use within educational institutions (such as schools, universities, training organizations) for activities such as teaching, research, and academic exchange. + * Scientific Research Use: Use within scientific research institutions, laboratories, and similar organizations for activities such as scientific research and experimental development. + * Charitable and Public Welfare Use: Use by charitable organizations, public welfare organizations, and similar non-profit entities for their public missions or internal operation of charitable activities, or to provide public services that do not directly generate commercial profit. + * Internal Operational Use (Non-profit Organizations): Use within the internal operations of non-profit organizations, such as for administrative management, membership management, internal communication, project management, and other non-directly profit-generating activities. + +Open Source and Free Software + +This project is open-source software, allowing users to access and use the source code under the premise of complying with this License. +This project aims to provide users with the broadest possible freedom for non-commercial use while ensuring the common development and healthy ecosystem of the community, and providing a clear path for commercial innovation. +Copyright is emphasized; all rights are jointly reserved by WJQserver Studio and Contributors. + +License Terms + +1. Permissions for Use + +* 1.1 Non-profit Use: You are granted permission to freely use the Software for any purpose in non-profit use scenarios. Specific non-profit use scenarios include but are not limited to the various situations listed in the Definition section. + +* 1.2 Commercial Use: You may use the Software in a commercial environment without additional authorization, but your commercial use must comply with the following terms: + + * 1.2.1 Open Source Inheritance (Copyleft) and Reciprocal Sharing: If you or your organization wish to use the Software or its Derivative Works for any commercial purpose, including but not limited to: + + * Profit-generating Distribution: Selling, renting, licensing, or distributing the Software or its Derivative Works. + * Profit-generating Services: Providing commercial services based on the Software or its Derivative Works, such as SaaS services, consulting services, custom development services, and paid technical support services. + * Embedded Commercial Applications: Embedding the Software or its Derivative Works into commercial products or solutions for sale. + * Internal Commercial Operations: Using modified versions within the internal operations of for-profit organizations to directly support their commercial activities, such as customized internal systems, generating commercial revenue directly or indirectly through means including but not limited to placing advertisements in the software or related services (e.g., Google Ads), in-app purchases, membership subscriptions, and charging for value-added features. + + You must choose one of the following two options: + + * i) Inherit this License and Open Source: You must distribute your Derivative Works under this License or a compatible open-source license and publicly disclose the entire source code of your Derivative Works, so that recipients of your Derivative Works also enjoy the same rights as you, including the right to further modify and use commercially. This option aims to promote the common development and knowledge sharing of the community, ensuring that commercial innovation achievements based on this Software can also contribute back to the community. + * ii) Obtain Explicit Authorization from the Licensor: If you do not wish to release your Derivative Works in an open-source manner, or wish to distribute them under another license, or you wish to use a modified version in commercial operations without open-sourcing it, you must obtain explicit written authorization from WJQserver Studio in advance. The specific terms and conditions of authorization will be determined separately by WJQserver Studio through negotiation. + +* 1.3 Maintain Statements: When publish services to public, you must not remove or modify the original copyright notices, license notices, and source statements contained in the Software. + +2. Reproduction and Distribution + +* 2.1 Reproduction and Distribution of Original Version: You may reproduce and distribute the original version of the Software, provided that the following conditions are met: + + * Retain All Statements: Completely retain all original copyright notices, license notices, source statements, and other proprietary notices. + * Accompany with License: When distributing the Software, you must also include the full text of this License to ensure that recipients are aware of and understand all terms of this License. + +* 2.2 Reproduction and Distribution of Derivative Works: You may reproduce and distribute Derivative Works based on the Software. Your distribution of Derivative Works will be subject to the constraints of Clause 1.3 of this License (Open Source Inheritance and Reciprocal Sharing). + +3. Modification Permissions + +* 3.1 Free Modification: You are granted permission to freely modify the Software, regardless of whether the purpose of modification is for non-profit use or commercial use. + +* 3.2 Constraints on Use and Distribution after Modification: When you use a modified version for commercial purposes or distribute a modified version, you need to comply with the provisions of Clause 1.3 of this License (Open Source Inheritance and Reciprocal Sharing) and Clause 2 (Reproduction and Distribution). Even if you do not distribute the modified version, as long as you use it for commercial purposes, you also need to comply with the open-source inheritance clause or obtain authorization. + +* 3.3 Contribution Acceptance: WJQserver Studio encourages community contribution of code. If you contribute code to this project, you need to agree that your contributed code is licensed under the terms of this License. + +4. Patent Rights + +* 4.1 No Patent Warranty, Risk Self-Bearing: The software is provided “AS IS”, and the Licensor and Contributors explicitly declare that they do not provide any form of warranty regarding patent infringement issues of this software, nor do they assume any responsibility and consequences arising from patent infringement. Users understand and agree that the patent risk of using this software is entirely borne by the users themselves. + +* 4.2 Handling of Patent Disputes: If any patent infringement allegations, lawsuits, or claims arise due to the user's use of this Software, the user shall be solely responsible for handling and bear all legal liabilities. The Licensor and Contributors are under no obligation to participate in any related legal proceedings, nor do they bear any costs or compensation arising therefrom. + +5. Disclaimer of Warranty + +* 5.1 “AS IS” Provision, No Warranty: The software is provided “AS IS” without any express or implied warranties, including but not limited to warranties of merchantability, fitness for a particular purpose, and non-infringement. + +* 5.2 Limitation of Liability: To the maximum extent permitted by applicable law, in no event shall the Licensor or any Contributor be liable for any direct, indirect, incidental, special, punitive, or consequential damages (including but not limited to procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this software, even if advised of the possibility of such damage. + +* 5.3 User Legal Responsibility: Users shall treat this project in accordance with local laws and regulations to ensure compliance with all applicable laws and regulations. + +6. License Term and Termination + +* 6.1 License Term: Unless the copyright holder proactively announces the abandonment of the copyright of this software, this License shall be effective indefinitely from the date of your acceptance. + +* 6.2 License Termination: If you fail to comply with any terms or conditions of this License, the Licensor has the right to terminate this License. Your License will automatically terminate upon your violation of the terms of this License. + +* 6.3 Effect after Termination: Upon termination of the License, all rights granted to you under this License will terminate immediately, but the licenses and rights obtained by recipients of software copies you have legally distributed before the termination of the License will not be affected and will remain valid. The Disclaimer of Warranty (Clause 5) and Limitation of Liability (Clause 5.2) shall remain in effect after the termination of this License. + +7. Revision of Terms + +* 7.1 Reservation of Revision Rights: The Licensor reserves the right to modify the terms of this License at any time to better adapt to legal, technological developments, and community needs. + +* 7.2 Effectiveness and Acceptance of Revisions: Revised terms will take effect upon publication, and unless otherwise stated, continued use, reproduction, distribution, or modification of the Software indicates your acceptance of the revised terms. The Licensor encourages users to periodically review the latest version of this License. + +8. Other + +* 8.1 Statutory Rights: This License does not affect your statutory rights as an end-user under applicable laws. + +* 8.2 Severability of Terms: If certain terms of this License are deemed unenforceable, the remaining terms shall remain in full force and effect. + +* 8.3 Version Updates: The Licensor may publish revised versions or new versions of this License. You may choose to continue using the old version of this License or choose to apply the new version. diff --git a/README.md b/README.md index b0d49f9..c388d19 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,9 @@ wget -O install-dev.sh https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghprox ## LICENSE -本项目使用WJQserver Studio License 2.0 [WJQserver Studio License 2.0](https://wjqserver-studio.github.io/LICENSE/LICENSE.html) +v3.5.2开始, 本项目使用 [WJQserver Studio License 2.0](https://wjqserver-studio.github.io/LICENSE/LICENSE.html) 和 [Mozilla Public License Version 2.0](https://mozilla.org/MPL/2.0/) 双重许可, 您可从中选择一个使用 + +前端位于单独仓库中, 且各个主题均存在各自的许可证, 本项目许可证并不包括前端 在v2.3.0之前, 本项目使用WJQserver Studio License 1.2 diff --git a/go.mod b/go.mod index 1ccfe73..1941ecd 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.24.3 require ( github.com/BurntSushi/toml v1.5.0 - github.com/WJQSERVER-STUDIO/httpc v0.5.1 + github.com/WJQSERVER-STUDIO/httpc v0.7.0 github.com/WJQSERVER-STUDIO/logger v1.7.3 github.com/cloudwego/hertz v0.10.0 github.com/hertz-contrib/http2 v0.1.8 diff --git a/go.sum b/go.sum index 4881472..368b49a 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,10 @@ github.com/WJQSERVER-STUDIO/go-utils/log v0.0.3 h1:t6nyLhmo9pSfVHm1Wu1WyLsTpXFSj github.com/WJQSERVER-STUDIO/go-utils/log v0.0.3/go.mod h1:j9Q+xnwpOfve7/uJnZ2izRQw6NNoXjvJHz7vUQAaLZE= github.com/WJQSERVER-STUDIO/httpc v0.5.1 h1:+TKCPYBuj7PAHuiduGCGAqsHAa4QtsUfoVwRN777q64= github.com/WJQSERVER-STUDIO/httpc v0.5.1/go.mod h1:M7KNUZjjhCkzzcg9lBPs9YfkImI+7vqjAyjdA19+joE= +github.com/WJQSERVER-STUDIO/httpc v0.6.0 h1:GXKc6BNGn5ALdDLkCsM+mP24jyi1S9QBLL2XQ1a2sPM= +github.com/WJQSERVER-STUDIO/httpc v0.6.0/go.mod h1:M7KNUZjjhCkzzcg9lBPs9YfkImI+7vqjAyjdA19+joE= +github.com/WJQSERVER-STUDIO/httpc v0.7.0 h1:iHhqlxppJBjlmvsIjvLZKRbWXqSdbeSGGofjHGmqGJc= +github.com/WJQSERVER-STUDIO/httpc v0.7.0/go.mod h1:M7KNUZjjhCkzzcg9lBPs9YfkImI+7vqjAyjdA19+joE= github.com/WJQSERVER-STUDIO/logger v1.7.3 h1:XoFJ1nBcZKyMvP4v0MZv5jL2q7IkAF7yfXgwyB3MLP4= github.com/WJQSERVER-STUDIO/logger v1.7.3/go.mod h1:yzXPtot0OvR1gzx4+rlFrv/sccUpz0gIXVBwUx3H7fM= github.com/bytedance/gopkg v0.1.1/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= diff --git a/proxy/nest.go b/proxy/nest.go index 84f55cd..2748ca6 100644 --- a/proxy/nest.go +++ b/proxy/nest.go @@ -1,7 +1,3 @@ -// Copyright 2025 WJQSERVER, WJQSERVER-STUDIO. All rights reserved. -// 使用本源代码受 WSL 2.0(WJQserver Studio License v2.0)与MPL 2.0(Mozilla Public License v2.0)许可协议的约束 -// 此段代码使用双重授权许可, 允许用户选择其中一种许可证 - package proxy import ( From 44673b9a3feb10a4937ca1771e6dcb70cd4a3cde Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Wed, 11 Jun 2025 11:39:10 +0800 Subject: [PATCH 24/99] 3.5.2 --- LICENSE-WSL | 2 +- VERSION | 2 +- api/api.go | 4 ++++ main.go | 11 +++++++++++ 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/LICENSE-WSL b/LICENSE-WSL index fbdd300..e3f452b 100644 --- a/LICENSE-WSL +++ b/LICENSE-WSL @@ -99,7 +99,7 @@ WJQserver Studio 开源许可证 * 8.3 版本更新: 授权方可能会发布本许可证的修订版本或新版本。您可以选择是继续使用本许可证的旧版本还是选择适用新版本。 WJQserver Studio Open Source License -Version v2.0 +Version v2.1 Copyright © WJQserver Studio 2024 diff --git a/VERSION b/VERSION index 3c8ff8c..80d13b7 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.5.1 \ No newline at end of file +3.5.2 \ No newline at end of file diff --git a/api/api.go b/api/api.go index e8ca10f..597a198 100644 --- a/api/api.go +++ b/api/api.go @@ -92,6 +92,8 @@ func HealthcheckHandler(c *app.RequestContext, ctx context.Context) { c.Response.Header.Set("Content-Type", "application/json") c.JSON(200, (map[string]interface{}{ "Status": "OK", + "Repo": "WJQSERVER-STUDIO/GHProxy", + "Author": "WJQSERVER-STUDIO", })) } @@ -99,6 +101,8 @@ func VersionHandler(c *app.RequestContext, ctx context.Context, version string) c.Response.Header.Set("Content-Type", "application/json") c.JSON(200, (map[string]interface{}{ "Version": version, + "Repo": "WJQSERVER-STUDIO/GHProxy", + "Author": "WJQSERVER-STUDIO", })) } diff --git a/main.go b/main.go index a441752..e2e898b 100644 --- a/main.go +++ b/main.go @@ -402,6 +402,16 @@ func init() { } } +var viaString string = "WJQSERVER-STUDIO/GHProxy" + +func viaHeader() app.HandlerFunc { + return func(ctx context.Context, c *app.RequestContext) { + protoVersion := "1.1" + c.Header("Via", protoVersion+" "+viaString) + c.Next(ctx) + } +} + func main() { if showVersion || showHelp { return @@ -450,6 +460,7 @@ func main() { r.Use(recovery.Recovery()) // Recovery中间件 r.Use(loggin.Middleware()) // log中间件 + r.Use(viaHeader()) setupApi(cfg, r, version) setupPages(cfg, r) From b86e58cddfbab15be1ae8e436cabad5fe1b77217 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Wed, 11 Jun 2025 11:40:00 +0800 Subject: [PATCH 25/99] 3.5.2 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a36f5a3..fc0d51c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 更新日志 +3.5.2 - 2025-06-11 +--- +- CHANGE: 加入MPL 2.0许可证, 项目转为双重许可 + 3.5.1 - 2025-06-09 --- - CHANGE: 大幅优化`Matcher`的性能, 实现零分配, 大幅提升性能; 单次操作时间: `254.3 ns/op` => `29.59 ns/op` From a5bf7686bd8d3dc31f0893a03d2510959acdb770 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Fri, 13 Jun 2025 15:23:46 +0800 Subject: [PATCH 26/99] 3.5.3 --- CHANGELOG.md | 4 ++++ VERSION | 2 +- go.mod | 2 +- go.sum | 18 ++---------------- main.go | 4 ++++ proxy/chunkreq.go | 2 ++ 6 files changed, 14 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc0d51c..7c4cabe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 更新日志 +3.5.3 - 2025-06-13 +--- +- CHANGE: 显式配置`WithStreamBody(true)` + 3.5.2 - 2025-06-11 --- - CHANGE: 加入MPL 2.0许可证, 项目转为双重许可 diff --git a/VERSION b/VERSION index 80d13b7..678fd88 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.5.2 \ No newline at end of file +3.5.3 \ No newline at end of file diff --git a/go.mod b/go.mod index 1941ecd..ba6d974 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.24.3 require ( github.com/BurntSushi/toml v1.5.0 github.com/WJQSERVER-STUDIO/httpc v0.7.0 - github.com/WJQSERVER-STUDIO/logger v1.7.3 + github.com/WJQSERVER-STUDIO/logger v1.8.0 github.com/cloudwego/hertz v0.10.0 github.com/hertz-contrib/http2 v0.1.8 golang.org/x/net v0.41.0 diff --git a/go.sum b/go.sum index 368b49a..d8d7e39 100644 --- a/go.sum +++ b/go.sum @@ -6,14 +6,10 @@ github.com/WJQSERVER-STUDIO/go-utils/limitreader v0.0.2 h1:8bBkKk6E2Zr+I5szL7gyc github.com/WJQSERVER-STUDIO/go-utils/limitreader v0.0.2/go.mod h1:yPX8xuZH+py7eLJwOYj3VVI/4/Yuy5+x8Mhq8qezcPg= github.com/WJQSERVER-STUDIO/go-utils/log v0.0.3 h1:t6nyLhmo9pSfVHm1Wu1WyLsTpXFSjSpQtVKqEDpiZ5Q= github.com/WJQSERVER-STUDIO/go-utils/log v0.0.3/go.mod h1:j9Q+xnwpOfve7/uJnZ2izRQw6NNoXjvJHz7vUQAaLZE= -github.com/WJQSERVER-STUDIO/httpc v0.5.1 h1:+TKCPYBuj7PAHuiduGCGAqsHAa4QtsUfoVwRN777q64= -github.com/WJQSERVER-STUDIO/httpc v0.5.1/go.mod h1:M7KNUZjjhCkzzcg9lBPs9YfkImI+7vqjAyjdA19+joE= -github.com/WJQSERVER-STUDIO/httpc v0.6.0 h1:GXKc6BNGn5ALdDLkCsM+mP24jyi1S9QBLL2XQ1a2sPM= -github.com/WJQSERVER-STUDIO/httpc v0.6.0/go.mod h1:M7KNUZjjhCkzzcg9lBPs9YfkImI+7vqjAyjdA19+joE= github.com/WJQSERVER-STUDIO/httpc v0.7.0 h1:iHhqlxppJBjlmvsIjvLZKRbWXqSdbeSGGofjHGmqGJc= github.com/WJQSERVER-STUDIO/httpc v0.7.0/go.mod h1:M7KNUZjjhCkzzcg9lBPs9YfkImI+7vqjAyjdA19+joE= -github.com/WJQSERVER-STUDIO/logger v1.7.3 h1:XoFJ1nBcZKyMvP4v0MZv5jL2q7IkAF7yfXgwyB3MLP4= -github.com/WJQSERVER-STUDIO/logger v1.7.3/go.mod h1:yzXPtot0OvR1gzx4+rlFrv/sccUpz0gIXVBwUx3H7fM= +github.com/WJQSERVER-STUDIO/logger v1.8.0 h1:AQ3Qe2kxiqpuOoDlRzseGP6u4LAaJc+ng4l8P+CK7Co= +github.com/WJQSERVER-STUDIO/logger v1.8.0/go.mod h1:yzXPtot0OvR1gzx4+rlFrv/sccUpz0gIXVBwUx3H7fM= github.com/bytedance/gopkg v0.1.1/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/gopkg v0.1.2 h1:8o2feYuxknDpN+O7kPwvSXfMEKfYvJYiA2K7aonoMEQ= github.com/bytedance/gopkg v0.1.2/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= @@ -91,16 +87,12 @@ github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyC github.com/wjqserver/modembed v0.0.1 h1:8ZDz7t9M5DLrUFlYgBUUmrMzxWsZPmHvOazkr/T2jEs= github.com/wjqserver/modembed v0.0.1/go.mod h1:sYbQJMAjSBsdYQrUsuHY380XXE1CuRh8g9yyCztTXOQ= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/arch v0.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU= -golang.org/x/arch v0.17.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc= golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 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-20250531010427-b6e5de432a8b h1:QoALfVG9rhQ/M7vYDScfPdWjGL9dlsVVM5VGh7aKoAA= -golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 h1:bsqhLWFR6G6xiQcb+JoGqdKdRU6WzPWmK8E0jxTjzo4= golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -112,8 +104,6 @@ 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.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= -golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= 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/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -143,12 +133,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= -golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= -golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/main.go b/main.go index e2e898b..afc923a 100644 --- a/main.go +++ b/main.go @@ -430,12 +430,14 @@ func main() { server.WithH2C(true), server.WithHostPorts(addr), server.WithTransport(standard.NewTransporter), + server.WithStreamBody(true), ) r.AddProtocol("h2", factory.NewServerFactory()) } else { r = server.New( server.WithHostPorts(addr), server.WithTransport(standard.NewTransporter), + server.WithStreamBody(true), ) } } else if cfg.Server.NetLib == "netpoll" || cfg.Server.NetLib == "" { @@ -444,12 +446,14 @@ func main() { server.WithH2C(true), server.WithHostPorts(addr), server.WithSenseClientDisconnection(cfg.Server.SenseClientDisconnection), + server.WithStreamBody(true), ) r.AddProtocol("h2", factory.NewServerFactory()) } else { r = server.New( server.WithHostPorts(addr), server.WithSenseClientDisconnection(cfg.Server.SenseClientDisconnection), + server.WithStreamBody(true), ) } } else { diff --git a/proxy/chunkreq.go b/proxy/chunkreq.go index c50b7fb..6e4c8f4 100644 --- a/proxy/chunkreq.go +++ b/proxy/chunkreq.go @@ -110,6 +110,8 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c bodyReader = limitreader.NewRateLimitedReader(bodyReader, bandwidthLimit, int(bandwidthBurst), ctx) } + defer bodyReader.Close() + if MatcherShell(u) && matchString(matcher) && cfg.Shell.Editor { // 判断body是不是gzip var compress string From d2a0177015a17a4f1ac1f4045499b79c29583f28 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sat, 14 Jun 2025 06:47:47 +0800 Subject: [PATCH 27/99] 25w46a --- CHANGELOG.md | 10 ++++++++++ DEV-VERSION | 2 +- README.md | 2 +- main.go | 2 +- proxy/chunkreq.go | 19 +++++++++++++------ proxy/gitreq.go | 11 ++++++----- 6 files changed, 32 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c4cabe..74eeb11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # 更新日志 +25w46b - 2025-06-14 +--- +- PRE-RELEASE: 此版本是v3.5.4预发布版本,请勿在生产环境中使用; +- CHANGE: 修改关闭行为以测试问题 + +25w46a - 2025-06-14 +--- +- PRE-RELEASE: 此版本是v3.5.4预发布版本,请勿在生产环境中使用; +- CHANGE: 修改payload行为以测试问题 + 3.5.3 - 2025-06-13 --- - CHANGE: 显式配置`WithStreamBody(true)` diff --git a/DEV-VERSION b/DEV-VERSION index ebe0357..09188a8 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w45a \ No newline at end of file +25w46a \ No newline at end of file diff --git a/README.md b/README.md index c388d19..3f4123d 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ wget -O install-dev.sh https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghprox ## LICENSE -v3.5.2开始, 本项目使用 [WJQserver Studio License 2.0](https://wjqserver-studio.github.io/LICENSE/LICENSE.html) 和 [Mozilla Public License Version 2.0](https://mozilla.org/MPL/2.0/) 双重许可, 您可从中选择一个使用 +v3.5.2开始, 本项目使用 [WJQserver Studio License 2.1](https://wjqserver-studio.github.io/LICENSE/LICENSE.html) 和 [Mozilla Public License Version 2.0](https://mozilla.org/MPL/2.0/) 双重许可, 您可从中选择一个使用 前端位于单独仓库中, 且各个主题均存在各自的许可证, 本项目许可证并不包括前端 diff --git a/main.go b/main.go index afc923a..7d6fb0e 100644 --- a/main.go +++ b/main.go @@ -507,7 +507,7 @@ func main() { proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c) }) - r.GET("/api.github.com/repos/:user/:repo/*filepath", func(ctx context.Context, c *app.RequestContext) { + r.Any("/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) }) diff --git a/proxy/chunkreq.go b/proxy/chunkreq.go index 6e4c8f4..9ec1cc2 100644 --- a/proxy/chunkreq.go +++ b/proxy/chunkreq.go @@ -1,6 +1,7 @@ package proxy import ( + "bytes" "context" "fmt" "ghproxy/config" @@ -23,16 +24,17 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c go func() { <-ctx.Done() if resp != nil && resp.Body != nil { - resp.Body.Close() - } - if req != nil { - req.Body.Close() + err := resp.Body.Close() + if err != nil { + logError("Failed to close response body: %v", err) + } } }() rb := client.NewRequestBuilder(string(c.Request.Method()), u) rb.NoDefaultHeaders() - rb.SetBody(c.Request.BodyStream()) + rb.SetBody(bytes.NewBuffer(c.Request.Body())) + //rb.SetBody(c.RequestBodyStream()) rb.WithContext(ctx) req, err = rb.Build() @@ -110,7 +112,12 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c bodyReader = limitreader.NewRateLimitedReader(bodyReader, bandwidthLimit, int(bandwidthBurst), ctx) } - defer bodyReader.Close() + defer func() { + err := bodyReader.Close() + if err != nil { + logError("Failed to close response body: %v", err) + } + }() if MatcherShell(u) && matchString(matcher) && cfg.Shell.Editor { // 判断body是不是gzip diff --git a/proxy/gitreq.go b/proxy/gitreq.go index 5667d21..de6bff7 100644 --- a/proxy/gitreq.go +++ b/proxy/gitreq.go @@ -17,15 +17,16 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co var ( req *http.Request resp *http.Response + err error ) go func() { <-ctx.Done() if resp != nil && resp.Body != nil { - resp.Body.Close() - } - if req != nil { - req.Body.Close() + err = resp.Body.Close() + if err != nil { + logError("Failed to close response body: %v", err) + } } }() @@ -51,7 +52,7 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co rb.SetBody(reqBodyReader) rb.WithContext(ctx) - req, err := rb.Build() + req, err = rb.Build() if err != nil { HandleError(c, fmt.Sprintf("Failed to create request: %v", err)) return From bd8412f15767cdd02a2ff22ebf983187a578c102 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sat, 14 Jun 2025 06:51:41 +0800 Subject: [PATCH 28/99] 25w46b --- DEV-VERSION | 2 +- proxy/chunkreq.go | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/DEV-VERSION b/DEV-VERSION index 09188a8..99cdc41 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w46a \ No newline at end of file +25w46b \ No newline at end of file diff --git a/proxy/chunkreq.go b/proxy/chunkreq.go index 9ec1cc2..5fa4728 100644 --- a/proxy/chunkreq.go +++ b/proxy/chunkreq.go @@ -1,7 +1,6 @@ package proxy import ( - "bytes" "context" "fmt" "ghproxy/config" @@ -33,8 +32,8 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c rb := client.NewRequestBuilder(string(c.Request.Method()), u) rb.NoDefaultHeaders() - rb.SetBody(bytes.NewBuffer(c.Request.Body())) - //rb.SetBody(c.RequestBodyStream()) + //rb.SetBody(bytes.NewBuffer(c.Request.Body())) + rb.SetBody(c.RequestBodyStream()) rb.WithContext(ctx) req, err = rb.Build() @@ -112,12 +111,14 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c bodyReader = limitreader.NewRateLimitedReader(bodyReader, bandwidthLimit, int(bandwidthBurst), ctx) } - defer func() { - err := bodyReader.Close() - if err != nil { - logError("Failed to close response body: %v", err) - } - }() + /* + defer func() { + err := bodyReader.Close() + if err != nil { + logError("Failed to close response body: %v", err) + } + }() + */ if MatcherShell(u) && matchString(matcher) && cfg.Shell.Editor { // 判断body是不是gzip From 41395b1d724dd66af11d409b7591c9c22ed1570c Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sat, 14 Jun 2025 07:16:43 +0800 Subject: [PATCH 29/99] 25w46c --- CHANGELOG.md | 5 +++++ DEV-VERSION | 2 +- proxy/chunkreq.go | 23 ++++++++++++++--------- proxy/handler.go | 5 ++++- proxy/routing.go | 6 +++++- 5 files changed, 29 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74eeb11..5c34c60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 更新日志 +25w46c - 2025-06-14 +--- +- PRE-RELEASE: 此版本是v3.5.4预发布版本,请勿在生产环境中使用; +- CHANGE: 移植来自于[GHProxy-Touka](https://github.com/WJQSERVER-STUDIO/ghproxy-touka)的blob处理逻辑与302处理逻辑 + 25w46b - 2025-06-14 --- - PRE-RELEASE: 此版本是v3.5.4预发布版本,请勿在生产环境中使用; diff --git a/DEV-VERSION b/DEV-VERSION index 99cdc41..7b8b745 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w46b \ No newline at end of file +25w46c \ No newline at end of file diff --git a/proxy/chunkreq.go b/proxy/chunkreq.go index 5fa4728..35461c9 100644 --- a/proxy/chunkreq.go +++ b/proxy/chunkreq.go @@ -57,6 +57,20 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c return } + // 处理302情况 + if resp.StatusCode == 302 { + finalURL := resp.Header.Get("Location") + if finalURL != "" { + err = resp.Body.Close() + if err != nil { + logError("Failed to close response body: %v", err) + } + c.Request.Header.Del("Referer") + logInfo("Internal Redirecting to %s", finalURL) + ChunkedProxyRequest(ctx, c, finalURL, cfg, matcher) + } + } + var ( bodySize int contentLength string @@ -111,15 +125,6 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c bodyReader = limitreader.NewRateLimitedReader(bodyReader, bandwidthLimit, int(bandwidthBurst), ctx) } - /* - defer func() { - err := bodyReader.Close() - if err != nil { - logError("Failed to close response body: %v", err) - } - }() - */ - if MatcherShell(u) && matchString(matcher) && cfg.Shell.Editor { // 判断body是不是gzip var compress string diff --git a/proxy/handler.go b/proxy/handler.go index bdd7ecb..80a5892 100644 --- a/proxy/handler.go +++ b/proxy/handler.go @@ -68,7 +68,10 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra // 处理blob/raw路径 if matcher == "blob" { - rawPath = strings.Replace(rawPath, "/blob/", "/raw/", 1) + rawPath = rawPath[10:] + rawPath = "raw.githubusercontent.com" + rawPath + rawPath = strings.Replace(rawPath, "/blob/", "/", 1) + matcher = "raw" } logDebug("Matched: %v", matcher) diff --git a/proxy/routing.go b/proxy/routing.go index a3135ec..9d68ca7 100644 --- a/proxy/routing.go +++ b/proxy/routing.go @@ -48,9 +48,13 @@ func RoutingHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra return } + // 处理blob/raw路径 // 处理blob/raw路径 if matcher == "blob" { - rawPath = strings.Replace(rawPath, "/blob/", "/raw/", 1) + rawPath = rawPath[10:] + rawPath = "raw.githubusercontent.com" + rawPath + rawPath = strings.Replace(rawPath, "/blob/", "/", 1) + matcher = "raw" } // 为rawpath加入https:// 头 From bbb108689a728ec0b21fe9d35a42443d2254ed3c Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sat, 14 Jun 2025 12:50:26 +0800 Subject: [PATCH 30/99] 3.5.4 --- CHANGELOG.md | 4 ++++ VERSION | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c34c60..d323c99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 更新日志 +3.5.4 - 2025-06-14 +--- +- CHANGE: 移植来自于[GHProxy-Touka](https://github.com/WJQSERVER-STUDIO/ghproxy-touka)的blob处理逻辑与302处理逻辑 + 25w46c - 2025-06-14 --- - PRE-RELEASE: 此版本是v3.5.4预发布版本,请勿在生产环境中使用; diff --git a/VERSION b/VERSION index 678fd88..e5b8a84 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.5.3 \ No newline at end of file +3.5.4 \ No newline at end of file From 8ab622d149d0f79a07ec2f045221fe718ba5d78f Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sat, 14 Jun 2025 22:05:45 +0800 Subject: [PATCH 31/99] update matcher for gist usercontent --- proxy/match.go | 36 ++++++++++++++++++++++++++++-------- proxy/matcher_test.go | 6 ++++++ 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/proxy/match.go b/proxy/match.go index 896a99b..f526461 100644 --- a/proxy/match.go +++ b/proxy/match.go @@ -10,14 +10,16 @@ import ( ) var ( - githubPrefix = "https://github.com/" - rawPrefix = "https://raw.githubusercontent.com/" - gistPrefix = "https://gist.github.com/" - apiPrefix = "https://api.github.com/" - githubPrefixLen int - rawPrefixLen int - gistPrefixLen int - apiPrefixLen int + githubPrefix = "https://github.com/" + rawPrefix = "https://raw.githubusercontent.com/" + gistPrefix = "https://gist.github.com/" + gistContentPrefix = "https://gist.githubusercontent.com/" + apiPrefix = "https://api.github.com/" + githubPrefixLen int + rawPrefixLen int + gistPrefixLen int + gistContentPrefixLen int + apiPrefixLen int ) func init() { @@ -25,6 +27,7 @@ func init() { rawPrefixLen = len(rawPrefix) gistPrefixLen = len(gistPrefix) apiPrefixLen = len(apiPrefix) + gistContentPrefixLen = len(gistContentPrefix) //log.Printf("githubPrefixLen: %d, rawPrefixLen: %d, gistPrefixLen: %d, apiPrefixLen: %d", githubPrefixLen, rawPrefixLen, gistPrefixLen, apiPrefixLen) } @@ -114,6 +117,23 @@ func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHPro return user, "", "gist", nil } + // 匹配 "https://gist.githubusercontent.com/" + if strings.HasPrefix(rawPath, gistContentPrefix) { + remaining := rawPath[gistContentPrefixLen:] + i := strings.IndexByte(remaining, '/') + if i <= 0 { + // case: https://gist.githubusercontent.com/user + // 这种情况下, gist_id 缺失, 但我们仍然可以认为 user 是有效的 + if len(remaining) > 0 { + return remaining, "", "gist", nil + } + return "", "", "", NewErrorWithStatusLookup(400, "malformed gist url: missing user") + } + // case: https://gist.githubusercontent.com/user/gist_id... + user := remaining[:i] + return user, "", "gist", nil + } + // 匹配 "https://api.github.com/" if strings.HasPrefix(rawPath, apiPrefix) { if !cfg.Auth.ForceAllowApi && (cfg.Auth.Method != "header" || !cfg.Auth.Enabled) { diff --git a/proxy/matcher_test.go b/proxy/matcher_test.go index 3293817..0c35381 100644 --- a/proxy/matcher_test.go +++ b/proxy/matcher_test.go @@ -87,6 +87,12 @@ func TestMatcher_Compatibility(t *testing.T) { config: cfgWithAuth, expectedUser: "user", expectedRepo: "", expectedMatcher: "gist", }, + { + name: "Gist UserContent Path", + rawPath: "https://gist.githubusercontent.com/user/abcdef1234567890", + config: cfgWithAuth, + expectedUser: "user", expectedRepo: "", expectedMatcher: "gist", + }, { name: "API Repos Path (with Auth)", rawPath: "https://api.github.com/repos/owner/repo/pulls", From 1b06260a14f5c057ad3393aacb006a093c46a2ff Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sat, 14 Jun 2025 22:11:31 +0800 Subject: [PATCH 32/99] 25w47a --- CHANGELOG.md | 5 +++++ DEV-VERSION | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d323c99..e0064c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 更新日志 +25w47a - 2025-06-14 +--- +- PRE-RELEASE: 此版本是v3.5.5预发布版本,请勿在生产环境中使用; +- CHANGE: 修正新匹配器的覆盖问题, 同时增加test的覆盖 + 3.5.4 - 2025-06-14 --- - CHANGE: 移植来自于[GHProxy-Touka](https://github.com/WJQSERVER-STUDIO/ghproxy-touka)的blob处理逻辑与302处理逻辑 diff --git a/DEV-VERSION b/DEV-VERSION index 7b8b745..55564ab 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w46c \ No newline at end of file +25w47a \ No newline at end of file From e0cbfed1e7fecdd0788c44416dc220b888a1a1aa Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sat, 14 Jun 2025 22:17:13 +0800 Subject: [PATCH 33/99] 3.5.5 --- CHANGELOG.md | 4 ++++ VERSION | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0064c0..006230b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 更新日志 +3.5.5 - 2025-06-14 +--- +- CHANGE: 修正新匹配器的覆盖问题, 同时增加test的覆盖 + 25w47a - 2025-06-14 --- - PRE-RELEASE: 此版本是v3.5.5预发布版本,请勿在生产环境中使用; diff --git a/VERSION b/VERSION index e5b8a84..1947319 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.5.4 \ No newline at end of file +3.5.5 \ No newline at end of file From 0008366e07001218519c033edd30fb4fe84d7c1e Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sat, 14 Jun 2025 23:06:11 +0800 Subject: [PATCH 34/99] 25w48a --- CHANGELOG.md | 5 +++++ DEV-VERSION | 2 +- proxy/chunkreq.go | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 006230b..b6bec5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 更新日志 +25w48a - 2025-06-14 +--- +- PRE-RELEASE: 此版本是v3.5.6预发布版本,请勿在生产环境中使用; +- CHANGE: 测试302重定向逻辑 + 3.5.5 - 2025-06-14 --- - CHANGE: 修正新匹配器的覆盖问题, 同时增加test的覆盖 diff --git a/DEV-VERSION b/DEV-VERSION index 55564ab..219b720 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w47a \ No newline at end of file +25w48a \ No newline at end of file diff --git a/proxy/chunkreq.go b/proxy/chunkreq.go index 35461c9..287da4c 100644 --- a/proxy/chunkreq.go +++ b/proxy/chunkreq.go @@ -68,6 +68,7 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c c.Request.Header.Del("Referer") logInfo("Internal Redirecting to %s", finalURL) ChunkedProxyRequest(ctx, c, finalURL, cfg, matcher) + return } } From cf5ae0d184e93b243c83a78a62a76e5125105496 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sun, 15 Jun 2025 11:02:16 +0800 Subject: [PATCH 35/99] fix blob rewrite --- proxy/handler.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/proxy/handler.go b/proxy/handler.go index 80a5892..d5fe677 100644 --- a/proxy/handler.go +++ b/proxy/handler.go @@ -68,8 +68,8 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra // 处理blob/raw路径 if matcher == "blob" { - rawPath = rawPath[10:] - rawPath = "raw.githubusercontent.com" + rawPath + rawPath = rawPath[18:] + rawPath = "https://raw.githubusercontent.com" + rawPath rawPath = strings.Replace(rawPath, "/blob/", "/", 1) matcher = "raw" } From fd7e270db4515d22375fc609ffbf63980a11b03a Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sun, 15 Jun 2025 15:14:15 +0800 Subject: [PATCH 36/99] 25w48b --- CHANGELOG.md | 6 ++++++ DEV-VERSION | 2 +- config/config.go | 19 +++++++++++-------- config/config.toml | 1 + go.mod | 5 +---- go.sum | 4 ++-- main.go | 12 ++++++++++++ proxy/chunkreq.go | 2 ++ proxy/gitreq.go | 1 + proxy/reqheader.go | 25 +++++++++++++++++++------ 10 files changed, 56 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6bec5b..68f814f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # 更新日志 +25w48b - 2025-06-15 +--- +- PRE-RELEASE: 此版本是v3.5.6预发布版本,请勿在生产环境中使用; +- FIX: 修正blob重写的生成问题 +- CHANGE: 验证与连接释放相关的修正 + 25w48a - 2025-06-14 --- - PRE-RELEASE: 此版本是v3.5.6预发布版本,请勿在生产环境中使用; diff --git a/DEV-VERSION b/DEV-VERSION index 219b720..52b7a35 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w48a \ No newline at end of file +25w48b \ No newline at end of file diff --git a/config/config.go b/config/config.go index 0060b63..5c5023a 100644 --- a/config/config.go +++ b/config/config.go @@ -26,6 +26,7 @@ type Config struct { host = "0.0.0.0" port = 8080 netlib = "netpoll" # "netpoll" / "std" "standard" "net/http" "net" +goPoolSize = 1024 sizeLimit = 125 # MB memLimit = 0 # MB H2C = true @@ -38,6 +39,7 @@ type ServerConfig struct { Host string `toml:"host"` NetLib string `toml:"netlib"` SenseClientDisconnection bool `toml:"senseClientDisconnection"` + GoPoolSize int `toml:"goPoolSize"` SizeLimit int `toml:"sizeLimit"` MemLimit int64 `toml:"memLimit"` H2C bool `toml:"H2C"` @@ -224,14 +226,15 @@ func FileExists(filename string) bool { func DefaultConfig() *Config { return &Config{ Server: ServerConfig{ - Port: 8080, - Host: "0.0.0.0", - NetLib: "netpoll", - SizeLimit: 125, - MemLimit: 0, - H2C: true, - Cors: "*", - Debug: false, + Port: 8080, + Host: "0.0.0.0", + NetLib: "netpoll", + GoPoolSize: 1024, + SizeLimit: 125, + MemLimit: 0, + H2C: true, + Cors: "*", + Debug: false, }, Httpc: HttpcConfig{ Mode: "auto", diff --git a/config/config.toml b/config/config.toml index 9ef2662..b60cb13 100644 --- a/config/config.toml +++ b/config/config.toml @@ -3,6 +3,7 @@ host = "0.0.0.0" port = 8080 netlib = "netpoll" # "netpoll" / "std" "standard" "net/http" "net" senseClientDisconnection = false +goPoolSize = 1024 sizeLimit = 125 # MB memLimit = 0 # MB H2C = true diff --git a/go.mod b/go.mod index ba6d974..b606255 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/BurntSushi/toml v1.5.0 github.com/WJQSERVER-STUDIO/httpc v0.7.0 github.com/WJQSERVER-STUDIO/logger v1.8.0 - github.com/cloudwego/hertz v0.10.0 + github.com/cloudwego/hertz v0.10.1-0.20250611091639-3dde619f5598 github.com/hertz-contrib/http2 v0.1.8 golang.org/x/net v0.41.0 golang.org/x/time v0.12.0 @@ -44,6 +44,3 @@ require ( ) replace github.com/nyaruka/phonenumbers => github.com/nyaruka/phonenumbers v1.6.1 // 1.6.3 has reflect leaking - -//replace github.com/WJQSERVER-STUDIO/httpc v0.5.1 => /data/github/WJQSERVER-STUDIO/httpc -//replace github.com/WJQSERVER-STUDIO/logger v1.6.0 => /data/github/WJQSERVER-STUDIO/logger diff --git a/go.sum b/go.sum index d8d7e39..730d2c6 100644 --- a/go.sum +++ b/go.sum @@ -24,8 +24,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.10.0 h1:V0vmBaLdQPlgL6w2TA6PZL1g6SGgQznFx6vqxWdCcKw= -github.com/cloudwego/hertz v0.10.0/go.mod h1:lRBohmcDkGx5TLK6QKFGdzJ6n3IXqGueHsOiXcYgXA4= +github.com/cloudwego/hertz v0.10.1-0.20250611091639-3dde619f5598 h1:8tVol3hNJS7+7f7yQIkP57tZMzUV3fOhn6pQ7t4R06k= +github.com/cloudwego/hertz v0.10.1-0.20250611091639-3dde619f5598/go.mod h1:lRBohmcDkGx5TLK6QKFGdzJ6n3IXqGueHsOiXcYgXA4= 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= diff --git a/main.go b/main.go index 7d6fb0e..a31f075 100644 --- a/main.go +++ b/main.go @@ -431,6 +431,7 @@ func main() { server.WithHostPorts(addr), server.WithTransport(standard.NewTransporter), server.WithStreamBody(true), + server.WithIdleTimeout(30*time.Second), ) r.AddProtocol("h2", factory.NewServerFactory()) } else { @@ -438,6 +439,7 @@ func main() { server.WithHostPorts(addr), server.WithTransport(standard.NewTransporter), server.WithStreamBody(true), + server.WithIdleTimeout(30*time.Second), ) } } else if cfg.Server.NetLib == "netpoll" || cfg.Server.NetLib == "" { @@ -447,6 +449,7 @@ func main() { server.WithHostPorts(addr), server.WithSenseClientDisconnection(cfg.Server.SenseClientDisconnection), server.WithStreamBody(true), + server.WithIdleTimeout(30*time.Second), ) r.AddProtocol("h2", factory.NewServerFactory()) } else { @@ -454,6 +457,7 @@ func main() { server.WithHostPorts(addr), server.WithSenseClientDisconnection(cfg.Server.SenseClientDisconnection), server.WithStreamBody(true), + server.WithIdleTimeout(30*time.Second), ) } } else { @@ -462,6 +466,14 @@ func main() { os.Exit(1) } + /* + if cfg.Server.GoPoolSize > 0 { + gopool.SetCap(int32(cfg.Server.GoPoolSize)) + } else { + gopool.SetCap(1024) + } + */ + r.Use(recovery.Recovery()) // Recovery中间件 r.Use(loggin.Middleware()) // log中间件 r.Use(viaHeader()) diff --git a/proxy/chunkreq.go b/proxy/chunkreq.go index 287da4c..8c977c9 100644 --- a/proxy/chunkreq.go +++ b/proxy/chunkreq.go @@ -28,6 +28,7 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c logError("Failed to close response body: %v", err) } } + c.Abort() }() rb := client.NewRequestBuilder(string(c.Request.Method()), u) @@ -152,6 +153,7 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c return } c.SetBodyStream(bodyReader, -1) + bodyReader.Close() } } diff --git a/proxy/gitreq.go b/proxy/gitreq.go index de6bff7..6d00640 100644 --- a/proxy/gitreq.go +++ b/proxy/gitreq.go @@ -144,4 +144,5 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co } c.SetBodyStream(bodyReader, -1) + bodyReader.Close() } diff --git a/proxy/reqheader.go b/proxy/reqheader.go index 8400821..ddb1fc6 100644 --- a/proxy/reqheader.go +++ b/proxy/reqheader.go @@ -49,6 +49,16 @@ var ( } ) +// copyHeader 将所有头部从 src 复制到 dst。 +// 对于多值头部,它会为每个值调用 Add,从而保留所有值。 +func copyHeader(dst, src http.Header) { + for k, vv := range src { + for _, v := range vv { + dst.Add(k, v) + } + } +} + func setRequestHeaders(c *app.RequestContext, req *http.Request, cfg *config.Config, matcher string) { if matcher == "raw" && cfg.Httpc.UseCustomRawHeaders { // 使用预定义Header @@ -56,20 +66,23 @@ func setRequestHeaders(c *app.RequestContext, req *http.Request, cfg *config.Con req.Header.Set(key, value) } } else if matcher == "clone" { + c.Request.Header.VisitAll(func(key, value []byte) { headerKey := string(key) headerValue := string(value) - if _, shouldRemove := cloneHeadersToRemove[headerKey]; !shouldRemove { - req.Header.Set(headerKey, headerValue) - } + req.Header.Set(headerKey, headerValue) }) + for key := range cloneHeadersToRemove { + req.Header.Del(key) + } } else { c.Request.Header.VisitAll(func(key, value []byte) { headerKey := string(key) headerValue := string(value) - if _, shouldRemove := reqHeadersToRemove[headerKey]; !shouldRemove { - req.Header.Set(headerKey, headerValue) - } + req.Header.Set(headerKey, headerValue) }) + for key := range cloneHeadersToRemove { + req.Header.Del(key) + } } } From 97b1f69f999e0cf6dc5b9bbae598cbf57679c102 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sun, 15 Jun 2025 15:51:50 +0800 Subject: [PATCH 37/99] 25w48c --- CHANGELOG.md | 5 +++++ DEV-VERSION | 2 +- proxy/chunkreq.go | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68f814f..bb10be8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 更新日志 +25w48c - 2025-06-15 +--- +- PRE-RELEASE: 此版本是v3.5.6预发布版本,请勿在生产环境中使用; +- CHANGE: 加入内部301处理 + 25w48b - 2025-06-15 --- - PRE-RELEASE: 此版本是v3.5.6预发布版本,请勿在生产环境中使用; diff --git a/DEV-VERSION b/DEV-VERSION index 52b7a35..b100103 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w48b \ No newline at end of file +25w48c \ No newline at end of file diff --git a/proxy/chunkreq.go b/proxy/chunkreq.go index 8c977c9..124cfb7 100644 --- a/proxy/chunkreq.go +++ b/proxy/chunkreq.go @@ -59,7 +59,7 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c } // 处理302情况 - if resp.StatusCode == 302 { + if resp.StatusCode == 302 || resp.StatusCode == 301 { finalURL := resp.Header.Get("Location") if finalURL != "" { err = resp.Body.Close() From 91c3ad7fd8e9a2fcbac9ee01edc30135fed255f6 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sun, 15 Jun 2025 16:42:42 +0800 Subject: [PATCH 38/99] 3.5.6 --- CHANGELOG.md | 5 +++++ VERSION | 2 +- proxy/reqheader.go | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb10be8..019844d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 更新日志 +3.5.6 - 2025-06-15 +--- +- FIX: 修正blob重写的生成问题 +- CHANGE: 改进302重定向逻辑 + 25w48c - 2025-06-15 --- - PRE-RELEASE: 此版本是v3.5.6预发布版本,请勿在生产环境中使用; diff --git a/VERSION b/VERSION index 1947319..01081db 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.5.5 \ No newline at end of file +3.5.6 \ No newline at end of file diff --git a/proxy/reqheader.go b/proxy/reqheader.go index ddb1fc6..8612a7e 100644 --- a/proxy/reqheader.go +++ b/proxy/reqheader.go @@ -81,7 +81,7 @@ func setRequestHeaders(c *app.RequestContext, req *http.Request, cfg *config.Con headerValue := string(value) req.Header.Set(headerKey, headerValue) }) - for key := range cloneHeadersToRemove { + for key := range reqHeadersToRemove { req.Header.Del(key) } } From a4d324a3610078c654851cfd5062847d924b72bc Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Mon, 16 Jun 2025 08:28:02 +0800 Subject: [PATCH 39/99] 4.0.0-beta.0 --- .github/workflows/build.yml | 5 +- CHANGELOG.md | 7 + DEV-VERSION | 2 +- README.md | 15 +- SECURITY.MD | 17 +- VERSION | 2 +- api/api.go | 108 +++++---- auth/auth-header.go | 9 +- auth/auth-parameters.go | 6 +- auth/auth.go | 25 +-- auth/blacklist.go | 2 +- auth/whitelist.go | 3 +- config/config.go | 61 ++---- config/config.toml | 11 +- docs/config.md | 398 ---------------------------------- docs/flag.md | 26 --- docs/menu.md | 19 -- go.mod | 32 +-- go.sum | 144 +----------- main.go | 365 +++++++++++-------------------- middleware/loggin/loggin.go | 32 --- middleware/nocache/nocache.go | 16 +- proxy/authparse.go | 2 +- proxy/authpass.go | 9 +- proxy/bandwidth.go | 6 - proxy/chunkreq.go | 50 +++-- proxy/dial.go | 26 ++- proxy/docker.go | 97 +++++---- proxy/error.go | 70 +++--- proxy/gitreq.go | 64 +++--- proxy/handler.go | 33 ++- proxy/httpc.go | 40 +--- proxy/match.go | 2 +- proxy/nest.go | 17 +- proxy/reqheader.go | 17 +- proxy/routing.go | 38 ++-- proxy/utils.go | 42 +--- rate/rate.go | 107 --------- 38 files changed, 497 insertions(+), 1428 deletions(-) delete mode 100644 docs/config.md delete mode 100644 docs/flag.md delete mode 100644 docs/menu.md delete mode 100644 middleware/loggin/loggin.go delete mode 100644 rate/rate.go diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e0306c0..2393134 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -141,4 +141,7 @@ jobs: push: true tags: | ${{ env.IMAGE_NAME }}:${{ env.VERSION }} - ${{ env.IMAGE_NAME }}:v3 + ${{ env.IMAGE_NAME }}:v4 + ${{ env.IMAGE_NAME }}:latest + wjqserver/ghproxy-touka:latest + wjqserver/ghproxy-touka:${{ env.VERSION }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 019844d..c78d251 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # 更新日志 +4.0.0-beta.0 - 2025-06-15 +--- +- BETA-TEST: 此版本是v4.0.0的测试版本,请勿在生产环境中使用; +- CHANGE: 移交到Touka框架 +- REMOVE: 移除req rate limit的total方式 +- CHANGE: 使用[reco](https://github.com/fenthope/reco)日志库, 异步使能 + 3.5.6 - 2025-06-15 --- - FIX: 修正blob重写的生成问题 diff --git a/DEV-VERSION b/DEV-VERSION index b100103..456c4f8 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w48c \ No newline at end of file +4.0.0-beta.0 \ No newline at end of file diff --git a/README.md b/README.md index 3f4123d..4364c51 100644 --- a/README.md +++ b/README.md @@ -6,16 +6,15 @@ ![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带来的优势), 同时支持多种额外功能 +GHProxy是一个基于Go的支持代理Github仓库资源和API的项目, 同时支持Docker镜像代理与脚本嵌套加速等多种功能 ## 项目说明 ### 项目特点 - ⚡ **基于 Go 语言实现,跨平台的同时提供高并发性能** -- 🌐 **使用字节旗下的 [HertZ](https://github.com/cloudwego/hertz) 作为 Web 框架** -- 📡 **使用 [Touka-HTTPC](https://github.com/satomitouka/touka-httpc) 作为 HTTP 客户端** +- 🌐 **使用自有[Touka框架](https://github.com/infinite-iroha/touka)作为 HTTP服务端框架** +- 📡 **使用 [Touka-HTTPC](https://github.com/WJQSERVER-STUDIO/httpc) 作为 HTTP 客户端** - 📥 **支持 Git clone、raw、releases 等文件拉取** - 🐳 **支持反代Docker, GHCR等镜像仓库** - 🎨 **支持多个前端主题** @@ -98,9 +97,9 @@ wget -O install-dev.sh https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghprox ## 项目简史 -**本项目是[WJQSERVER-STUDIO/ghproxy-go](https://github.com/WJQSERVER-STUDIO/ghproxy-go)的重构版本,实现了原项目原定功能的同时,进一步优化了性能** -关于此项目的详细开发过程,请参看Commit记录与[CHANGELOG.md](https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/CHANGELOG.md) +本项目旨在于构建一个高效且功能多样的GHProxy +- v4.0.0 迁移到[Touka框架](https://github.com/infinite-iroha/touka) - v3.0.0 迁移到HertZ框架, 进一步提升效率 - v2.4.1 对路径匹配进行优化 - v2.0.0 对`proxy`核心模块进行了重构,大幅优化内存占用 @@ -121,10 +120,6 @@ v3.5.2开始, 本项目使用 [WJQserver Studio License 2.1](https://wjqserver-s 如果您觉得本项目对您有帮助,欢迎赞助支持,您的赞助将用于Demo服务器开支及开发者时间成本支出,感谢您的支持! -为爱发电,开源不易 - -爱发电: https://afdian.com/a/wjqserver - USDT(TRC20): `TNfSYG6F2vkiibd6J6mhhHNWDgWgNdF5hN` ### 捐赠列表 diff --git a/SECURITY.MD b/SECURITY.MD index 535d84e..7696664 100644 --- a/SECURITY.MD +++ b/SECURITY.MD @@ -6,10 +6,13 @@ | 版本 | 是否支持 | | --- | --- | -| v3.x.x | :white_check_mark: 当前最新版本序列 | +| v4.x.x | :white_check_mark: 当前最新版本序列 | +| v3.x.x | :x: 这些版本已结束生命周期,不受支持 | | v2.x.x | :x: 这些版本已结束生命周期,不受支持 | | v1.x.x | :x: 这些版本已结束生命周期,不受支持 | -| 25w*a/b/c... | :warning: 此为PRE-RELEASE版本,用于开发与测试,可能存在未知的问题 | +| *-rc.x | :warning: 此为PRE-RELEASE预发布版本,用于测试问题 | +| *-beta.x | :warning: 此为Beta测试版本,用于开发与测试,可能存在未知的问题 | +| 25w*a/b/c... | :warning: 此为PRE-RELEASE版本,用于开发与测试,可能存在未知的问题 生命周期已完全结束 | | 24w*a/b/c... | :warning: 此为PRE-RELEASE版本,用于开发与测试,可能存在未知的问题 生命周期已完全结束 | | v0.x.x | :x: 这些版本不再受支持 | @@ -17,9 +20,15 @@ 本项目为开源项目,开发者不对使用本项目造成的任何损失或问题承担责任。用户需自行评估并承担使用本项目的风险。 -使用本项目,请遵循 **[WSL 2.0 (WJQSERVER-STUDIO LICENSE 2.0)](https://wjqserver-studio.github.io/LICENSE/LICENSE.html)** 协议。 +使用本项目,请遵循 **[WSL 2.1 (WJQSERVER-STUDIO LICENSE 2.1)](https://wjqserver-studio.github.io/LICENSE/LICENSE.html)** 协议 或 [Mozilla Public License Version 2.0](https://mozilla.org/MPL/2.0/) 。 -本项目所有文件均受到 WSL 2.0 (WJQSERVER-STUDIO LICENSE 2.0) 协议保护,任何人不得在任何情况下以非 WSL 2.0 (WJQSERVER-STUDIO LICENSE 2.0) 协议内规定的方式使用,复制,修改,编译,发布,分发,再许可,或者出售本项目的任何部分。 +#### 选择WSL 2.1时 + +本项目所有文件均受到 WSL 2.1 (WJQSERVER-STUDIO LICENSE 2.1) 协议保护,任何人不得在任何情况下以非 WSL 2.1 (WJQSERVER-STUDIO LICENSE 2.1) 协议内规定的方式使用,复制,修改,编译,发布,分发,再许可,或者出售本项目的任何部分。 + +#### 选择MPL 2.0时 + +本项目内文件除特别版权标注声明外, 均受到 [Mozilla Public License Version 2.0](https://mozilla.org/MPL/2.0/) 授权保护, 具体条款参看 [Mozilla Public License Version 2.0](https://mozilla.org/MPL/2.0/) ## 报告漏洞 diff --git a/VERSION b/VERSION index 01081db..0c89fc9 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.5.6 \ No newline at end of file +4.0.0 \ No newline at end of file diff --git a/api/api.go b/api/api.go index 597a198..b1bb61c 100644 --- a/api/api.go +++ b/api/api.go @@ -1,95 +1,85 @@ package api import ( - "context" "ghproxy/config" "ghproxy/middleware/nocache" - "github.com/WJQSERVER-STUDIO/logger" - "github.com/cloudwego/hertz/pkg/app" - "github.com/cloudwego/hertz/pkg/app/server" + "github.com/infinite-iroha/touka" ) -var ( - logw = logger.Logw - logDump = logger.LogDump - logDebug = logger.LogDebug - logInfo = logger.LogInfo - logWarning = logger.LogWarning - logError = logger.LogError -) - -func InitHandleRouter(cfg *config.Config, r *server.Hertz, version string) { +func InitHandleRouter(cfg *config.Config, r *touka.Engine, version string) { apiRouter := r.Group("/api", nocache.NoCacheMiddleware()) { - apiRouter.GET("/size_limit", func(ctx context.Context, c *app.RequestContext) { - SizeLimitHandler(cfg, c, ctx) + apiRouter.GET("/size_limit", func(c *touka.Context) { + SizeLimitHandler(cfg, c) }) - apiRouter.GET("/whitelist/status", func(ctx context.Context, c *app.RequestContext) { - WhiteListStatusHandler(cfg, c, ctx) + apiRouter.GET("/whitelist/status", func(c *touka.Context) { + WhiteListStatusHandler(cfg, c) }) - apiRouter.GET("/blacklist/status", func(ctx context.Context, c *app.RequestContext) { - BlackListStatusHandler(cfg, c, ctx) + apiRouter.GET("/blacklist/status", func(c *touka.Context) { + BlackListStatusHandler(cfg, c) }) - apiRouter.GET("/cors/status", func(ctx context.Context, c *app.RequestContext) { - CorsStatusHandler(cfg, c, ctx) + apiRouter.GET("/cors/status", func(c *touka.Context) { + CorsStatusHandler(cfg, c) }) - apiRouter.GET("/healthcheck", func(ctx context.Context, c *app.RequestContext) { - HealthcheckHandler(c, ctx) + apiRouter.GET("/healthcheck", func(c *touka.Context) { + HealthcheckHandler(c) }) - apiRouter.GET("/version", func(ctx context.Context, c *app.RequestContext) { - VersionHandler(c, ctx, version) + apiRouter.GET("/ok", func(c *touka.Context) { + HealthcheckHandler(c) }) - apiRouter.GET("/rate_limit/status", func(ctx context.Context, c *app.RequestContext) { - RateLimitStatusHandler(cfg, c, ctx) + apiRouter.GET("/version", func(c *touka.Context) { + VersionHandler(c, version) }) - apiRouter.GET("/rate_limit/limit", func(ctx context.Context, c *app.RequestContext) { - RateLimitLimitHandler(cfg, c, ctx) + apiRouter.GET("/rate_limit/status", func(c *touka.Context) { + RateLimitStatusHandler(cfg, c) }) - apiRouter.GET("/smartgit/status", func(ctx context.Context, c *app.RequestContext) { - SmartGitStatusHandler(cfg, c, ctx) + apiRouter.GET("/rate_limit/limit", func(c *touka.Context) { + RateLimitLimitHandler(cfg, c) }) - apiRouter.GET("/shell_nest/status", func(ctx context.Context, c *app.RequestContext) { - shellNestStatusHandler(cfg, c, ctx) + apiRouter.GET("/smartgit/status", func(c *touka.Context) { + SmartGitStatusHandler(cfg, c) }) - apiRouter.GET("/oci_proxy/status", func(ctx context.Context, c *app.RequestContext) { - ociProxyStatusHandler(cfg, c, ctx) + apiRouter.GET("/shell_nest/status", func(c *touka.Context) { + shellNestStatusHandler(cfg, c) + }) + apiRouter.GET("/oci_proxy/status", func(c *touka.Context) { + ociProxyStatusHandler(cfg, c) }) } - logInfo("API router Init success") } -func SizeLimitHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) { +func SizeLimitHandler(cfg *config.Config, c *touka.Context) { sizeLimit := cfg.Server.SizeLimit - c.Response.Header.Set("Content-Type", "application/json") + c.SetHeader("Content-Type", "application/json") c.JSON(200, (map[string]interface{}{ "MaxResponseBodySize": sizeLimit, })) } -func WhiteListStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) { - c.Response.Header.Set("Content-Type", "application/json") +func WhiteListStatusHandler(cfg *config.Config, c *touka.Context) { + c.SetHeader("Content-Type", "application/json") c.JSON(200, (map[string]interface{}{ "Whitelist": cfg.Whitelist.Enabled, })) } -func BlackListStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) { - c.Response.Header.Set("Content-Type", "application/json") +func BlackListStatusHandler(cfg *config.Config, c *touka.Context) { + c.SetHeader("Content-Type", "application/json") c.JSON(200, (map[string]interface{}{ "Blacklist": cfg.Blacklist.Enabled, })) } -func CorsStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) { - c.Response.Header.Set("Content-Type", "application/json") +func CorsStatusHandler(cfg *config.Config, c *touka.Context) { + c.SetHeader("Content-Type", "application/json") c.JSON(200, (map[string]interface{}{ "Cors": cfg.Server.Cors, })) } -func HealthcheckHandler(c *app.RequestContext, ctx context.Context) { - c.Response.Header.Set("Content-Type", "application/json") +func HealthcheckHandler(c *touka.Context) { + c.SetHeader("Content-Type", "application/json") c.JSON(200, (map[string]interface{}{ "Status": "OK", "Repo": "WJQSERVER-STUDIO/GHProxy", @@ -97,8 +87,8 @@ func HealthcheckHandler(c *app.RequestContext, ctx context.Context) { })) } -func VersionHandler(c *app.RequestContext, ctx context.Context, version string) { - c.Response.Header.Set("Content-Type", "application/json") +func VersionHandler(c *touka.Context, version string) { + c.SetHeader("Content-Type", "application/json") c.JSON(200, (map[string]interface{}{ "Version": version, "Repo": "WJQSERVER-STUDIO/GHProxy", @@ -106,36 +96,36 @@ func VersionHandler(c *app.RequestContext, ctx context.Context, version string) })) } -func RateLimitStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) { - c.Response.Header.Set("Content-Type", "application/json") +func RateLimitStatusHandler(cfg *config.Config, c *touka.Context) { + c.SetHeader("Content-Type", "application/json") c.JSON(200, (map[string]interface{}{ "RateLimit": cfg.RateLimit.Enabled, })) } -func RateLimitLimitHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) { - c.Response.Header.Set("Content-Type", "application/json") +func RateLimitLimitHandler(cfg *config.Config, c *touka.Context) { + c.SetHeader("Content-Type", "application/json") c.JSON(200, (map[string]interface{}{ "RatePerMinute": cfg.RateLimit.RatePerMinute, })) } -func SmartGitStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) { - c.Response.Header.Set("Content-Type", "application/json") +func SmartGitStatusHandler(cfg *config.Config, c *touka.Context) { + c.SetHeader("Content-Type", "application/json") c.JSON(200, (map[string]interface{}{ "enabled": cfg.GitClone.Mode == "cache", })) } -func shellNestStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) { - c.Response.Header.Set("Content-Type", "application/json") +func shellNestStatusHandler(cfg *config.Config, c *touka.Context) { + c.SetHeader("Content-Type", "application/json") c.JSON(200, (map[string]interface{}{ "enabled": cfg.Shell.Editor, })) } -func ociProxyStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) { - c.Response.Header.Set("Content-Type", "application/json") +func ociProxyStatusHandler(cfg *config.Config, c *touka.Context) { + c.SetHeader("Content-Type", "application/json") c.JSON(200, (map[string]interface{}{ "enabled": cfg.Docker.Enabled, "target": cfg.Docker.Target, diff --git a/auth/auth-header.go b/auth/auth-header.go index 1457a13..5180c05 100644 --- a/auth/auth-header.go +++ b/auth/auth-header.go @@ -4,22 +4,21 @@ import ( "fmt" "ghproxy/config" - "github.com/cloudwego/hertz/pkg/app" + "github.com/infinite-iroha/touka" ) -func AuthHeaderHandler(c *app.RequestContext, cfg *config.Config) (isValid bool, err error) { +func AuthHeaderHandler(c *touka.Context, cfg *config.Config) (isValid bool, err error) { if !cfg.Auth.Enabled { return true, nil } // 获取"GH-Auth"的值 var authToken string if cfg.Auth.Key != "" { - authToken = string(c.GetHeader(cfg.Auth.Key)) + authToken = string(c.Request.Header.Get(cfg.Auth.Key)) } else { - authToken = string(c.GetHeader("GH-Auth")) + authToken = string(c.Request.Header.Get("GH-Auth")) } - logDebug("%s %s %s %s %s AUTH_TOKEN: %s", c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), authToken) if authToken == "" { return false, fmt.Errorf("Auth token not found") } diff --git a/auth/auth-parameters.go b/auth/auth-parameters.go index 2167b24..4a4fecd 100644 --- a/auth/auth-parameters.go +++ b/auth/auth-parameters.go @@ -4,10 +4,10 @@ import ( "fmt" "ghproxy/config" - "github.com/cloudwego/hertz/pkg/app" + "github.com/infinite-iroha/touka" ) -func AuthParametersHandler(c *app.RequestContext, cfg *config.Config) (isValid bool, err error) { +func AuthParametersHandler(c *touka.Context, cfg *config.Config) (isValid bool, err error) { if !cfg.Auth.Enabled { return true, nil } @@ -19,8 +19,6 @@ func AuthParametersHandler(c *app.RequestContext, cfg *config.Config) (isValid b authToken = c.Query("auth_token") } - logDebug("%s %s %s %s %s AUTH_TOKEN: %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), authToken) - if authToken == "" { return false, fmt.Errorf("Auth token not found") } diff --git a/auth/auth.go b/auth/auth.go index d817dc4..dcc7b29 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -4,38 +4,25 @@ import ( "fmt" "ghproxy/config" - "github.com/WJQSERVER-STUDIO/logger" - "github.com/cloudwego/hertz/pkg/app" -) - -var ( - logw = logger.Logw - logDump = logger.LogDump - logDebug = logger.LogDebug - logInfo = logger.LogInfo - logWarning = logger.LogWarning - logError = logger.LogError + "github.com/infinite-iroha/touka" ) func Init(cfg *config.Config) { if cfg.Blacklist.Enabled { err := InitBlacklist(cfg) if err != nil { - logError(err.Error()) - return + panic(err.Error()) } } if cfg.Whitelist.Enabled { err := InitWhitelist(cfg) if err != nil { - logError(err.Error()) - return + panic(err.Error()) } } - logDebug("Auth Init") } -func AuthHandler(c *app.RequestContext, cfg *config.Config) (isValid bool, err error) { +func AuthHandler(c *touka.Context, cfg *config.Config) (isValid bool, err error) { if cfg.Auth.Method == "parameters" { isValid, err = AuthParametersHandler(c, cfg) return isValid, err @@ -43,10 +30,10 @@ func AuthHandler(c *app.RequestContext, cfg *config.Config) (isValid bool, err e isValid, err = AuthHeaderHandler(c, cfg) return isValid, err } else if cfg.Auth.Method == "" { - logError("Auth method not set") + c.Errorf("Auth method not set") return true, nil } else { - logError("Auth method not supported %s", cfg.Auth.Method) + c.Errorf("Auth method not supported %s", cfg.Auth.Method) return false, fmt.Errorf("%s", fmt.Sprintf("Auth method %s not supported", cfg.Auth.Method)) } } diff --git a/auth/blacklist.go b/auth/blacklist.go index fd6b276..5ccc73c 100644 --- a/auth/blacklist.go +++ b/auth/blacklist.go @@ -7,7 +7,7 @@ import ( "strings" "sync" - json "github.com/bytedance/sonic" + "encoding/json" ) type Blacklist struct { diff --git a/auth/whitelist.go b/auth/whitelist.go index b4071d1..ee93c20 100644 --- a/auth/whitelist.go +++ b/auth/whitelist.go @@ -1,13 +1,12 @@ package auth import ( + "encoding/json" "fmt" "ghproxy/config" "os" "strings" "sync" - - json "github.com/bytedance/sonic" ) // Whitelist 用于存储白名单信息 diff --git a/config/config.go b/config/config.go index 5c5023a..3775a12 100644 --- a/config/config.go +++ b/config/config.go @@ -25,26 +25,19 @@ type Config struct { [server] host = "0.0.0.0" port = 8080 -netlib = "netpoll" # "netpoll" / "std" "standard" "net/http" "net" -goPoolSize = 1024 sizeLimit = 125 # MB memLimit = 0 # MB -H2C = true cors = "*" # "*"/"" -> "*" ; "nil" -> "" ; debug = false */ type ServerConfig struct { - Port int `toml:"port"` - Host string `toml:"host"` - NetLib string `toml:"netlib"` - SenseClientDisconnection bool `toml:"senseClientDisconnection"` - GoPoolSize int `toml:"goPoolSize"` - SizeLimit int `toml:"sizeLimit"` - MemLimit int64 `toml:"memLimit"` - H2C bool `toml:"H2C"` - Cors string `toml:"cors"` - Debug bool `toml:"debug"` + Port int `toml:"port"` + Host string `toml:"host"` + SizeLimit int `toml:"sizeLimit"` + MemLimit int64 `toml:"memLimit"` + Cors string `toml:"cors"` + Debug bool `toml:"debug"` } /* @@ -98,11 +91,9 @@ type PagesConfig struct { } type LogConfig struct { - LogFilePath string `toml:"logFilePath"` - MaxLogSize int `toml:"maxLogSize"` - Level string `toml:"level"` - Async bool `toml:"async"` - HertZLogPath string `toml:"hertzLogPath"` + LogFilePath string `toml:"logFilePath"` + MaxLogSize int64 `toml:"maxLogSize"` + Level string `toml:"level"` } /* @@ -138,7 +129,6 @@ type WhitelistConfig struct { /* [rateLimit] enabled = false -rateMethod = "total" # "total" or "ip" ratePerMinute = 100 burst = 10 @@ -151,10 +141,9 @@ burst = 10 */ type RateLimitConfig struct { - Enabled bool `toml:"enabled"` - RateMethod string `toml:"rateMethod"` - RatePerMinute int `toml:"ratePerMinute"` - Burst int `toml:"burst"` + Enabled bool `toml:"enabled"` + RatePerMinute int `toml:"ratePerMinute"` + Burst int `toml:"burst"` BandwidthLimit BandwidthLimitConfig } @@ -226,15 +215,12 @@ func FileExists(filename string) bool { func DefaultConfig() *Config { return &Config{ Server: ServerConfig{ - Port: 8080, - Host: "0.0.0.0", - NetLib: "netpoll", - GoPoolSize: 1024, - SizeLimit: 125, - MemLimit: 0, - H2C: true, - Cors: "*", - Debug: false, + Port: 8080, + Host: "0.0.0.0", + SizeLimit: 125, + MemLimit: 0, + Cors: "*", + Debug: false, }, Httpc: HttpcConfig{ Mode: "auto", @@ -257,10 +243,9 @@ func DefaultConfig() *Config { StaticDir: "/data/www", }, Log: LogConfig{ - LogFilePath: "/data/ghproxy/log/ghproxy.log", - MaxLogSize: 10, - Level: "info", - HertZLogPath: "/data/ghproxy/log/hertz.log", + LogFilePath: "/data/ghproxy/log/ghproxy.log", + MaxLogSize: 10, + Level: "info", }, Auth: AuthConfig{ Enabled: false, @@ -280,8 +265,8 @@ func DefaultConfig() *Config { WhitelistFile: "/data/ghproxy/config/whitelist.json", }, RateLimit: RateLimitConfig{ - Enabled: false, - RateMethod: "total", + Enabled: false, + //RateMethod: "total", RatePerMinute: 100, Burst: 10, BandwidthLimit: BandwidthLimitConfig{ diff --git a/config/config.toml b/config/config.toml index b60cb13..27585b2 100644 --- a/config/config.toml +++ b/config/config.toml @@ -1,12 +1,8 @@ [server] host = "0.0.0.0" port = 8080 -netlib = "netpoll" # "netpoll" / "std" "standard" "net/http" "net" -senseClientDisconnection = false -goPoolSize = 1024 sizeLimit = 125 # MB memLimit = 0 # MB -H2C = true cors = "*" # "*"/"" -> "*" ; "nil" -> "" ; debug = false @@ -34,9 +30,7 @@ staticDir = "/data/www" [log] logFilePath = "/data/ghproxy/log/ghproxy.log" maxLogSize = 5 # MB -level = "info" # dump, debug, info, warn, error, none -async = false -hertzLogPath = "/data/ghproxy/log/hertz.log" +level = "info" # debug, info, warn, error, none [auth] method = "parameters" # "header" or "parameters" @@ -57,7 +51,6 @@ whitelistFile = "/data/ghproxy/config/whitelist.json" [rateLimit] enabled = false -rateMethod = "total" # "ip" or "total" ratePerMinute = 180 burst = 5 @@ -74,4 +67,4 @@ 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 +target = "dockerhub" # ghcr/dockerhub/ custom \ No newline at end of file diff --git a/docs/config.md b/docs/config.md deleted file mode 100644 index a01f762..0000000 --- a/docs/config.md +++ /dev/null @@ -1,398 +0,0 @@ -# ghproxy 用户配置文档 - -> 弃用, 请转到 [GHProxy项目文档](https://wjqserver-docs.pages.dev/docs/ghproxy/) - -`ghproxy` 的配置主要通过修改 `config` 目录下的 `config.toml`、`blacklist.json` 和 `whitelist.json` 文件来实现。本文档将详细介绍这些配置文件的作用以及用户可以自定义的配置选项。 - -## `config.toml` - 主配置文件 - -`config.toml` 是 `ghproxy` 的主配置文件,采用 TOML 格式。您可以通过修改此文件来定制 `ghproxy` 的各项功能,例如服务器端口、连接设置、Git 克隆模式、日志级别、认证方式、黑白名单以及限速策略等。 - -以下是 `config.toml` 文件的详细配置项说明: - -```toml name=config/config.toml -[server] -host = "0.0.0.0" -port = 8080 -netlib = "netpoll" # "netpoll" / "std" "standard" "net/http" "net" -sizeLimit = 125 # MB -memLimit = 0 # MB -H2C = true -cors = "*" # "*"/"" -> "*" ; "nil" -> "" ; -debug = false - -[httpc] -mode = "auto" # "auto" or "advanced" -maxIdleConns = 100 # only for advanced mode -maxIdleConnsPerHost = 60 # only for advanced mode -maxConnsPerHost = 0 # only for advanced mode -useCustomRawHeaders = false - -[gitclone] -mode = "bypass" # bypass / cache -smartGitAddr = "http://127.0.0.1:8080" -ForceH2C = false - -[shell] -editor = false -rewriteAPI = false - -[pages] -mode = "internal" # "internal" or "external" -theme = "bootstrap" # "bootstrap" or "nebula" -staticDir = "/data/www" - -[log] -logFilePath = "/data/ghproxy/log/ghproxy.log" -maxLogSize = 5 # MB -level = "info" # dump, debug, info, warn, error, none -hertzLogPath = "/data/ghproxy/log/hertz.log" - -[auth] -method = "parameters" # "header" or "parameters" -token = "token" -key = "" -enabled = false -passThrough = false -ForceAllowApi = false - -[blacklist] -blacklistFile = "/data/ghproxy/config/blacklist.json" -enabled = false - -[whitelist] -enabled = false -whitelistFile = "/data/ghproxy/config/whitelist.json" - -[rateLimit] -enabled = false -rateMethod = "total" # "ip" or "total" -ratePerMinute = 180 -burst = 5 - -[rateLimit.bandwidthLimit] - enabled = false - totalLimit = "100mbps" - totalBurst = "100mbps" - singleLimit = "10mbps" - singleBurst = "10mbps" - -[outbound] -enabled = false -url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890" - -[docker] -enabled = false -target = "ghcr" # ghcr/dockerhub or "xx.example.com" -``` - -### 配置项详细说明 - -* **`[server]` - 服务器配置** - - * `host`: 监听地址。 - * 类型: 字符串 (`string`) - * 默认值: `"0.0.0.0"` (监听所有) - * 说明: 设置 `ghproxy` 监听的网络地址。通常设置为 `"0.0.0.0"` 以监听所有可用的网络接口。 - * `port`: 监听端口。 - * 类型: 整数 (`int`) - * 默认值: `8080` - * 说明: 设置 `ghproxy` 监听的端口号。 - * `netlib`: 底层网络库。 - * 类型: 字符串 (`string`) - * 默认值: `""` (HertZ默认处置) - * 说明: `"std"` `"standard"` `"net/http"` `"net"` 均会被设置为go标准库`net/http`, 设置为`"netpoll"`或`""`会由`HertZ`默认逻辑处理 - * `sizeLimit`: 请求体大小限制。 - * 类型: 整数 (`int`) - * 默认值: `125` (MB) - * 说明: 限制允许接收的请求体最大大小,单位为 MB。用于防止过大的请求导致服务压力过大。 - * `memLimit`: `runtime`内存限制 - * 类型: 整数 (`int64`) - * 默认值: `0` (不传入) - * 说明: 给`runtime`的指标, 让gc行为更高效 - * `H2C`: 是否启用 H2C (HTTP/2 Cleartext) 传输。 - * 类型: 布尔值 (`bool`) - * 默认值: `true` (启用) - * 说明: 启用后,允许客户端使用 HTTP/2 协议进行无加密传输,提升性能。 - * `cors`: CORS (跨域资源共享) 设置。 - * 类型: 字符串 (`string`) - * 默认值: `"*"` (允许所有来源) - * 可选值: - * `""` 或`"*"`: 允许所有来源跨域访问。 - * `"nil"`: 禁用 CORS。 - * 具体的域名: 例如 `"https://example.com"`,只允许来自指定域名的跨域请求。 - * 说明: 配置 CORS 策略,用于控制哪些域名可以跨域访问 `ghproxy` 服务。 - * `debug`: 是否启用调试模式。 - * 类型: 布尔值 (`bool`) - * 默认值: `false` (禁用) - * 说明: 启用后,`ghproxy` 会输出更详细的日志信息,用于开发和调试。 - -* **`[httpc]` - HTTP 客户端配置** - - * `mode`: HTTP 客户端模式。 - * 类型: 字符串 (`string`) - * 默认值: `"auto"` (自动模式) - * 可选值: - * `"auto"`: 自动模式,使用默认的 HTTP 客户端配置,适用于大多数场景。 - * `"advanced"`: 高级模式,允许自定义连接池参数,可以更精细地控制 HTTP 客户端的行为。 - * 说明: 选择 HTTP 客户端的运行模式。 - * `maxIdleConns`: 最大空闲连接数 (仅在高级模式下生效)。 - * 类型: 整数 (`int`) - * 默认值: `100` - * 说明: 设置 HTTP 客户端连接池中保持的最大空闲连接数。 - * `maxIdleConnsPerHost`: 每个主机最大空闲连接数 (仅在高级模式下生效)。 - * 类型: 整数 (`int`) - * 默认值: `60` - * 说明: 设置 HTTP 客户端连接池中,每个主机允许保持的最大空闲连接数。 - * `maxConnsPerHost`: 每个主机最大连接数 (仅在高级模式下生效)。 - * 类型: 整数 (`int`) - * 默认值: `0` (不限制) - * 说明: 设置 HTTP 客户端连接池中,每个主机允许建立的最大连接数。设置为 `0` 表示不限制。 - * `useCustomRawHeaders`: 使用预定义header避免github waf对应zh-CN的封锁 - * 类型: 布尔值(`bool`) - * 默认值: `false`(停用) - * 说明: 启用后, 拉取raw文件会使用程序预定义的固定headers, 而不是原先的复制行为 - -* **`[gitclone]` - Git 克隆配置** - - * `mode`: Git 克隆模式。 - * 类型: 字符串 (`string`) - * 默认值: `"bypass"` (绕过模式) - * 可选值: - * `"bypass"`: 绕过模式,直接克隆 GitHub 仓库,不使用任何缓存加速。 - * `"cache"`: 缓存模式,使用智能 Git 服务加速克隆,需要配置 `smartGitAddr`。 - * 说明: 选择 Git 克隆的模式。 - * `smartGitAddr`: 智能 Git 服务地址 (仅在缓存模式下生效)。 - * 类型: 字符串 (`string`) - * 默认值: `"http://127.0.0.1:8080"` - * 说明: 当 `mode` 设置为 `"cache"` 时,需要配置智能 Git 服务的地址,用于加速 Git 克隆。 - * `ForceH2C`: 是否强制使用 H2C 连接到智能 Git 服务。 - * 类型: 布尔值 (`bool`) - * 默认值: `false` (不强制) - * 说明: 如果智能 Git 服务支持 H2C,可以设置为 `true` 以强制使用 H2C 连接,提升性能。 - -* **`[shell]` - Shell 嵌套加速功能配置** - - * `editor`: 是否启用编辑(嵌套加速)功能。 - * 类型: 布尔值 (`bool`) - * 默认值: `false` (禁用) - * 说明: 启用后, 会修改`.sh`文件内容以实现嵌套加速 - * `rewriteAPI`: 是否重写 API 地址。 - * 类型: 布尔值 (`bool`) - * 默认值: `false` (禁用) - * 说明: 启用后,`ghproxy` 会重写脚本内的Github API地址。 - -* **`[pages]` - Pages 服务配置** - - * `mode`: Pages 服务模式。 - * 类型: 字符串 (`string`) - * 默认值: `"internal"` (内置 Pages 服务) - * 可选值: - * `"internal"`: 使用 `ghproxy` 内置的 Pages 服务。 - * `"external"`: 使用外部 Pages 位置。 - * 说明: 选择 Pages 服务的运行模式。 - * `theme`: Pages 主题。 - * 类型: 字符串 (`string`) - * 默认值: `"bootstrap"` - * 可选值: 参看[GHProxy项目前端仓库](https://github.com/WJQSERVER-STUDIO/GHProxy-Frontend) - * 说明: 设置内置 Pages 服务使用的主题。 - * `staticDir`: 静态文件目录。 - * 类型: 字符串 (`string`) - * 默认值: `"/data/www"` - * 说明: 指定外置 Pages 服务使用的静态文件目录。 - -* **`[log]` - 日志配置** - - * `logFilePath`: 日志文件路径。 - * 类型: 字符串 (`string`) - * 默认值: `"/data/ghproxy/log/ghproxy.log"` - * 说明: 设置 `ghproxy` 日志文件的存储路径。 - * `maxLogSize`: 最大日志文件大小。 - * 类型: 整数 (`int`) - * 默认值: `5` (MB) - * 说明: 设置单个日志文件的最大大小,单位为 MB。当日志文件大小超过此限制时,会进行日志轮转。 - * `level`: 日志级别。 - * 类型: 字符串 (`string`) - * 默认值: `"info"` - * 可选值: `"dump"`, `"debug"`, `"info"`, `"warn"`, `"error"`, `"none"` - * 说明: 设置日志输出的级别。级别越高,输出的日志信息越少。 - * `"dump"`: 输出所有日志,包括最详细的调试信息。 - * `"debug"`: 输出调试信息、信息、警告和错误日志。 - * `"info"`: 输出信息、警告和错误日志。 - * `"warn"`: 输出警告和错误日志。 - * `"error"`: 仅输出错误日志。 - * `"none"`: 禁用所有日志输出。 - * `hertzLogPath`: `HertZ`日志文件路径。 - * 类型: 字符串 (`string`) - * 默认值: `"/data/ghproxy/log/hertz.log"` - * 说明: 设置 `HertZ` 日志文件的存储路径。 - -* **`[auth]` - 认证配置** - - * `enabled`: 是否启用认证。 - * 类型: 布尔值 (`bool`) - * 默认值: `false` (禁用) - * 说明: 启用后,需要提供正确的认证信息才能访问 `ghproxy` 服务。 - * `method`: 认证方法。 - * 类型: 字符串 (`string`) - * 默认值: `"parameters"` (URL 参数) - * 可选值: `"header"` 或 `"parameters"` - * `"header"`: 通过请求头 `GH-Auth` 或自定义请求头 (通过 `key` 配置) 传递认证 Token。 - * `"parameters"`: 通过 URL 参数 `auth_token` 或自定义 URL 参数名 (通过 `Key` 配置) 传递认证 Token。 - * 说明: 选择认证信息传递的方式。 - * `key`: 自定义认证 Key。 - * 类型: 字符串 (`string`) - * 默认值: `""` (空字符串,使用默认的 `GH-Auth` 请求头或 `auth_token` URL 参数名) - * 说明: 可以自定义认证时使用的请求头名称或 URL 参数名。如果为空,则使用默认的 `GH-Auth` 请求头或 `auth_token` URL 参数名。 - * `token`: 认证 Token。 - * 类型: 字符串 (`string`) - * 默认值: `"token"` - * 说明: 设置认证时需要提供的 Token 值。 - * `passThrough`: 是否认证参数透穿到Github。 - * 类型: 布尔值 (`bool`) - * 默认值: `false` (不允许) - * 说明: 如果设置为 `true`,相关参数会被透穿到Github。 - * `ForceAllowApi`: 是否强制允许 API 访问。 - * 类型: 布尔值 (`bool`) - * 默认值: `false` (不强制允许) - * 说明: 如果设置为 `true`,则强制允许对 GitHub API 的访问,即使未启用认证或认证失败。 - -* **`[blacklist]` - 黑名单配置** - - * `enabled`: 是否启用黑名单。 - * 类型: 布尔值 (`bool`) - * 默认值: `false` (禁用) - * 说明: 启用后,`ghproxy` 将根据 `blacklist.json` 文件中的规则阻止对特定用户或仓库的访问。 - * `blacklistFile`: 黑名单文件路径。 - * 类型: 字符串 (`string`) - * 默认值: `"/data/ghproxy/config/blacklist.json"` - * 说明: 指定黑名单配置文件的路径。 - -* **`[whitelist]` - 白名单配置** - - * `enabled`: 是否启用白名单。 - * 类型: 布尔值 (`bool`) - * 默认值: `false` (禁用) - * 说明: 启用后,`ghproxy` 将只允许访问 `whitelist.json` 文件中规则指定的用户或仓库。白名单的优先级高于黑名单。 - * `whitelistFile`: 白名单文件路径。 - * 类型: 字符串 (`string`) - * 默认值: `"/data/ghproxy/config/whitelist.json"` - * 说明: 指定白名单配置文件的路径。 - -* **`[rateLimit]` - 限速配置** - - * `enabled`: 是否启用限速。 - * 类型: 布尔值 (`bool`) - * 默认值: `false` (禁用) - * 说明: 启用后,`ghproxy` 将根据配置的策略限制请求速率,防止服务被滥用。 - * `rateMethod`: 限速方法。 - * 类型: 字符串 (`string`) - * 默认值: `"total"` (全局限速) - * 可选值: `"ip"` 或 `"total"` - * `"ip"`: 基于客户端 IP 地址进行限速,每个 IP 地址都有独立的速率限制。 - * `"total"`: 全局限速,所有客户端共享同一个速率限制。 - * 说明: 选择限速的策略。 - * `ratePerMinute`: 每分钟允许的请求数。 - * 类型: 整数 (`int`) - * 默认值: `180` - * 说明: 设置每分钟允许通过的最大请求数。 - * `burst`: 突发请求数。 - * 类型: 整数 (`int`) - * 默认值: `5` - * 说明: 允许在短时间内超过 `ratePerMinute` 的突发请求数。 - * **`[rateLimit.bandwidthLimit]` 带宽速率限制** - * `enabled`: 是否启用带宽速率限制。 - * 类型: 布尔值 (`bool`) - * 默认值: `false` (禁用) - * 说明: 启用后,`ghproxy` 将根据配置的策略限制带宽使用,防止服务被滥用。 - * `totalLimit`: 全局带宽限制。 - * 类型: 字符串 (`string`) - * 默认值: `"100mbps"` - * 说明: 设置全局最大带宽使用量。支持的单位有 "kbps", "mbps", "gbps"。 - * `totalBurst`: 全局突发带宽。 - * 类型: 字符串 (`string`) - * 默认值: `"100mbps"` - * 说明: 设置全局突发带宽使用量。支持的单位有 "kbps", "mbps", "gbps"。 - * `singleLimit`: 单个连接带宽限制。 - * 类型: 字符串 (`string`) - * 默认值: `"10mbps"` - * 说明: 设置单个连接的最大带宽使用量。支持的单位有 "kbps", "mbps", "gbps"。 - * `singleBurst`: 单个连接突发带宽。 - * 类型: 字符串 (`string`) - * 默认值: `"10mbps"` - * 说明: 设置单个连接的突发带宽使用量。支持的单位有 "kbps", "mbps", "gbps"。 - -* **`[outbound]` - 出站代理配置** - - * `enabled`: 是否启用出站代理。 - * 类型: 布尔值 (`bool`) - * 默认值: `false` (禁用) - * 说明: 启用后,`ghproxy` 将通过配置的代理服务器转发所有出站请求。 - * `url`: 出站代理 URL。 - * 类型: 字符串 (`string`) - * 默认值: `"socks5://127.0.0.1:1080"` - * 支持协议: `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)。 - * 自定义, 支持传入自定义target, 例如`"docker.example.com"` - -## `blacklist.json` - 黑名单配置 - -`blacklist.json` 文件用于配置黑名单规则,阻止对特定用户或仓库的访问。 - -```json name=config/blacklist.json -{ - "blacklist": [ - "eviluser", - "spamuser/bad-repo", - "malwareuser/*" - ] -} -``` - -### 黑名单规则说明 - -* `blacklist`: 一个 JSON 数组,包含黑名单规则,每条规则为一个字符串。 - * **用户名**: 例如 `"eviluser"`,阻止所有名为 `eviluser` 的用户的访问。 - * **仓库名**: 例如 `"spamuser/bad-repo"`,阻止访问 `spamuser` 用户下的 `bad-repo` 仓库。 - * **通配符**: 例如 `"malwareuser/*"`,使用 `*` 通配符,阻止访问 `malwareuser` 用户下的所有仓库。 - * **缩略写法**: 例如 `"example"`, 等同于 `"example/*"`, 允许访问 `example` 用户下的所有仓库。 - -## `whitelist.json` - 白名单配置 - -`whitelist.json` 文件用于配置白名单规则,只允许访问白名单中指定的用户或仓库。白名单的优先级高于黑名单,如果一个请求同时匹配黑名单和白名单,则白名单生效,请求将被允许。 - -```json name=config/whitelist.json -{ - "whitelist": [ - "white/list", - "white/test1", - "example/*", - "example" - ] -} -``` - -### 白名单规则说明 - -* `whitelist`: 一个 JSON 数组,包含白名单规则,每条规则为一个字符串。 - * **仓库名**: 例如 `"white/list"`,允许访问 `white` 用户下的 `list` 仓库。 - * **仓库名**: 例如 `"white/test1"`,允许访问 `white` 用户下的 `test1` 仓库。 - * **通配符**: 例如 `"example/*"`,使用 `*` 通配符,允许访问 `example` 用户下的所有仓库。 - * **缩略写法**: 例如 `"example"`, 等同于 `"example/*"`, 允许访问 `example` 用户下的所有仓库。 - ---- \ No newline at end of file diff --git a/docs/flag.md b/docs/flag.md deleted file mode 100644 index c3aa0b7..0000000 --- a/docs/flag.md +++ /dev/null @@ -1,26 +0,0 @@ -# Flag - -> 弃用, 请转到 [GHProxy项目文档](https://wjqserver-docs.pages.dev/docs/ghproxy/) - -GHProxy接受以下flag传入 - -```bash -root@root:/data/ghproxy$ ghproxy -h - -c string - config file path (default "/data/ghproxy/config/config.toml") - -cfg value - exit - -h show help message and exit - -v show version and exit -``` - -- `-c` - 类型: `string` - 默认值: `/data/ghproxy/config/config.toml` - 示例: `ghproxy -c /data/ghproxy/demo.toml` -- `-cfg` - 已弃用, 被`-c`替代 -- `-h` - 显示帮助信息 -- `-v` - 显示版本号 diff --git a/docs/menu.md b/docs/menu.md deleted file mode 100644 index 7e2c0ed..0000000 --- a/docs/menu.md +++ /dev/null @@ -1,19 +0,0 @@ -## GHProxy 文档 - -> 弃用, 请转到 [GHProxy项目文档](https://wjqserver-docs.pages.dev/docs/ghproxy/) - -### 配置文件 - -https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/docs/config.md - -### Flag - -https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/docs/flag.md - -### 部署 - -参看 https://blog.wjqserver.com/post/ghproxy-deploy-with-smart-git/ - -### 前端 - -https://github.com/WJQSERVER-STUDIO/GHProxy-Frontend \ No newline at end of file diff --git a/go.mod b/go.mod index b606255..b073121 100644 --- a/go.mod +++ b/go.mod @@ -1,46 +1,26 @@ module ghproxy -go 1.24.3 +go 1.24.4 require ( github.com/BurntSushi/toml v1.5.0 github.com/WJQSERVER-STUDIO/httpc v0.7.0 - github.com/WJQSERVER-STUDIO/logger v1.8.0 - github.com/cloudwego/hertz v0.10.1-0.20250611091639-3dde619f5598 - github.com/hertz-contrib/http2 v0.1.8 golang.org/x/net v0.41.0 golang.org/x/time v0.12.0 ) require ( github.com/WJQSERVER-STUDIO/go-utils/limitreader v0.0.2 - github.com/bytedance/sonic v1.13.3 + github.com/fenthope/ikumi v0.0.2 + 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.2.4 github.com/wjqserver/modembed v0.0.1 ) require ( github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4 // indirect - github.com/WJQSERVER-STUDIO/go-utils/log v0.0.3 // indirect - github.com/bytedance/gopkg v0.1.2 // indirect - github.com/bytedance/sonic/loader v0.2.4 // indirect - github.com/cloudwego/base64x v0.1.5 // indirect - github.com/cloudwego/gopkg v0.1.4 // indirect - github.com/cloudwego/netpoll v0.7.0 // indirect - github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/google/go-cmp v0.7.0 // indirect - github.com/klauspost/cpuid/v2 v2.2.10 // indirect - github.com/nyaruka/phonenumbers v1.6.3 // indirect - github.com/tidwall/gjson v1.18.0 // indirect - github.com/tidwall/match v1.1.1 // indirect - github.com/tidwall/pretty v1.2.1 // indirect - github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - golang.org/x/arch v0.18.0 // indirect - golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/text v0.26.0 // indirect - google.golang.org/protobuf v1.36.6 // indirect ) - -replace github.com/nyaruka/phonenumbers => github.com/nyaruka/phonenumbers v1.6.1 // 1.6.3 has reflect leaking diff --git a/go.sum b/go.sum index 730d2c6..9f0ed93 100644 --- a/go.sum +++ b/go.sum @@ -4,149 +4,25 @@ github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4 h1:JLtFd00AdFg/TP+dtvIzLkdHwKU github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4/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/go-utils/log v0.0.3 h1:t6nyLhmo9pSfVHm1Wu1WyLsTpXFSjSpQtVKqEDpiZ5Q= -github.com/WJQSERVER-STUDIO/go-utils/log v0.0.3/go.mod h1:j9Q+xnwpOfve7/uJnZ2izRQw6NNoXjvJHz7vUQAaLZE= github.com/WJQSERVER-STUDIO/httpc v0.7.0 h1:iHhqlxppJBjlmvsIjvLZKRbWXqSdbeSGGofjHGmqGJc= github.com/WJQSERVER-STUDIO/httpc v0.7.0/go.mod h1:M7KNUZjjhCkzzcg9lBPs9YfkImI+7vqjAyjdA19+joE= -github.com/WJQSERVER-STUDIO/logger v1.8.0 h1:AQ3Qe2kxiqpuOoDlRzseGP6u4LAaJc+ng4l8P+CK7Co= -github.com/WJQSERVER-STUDIO/logger v1.8.0/go.mod h1:yzXPtot0OvR1gzx4+rlFrv/sccUpz0gIXVBwUx3H7fM= -github.com/bytedance/gopkg v0.1.1/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= -github.com/bytedance/gopkg v0.1.2 h1:8o2feYuxknDpN+O7kPwvSXfMEKfYvJYiA2K7aonoMEQ= -github.com/bytedance/gopkg v0.1.2/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= -github.com/bytedance/mockey v1.2.12 h1:aeszOmGw8CPX8CRx1DZ/Glzb1yXvhjDh6jdFBNZjsU4= -github.com/bytedance/mockey v1.2.12/go.mod h1:3ZA4MQasmqC87Tw0w7Ygdy7eHIc2xgpZ8Pona5rsYIk= -github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0= -github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= -github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= -github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= -github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= -github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= -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.10.1-0.20250611091639-3dde619f5598 h1:8tVol3hNJS7+7f7yQIkP57tZMzUV3fOhn6pQ7t4R06k= -github.com/cloudwego/hertz v0.10.1-0.20250611091639-3dde619f5598/go.mod h1:lRBohmcDkGx5TLK6QKFGdzJ6n3IXqGueHsOiXcYgXA4= -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= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/fenthope/ikumi v0.0.2 h1:5oaSTf/Msp7M2O3o/X20omKWEQbFhX4KV0CVF21oCdk= +github.com/fenthope/ikumi v0.0.2/go.mod h1:IYbxzOGndZv/yRrbVMyV6dxh06X2wXCbfxrTRM1IruU= +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-20250517221953-25912455fbc8 h1:o8UqXPI6SVwQt04RGsqKp3qqmbOfTNMqDrWsc4O47kk= +github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8/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/hertz-contrib/http2 v0.1.8 h1:kjfCGkUxJZHgfPsnRjx1FLJBG55KvtvSQD214guBQLw= -github.com/hertz-contrib/http2 v0.1.8/go.mod h1:m42hrl8fiTwE4p8c7JdRUZpkePEthvV89q3elL2GeD0= -github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= -github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= -github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/nyaruka/phonenumbers v1.6.1 h1:XAJcTdYow16VrVKfglznMpJZz8KMJoMjx/91sX+K940= -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/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= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= -github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= -github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= -github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= -github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= -github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/infinite-iroha/touka v0.2.4 h1:P1nmQYv4VEiTIahCw356VcFvpTFB9i11c31LeLh6WbM= +github.com/infinite-iroha/touka v0.2.4/go.mod h1:2MBPtsM+5ClIZ/E1mPEKx1Rb+KIndTwZlIa2CwRPV7A= 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= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc= -golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -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-20250606033433-dcc06ee1d476 h1:bsqhLWFR6G6xiQcb+JoGqdKdRU6WzPWmK8E0jxTjzo4= -golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= -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= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -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.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= -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= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= -golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= diff --git a/main.go b/main.go index a31f075..a3174a7 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,6 @@ package main import ( - "context" "embed" "flag" "fmt" @@ -14,35 +13,28 @@ import ( "ghproxy/api" "ghproxy/auth" "ghproxy/config" - "ghproxy/middleware/loggin" "ghproxy/proxy" - "ghproxy/rate" + "ghproxy/weakcache" - "github.com/WJQSERVER-STUDIO/logger" - "github.com/hertz-contrib/http2/factory" + "github.com/fenthope/ikumi" + "github.com/fenthope/reco" + "github.com/fenthope/record" + "github.com/infinite-iroha/touka" "github.com/wjqserver/modembed" - - "github.com/cloudwego/hertz/pkg/app" - "github.com/cloudwego/hertz/pkg/app/middlewares/server/recovery" - "github.com/cloudwego/hertz/pkg/app/server" - "github.com/cloudwego/hertz/pkg/common/adaptor" - "github.com/cloudwego/hertz/pkg/common/hlog" - "github.com/cloudwego/hertz/pkg/network/standard" + "golang.org/x/time/rate" _ "net/http/pprof" ) var ( cfg *config.Config - r *server.Hertz + r *touka.Engine configfile = "/data/ghproxy/config/config.toml" hertZfile *os.File cfgfile string version string runMode string - limiter *rate.RateLimiter - iplimiter *rate.IPRateLimiter showVersion bool showHelp bool ) @@ -57,12 +49,12 @@ var ( ) var ( - logw = logger.Logw - logDump = logger.LogDump - logDebug = logger.LogDebug - logInfo = logger.LogInfo - logWarning = logger.LogWarning - logError = logger.LogError + logger *reco.Logger + logDump = logger.Debugf + logDebug = logger.Debugf + logInfo = logger.Infof + logWarning = logger.Warnf + logError = logger.Errorf ) func readFlag() { @@ -127,39 +119,28 @@ func loadConfig() { func setupLogger(cfg *config.Config) { var err error - - err = logger.Init(cfg.Log.LogFilePath, cfg.Log.MaxLogSize) + if cfg.Log.Level == "" { + cfg.Log.Level = "info" + } + recoLevel := reco.ParseLevel(cfg.Log.Level) + logger, err = reco.New(reco.Config{ + Level: recoLevel, + Mode: reco.ModeText, + FilePath: cfg.Log.LogFilePath, + MaxFileSizeMB: cfg.Log.MaxLogSize, + EnableRotation: true, + Async: true, + }) if err != nil { fmt.Printf("Failed to initialize logger: %v\n", err) os.Exit(1) } - err = logger.SetLogLevel(cfg.Log.Level) - if err != nil { - fmt.Printf("Logger Level Error: %v\n", err) - os.Exit(1) - } - logger.SetAsync(cfg.Log.Async) + logger.SetLevel(recoLevel) fmt.Printf("Log Level: %s\n", cfg.Log.Level) - logDebug("Config File Path: ", cfgfile) - logDebug("Loaded config: %v\n", cfg) - logInfo("Logger Initialized Successfully") -} - -func setupHertZLogger(cfg *config.Config) { - var err error - - if cfg.Log.HertZLogPath != "" { - hertZfile, err = os.OpenFile(cfg.Log.HertZLogPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) - if err != nil { - hlog.SetOutput(os.Stdout) - logWarning("Failed to open hertz log file: %v", err) - } else { - hlog.SetOutput(hertZfile) - } - hlog.SetLevel(hlog.LevelInfo) - } - + logger.Debugf("Config File Path: %s", cfgfile) + logger.Debugf("Loaded config: %v", cfg) + logger.Infof("Logger Initialized Successfully") } func setMemLimit(cfg *config.Config) { @@ -173,22 +154,10 @@ func loadlist(cfg *config.Config) { auth.Init(cfg) } -func setupApi(cfg *config.Config, r *server.Hertz, version string) { +func setupApi(cfg *config.Config, r *touka.Engine, version string) { api.InitHandleRouter(cfg, r, version) } -func setupRateLimit(cfg *config.Config) { - if cfg.RateLimit.Enabled { - if cfg.RateLimit.RateMethod == "ip" { - iplimiter = rate.NewIPRateLimiter(cfg.RateLimit.RatePerMinute, cfg.RateLimit.Burst, 1*time.Minute) - } else if cfg.RateLimit.RateMethod == "total" { - limiter = rate.New(cfg.RateLimit.RatePerMinute, cfg.RateLimit.Burst, 1*time.Minute) - } else { - logError("Invalid RateLimit Method: %s", cfg.RateLimit.RateMethod) - } - } -} - func InitReq(cfg *config.Config) { err := proxy.InitReq(cfg) if err != nil { @@ -241,7 +210,7 @@ func loadEmbeddedPages(cfg *config.Config) (fs.FS, fs.FS, error) { } // setupPages 设置页面路由 -func setupPages(cfg *config.Config, r *server.Hertz) { +func setupPages(cfg *config.Config, r *touka.Engine) { switch cfg.Pages.Mode { case "internal": err := setInternalRoute(cfg, r) @@ -252,21 +221,7 @@ func setupPages(cfg *config.Config, r *server.Hertz) { } case "external": - // 设置外部资源路径 - indexPagePath := fmt.Sprintf("%s/index.html", cfg.Pages.StaticDir) - faviconPath := fmt.Sprintf("%s/favicon.ico", cfg.Pages.StaticDir) - javascriptsPath := fmt.Sprintf("%s/script.js", cfg.Pages.StaticDir) - stylesheetsPath := fmt.Sprintf("%s/style.css", cfg.Pages.StaticDir) - bootstrapPath := fmt.Sprintf("%s/bootstrap.min.css", cfg.Pages.StaticDir) - bootstrapBundlePath := fmt.Sprintf("%s/bootstrap.bundle.min.js", cfg.Pages.StaticDir) - - // 设置外部资源路由 - r.StaticFile("/", indexPagePath) - r.StaticFile("/favicon.ico", faviconPath) - r.StaticFile("/script.js", javascriptsPath) - r.StaticFile("/style.css", stylesheetsPath) - r.StaticFile("/bootstrap.min.css", bootstrapPath) - r.StaticFile("/bootstrap.bundle.min.js", bootstrapBundlePath) + r.SetUnMatchFS(http.Dir(cfg.Pages.StaticDir)) default: // 处理无效的Pages Mode @@ -282,13 +237,24 @@ func setupPages(cfg *config.Config, r *server.Hertz) { } } -func pageCacheHeader() func(ctx context.Context, c *app.RequestContext) { - return func(ctx context.Context, c *app.RequestContext) { - c.Header("Cache-Control", "public, max-age=3600, must-revalidate") +var viaString string = "WJQSERVER-STUDIO/GHProxy" + +func pageCacheHeader() func(c *touka.Context) { + return func(c *touka.Context) { + c.AddHeader("Cache-Control", "public, max-age=3600, must-revalidate") + c.Next() } } -func setInternalRoute(cfg *config.Config, r *server.Hertz) error { +func viaHeader() func(c *touka.Context) { + return func(c *touka.Context) { + protoVersion := fmt.Sprintf("%d.%d", c.Request.ProtoMajor, c.Request.ProtoMinor) + c.AddHeader("Via", protoVersion+" "+viaString) + c.Next() + } +} + +func setInternalRoute(cfg *config.Config, r *touka.Engine) error { // 加载嵌入式资源 pages, assets, err := loadEmbeddedPages(cfg) @@ -296,69 +262,14 @@ func setInternalRoute(cfg *config.Config, r *server.Hertz) error { logError("Failed when processing pages: %s", err) return err } - /* - // 设置嵌入式资源路由 - r.GET("/", func(ctx context.Context, c *app.RequestContext) { - staticServer := http.FileServer(http.FS(pages)) - req, err := adaptor.GetCompatRequest(&c.Request) - if err != nil { - logError("%s", err) - return - } - staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req) - }) - r.GET("/favicon.ico", func(ctx context.Context, c *app.RequestContext) { - staticServer := http.FileServer(http.FS(assets)) - req, err := adaptor.GetCompatRequest(&c.Request) - if err != nil { - logError("%s", err) - return - } - staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req) - }) - r.GET("/script.js", func(ctx context.Context, c *app.RequestContext) { - staticServer := http.FileServer(http.FS(pages)) - req, err := adaptor.GetCompatRequest(&c.Request) - if err != nil { - logError("%s", err) - return - } - staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req) - }) - r.GET("/style.css", func(ctx context.Context, c *app.RequestContext) { - staticServer := http.FileServer(http.FS(pages)) - req, err := adaptor.GetCompatRequest(&c.Request) - if err != nil { - logError("%s", err) - return - } - staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req) - }) - r.GET("/bootstrap.min.css", func(ctx context.Context, c *app.RequestContext) { - staticServer := http.FileServer(http.FS(assets)) - req, err := adaptor.GetCompatRequest(&c.Request) - if err != nil { - logError("%s", err) - return - } - staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req) - }) - r.GET("/bootstrap.bundle.min.js", func(ctx context.Context, c *app.RequestContext) { - staticServer := http.FileServer(http.FS(assets)) - req, err := adaptor.GetCompatRequest(&c.Request) - if err != nil { - logError("%s", err) - return - } - staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req) - }) - */ - r.GET("/", pageCacheHeader(), adaptor.HertzHandler(http.FileServer(http.FS(pages)))) - r.GET("/favicon.ico", pageCacheHeader(), adaptor.HertzHandler(http.FileServer(http.FS(assets)))) - r.GET("/script.js", pageCacheHeader(), adaptor.HertzHandler(http.FileServer(http.FS(pages)))) - r.GET("/style.css", pageCacheHeader(), adaptor.HertzHandler(http.FileServer(http.FS(pages)))) - r.GET("/bootstrap.min.css", pageCacheHeader(), adaptor.HertzHandler(http.FileServer(http.FS(assets)))) - r.GET("/bootstrap.bundle.min.js", pageCacheHeader(), adaptor.HertzHandler(http.FileServer(http.FS(assets)))) + + r.HandleFunc([]string{"GET"}, "/favicon.ico", pageCacheHeader(), touka.FileServer(http.FS(assets))) + r.HandleFunc([]string{"GET"}, "/", pageCacheHeader(), touka.FileServer(http.FS(pages))) + r.HandleFunc([]string{"GET"}, "/script.js", pageCacheHeader(), touka.FileServer(http.FS(pages))) + r.HandleFunc([]string{"GET"}, "/style.css", pageCacheHeader(), touka.FileServer(http.FS(pages))) + r.HandleFunc([]string{"GET"}, "/bootstrap.min.css", pageCacheHeader(), touka.FileServer(http.FS(assets))) + r.HandleFunc([]string{"GET"}, "/bootstrap.bundle.min.js", pageCacheHeader(), touka.FileServer(http.FS(assets))) + return nil } @@ -381,11 +292,9 @@ func init() { loadConfig() if cfg != nil { // 在setupLogger前添加空值检查 setupLogger(cfg) - setupHertZLogger(cfg) InitReq(cfg) setMemLimit(cfg) loadlist(cfg) - setupRateLimit(cfg) if cfg.Docker.Enabled { wcache = proxy.InitWeakCache() } @@ -402,129 +311,103 @@ func init() { } } -var viaString string = "WJQSERVER-STUDIO/GHProxy" - -func viaHeader() app.HandlerFunc { - return func(ctx context.Context, c *app.RequestContext) { - protoVersion := "1.1" - c.Header("Via", protoVersion+" "+viaString) - c.Next(ctx) - } -} - func main() { if showVersion || showHelp { return } - logDebug("Run Mode: %s Netlib: %s", runMode, cfg.Server.NetLib) if cfg == nil { fmt.Println("Config not loaded, exiting.") return } - addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port) - if cfg.Server.NetLib == "std" || cfg.Server.NetLib == "standard" || cfg.Server.NetLib == "net" || cfg.Server.NetLib == "net/http" { - if cfg.Server.H2C { - r = server.New( - server.WithH2C(true), - server.WithHostPorts(addr), - server.WithTransport(standard.NewTransporter), - server.WithStreamBody(true), - server.WithIdleTimeout(30*time.Second), - ) - r.AddProtocol("h2", factory.NewServerFactory()) - } else { - r = server.New( - server.WithHostPorts(addr), - server.WithTransport(standard.NewTransporter), - server.WithStreamBody(true), - server.WithIdleTimeout(30*time.Second), - ) - } - } else if cfg.Server.NetLib == "netpoll" || cfg.Server.NetLib == "" { - if cfg.Server.H2C { - r = server.New( - server.WithH2C(true), - server.WithHostPorts(addr), - server.WithSenseClientDisconnection(cfg.Server.SenseClientDisconnection), - server.WithStreamBody(true), - server.WithIdleTimeout(30*time.Second), - ) - r.AddProtocol("h2", factory.NewServerFactory()) - } else { - r = server.New( - server.WithHostPorts(addr), - server.WithSenseClientDisconnection(cfg.Server.SenseClientDisconnection), - server.WithStreamBody(true), - server.WithIdleTimeout(30*time.Second), - ) - } - } else { - logError("Invalid NetLib: %s", cfg.Server.NetLib) - fmt.Printf("Invalid NetLib: %s\n", cfg.Server.NetLib) - os.Exit(1) - } + r := touka.Default() + r.SetProtocols(&touka.ProtocolsConfig{ + Http1: true, + Http2_Cleartext: true, + }) + r.Use(touka.Recovery()) // Recovery中间件 + r.SetLogger(logger) + r.Use(record.Middleware()) // log中间件 + r.Use(viaHeader()) /* - if cfg.Server.GoPoolSize > 0 { - gopool.SetCap(int32(cfg.Server.GoPoolSize)) - } else { - gopool.SetCap(1024) - } + r.Use(compress.Compression(compress.CompressOptions{ + Algorithms: map[string]compress.AlgorithmConfig{ + compress.EncodingGzip: { + Level: gzip.BestCompression, // Gzip最高压缩比 + PoolEnabled: true, // 启用Gzip压缩器的对象池 + }, + compress.EncodingDeflate: { + Level: flate.DefaultCompression, // Deflate默认压缩比 + PoolEnabled: false, // Deflate不启用对象池 + }, + compress.EncodingZstd: { + Level: int(zstd.SpeedBestCompression), // Zstandard最佳压缩比 + PoolEnabled: true, // 启用Zstandard压缩器的对象池 + }, + }, + })) */ - r.Use(recovery.Recovery()) // Recovery中间件 - r.Use(loggin.Middleware()) // log中间件 - r.Use(viaHeader()) + if cfg.RateLimit.Enabled { + r.Use(ikumi.TokenRateLimit(ikumi.TokenRateLimiterOptions{ + Limit: rate.Limit(cfg.RateLimit.RatePerMinute), + Burst: cfg.RateLimit.Burst, + })) + } setupApi(cfg, r, version) setupPages(cfg, r) - r.GET("/github.com/:user/:repo/releases/*filepath", func(ctx context.Context, c *app.RequestContext) { + r.GET("/github.com/:user/:repo/releases/*filepath", func(c *touka.Context) { c.Set("matcher", "releases") - proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) + proxy.RoutingHandler(cfg)(c) }) - r.GET("/github.com/:user/:repo/archive/*filepath", func(ctx context.Context, c *app.RequestContext) { + r.GET("/github.com/:user/:repo/archive/*filepath", func(c *touka.Context) { c.Set("matcher", "releases") - proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) + proxy.RoutingHandler(cfg)(c) }) - r.GET("/github.com/:user/:repo/blob/*filepath", func(ctx context.Context, c *app.RequestContext) { + r.GET("/github.com/:user/:repo/blob/*filepath", func(c *touka.Context) { c.Set("matcher", "blob") - proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) + proxy.RoutingHandler(cfg)(c) }) - r.GET("/github.com/:user/:repo/raw/*filepath", func(ctx context.Context, c *app.RequestContext) { + r.GET("/github.com/:user/:repo/raw/*filepath", func(c *touka.Context) { c.Set("matcher", "raw") - proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) + proxy.RoutingHandler(cfg)(c) }) - r.GET("/github.com/:user/:repo/info/*filepath", func(ctx context.Context, c *app.RequestContext) { + r.GET("/github.com/:user/:repo/info/*filepath", func(c *touka.Context) { c.Set("matcher", "clone") - proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) + proxy.RoutingHandler(cfg)(c) }) - r.GET("/github.com/:user/:repo/git-upload-pack", func(ctx context.Context, c *app.RequestContext) { + r.GET("/github.com/:user/:repo/git-upload-pack", func(c *touka.Context) { c.Set("matcher", "clone") - proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) + proxy.RoutingHandler(cfg)(c) + }) + r.POST("/github.com/:user/:repo/git-upload-pack", func(c *touka.Context) { + c.Set("matcher", "clone") + proxy.RoutingHandler(cfg)(c) }) - r.GET("/raw.githubusercontent.com/:user/:repo/*filepath", func(ctx context.Context, c *app.RequestContext) { + r.GET("/raw.githubusercontent.com/:user/:repo/*filepath", func(c *touka.Context) { c.Set("matcher", "raw") - proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) + proxy.RoutingHandler(cfg)(c) }) - r.GET("/gist.githubusercontent.com/:user/*filepath", func(ctx context.Context, c *app.RequestContext) { + r.GET("/gist.githubusercontent.com/:user/*filepath", func(c *touka.Context) { c.Set("matcher", "gist") - proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c) + proxy.NoRouteHandler(cfg)(c) }) - r.Any("/api.github.com/repos/:user/:repo/*filepath", func(ctx context.Context, c *app.RequestContext) { + r.ANY("/api.github.com/repos/:user/:repo/*filepath", func(c *touka.Context) { c.Set("matcher", "api") - proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) + proxy.RoutingHandler(cfg)(c) }) - r.GET("/v2/", func(ctx context.Context, c *app.RequestContext) { + r.GET("/v2/", func(c *touka.Context) { emptyJSON := "{}" c.Header("Content-Type", "application/json") c.Header("Content-Length", fmt.Sprint(len(emptyJSON))) @@ -532,26 +415,27 @@ func main() { c.Header("Docker-Distribution-API-Version", "registry/2.0") c.Status(200) - c.Write([]byte(emptyJSON)) + c.Writer.Write([]byte(emptyJSON)) }) - r.Any("/v2/:target/:user/:repo/*filepath", func(ctx context.Context, c *app.RequestContext) { - proxy.GhcrWithImageRouting(cfg)(ctx, c) + r.ANY("/v2/:target/:user/:repo/*filepath", func(c *touka.Context) { + proxy.GhcrWithImageRouting(cfg)(c) }) /* - r.Any("/v2/:target/*filepath", func(ctx context.Context, c *app.RequestContext) { - proxy.GhcrRouting(cfg)(ctx, c) + r.Any("/v2/:target/*filepath", func( c *touka.Context) { + proxy.GhcrRouting(cfg)(c) }) */ - r.NoRoute(func(ctx context.Context, c *app.RequestContext) { - proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c) + r.NoRoute(func(c *touka.Context) { + proxy.NoRouteHandler(cfg)(c) }) fmt.Printf("GHProxy Version: %s\n", version) fmt.Printf("A Go Based High-Performance Github Proxy \n") fmt.Printf("Made by WJQSERVER-STUDIO\n") + fmt.Printf("Power by Touka\n") if cfg.Server.Debug { go func() { @@ -563,16 +447,13 @@ func main() { } defer logger.Close() - defer func() { - if hertZfile != nil { - err := hertZfile.Close() - if err != nil { - logError("Failed to close hertz log file: %v", err) - } - } - }() - r.Spin() + addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port) + err := r.RunShutdown(addr) + if err != nil { + logError("Server Run Error: %v", err) + fmt.Printf("Server Run Error: %v\n", err) + } fmt.Println("Program Exit") } diff --git a/middleware/loggin/loggin.go b/middleware/loggin/loggin.go deleted file mode 100644 index 62dccc0..0000000 --- a/middleware/loggin/loggin.go +++ /dev/null @@ -1,32 +0,0 @@ -package loggin - -import ( - "context" - "time" - - "github.com/WJQSERVER-STUDIO/logger" - "github.com/cloudwego/hertz/pkg/app" -) - -var ( - logw = logger.Logw - logDump = logger.LogDump - logDebug = logger.LogDebug - logInfo = logger.LogInfo - logWarning = logger.LogWarning - logError = logger.LogError -) - -// 日志中间件 -func Middleware() app.HandlerFunc { - return func(ctx context.Context, c *app.RequestContext) { - startTime := time.Now() - - c.Next(ctx) - - endTime := time.Now() - timingResults := endTime.Sub(startTime) - - logInfo("%s %s %s %s %s %d %v ", c.ClientIP(), c.Method(), c.Request.Header.GetProtocol(), string(c.Path()), c.Request.Header.UserAgent(), c.Response.StatusCode(), timingResults) - } -} diff --git a/middleware/nocache/nocache.go b/middleware/nocache/nocache.go index 4e8f0d3..ba3b900 100644 --- a/middleware/nocache/nocache.go +++ b/middleware/nocache/nocache.go @@ -1,17 +1,15 @@ package nocache import ( - "context" - - "github.com/cloudwego/hertz/pkg/app" + "github.com/infinite-iroha/touka" ) -func NoCacheMiddleware() app.HandlerFunc { - return func(ctx context.Context, c *app.RequestContext) { +func NoCacheMiddleware() touka.HandlerFunc { + return func(c *touka.Context) { // 设置禁止缓存的响应头 - c.Response.Header.Set("Cache-Control", "no-store, no-cache, must-revalidate") - c.Response.Header.Set("Pragma", "no-cache") - c.Response.Header.Set("Expires", "0") - c.Next(ctx) // 继续处理请求 + c.SetHeader("Cache-Control", "no-store, no-cache, must-revalidate") + c.SetHeader("Pragma", "no-cache") + c.SetHeader("Expires", "0") + c.Next() // 继续处理请求 } } diff --git a/proxy/authparse.go b/proxy/authparse.go index f353db9..5116365 100644 --- a/proxy/authparse.go +++ b/proxy/authparse.go @@ -34,7 +34,7 @@ func parseBearerWWWAuthenticateHeader(headerValue string) (*BearerAuthParams, er trimmedPair := strings.TrimSpace(pair) keyValue := strings.SplitN(trimmedPair, "=", 2) if len(keyValue) != 2 { - logWarning("Skipping malformed parameter '%s' in Www-Authenticate header: %s", pair, headerValue) + //logWarning("Skipping malformed parameter '%s' in Www-Authenticate header: %s", pair, headerValue) continue } key := strings.TrimSpace(keyValue[0]) diff --git a/proxy/authpass.go b/proxy/authpass.go index 16887c7..f46b8ff 100644 --- a/proxy/authpass.go +++ b/proxy/authpass.go @@ -4,20 +4,19 @@ import ( "ghproxy/config" "net/http" - "github.com/cloudwego/hertz/pkg/app" + "github.com/infinite-iroha/touka" ) -func AuthPassThrough(c *app.RequestContext, cfg *config.Config, req *http.Request) { +func AuthPassThrough(c *touka.Context, cfg *config.Config, req *http.Request) { if cfg.Auth.PassThrough { token := c.Query("token") if token != "" { - logDebug("%s %s %s %s %s Auth-PassThrough: token %s", c.ClientIP(), c.Method(), string(c.Path()), c.UserAgent(), c.Request.Header.GetProtocol(), token) switch cfg.Auth.Method { case "parameters": if !cfg.Auth.Enabled { req.Header.Set("Authorization", "token "+token) } else { - logWarning("%s %s %s %s %s Auth-Error: Conflict Auth Method", c.ClientIP(), c.Method(), string(c.Path()), c.UserAgent(), c.Request.Header.GetProtocol()) + c.Warnf("%s %s %s %s %s Auth-Error: Conflict Auth Method", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.UserAgent(), c.Request.Proto) ErrorPage(c, NewErrorWithStatusLookup(500, "Conflict Auth Method")) return } @@ -26,7 +25,7 @@ func AuthPassThrough(c *app.RequestContext, cfg *config.Config, req *http.Reques req.Header.Set("Authorization", "token "+token) } default: - logWarning("%s %s %s %s %s Invalid Auth Method / Auth Method is not be set", c.ClientIP(), c.Method(), string(c.Path()), c.UserAgent(), c.Request.Header.GetProtocol()) + c.Warnf("%s %s %s %s %s Invalid Auth Method / Auth Method is not be set", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.UserAgent(), c.Request.Proto) ErrorPage(c, NewErrorWithStatusLookup(500, "Invalid Auth Method / Auth Method is not be set")) return } diff --git a/proxy/bandwidth.go b/proxy/bandwidth.go index a7591c2..3733e15 100644 --- a/proxy/bandwidth.go +++ b/proxy/bandwidth.go @@ -15,7 +15,6 @@ var ( func UnDefiendRateStringErrHandle(err error) error { if errors.Is(err, &limitreader.UnDefiendRateStringErr{}) { - logWarning("UnDefiendRateStringErr: %s", err) return nil } return err @@ -28,18 +27,15 @@ func SetGlobalRateLimit(cfg *config.Config) error { var totalBurst rate.Limit totalLimit, err = limitreader.ParseRate(cfg.RateLimit.BandwidthLimit.TotalLimit) if UnDefiendRateStringErrHandle(err) != nil { - logError("Failed to parse total bandwidth limit: %v", err) return err } totalBurst, err = limitreader.ParseRate(cfg.RateLimit.BandwidthLimit.TotalBurst) if UnDefiendRateStringErrHandle(err) != nil { - logError("Failed to parse total bandwidth burst: %v", err) return err } limitreader.SetGlobalRateLimit(totalLimit, int(totalBurst)) err = SetBandwidthLimit(cfg) if UnDefiendRateStringErrHandle(err) != nil { - logError("Failed to set bandwidth limit: %v", err) return err } } else { @@ -52,12 +48,10 @@ func SetBandwidthLimit(cfg *config.Config) error { var err error bandwidthLimit, err = limitreader.ParseRate(cfg.RateLimit.BandwidthLimit.SingleLimit) if UnDefiendRateStringErrHandle(err) != nil { - logError("Failed to parse bandwidth limit: %v", err) return err } bandwidthBurst, err = limitreader.ParseRate(cfg.RateLimit.BandwidthLimit.SingleBurst) if UnDefiendRateStringErrHandle(err) != nil { - logError("Failed to parse bandwidth burst: %v", err) return err } return nil diff --git a/proxy/chunkreq.go b/proxy/chunkreq.go index 124cfb7..1fca9d9 100644 --- a/proxy/chunkreq.go +++ b/proxy/chunkreq.go @@ -9,10 +9,10 @@ import ( "strconv" "github.com/WJQSERVER-STUDIO/go-utils/limitreader" - "github.com/cloudwego/hertz/pkg/app" + "github.com/infinite-iroha/touka" ) -func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, cfg *config.Config, matcher string) { +func ChunkedProxyRequest(ctx context.Context, c *touka.Context, u string, cfg *config.Config, matcher string) { var ( req *http.Request @@ -23,18 +23,16 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c go func() { <-ctx.Done() if resp != nil && resp.Body != nil { - err := resp.Body.Close() - if err != nil { - logError("Failed to close response body: %v", err) - } + resp.Body.Close() + } + if req != nil && req.Body != nil { + req.Body.Close() } - c.Abort() }() - rb := client.NewRequestBuilder(string(c.Request.Method()), u) + rb := client.NewRequestBuilder(c.Request.Method, u) rb.NoDefaultHeaders() - //rb.SetBody(bytes.NewBuffer(c.Request.Body())) - rb.SetBody(c.RequestBodyStream()) + rb.SetBody(c.Request.Body) rb.WithContext(ctx) req, err = rb.Build() @@ -60,19 +58,21 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c // 处理302情况 if resp.StatusCode == 302 || resp.StatusCode == 301 { + //c.Debugf("resp header %s", resp.Header) finalURL := resp.Header.Get("Location") if finalURL != "" { err = resp.Body.Close() if err != nil { - logError("Failed to close response body: %v", err) + c.Errorf("Failed to close response body: %v", err) } - c.Request.Header.Del("Referer") - logInfo("Internal Redirecting to %s", finalURL) + c.Infof("Internal Redirecting to %s", finalURL) ChunkedProxyRequest(ctx, c, finalURL, cfg, matcher) return } } + // 处理响应体大小限制 + var ( bodySize int contentLength string @@ -84,17 +84,17 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c 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) + 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 } 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.Errorf("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) + 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 } } @@ -127,6 +127,8 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c bodyReader = limitreader.NewRateLimitedReader(bodyReader, bandwidthLimit, int(bandwidthBurst), ctx) } + defer bodyReader.Close() + if MatcherShell(u) && matchString(matcher) && cfg.Shell.Editor { // 判断body是不是gzip var compress string @@ -134,26 +136,26 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c compress = "gzip" } - logDebug("Use Shell Editor: %s %s %s %s %s", c.ClientIP(), c.Request.Method(), u, c.Request.Header.Get("User-Agent"), c.Request.Header.GetProtocol()) + c.Debugf("Use Shell Editor: %s %s %s %s %s", c.ClientIP(), c.Request.Method, u, c.UserAgent(), c.Request.Proto) c.Header("Content-Length", "") var reader io.Reader - reader, _, err = processLinks(bodyReader, compress, string(c.Request.Host()), cfg) - c.SetBodyStream(reader, -1) + reader, _, err = processLinks(bodyReader, compress, c.Request.Host, cfg, c) + c.WriteStream(reader) if err != nil { - logError("%s %s %s %s %s Failed to copy response body: %v", c.ClientIP(), c.Request.Method(), u, c.Request.Header.Get("User-Agent"), c.Request.Header.GetProtocol(), err) + c.Errorf("%s %s %s %s %s Failed to copy response body: %v", c.ClientIP(), c.Request.Method, u, c.UserAgent(), c.Request.Proto, err) ErrorPage(c, NewErrorWithStatusLookup(500, fmt.Sprintf("Failed to copy response body: %v", err))) return } } else { if contentLength != "" { - c.SetBodyStream(bodyReader, bodySize) + c.SetHeader("Content-Length", contentLength) + c.WriteStream(bodyReader) return } - c.SetBodyStream(bodyReader, -1) - bodyReader.Close() + c.WriteStream(bodyReader) } } diff --git a/proxy/dial.go b/proxy/dial.go index 075603e..ddf5028 100644 --- a/proxy/dial.go +++ b/proxy/dial.go @@ -7,6 +7,7 @@ package proxy import ( "ghproxy/config" + "log" "net/http" "net/url" "strings" @@ -24,7 +25,8 @@ func initTransport(cfg *config.Config, transport *http.Transport) { // 如果代理 URL 未设置,使用环境变量中的代理配置 if cfg.Outbound.Url == "" { transport.Proxy = http.ProxyFromEnvironment - logWarning("Outbound proxy is not set, using environment variables") + //logWarning("Outbound proxy is not set, using environment variables") + log.Printf("Outbound proxy is not set, using environment variables") return } @@ -32,7 +34,7 @@ func initTransport(cfg *config.Config, transport *http.Transport) { proxyInfo, err := url.Parse(cfg.Outbound.Url) if err != nil { // 如果解析失败,记录错误日志并使用环境变量中的代理配置 - logError("Failed to parse outbound proxy URL %v", err) + log.Printf("Failed to parse outbound proxy URL %v", err) transport.Proxy = http.ProxyFromEnvironment return } @@ -41,7 +43,7 @@ func initTransport(cfg *config.Config, transport *http.Transport) { switch strings.ToLower(proxyInfo.Scheme) { case "http", "https": // 如果是 HTTP/HTTPS 代理 transport.Proxy = http.ProxyURL(proxyInfo) // 设置 HTTP(S) 代理 - logInfo("Using HTTP(S) proxy: %s", proxyInfo.Redacted()) + log.Printf("Using HTTP(S) proxy: %s", cfg.Outbound.Url) case "socks5": // 如果是 SOCKS5 代理 // 调用 newProxyDial 创建 SOCKS5 代理拨号器 proxyDialer := newProxyDial(cfg.Outbound.Url) @@ -53,11 +55,14 @@ func initTransport(cfg *config.Config, transport *http.Transport) { } else { // 如果不支持 ContextDialer,则回退到传统的 Dial 方法 transport.Dial = proxyDialer.Dial - logWarning("SOCKS5 dialer does not support ContextDialer, using legacy Dial") + //logWarning("SOCKS5 dialer does not support ContextDialer, using legacy Dial") + log.Printf("SOCKS5 dialer does not support ContextDialer, using legacy Dial") } - logInfo("Using SOCKS5 proxy chain: %s", cfg.Outbound.Url) + //logInfo("Using SOCKS5 proxy chain: %s", cfg.Outbound.Url) + log.Printf("Using SOCKS5 proxy chain: %s", cfg.Outbound.Url) default: // 如果代理协议不支持 - logError("Unsupported proxy scheme: %s", proxyInfo.Scheme) + //logError("Unsupported proxy scheme: %s", proxyInfo.Scheme) + log.Printf("Unsupported proxy scheme: %s", proxyInfo.Scheme) transport.Proxy = http.ProxyFromEnvironment // 回退到环境变量代理 } } @@ -77,13 +82,15 @@ func newProxyDial(proxyUrls string) proxy.Dialer { urlInfo, err := url.Parse(proxyUrl) if err != nil { // 如果 URL 解析失败,记录错误日志并跳过 - logError("Failed to parse proxy URL %q: %v", proxyUrl, err) + //logError("Failed to parse proxy URL %q: %v", proxyUrl, err) + log.Printf("Failed to parse proxy URL %q: %v", proxyUrl, err) continue } // 检查代理协议是否为 SOCKS5 if urlInfo.Scheme != "socks5" { - logWarning("Skipping non-SOCKS5 proxy: %s", urlInfo.Scheme) + // logWarning("Skipping non-SOCKS5 proxy: %s", urlInfo.Scheme) + log.Printf("Skipping non-SOCKS5 proxy: %s", urlInfo.Scheme) continue } @@ -94,7 +101,8 @@ func newProxyDial(proxyUrls string) proxy.Dialer { dialer, err := createSocksDialer(urlInfo.Host, auth, proxyDialer) if err != nil { // 如果创建失败,记录错误日志并跳过 - logError("Failed to create SOCKS5 dialer for %q: %v", proxyUrl, err) + //logError("Failed to create SOCKS5 dialer for %q: %v", proxyUrl, err) + log.Printf("Failed to create SOCKS5 dialer for %q: %v", proxyUrl, err) continue } diff --git a/proxy/docker.go b/proxy/docker.go index 615041e..23cdd51 100644 --- a/proxy/docker.go +++ b/proxy/docker.go @@ -2,9 +2,10 @@ package proxy import ( "context" + "encoding/json" "fmt" - json "github.com/bytedance/sonic" + "github.com/infinite-iroha/touka" "ghproxy/config" "ghproxy/weakcache" @@ -14,7 +15,6 @@ import ( "strings" "github.com/WJQSERVER-STUDIO/go-utils/limitreader" - "github.com/cloudwego/hertz/pkg/app" ) var ( @@ -35,8 +35,8 @@ func InitWeakCache() *weakcache.Cache[string] { return cache } -func GhcrWithImageRouting(cfg *config.Config) app.HandlerFunc { - return func(ctx context.Context, c *app.RequestContext) { +func GhcrWithImageRouting(cfg *config.Config) touka.HandlerFunc { + return func(c *touka.Context) { charToFind := '.' reqTarget := c.Param("target") @@ -57,7 +57,7 @@ func GhcrWithImageRouting(cfg *config.Config) app.HandlerFunc { target = reqTarget } } else { - path = string(c.Request.RequestURI()) + path = c.GetRequestURI() reqImageUser = c.Param("target") reqImageName = c.Param("user") } @@ -67,24 +67,25 @@ func GhcrWithImageRouting(cfg *config.Config) app.HandlerFunc { Image: fmt.Sprintf("%s/%s", reqImageUser, reqImageName), } - GhcrToTarget(ctx, c, cfg, target, path, image) + GhcrToTarget(c, cfg, target, path, image) } } -func GhcrToTarget(ctx context.Context, c *app.RequestContext, cfg *config.Config, target string, path string, image *imageInfo) { +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+"?"+string(c.Request.QueryString()), image, cfg, target) + GhcrRequest(ctx, c, "https://"+target+"/v2/"+path+"?"+c.GetReqQueryString(), image, cfg, target) } else { if cfg.Docker.Target == "ghcr" { - GhcrRequest(ctx, c, "https://"+ghcrTarget+string(c.Request.RequestURI()), image, cfg, ghcrTarget) + GhcrRequest(ctx, c, "https://"+ghcrTarget+c.GetRequestURI(), image, cfg, ghcrTarget) } else if cfg.Docker.Target == "dockerhub" { - GhcrRequest(ctx, c, "https://"+dockerhubTarget+string(c.Request.RequestURI()), image, cfg, dockerhubTarget) + GhcrRequest(ctx, c, "https://"+dockerhubTarget+c.GetRequestURI(), image, cfg, dockerhubTarget) } else if cfg.Docker.Target != "" { // 自定义taget - GhcrRequest(ctx, c, "https://"+cfg.Docker.Target+string(c.Request.RequestURI()), image, cfg, cfg.Docker.Target) + GhcrRequest(ctx, c, "https://"+cfg.Docker.Target+c.GetRequestURI(), image, cfg, cfg.Docker.Target) } else { // 配置为空 ErrorPage(c, NewErrorWithStatusLookup(403, "Docker Target is not set")) @@ -98,10 +99,10 @@ func GhcrToTarget(ctx context.Context, c *app.RequestContext, cfg *config.Config } } -func GhcrRequest(ctx context.Context, c *app.RequestContext, 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 ( - method []byte + method string req *http.Request resp *http.Response err error @@ -117,11 +118,11 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, image *im } }() - method = c.Request.Method() + method = c.Request.Method - rb := ghcrclient.NewRequestBuilder(string(method), u) + rb := ghcrclient.NewRequestBuilder(method, u) rb.NoDefaultHeaders() - rb.SetBody(c.Request.BodyStream()) + rb.SetBody(c.Request.Body) rb.WithContext(ctx) req, err = rb.Build() @@ -130,17 +131,18 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, image *im return } - c.Request.Header.VisitAll(func(key, value []byte) { - headerKey := string(key) - headerValue := string(value) - req.Header.Add(headerKey, headerValue) - }) + //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) if image != nil { token, exist := cache.Get(image.Image) if exist { - logDebug("Use Cache Token: %s", token) + c.Debugf("Use Cache Token: %s", token) req.Header.Set("Authorization", "Bearer "+token) } } @@ -154,7 +156,7 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, image *im // 处理状态码 if resp.StatusCode == 401 { // 请求target /v2/路径 - if string(c.Request.URI().Path()) != "/v2/" { + if string(c.GetRequestURIPath()) != "/v2/" { resp.Body.Close() if image == nil { ErrorPage(c, NewErrorWithStatusLookup(401, "Unauthorized")) @@ -164,13 +166,13 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, image *im // 更新kv if token != "" { - logDump("Update Cache Token: %s", token) + c.Debugf("Update Cache Token: %s", token) cache.Put(image.Image, token) } rb := ghcrclient.NewRequestBuilder(string(method), u) rb.NoDefaultHeaders() - rb.SetBody(c.Request.BodyStream()) + rb.SetBody(c.Request.Body) rb.WithContext(ctx) req, err = rb.Build() @@ -178,12 +180,14 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, image *im 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) - }) + /* + 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) if token != "" { @@ -214,27 +218,30 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, image *im 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) + 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 } 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.Errorf("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) + 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 } } // 复制响应头,排除需要移除的 header - for key, values := range resp.Header { - for _, value := range values { - c.Response.Header.Add(key, value) + /* + for key, values := range resp.Header { + for _, value := range values { + c.Response.Header.Add(key, value) + } } - } + */ + copyHeader(resp.Header, c.GetAllReqHeader()) c.Status(resp.StatusCode) @@ -256,7 +263,7 @@ type AuthToken struct { Token string `json:"token"` } -func ChallengeReq(target string, image *imageInfo, ctx context.Context, c *app.RequestContext) (token string) { +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 @@ -280,7 +287,7 @@ func ChallengeReq(target string, image *imageInfo, ctx context.Context, c *app.R defer resp401.Body.Close() bearer, err := parseBearerWWWAuthenticateHeader(resp401.Header.Get("Www-Authenticate")) if err != nil { - logError("Failed to parse Www-Authenticate header: %v", err) + c.Errorf("Failed to parse Www-Authenticate header: %v", err) return } @@ -296,13 +303,13 @@ func ChallengeReq(target string, image *imageInfo, ctx context.Context, c *app.R getAuthReq, err := getAuthRB.Build() if err != nil { - logError("Failed to create request: %v", err) + c.Errorf("Failed to create request: %v", err) return } authResp, err := ghcrclient.Do(getAuthReq) if err != nil { - logError("Failed to send request: %v", err) + c.Errorf("Failed to send request: %v", err) return } @@ -310,7 +317,7 @@ func ChallengeReq(target string, image *imageInfo, ctx context.Context, c *app.R bodyBytes, err := io.ReadAll(authResp.Body) if err != nil { - logError("Failed to read auth response body: %v", err) + c.Errorf("Failed to read auth response body: %v", err) return } @@ -318,7 +325,7 @@ func ChallengeReq(target string, image *imageInfo, ctx context.Context, c *app.R var authToken AuthToken err = json.Unmarshal(bodyBytes, &authToken) if err != nil { - logError("Failed to decode auth response body: %v", err) + c.Errorf("Failed to decode auth response body: %v", err) return } token = authToken.Token diff --git a/proxy/error.go b/proxy/error.go index 1820259..be4c578 100644 --- a/proxy/error.go +++ b/proxy/error.go @@ -11,24 +11,13 @@ import ( "html/template" "io/fs" - "github.com/WJQSERVER-STUDIO/logger" - "github.com/cloudwego/hertz/pkg/app" lru "github.com/hashicorp/golang-lru/v2" + "github.com/infinite-iroha/touka" ) -// 日志模块 -var ( - logw = logger.Logw - logDump = logger.LogDump - logDebug = logger.LogDebug - logInfo = logger.LogInfo - logWarning = logger.LogWarning - logError = logger.LogError -) - -func HandleError(c *app.RequestContext, message string) { +func HandleError(c *touka.Context, message string) { ErrorPage(c, NewErrorWithStatusLookup(500, message)) - logError("Error handled: %s", message) + c.Errorf("%s %s %s %s %s Error: %v", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.UserAgent(), c.Request.Proto, message) } type GHProxyErrors struct { @@ -131,18 +120,18 @@ type ErrorPageData struct { // ToCacheKey 为 ErrorPageData 生成一个唯一的 SHA256 字符串键。 // 使用 gob 序列化来确保结构体内容到字节序列的顺序一致性,然后计算哈希。 -func (d ErrorPageData) ToCacheKey() string { +func (d ErrorPageData) ToCacheKey() (string, error) { var buf bytes.Buffer enc := gob.NewEncoder(&buf) err := enc.Encode(d) if err != nil { - logError("Failed to gob encode ErrorPageData for cache key: %v", err) - return "" + //logError("Failed to gob encode ErrorPageData for cache key: %v", err) + return "", fmt.Errorf("failed to gob encode ErrorPageData for cache key: %w", err) } hasher := sha256.New() hasher.Write(buf.Bytes()) - return hex.EncodeToString(hasher.Sum(nil)) + return hex.EncodeToString(hasher.Sum(nil)), nil } func ErrPageUnwarper(errInfo *GHProxyErrors) ErrorPageData { @@ -184,7 +173,7 @@ func NewSizedLRUCache(maxBytes int64) (*SizedLRUCache, error) { 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) + //logDebug("LRU evicted key: %s, size: %d, current total: %d", key, len(value), c.currentBytes) }) if err != nil { return nil, err @@ -206,7 +195,7 @@ func (c *SizedLRUCache) Add(key string, value []byte) { // 如果待添加的条目本身就大于缓存的最大容量,则不进行缓存。 if itemSize > c.maxBytes { - logWarning("Item key %s (size %d) larger than cache max capacity %d. Not caching.", key, itemSize, c.maxBytes) + //c.Warnf("Item key %s (size %d) larger than cache max capacity %d. Not caching.", key, itemSize, c.maxBytes) return } @@ -214,23 +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) + //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 { - _, oldVal, existed := c.cache.RemoveOldest() + _, _, existed := c.cache.RemoveOldest() if !existed { - logWarning("Attempted to remove oldest, but item not found.") + //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) + //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) + //logDebug("Item added: key %s, size: %d, current total: %d", key, itemSize, c.currentBytes) } const maxErrorPageCacheBytes = 512 * 1024 // 错误页面缓存的最大容量:512KB @@ -242,7 +231,7 @@ func init() { var err error errorPageCache, err = NewSizedLRUCache(maxErrorPageCacheBytes) if err != nil { - logError("Failed to initialize error page LRU cache: %v", err) + // logError("Failed to initialize error page LRU cache: %v", err) panic(err) } } @@ -293,37 +282,50 @@ func htmlTemplateRender(data interface{}) ([]byte, error) { return buf.Bytes(), nil } -func ErrorPage(c *app.RequestContext, errInfo *GHProxyErrors) { +func ErrorPage(c *touka.Context, errInfo *GHProxyErrors) { // 将 errInfo 转换为 ErrorPageData 结构体 + var err error + var cacheKey string pageDataStruct := ErrPageUnwarper(errInfo) // 使用 ErrorPageData 生成一个唯一的 SHA256 缓存键 - cacheKey := pageDataStruct.ToCacheKey() + cacheKey, err = pageDataStruct.ToCacheKey() + if err != nil { + c.Warnf("Failed to generate cache key for error page: %v", err) + fallbackErrorJson(c, errInfo) + return + } + + // 检查生成的缓存键是否为空,这可能表示序列化或哈希计算失败 + if cacheKey == "" { c.JSON(errInfo.StatusCode, map[string]string{"error": errInfo.ErrorMessage}) - logWarning("Failed to generate cache key for error page: %v", errInfo) + c.Warnf("Failed to generate cache key for error page: %v", errInfo) return } var pageData []byte - var err error // 尝试从缓存中获取页面数据 if cachedPage, found := errorPageCache.Get(cacheKey); found { pageData = cachedPage - logDebug("Serving error page from cache (Key: %s)", cacheKey) + c.Debugf("Serving error page from cache (Key: %s)", cacheKey) } else { // 如果不在缓存中,则渲染页面 pageData, err = htmlTemplateRender(pageDataStruct) if err != nil { c.JSON(errInfo.StatusCode, map[string]string{"error": errInfo.ErrorMessage}) - logWarning("Failed to render error page for status %d (Key: %s): %v", errInfo.StatusCode, cacheKey, err) + c.Warnf("Failed to render error page for status %d (Key: %s): %v", errInfo.StatusCode, cacheKey, err) return } // 将渲染结果存入缓存 errorPageCache.Add(cacheKey, pageData) - logDebug("Cached error page (Key: %s, Size: %d bytes)", cacheKey, len(pageData)) + c.Debugf("Cached error page (Key: %s, Size: %d bytes)", cacheKey, len(pageData)) } - c.Data(errInfo.StatusCode, "text/html; charset=utf-8", pageData) + c.Raw(errInfo.StatusCode, "text/html; charset=utf-8", pageData) +} + +func fallbackErrorJson(c *touka.Context, errInfo *GHProxyErrors) { + c.JSON(errInfo.StatusCode, map[string]string{"error": errInfo.ErrorMessage}) } diff --git a/proxy/gitreq.go b/proxy/gitreq.go index 6d00640..a8e2905 100644 --- a/proxy/gitreq.go +++ b/proxy/gitreq.go @@ -1,7 +1,6 @@ package proxy import ( - "bytes" "context" "fmt" "ghproxy/config" @@ -9,30 +8,36 @@ import ( "strconv" "github.com/WJQSERVER-STUDIO/go-utils/limitreader" - "github.com/cloudwego/hertz/pkg/app" + "github.com/infinite-iroha/touka" ) -func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Config, mode string) { +func GitReq(ctx context.Context, c *touka.Context, u string, cfg *config.Config, mode string) { var ( - req *http.Request resp *http.Response - err error ) go func() { <-ctx.Done() if resp != nil && resp.Body != nil { - err = resp.Body.Close() - if err != nil { - logError("Failed to close response body: %v", err) - } + resp.Body.Close() } }() - method := string(c.Request.Method()) + /* + fullBody, err := c.GetReqBodyFull() + if err != nil { + HandleError(c, fmt.Sprintf("Failed to read request body: %v", err)) + return + } + reqBodyReader := bytes.NewBuffer(fullBody) + */ - reqBodyReader := bytes.NewBuffer(c.Request.Body()) + reqBodyReader, err := c.GetReqBodyBuffer() + if err != nil { + HandleError(c, fmt.Sprintf("Failed to read request body: %v", err)) + return + } //bodyReader := c.Request.BodyStream() // 不可替换为此实现 @@ -47,12 +52,12 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co } if cfg.GitClone.Mode == "cache" { - rb := gitclient.NewRequestBuilder(method, u) + rb := gitclient.NewRequestBuilder(c.Request.Method, u) rb.NoDefaultHeaders() rb.SetBody(reqBodyReader) rb.WithContext(ctx) - req, err = rb.Build() + req, err := rb.Build() if err != nil { HandleError(c, fmt.Sprintf("Failed to create request: %v", err)) return @@ -66,8 +71,9 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co HandleError(c, fmt.Sprintf("Failed to send request: %v", err)) return } + defer resp.Body.Close() } else { - rb := client.NewRequestBuilder(string(c.Request.Method()), u) + rb := client.NewRequestBuilder(c.Request.Method, u) rb.NoDefaultHeaders() rb.SetBody(reqBodyReader) rb.WithContext(ctx) @@ -86,6 +92,7 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co HandleError(c, fmt.Sprintf("Failed to send request: %v", err)) return } + defer resp.Body.Close() } contentLength := resp.Header.Get("Content-Length") @@ -93,21 +100,25 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co 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) + 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) } if err == nil && size > sizelimit { - finalURL := []byte(resp.Request.URL.String()) + finalURL := resp.Request.URL.String() c.Redirect(http.StatusMovedPermanently, 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) + 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, size) return } } - for key, values := range resp.Header { - for _, value := range values { - c.Response.Header.Add(key, value) + /* + for key, values := range resp.Header { + for _, value := range values { + c.Response.Header.Add(key, value) + } } - } + */ + //copyHeader( resp.Header) + c.SetHeaders(resp.Header) headersToRemove := map[string]struct{}{ "Content-Security-Policy": {}, @@ -132,17 +143,20 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co c.Status(resp.StatusCode) if cfg.GitClone.Mode == "cache" { - c.Response.Header.Set("Cache-Control", "no-store, no-cache, must-revalidate") - c.Response.Header.Set("Pragma", "no-cache") - c.Response.Header.Set("Expires", "0") + c.SetHeader("Cache-Control", "no-store, no-cache, must-revalidate") + c.SetHeader("Pragma", "no-cache") + c.SetHeader("Expires", "0") } bodyReader := resp.Body + // 读取body内容 + //bodyContent, _ := io.ReadAll(bodyReader) + // c.Infof("%s", bodyContent) + if cfg.RateLimit.BandwidthLimit.Enabled { bodyReader = limitreader.NewRateLimitedReader(bodyReader, bandwidthLimit, int(bandwidthBurst), ctx) } c.SetBodyStream(bodyReader, -1) - bodyReader.Close() } diff --git a/proxy/handler.go b/proxy/handler.go index d5fe677..48d8c25 100644 --- a/proxy/handler.go +++ b/proxy/handler.go @@ -1,39 +1,37 @@ package proxy import ( - "context" "fmt" "ghproxy/config" - "ghproxy/rate" "regexp" "strings" - "github.com/cloudwego/hertz/pkg/app" + "github.com/infinite-iroha/touka" ) var re = regexp.MustCompile(`^(http:|https:)?/?/?(.*)`) // 匹配http://或https://开头的路径 -func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *rate.IPRateLimiter) app.HandlerFunc { - return func(ctx context.Context, c *app.RequestContext) { - +func NoRouteHandler(cfg *config.Config) touka.HandlerFunc { + return func(c *touka.Context) { + var ctx = c.Request.Context() var shoudBreak bool - shoudBreak = rateCheck(cfg, c, limiter, iplimiter) - if shoudBreak { - return - } + // shoudBreak = rateCheck(cfg, c, limiter, iplimiter) + // if shoudBreak { + // return + // } var ( rawPath string matches []string ) - rawPath = strings.TrimPrefix(string(c.Request.RequestURI()), "/") // 去掉前缀/ - matches = re.FindStringSubmatch(rawPath) // 匹配路径 + rawPath = strings.TrimPrefix(c.GetRequestURI(), "/") // 去掉前缀/ + matches = re.FindStringSubmatch(rawPath) // 匹配路径 // 匹配路径错误处理 if len(matches) < 3 { - logWarning("%s %s %s %s %s Invalid URL", c.ClientIP(), c.Method(), c.Path(), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol()) - ErrorPage(c, NewErrorWithStatusLookup(400, fmt.Sprintf("Invalid URL Format: %s", c.Path()))) + c.Warnf("%s %s %s %s %s Invalid URL", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.UserAgent(), c.Request.Proto) + ErrorPage(c, NewErrorWithStatusLookup(400, fmt.Sprintf("Invalid URL Format: %s", c.GetRequestURI()))) return } @@ -53,9 +51,6 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra return } - logDump("%s %s %s %s %s Matched-Username: %s, Matched-Repo: %s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo) - logDump("%s", c.Request.Header.Header()) - shoudBreak = listCheck(cfg, c, user, repo, rawPath) if shoudBreak { return @@ -74,8 +69,6 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra matcher = "raw" } - logDebug("Matched: %v", matcher) - switch matcher { case "releases", "blob", "raw", "gist", "api": ChunkedProxyRequest(ctx, c, rawPath, cfg, matcher) @@ -83,7 +76,7 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra GitReq(ctx, c, rawPath, cfg, "git") default: ErrorPage(c, NewErrorWithStatusLookup(500, "Matched But Not Matched")) - logError("Matched But Not Matched Path: %s rawPath: %s matcher: %s", c.Path(), rawPath, matcher) + c.Errorf("Matched But Not Matched Path: %s rawPath: %s matcher: %s", c.GetRequestURIPath(), rawPath, matcher) return } } diff --git a/proxy/httpc.go b/proxy/httpc.go index 4d8bc4c..1cd9100 100644 --- a/proxy/httpc.go +++ b/proxy/httpc.go @@ -39,7 +39,7 @@ func initHTTPClient(cfg *config.Config) { proTolcols.SetHTTP1(true) proTolcols.SetHTTP2(true) proTolcols.SetUnencryptedHTTP2(true) - if cfg.Httpc.Mode == "auto" { + if cfg.Httpc.Mode == "auto" || cfg.Httpc.Mode == "" { tr = &http.Transport{ IdleConnTimeout: 30 * time.Second, @@ -57,16 +57,7 @@ func initHTTPClient(cfg *config.Config) { Protocols: proTolcols, } } else { - // 错误的模式 - logError("unknown httpc mode: %s", cfg.Httpc.Mode) - fmt.Println("unknown httpc mode: ", cfg.Httpc.Mode) - logWarning("use Auto to Run HTTP Client") - fmt.Println("use Auto to Run HTTP Client") - tr = &http.Transport{ - IdleConnTimeout: 30 * time.Second, - WriteBufferSize: 32 * 1024, // 32KB - ReadBufferSize: 32 * 1024, // 32KB - } + panic("unknown httpc mode: " + cfg.Httpc.Mode) } if cfg.Outbound.Enabled { initTransport(cfg, tr) @@ -86,7 +77,7 @@ func initHTTPClient(cfg *config.Config) { func initGitHTTPClient(cfg *config.Config) { - if cfg.Httpc.Mode == "auto" { + if cfg.Httpc.Mode == "auto" || cfg.Httpc.Mode == "" { gittr = &http.Transport{ IdleConnTimeout: 30 * time.Second, WriteBufferSize: 32 * 1024, // 32KB @@ -101,17 +92,7 @@ func initGitHTTPClient(cfg *config.Config) { ReadBufferSize: 32 * 1024, // 32KB } } else { - // 错误的模式 - logError("unknown httpc mode: %s", cfg.Httpc.Mode) - fmt.Println("unknown httpc mode: ", cfg.Httpc.Mode) - logWarning("use Auto to Run HTTP Client") - fmt.Println("use Auto to Run HTTP Client") - gittr = &http.Transport{ - //MaxIdleConns: 160, - IdleConnTimeout: 30 * time.Second, - WriteBufferSize: 32 * 1024, // 32KB - ReadBufferSize: 32 * 1024, // 32KB - } + panic("unknown httpc mode: " + cfg.Httpc.Mode) } if cfg.Outbound.Enabled { initTransport(cfg, gittr) @@ -157,7 +138,7 @@ func initGhcrHTTPClient(cfg *config.Config) { var proTolcols = new(http.Protocols) proTolcols.SetHTTP1(true) proTolcols.SetHTTP2(true) - if cfg.Httpc.Mode == "auto" { + if cfg.Httpc.Mode == "auto" || cfg.Httpc.Mode == "" { ghcrtr = &http.Transport{ IdleConnTimeout: 30 * time.Second, @@ -175,16 +156,7 @@ func initGhcrHTTPClient(cfg *config.Config) { Protocols: proTolcols, } } else { - // 错误的模式 - logError("unknown httpc mode: %s", cfg.Httpc.Mode) - fmt.Println("unknown httpc mode: ", cfg.Httpc.Mode) - logWarning("use Auto to Run HTTP Client") - fmt.Println("use Auto to Run HTTP Client") - ghcrtr = &http.Transport{ - IdleConnTimeout: 30 * time.Second, - WriteBufferSize: 32 * 1024, // 32KB - ReadBufferSize: 32 * 1024, // 32KB - } + panic(fmt.Sprintf("unknown httpc mode: %s", cfg.Httpc.Mode)) } if cfg.Outbound.Enabled { initTransport(cfg, ghcrtr) diff --git a/proxy/match.go b/proxy/match.go index f526461..8050779 100644 --- a/proxy/match.go +++ b/proxy/match.go @@ -26,8 +26,8 @@ func init() { githubPrefixLen = len(githubPrefix) rawPrefixLen = len(rawPrefix) gistPrefixLen = len(gistPrefix) - apiPrefixLen = len(apiPrefix) gistContentPrefixLen = len(gistContentPrefix) + apiPrefixLen = len(apiPrefix) //log.Printf("githubPrefixLen: %d, rawPrefixLen: %d, gistPrefixLen: %d, apiPrefixLen: %d", githubPrefixLen, rawPrefixLen, gistPrefixLen, apiPrefixLen) } diff --git a/proxy/nest.go b/proxy/nest.go index 2748ca6..4f93f20 100644 --- a/proxy/nest.go +++ b/proxy/nest.go @@ -7,6 +7,8 @@ import ( "ghproxy/config" "io" "strings" + + "github.com/infinite-iroha/touka" ) func EditorMatcher(rawPath string, cfg *config.Config) (bool, error) { @@ -52,21 +54,19 @@ func modifyURL(url string, host string, cfg *config.Config) string { // 去除url内的https://或http:// matched, err := EditorMatcher(url, cfg) if err != nil { - logDump("Invalid URL: %s", url) return url } if matched { var u = url u = strings.TrimPrefix(u, "https://") u = strings.TrimPrefix(u, "http://") - logDump("Modified URL: %s", "https://"+host+"/"+u) return "https://" + host + "/" + u } return url } // processLinks 处理链接,返回包含处理后数据的 io.Reader -func processLinks(input io.ReadCloser, compress string, host string, cfg *config.Config) (readerOut io.Reader, written int64, err error) { +func processLinks(input io.ReadCloser, compress string, host string, cfg *config.Config, c *touka.Context) (readerOut io.Reader, written int64, err error) { pipeReader, pipeWriter := io.Pipe() // 创建 io.Pipe readerOut = pipeReader @@ -75,11 +75,11 @@ func processLinks(input io.ReadCloser, compress string, host string, cfg *config if pipeWriter != nil { // 确保 pipeWriter 关闭,即使发生错误 if err != nil { if closeErr := pipeWriter.CloseWithError(err); closeErr != nil { // 如果有错误,传递错误给 reader - logError("pipeWriter close with error failed: %v, original error: %v", closeErr, err) + c.Errorf("pipeWriter close with error failed: %v, original error: %v", closeErr, err) } } else { if closeErr := pipeWriter.Close(); closeErr != nil { // 没有错误,正常关闭 - logError("pipeWriter close failed: %v", closeErr) + c.Errorf("pipeWriter close failed: %v", closeErr) if err == nil { // 如果之前没有错误,记录关闭错误 err = closeErr } @@ -90,7 +90,7 @@ func processLinks(input io.ReadCloser, compress string, host string, cfg *config defer func() { if err := input.Close(); err != nil { - logError("input close failed: %v", err) + c.Errorf("input close failed: %v", err) } }() @@ -127,7 +127,7 @@ func processLinks(input io.ReadCloser, compress string, host string, cfg *config if gzipWriter != nil { if closeErr = gzipWriter.Close(); closeErr != nil { - logError("gzipWriter close failed %v", closeErr) + c.Errorf("gzipWriter close failed %v", closeErr) // 如果已经存在错误,则保留。否则,记录此错误。 if err == nil { err = closeErr @@ -135,7 +135,7 @@ func processLinks(input io.ReadCloser, compress string, host string, cfg *config } } if flushErr := bufWriter.Flush(); flushErr != nil { - logError("writer flush failed %v", flushErr) + c.Errorf("writer flush failed %v", flushErr) // 如果已经存在错误,则保留。否则,记录此错误。 if err == nil { err = flushErr @@ -156,7 +156,6 @@ func processLinks(input io.ReadCloser, compress string, host string, cfg *config // 替换所有匹配的 URL modifiedLine := urlPattern.ReplaceAllStringFunc(line, func(originalURL string) string { - logDump("originalURL: %s", originalURL) return modifyURL(originalURL, host, cfg) // 假设 modifyURL 函数已定义 }) diff --git a/proxy/reqheader.go b/proxy/reqheader.go index 8612a7e..c89dc76 100644 --- a/proxy/reqheader.go +++ b/proxy/reqheader.go @@ -4,7 +4,7 @@ import ( "ghproxy/config" "net/http" - "github.com/cloudwego/hertz/pkg/app" + "github.com/infinite-iroha/touka" ) var ( @@ -59,28 +59,19 @@ func copyHeader(dst, src http.Header) { } } -func setRequestHeaders(c *app.RequestContext, req *http.Request, cfg *config.Config, matcher string) { +func setRequestHeaders(c *touka.Context, req *http.Request, cfg *config.Config, matcher string) { if matcher == "raw" && cfg.Httpc.UseCustomRawHeaders { // 使用预定义Header for key, value := range defaultHeaders { req.Header.Set(key, value) } } else if matcher == "clone" { - - c.Request.Header.VisitAll(func(key, value []byte) { - headerKey := string(key) - headerValue := string(value) - req.Header.Set(headerKey, headerValue) - }) + copyHeader(req.Header, c.Request.Header) for key := range cloneHeadersToRemove { req.Header.Del(key) } } else { - c.Request.Header.VisitAll(func(key, value []byte) { - headerKey := string(key) - headerValue := string(value) - req.Header.Set(headerKey, headerValue) - }) + copyHeader(req.Header, c.Request.Header) for key := range reqHeadersToRemove { req.Header.Del(key) } diff --git a/proxy/routing.go b/proxy/routing.go index 9d68ca7..6c68b9c 100644 --- a/proxy/routing.go +++ b/proxy/routing.go @@ -1,42 +1,43 @@ package proxy import ( - "context" "ghproxy/config" - "ghproxy/rate" "strings" - "github.com/cloudwego/hertz/pkg/app" + "github.com/infinite-iroha/touka" ) -func RoutingHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *rate.IPRateLimiter) app.HandlerFunc { - return func(ctx context.Context, c *app.RequestContext) { +func RoutingHandler(cfg *config.Config) touka.HandlerFunc { + return func(c *touka.Context) { var shoudBreak bool - shoudBreak = rateCheck(cfg, c, limiter, iplimiter) - if shoudBreak { - return - } + // shoudBreak = rateCheck(cfg, c, limiter, iplimiter) + // if shoudBreak { + // return + //} var ( rawPath string ) - rawPath = strings.TrimPrefix(string(c.Request.RequestURI()), "/") // 去掉前缀/ + rawPath = strings.TrimPrefix(c.GetRequestURI(), "/") // 去掉前缀/ var ( - user string - repo string - matcher string + user string + repo string ) user = c.Param("user") repo = c.Param("repo") - matcher = c.GetString("matcher") + matcher, exists := c.GetString("matcher") + if !exists { + ErrorPage(c, NewErrorWithStatusLookup(500, "Matcher Not Found in Context")) + c.Errorf("Matcher Not Found in Context Path: %s", c.GetRequestURIPath()) + return + } - logDump("%s %s %s %s %s Matched-Username: %s, Matched-Repo: %s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo) - logDump("%s", c.Request.Header.Header()) + ctx := c.Request.Context() shoudBreak = listCheck(cfg, c, user, repo, rawPath) if shoudBreak { @@ -48,7 +49,6 @@ func RoutingHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra return } - // 处理blob/raw路径 // 处理blob/raw路径 if matcher == "blob" { rawPath = rawPath[10:] @@ -60,8 +60,6 @@ func RoutingHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra // 为rawpath加入https:// 头 rawPath = "https://" + rawPath - logDebug("Matched: %v", matcher) - switch matcher { case "releases", "blob", "raw", "gist", "api": ChunkedProxyRequest(ctx, c, rawPath, cfg, matcher) @@ -69,7 +67,7 @@ func RoutingHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra GitReq(ctx, c, rawPath, cfg, "git") default: ErrorPage(c, NewErrorWithStatusLookup(500, "Matched But Not Matched")) - logError("Matched But Not Matched Path: %s rawPath: %s matcher: %s", c.Path(), rawPath, matcher) + c.Errorf("Matched But Not Matched Path: %s rawPath: %s matcher: %s", c.GetRequestURIPath(), rawPath, matcher) return } } diff --git a/proxy/utils.go b/proxy/utils.go index e770c2b..e923742 100644 --- a/proxy/utils.go +++ b/proxy/utils.go @@ -4,12 +4,11 @@ import ( "fmt" "ghproxy/auth" "ghproxy/config" - "ghproxy/rate" - "github.com/cloudwego/hertz/pkg/app" + "github.com/infinite-iroha/touka" ) -func listCheck(cfg *config.Config, c *app.RequestContext, user string, repo string, rawPath string) bool { +func listCheck(cfg *config.Config, c *touka.Context, user string, repo string, rawPath string) bool { if cfg.Auth.ForceAllowApi && cfg.Auth.ForceAllowApiPassList { return false } @@ -18,7 +17,7 @@ func listCheck(cfg *config.Config, c *app.RequestContext, user string, repo stri whitelist := auth.CheckWhitelist(user, repo) if !whitelist { ErrorPage(c, NewErrorWithStatusLookup(403, fmt.Sprintf("Whitelist Blocked repo: %s/%s", user, repo))) - logInfo("%s %s %s %s %s Whitelist Blocked repo: %s/%s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo) + c.Infof("%s %s %s %s %s Whitelist Blocked repo: %s/%s", c.ClientIP(), c.Request.Method, rawPath, c.UserAgent(), c.Request.Proto, user, repo) return true } } @@ -28,7 +27,7 @@ func listCheck(cfg *config.Config, c *app.RequestContext, user string, repo stri blacklist := auth.CheckBlacklist(user, repo) if blacklist { ErrorPage(c, NewErrorWithStatusLookup(403, fmt.Sprintf("Blacklist Blocked repo: %s/%s", user, repo))) - logInfo("%s %s %s %s %s Blacklist Blocked repo: %s/%s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo) + c.Infof("%s %s %s %s %s Blacklist Blocked repo: %s/%s", c.ClientIP(), c.Request.Method, rawPath, c.UserAgent(), c.Request.Proto, user, repo) return true } } @@ -37,13 +36,13 @@ func listCheck(cfg *config.Config, c *app.RequestContext, user string, repo stri } // 鉴权 -func authCheck(c *app.RequestContext, cfg *config.Config, matcher string, rawPath string) bool { +func authCheck(c *touka.Context, cfg *config.Config, matcher string, rawPath string) bool { var err error if matcher == "api" && !cfg.Auth.ForceAllowApi { if cfg.Auth.Method != "header" || !cfg.Auth.Enabled { ErrorPage(c, NewErrorWithStatusLookup(403, "Github API Req without AuthHeader is Not Allowed")) - logInfo("%s %s %s AuthHeader Unavailable", c.ClientIP(), c.Method(), rawPath) + c.Infof("%s %s %s AuthHeader Unavailable", c.ClientIP(), c.Request.Method, rawPath) return true } } @@ -54,34 +53,7 @@ func authCheck(c *app.RequestContext, cfg *config.Config, matcher string, rawPat authcheck, err = auth.AuthHandler(c, cfg) if !authcheck { ErrorPage(c, NewErrorWithStatusLookup(401, fmt.Sprintf("Unauthorized: %v", err))) - logInfo("%s %s %s %s %s Auth-Error: %v", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), err) - return true - } - } - - return false -} - -func rateCheck(cfg *config.Config, c *app.RequestContext, limiter *rate.RateLimiter, iplimiter *rate.IPRateLimiter) bool { - // 限制访问频率 - if cfg.RateLimit.Enabled { - - var allowed bool - - switch cfg.RateLimit.RateMethod { - case "ip": - allowed = iplimiter.Allow(c.ClientIP()) - case "total": - allowed = limiter.Allow() - default: - logWarning("Invalid RateLimit Method") - ErrorPage(c, NewErrorWithStatusLookup(500, "Invalid RateLimit Method")) - return true - } - - if !allowed { - ErrorPage(c, NewErrorWithStatusLookup(429, fmt.Sprintf("Too Many Requests; Rate Limit is %d per minute", cfg.RateLimit.RatePerMinute))) - logInfo("%s %s %s %s %s 429-TooManyRequests", c.ClientIP(), c.Method(), c.Request.RequestURI(), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol()) + c.Infof("%s %s %s %s %s Auth-Error: %v", c.ClientIP(), c.Request.Method, rawPath, c.UserAgent(), c.Request.Proto, err) return true } } diff --git a/rate/rate.go b/rate/rate.go deleted file mode 100644 index 390b8da..0000000 --- a/rate/rate.go +++ /dev/null @@ -1,107 +0,0 @@ -package rate - -import ( - "sync" - "time" - - "github.com/WJQSERVER-STUDIO/logger" - "golang.org/x/time/rate" -) - -// 日志模块 -var ( - logw = logger.Logw - logDump = logger.LogDump - logDebug = logger.LogDebug - logInfo = logger.LogInfo - logWarning = logger.LogWarning - logError = logger.LogError -) - -// RateLimiter 总体限流器 -type RateLimiter struct { - limiter *rate.Limiter -} - -// New 创建一个总体限流器 -func New(limit int, burst int, duration time.Duration) *RateLimiter { - if limit <= 0 { - limit = 1 - logWarning("rate limit per minute must be positive, setting to 1") - } - if burst <= 0 { - burst = 1 - logWarning("rate limit burst must be positive, setting to 1") - } - - rateLimit := rate.Limit(float64(limit) / duration.Seconds()) - - return &RateLimiter{ - limiter: rate.NewLimiter(rateLimit, burst), - } -} - -// Allow 检查是否允许请求通过 -func (rl *RateLimiter) Allow() bool { - return rl.limiter.Allow() -} - -// IPRateLimiter 基于IP的限流器 -type IPRateLimiter struct { - limiters map[string]*RateLimiter // 用户级限流器 map - mu sync.RWMutex // 保护 limiters map - limit int // 每 duration 时间段内允许的请求数 - burst int // 突发请求数 - duration time.Duration // 限流周期 -} - -// NewIPRateLimiter 创建一个基于IP的限流器 -func NewIPRateLimiter(ipLimit int, ipBurst int, duration time.Duration) *IPRateLimiter { - if ipLimit <= 0 { - ipLimit = 1 - logWarning("IP rate limit per minute must be positive, setting to 1") - } - if ipBurst <= 0 { - ipBurst = 1 - logWarning("IP rate limit burst must be positive, setting to 1") - } - - logInfo("IP Rate Limiter initialized with limit: %d, burst: %d, duration: %v", ipLimit, ipBurst, duration) - - return &IPRateLimiter{ - limiters: make(map[string]*RateLimiter), - limit: ipLimit, - burst: ipBurst, - duration: duration, - } -} - -// Allow 检查给定IP的请求是否允许通过 -func (rl *IPRateLimiter) Allow(ip string) bool { - if ip == "" { - logWarning("empty ip for rate limiting") - return false - } - - // 使用读锁快速查找 - rl.mu.RLock() - limiter, found := rl.limiters[ip] - rl.mu.RUnlock() - - if found { - return limiter.Allow() - } - - // 未找到,获取写锁来创建和添加 - rl.mu.Lock() - // 双重检查 - limiter, found = rl.limiters[ip] - if !found { - newL := New(rl.limit, rl.burst, rl.duration) - rl.limiters[ip] = newL - limiter = newL - } - rl.mu.Unlock() - - return limiter.Allow() -} From 1636bf1548a7e20724ba09889a3ad25697cbb456 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Mon, 16 Jun 2025 08:45:47 +0800 Subject: [PATCH 40/99] update auth init --- auth/auth.go | 7 ++++--- main.go | 6 +++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/auth/auth.go b/auth/auth.go index dcc7b29..ad8efaf 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -7,19 +7,20 @@ import ( "github.com/infinite-iroha/touka" ) -func Init(cfg *config.Config) { +func ListInit(cfg *config.Config) error { if cfg.Blacklist.Enabled { err := InitBlacklist(cfg) if err != nil { - panic(err.Error()) + return err } } if cfg.Whitelist.Enabled { err := InitWhitelist(cfg) if err != nil { - panic(err.Error()) + return err } } + return nil } func AuthHandler(c *touka.Context, cfg *config.Config) (isValid bool, err error) { diff --git a/main.go b/main.go index a3174a7..f7cb6b1 100644 --- a/main.go +++ b/main.go @@ -151,7 +151,11 @@ func setMemLimit(cfg *config.Config) { } func loadlist(cfg *config.Config) { - auth.Init(cfg) + err := auth.ListInit(cfg) + if err != nil { + logger.Errorf("Failed to initialize list: %v", err) + } + } func setupApi(cfg *config.Config, r *touka.Engine, version string) { From ceda8220fd7868f7cf871c10ef7355c3b1f0d797 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Mon, 16 Jun 2025 08:50:05 +0800 Subject: [PATCH 41/99] fix resp header setting --- proxy/chunkreq.go | 9 +++------ proxy/docker.go | 2 +- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/proxy/chunkreq.go b/proxy/chunkreq.go index 1fca9d9..9227b78 100644 --- a/proxy/chunkreq.go +++ b/proxy/chunkreq.go @@ -100,12 +100,9 @@ func ChunkedProxyRequest(ctx context.Context, c *touka.Context, u string, cfg *c } // 复制响应头,排除需要移除的 header - for key, values := range resp.Header { - if _, shouldRemove := respHeadersToRemove[key]; !shouldRemove { - for _, value := range values { - c.Header(key, value) - } - } + c.SetHeaders(resp.Header) + for key := range respHeadersToRemove { + c.DelHeader(key) } switch cfg.Server.Cors { diff --git a/proxy/docker.go b/proxy/docker.go index 23cdd51..44e4a72 100644 --- a/proxy/docker.go +++ b/proxy/docker.go @@ -241,7 +241,7 @@ func GhcrRequest(ctx context.Context, c *touka.Context, u string, image *imageIn } } */ - copyHeader(resp.Header, c.GetAllReqHeader()) + c.SetHeaders(resp.Header) c.Status(resp.StatusCode) From 92432121e5c155b5ab5825faa7632331d1c3e110 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Mon, 16 Jun 2025 09:19:34 +0800 Subject: [PATCH 42/99] 4.0.0-rc.0 & 4.0.0 ready --- CHANGELOG.md | 13 +++++++++++++ DEV-VERSION | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c78d251..e840266 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # 更新日志 +4.0.0 - 2025-06-16 +--- +- CHANGE: 移交到Touka框架 +- REMOVE: 移除req rate limit的total方式 +- CHANGE: 使用[reco](https://github.com/fenthope/reco)日志库, 异步使能 + +4.0.0-rc.0 - 2025-06-16 +--- +- PRE-RELEASE: 此版本是v4.0.0预发布版本,请勿在生产环境中使用; +- CHANGE: 移交到Touka框架 +- REMOVE: 移除req rate limit的total方式 +- CHANGE: 使用[reco](https://github.com/fenthope/reco)日志库, 异步使能 + 4.0.0-beta.0 - 2025-06-15 --- - BETA-TEST: 此版本是v4.0.0的测试版本,请勿在生产环境中使用; diff --git a/DEV-VERSION b/DEV-VERSION index 456c4f8..efe3c7d 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -4.0.0-beta.0 \ No newline at end of file +4.0.0-rc.0 \ No newline at end of file From 5b253998ce671d0675458717f6185abba2ae0829 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Mon, 16 Jun 2025 09:26:05 +0800 Subject: [PATCH 43/99] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e840266..dfe5319 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - CHANGE: 移交到Touka框架 - REMOVE: 移除req rate limit的total方式 - CHANGE: 使用[reco](https://github.com/fenthope/reco)日志库, 异步使能 +- FIX: 更换HTTP框架以解决v3可能存在的内存分配与回收问题 4.0.0-rc.0 - 2025-06-16 --- From eb113b419134b63187322eefe7cdd79941d87084 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Tue, 17 Jun 2025 14:45:14 +0800 Subject: [PATCH 44/99] add docker login basic auth support --- CHANGELOG.md | 5 +++++ DEV-VERSION | 2 +- config/config.go | 10 ++++++++-- config/config.toml | 6 +++++- go.mod | 3 +++ go.sum | 4 ++-- main.go | 22 ++++++++++++++++++++-- 7 files changed, 44 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dfe5319..cede3b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 更新日志 +4.1.0-beta.0 - 2025-06-17 +--- +- BETA-TEST: 此版本是v4.1.0的测试版本,请勿在生产环境中使用; +- ADD: 加入基于`basic auth`的docker鉴权支持 + 4.0.0 - 2025-06-16 --- - CHANGE: 移交到Touka框架 diff --git a/DEV-VERSION b/DEV-VERSION index efe3c7d..3768596 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -4.0.0-rc.0 \ No newline at end of file +4.1.0-beta.0 \ No newline at end of file diff --git a/config/config.go b/config/config.go index 3775a12..ba2ef62 100644 --- a/config/config.go +++ b/config/config.go @@ -169,10 +169,16 @@ type OutboundConfig struct { [docker] enabled = false target = "ghcr" # ghcr/dockerhub +auth = false +[docker.credentials] +user1 = "testpass" +test = "test123" */ type DockerConfig struct { - Enabled bool `toml:"enabled"` - Target string `toml:"target"` + Enabled bool `toml:"enabled"` + Target string `toml:"target"` + Auth bool `toml:"auth"` + Credentials map[string]string `toml:"credentials"` } // LoadConfig 从 TOML 配置文件加载配置 diff --git a/config/config.toml b/config/config.toml index 27585b2..ecf4959 100644 --- a/config/config.toml +++ b/config/config.toml @@ -67,4 +67,8 @@ url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890" [docker] enabled = false -target = "dockerhub" # ghcr/dockerhub/ custom \ No newline at end of file +target = "dockerhub" # ghcr/dockerhub/ custom +auth = false +[docker.credentials] +user1 = "testpass" +test = "test123" \ No newline at end of file diff --git a/go.mod b/go.mod index b073121..3ea5a1e 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( require ( github.com/WJQSERVER-STUDIO/go-utils/limitreader v0.0.2 + github.com/fenthope/bauth v0.0.1 github.com/fenthope/ikumi v0.0.2 github.com/fenthope/reco v0.0.3 github.com/fenthope/record v0.0.3 @@ -24,3 +25,5 @@ require ( github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect ) + +replace github.com/infinite-iroha/touka => /data/github/WJQSERVER/touka diff --git a/go.sum b/go.sum index 9f0ed93..8621e1d 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/WJQSERVER-STUDIO/go-utils/limitreader v0.0.2 h1:8bBkKk6E2Zr+I5szL7gyc github.com/WJQSERVER-STUDIO/go-utils/limitreader v0.0.2/go.mod h1:yPX8xuZH+py7eLJwOYj3VVI/4/Yuy5+x8Mhq8qezcPg= github.com/WJQSERVER-STUDIO/httpc v0.7.0 h1:iHhqlxppJBjlmvsIjvLZKRbWXqSdbeSGGofjHGmqGJc= github.com/WJQSERVER-STUDIO/httpc v0.7.0/go.mod h1:M7KNUZjjhCkzzcg9lBPs9YfkImI+7vqjAyjdA19+joE= +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= github.com/fenthope/ikumi v0.0.2/go.mod h1:IYbxzOGndZv/yRrbVMyV6dxh06X2wXCbfxrTRM1IruU= github.com/fenthope/reco v0.0.3 h1:RmnQ0D9a8PWtwOODawitTe4BztTnS9wYwrDbipISNq4= @@ -16,8 +18,6 @@ github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8 h1:o8UqXPI github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8/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.2.4 h1:P1nmQYv4VEiTIahCw356VcFvpTFB9i11c31LeLh6WbM= -github.com/infinite-iroha/touka v0.2.4/go.mod h1:2MBPtsM+5ClIZ/E1mPEKx1Rb+KIndTwZlIa2CwRPV7A= 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= diff --git a/main.go b/main.go index f7cb6b1..16109cd 100644 --- a/main.go +++ b/main.go @@ -15,6 +15,8 @@ import ( "ghproxy/config" "ghproxy/proxy" + "github.com/fenthope/bauth" + "ghproxy/weakcache" "github.com/fenthope/ikumi" @@ -334,7 +336,7 @@ func main() { r.Use(touka.Recovery()) // Recovery中间件 r.SetLogger(logger) r.Use(record.Middleware()) // log中间件 - r.Use(viaHeader()) + //r.Use(viaHeader()) /* r.Use(compress.Compression(compress.CompressOptions{ Algorithms: map[string]compress.AlgorithmConfig{ @@ -362,6 +364,7 @@ func main() { } setupApi(cfg, r, version) setupPages(cfg, r) + r.RedirectTrailingSlash = false r.GET("/github.com/:user/:repo/releases/*filepath", func(c *touka.Context) { c.Set("matcher", "releases") @@ -411,7 +414,7 @@ func main() { proxy.RoutingHandler(cfg)(c) }) - r.GET("/v2/", func(c *touka.Context) { + r.GET("/v2/", r.UseIf(cfg.Docker.Auth, bauth.BasicAuthForStatic(cfg.Docker.Credentials, "GHProxy Docker Proxy")), func(c *touka.Context) { emptyJSON := "{}" c.Header("Content-Type", "application/json") c.Header("Content-Length", fmt.Sprint(len(emptyJSON))) @@ -422,6 +425,11 @@ func main() { c.Writer.Write([]byte(emptyJSON)) }) + r.GET("/v2", func(c *touka.Context) { + // 重定向到 /v2/ + c.Redirect(http.StatusMovedPermanently, "/v2/") + }) + r.ANY("/v2/:target/:user/:repo/*filepath", func(c *touka.Context) { proxy.GhcrWithImageRouting(cfg)(c) }) @@ -461,3 +469,13 @@ func main() { fmt.Println("Program Exit") } + +// copyHeader 将所有头部从 src 复制到 dst。 +// 对于多值头部,它会为每个值调用 Add,从而保留所有值。 +func copyHeader(dst, src http.Header) { + for k, vv := range src { + for _, v := range vv { + dst.Add(k, v) + } + } +} From 3e03f47ef7c647053b95238586243246acdacc28 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Tue, 17 Jun 2025 14:47:23 +0800 Subject: [PATCH 45/99] update deps rebuild 4.1.0-beta.0 --- DEV-VERSION | 2 +- go.mod | 4 +--- go.sum | 2 ++ 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/DEV-VERSION b/DEV-VERSION index 3768596..5144c45 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -4.1.0-beta.0 \ No newline at end of file +4.1.0-beta.0 diff --git a/go.mod b/go.mod index 3ea5a1e..db39a60 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ 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.2.4 + github.com/infinite-iroha/touka v0.2.5 github.com/wjqserver/modembed v0.0.1 ) @@ -25,5 +25,3 @@ require ( github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect ) - -replace github.com/infinite-iroha/touka => /data/github/WJQSERVER/touka diff --git a/go.sum b/go.sum index 8621e1d..1c3af22 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,8 @@ github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8 h1:o8UqXPI github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8/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.2.5 h1:x7lcKk0MIHGrPb9TMmgC+nG57G4SeFGflwrta2Lz3jo= +github.com/infinite-iroha/touka v0.2.5/go.mod h1:2MBPtsM+5ClIZ/E1mPEKx1Rb+KIndTwZlIa2CwRPV7A= 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= From 6eae6382568a7b21c28f13fec38ad627754caa1c Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Tue, 17 Jun 2025 15:07:31 +0800 Subject: [PATCH 46/99] remove dev codes --- main.go | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/main.go b/main.go index 16109cd..1dcdea9 100644 --- a/main.go +++ b/main.go @@ -336,7 +336,7 @@ func main() { r.Use(touka.Recovery()) // Recovery中间件 r.SetLogger(logger) r.Use(record.Middleware()) // log中间件 - //r.Use(viaHeader()) + r.Use(viaHeader()) /* r.Use(compress.Compression(compress.CompressOptions{ Algorithms: map[string]compress.AlgorithmConfig{ @@ -469,13 +469,3 @@ func main() { fmt.Println("Program Exit") } - -// copyHeader 将所有头部从 src 复制到 dst。 -// 对于多值头部,它会为每个值调用 Add,从而保留所有值。 -func copyHeader(dst, src http.Header) { - for k, vv := range src { - for _, v := range vv { - dst.Add(k, v) - } - } -} From e5bc171f253734c152bb248da598bc2ae6408350 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Tue, 17 Jun 2025 16:43:51 +0800 Subject: [PATCH 47/99] 4.1.0-rc.0 --- CHANGELOG.md | 5 +++++ DEV-VERSION | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cede3b9..bd58a24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 更新日志 +4.1.0-rc.0 - 2025-06-17 +--- +- PRE-RELEASE: 此版本是v4.1.0预发布版本,请勿在生产环境中使用; +- ADD: 加入基于`basic auth`的docker鉴权支持 + 4.1.0-beta.0 - 2025-06-17 --- - BETA-TEST: 此版本是v4.1.0的测试版本,请勿在生产环境中使用; diff --git a/DEV-VERSION b/DEV-VERSION index 5144c45..038c952 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -4.1.0-beta.0 +4.1.0-rc.0 From e629b5db47a3f484baa71fb12c9bddfe475d9702 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Tue, 17 Jun 2025 17:04:34 +0800 Subject: [PATCH 48/99] 4.1.0 --- CHANGELOG.md | 4 ++++ VERSION | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd58a24..e378591 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 更新日志 +4.1.0 - 2025-06-17 +--- +- ADD: 加入基于`basic auth`的docker鉴权支持 + 4.1.0-rc.0 - 2025-06-17 --- - PRE-RELEASE: 此版本是v4.1.0预发布版本,请勿在生产环境中使用; diff --git a/VERSION b/VERSION index 0c89fc9..99eba4d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.0.0 \ No newline at end of file +4.1.0 \ No newline at end of file From 933aeee518f3e29f7f32364e08536ea87770b6f2 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Wed, 18 Jun 2025 09:05:45 +0800 Subject: [PATCH 49/99] 4.1.1 --- CHANGELOG.md | 4 ++++ VERSION | 2 +- go.mod | 2 +- go.sum | 4 ++-- main.go | 21 +++++++++++++-------- proxy/gitreq.go | 7 ------- 6 files changed, 21 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e378591..28e8976 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 更新日志 +4.1.1 - 2025-06-17 +--- +- CHANGE: 更新touka框架到v0.2.6, 解决MidwareX的一些状态问题 + 4.1.0 - 2025-06-17 --- - ADD: 加入基于`basic auth`的docker鉴权支持 diff --git a/VERSION b/VERSION index 99eba4d..2582ddd 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.1.0 \ No newline at end of file +4.1.1 \ No newline at end of file diff --git a/go.mod b/go.mod index db39a60..6c23592 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ 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.2.5 + github.com/infinite-iroha/touka v0.2.6 github.com/wjqserver/modembed v0.0.1 ) diff --git a/go.sum b/go.sum index 1c3af22..07c6e9b 100644 --- a/go.sum +++ b/go.sum @@ -18,8 +18,8 @@ github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8 h1:o8UqXPI github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8/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.2.5 h1:x7lcKk0MIHGrPb9TMmgC+nG57G4SeFGflwrta2Lz3jo= -github.com/infinite-iroha/touka v0.2.5/go.mod h1:2MBPtsM+5ClIZ/E1mPEKx1Rb+KIndTwZlIa2CwRPV7A= +github.com/infinite-iroha/touka v0.2.6 h1:Y2zJTklfJZYO70jF9LPKq261IMt1vV8L1JBKUquQKIk= +github.com/infinite-iroha/touka v0.2.6/go.mod h1:2MBPtsM+5ClIZ/E1mPEKx1Rb+KIndTwZlIa2CwRPV7A= 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= diff --git a/main.go b/main.go index 1dcdea9..5a37296 100644 --- a/main.go +++ b/main.go @@ -414,16 +414,21 @@ func main() { proxy.RoutingHandler(cfg)(c) }) - r.GET("/v2/", r.UseIf(cfg.Docker.Auth, bauth.BasicAuthForStatic(cfg.Docker.Credentials, "GHProxy Docker Proxy")), func(c *touka.Context) { - emptyJSON := "{}" - c.Header("Content-Type", "application/json") - c.Header("Content-Length", fmt.Sprint(len(emptyJSON))) + r.GET("/v2/", + r.UseIf(cfg.Docker.Auth, func() touka.HandlerFunc { + return bauth.BasicAuthForStatic(cfg.Docker.Credentials, "GHProxy Docker Proxy") + }), + func(c *touka.Context) { + emptyJSON := "{}" + c.Header("Content-Type", "application/json") + c.Header("Content-Length", fmt.Sprint(len(emptyJSON))) - c.Header("Docker-Distribution-API-Version", "registry/2.0") + c.Header("Docker-Distribution-API-Version", "registry/2.0") - c.Status(200) - c.Writer.Write([]byte(emptyJSON)) - }) + c.Status(200) + c.Writer.Write([]byte(emptyJSON)) + }, + ) r.GET("/v2", func(c *touka.Context) { // 重定向到 /v2/ diff --git a/proxy/gitreq.go b/proxy/gitreq.go index a8e2905..e4f0d95 100644 --- a/proxy/gitreq.go +++ b/proxy/gitreq.go @@ -17,13 +17,6 @@ func GitReq(ctx context.Context, c *touka.Context, u string, cfg *config.Config, resp *http.Response ) - go func() { - <-ctx.Done() - if resp != nil && resp.Body != nil { - resp.Body.Close() - } - }() - /* fullBody, err := c.GetReqBodyFull() if err != nil { From eb3bf16e06ec799d2740a02ff59ea4a25fb1d753 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Wed, 18 Jun 2025 10:35:01 +0800 Subject: [PATCH 50/99] update design theme --- config/config.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/config/config.go b/config/config.go index ba2ef62..d88daa0 100644 --- a/config/config.go +++ b/config/config.go @@ -245,7 +245,7 @@ func DefaultConfig() *Config { }, Pages: PagesConfig{ Mode: "internal", - Theme: "bootstrap", + Theme: "hub", StaticDir: "/data/www", }, Log: LogConfig{ @@ -271,8 +271,7 @@ func DefaultConfig() *Config { WhitelistFile: "/data/ghproxy/config/whitelist.json", }, RateLimit: RateLimitConfig{ - Enabled: false, - //RateMethod: "total", + Enabled: false, RatePerMinute: 100, Burst: 10, BandwidthLimit: BandwidthLimitConfig{ @@ -289,7 +288,11 @@ func DefaultConfig() *Config { }, Docker: DockerConfig{ Enabled: false, - Target: "ghcr", + Target: "dockerhub", + Auth: false, + Credentials: map[string]string{ + "testpass": "test123", + }, }, } } From d4237f0463dd16c3f7099b279d4a0512585ff1f1 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Wed, 18 Jun 2025 10:35:08 +0800 Subject: [PATCH 51/99] 4.1.2-rc.0 --- CHANGELOG.md | 7 ++++++- DEV-VERSION | 2 +- VERSION | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28e8976..c2056f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ # 更新日志 -4.1.1 - 2025-06-17 +4.1.2-rc.0 - 2025-06-17 +--- +- PRE-RELEASE: 此版本是v4.1.2预发布版本,请勿在生产环境中使用; +- CHANGE: 更新`design`主题, 更新默认配置生成 + +4.1.1 - 2025-06-18 --- - CHANGE: 更新touka框架到v0.2.6, 解决MidwareX的一些状态问题 diff --git a/DEV-VERSION b/DEV-VERSION index 038c952..85486c3 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -4.1.0-rc.0 +4.1.2-rc.0 \ No newline at end of file diff --git a/VERSION b/VERSION index 2582ddd..cd9b8f5 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.1.1 \ No newline at end of file +4.1.2 \ No newline at end of file From ff5f77edc9ae39fcbf954e00822954aef216ee50 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Wed, 18 Jun 2025 17:22:41 +0800 Subject: [PATCH 52/99] fix changelog --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2056f0..7c25d2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ # 更新日志 -4.1.2-rc.0 - 2025-06-17 +4.1.2 - 2025-06-18 +--- +- PRE-RELEASE: 此版本是v4.1.2预发布版本,请勿在生产环境中使用; +- CHANGE: 更新`design`主题, 更新默认配置生成 + +4.1.2-rc.0 - 2025-06-18 --- - PRE-RELEASE: 此版本是v4.1.2预发布版本,请勿在生产环境中使用; - CHANGE: 更新`design`主题, 更新默认配置生成 From 79692965a6a2384f8a9dcee499f03b9d20275838 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Wed, 18 Jun 2025 17:22:58 +0800 Subject: [PATCH 53/99] refix changelog --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c25d2b..f058936 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,6 @@ 4.1.2 - 2025-06-18 --- -- PRE-RELEASE: 此版本是v4.1.2预发布版本,请勿在生产环境中使用; - CHANGE: 更新`design`主题, 更新默认配置生成 4.1.2-rc.0 - 2025-06-18 From 781e175721f251f2135111192707d41be884574c Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Wed, 25 Jun 2025 17:53:35 +0800 Subject: [PATCH 54/99] 4.1.3 --- CHANGELOG.md | 9 +++++++++ DEV-VERSION | 2 +- VERSION | 2 +- go.mod | 4 ++-- go.sum | 8 ++++---- main.go | 3 ++- 6 files changed, 19 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f058936..08a9c5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # 更新日志 +4.1.3 - 2025-06-25 +--- +- CHANGE: 更新`touka`版本, 使用新的方式配置slash重定向功能 + +4.1.3-rc.0 - 2025-06-25 +--- +- PRE-RELEASE: 此版本是v4.1.3预发布版本,请勿在生产环境中使用; +- CHANGE: 更新`touka`版本, 使用新的方式配置slash重定向功能 + 4.1.2 - 2025-06-18 --- - CHANGE: 更新`design`主题, 更新默认配置生成 diff --git a/DEV-VERSION b/DEV-VERSION index 85486c3..2be11c0 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -4.1.2-rc.0 \ No newline at end of file +4.1.3-rc.0 \ No newline at end of file diff --git a/VERSION b/VERSION index cd9b8f5..8c7fafd 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.1.2 \ No newline at end of file +4.1.3 \ No newline at end of file diff --git a/go.mod b/go.mod index 6c23592..4019854 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.24.4 require ( github.com/BurntSushi/toml v1.5.0 - github.com/WJQSERVER-STUDIO/httpc v0.7.0 + github.com/WJQSERVER-STUDIO/httpc v0.7.1 golang.org/x/net v0.41.0 golang.org/x/time v0.12.0 ) @@ -16,7 +16,7 @@ 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.2.6 + github.com/infinite-iroha/touka v0.2.8 github.com/wjqserver/modembed v0.0.1 ) diff --git a/go.sum b/go.sum index 07c6e9b..261f233 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4 h1:JLtFd00AdFg/TP+dtvIzLkdHwKU github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4/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.7.0 h1:iHhqlxppJBjlmvsIjvLZKRbWXqSdbeSGGofjHGmqGJc= -github.com/WJQSERVER-STUDIO/httpc v0.7.0/go.mod h1:M7KNUZjjhCkzzcg9lBPs9YfkImI+7vqjAyjdA19+joE= +github.com/WJQSERVER-STUDIO/httpc v0.7.1 h1:D3NlfY52pwKIOSzkdRrLinUynyKELrcPZEO8QjlBq2M= +github.com/WJQSERVER-STUDIO/httpc v0.7.1/go.mod h1:M7KNUZjjhCkzzcg9lBPs9YfkImI+7vqjAyjdA19+joE= 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= @@ -18,8 +18,8 @@ github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8 h1:o8UqXPI github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8/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.2.6 h1:Y2zJTklfJZYO70jF9LPKq261IMt1vV8L1JBKUquQKIk= -github.com/infinite-iroha/touka v0.2.6/go.mod h1:2MBPtsM+5ClIZ/E1mPEKx1Rb+KIndTwZlIa2CwRPV7A= +github.com/infinite-iroha/touka v0.2.8 h1:PH4oR0fUjNr6t+Q3xkpqK+Q+kOFk7LN3xvy81xydu7Y= +github.com/infinite-iroha/touka v0.2.8/go.mod h1:e2LRc8FoSU8qjxSlyh3J8gGsBGKQ2VN9bQMU4sIrqnE= 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= diff --git a/main.go b/main.go index 5a37296..5910078 100644 --- a/main.go +++ b/main.go @@ -364,7 +364,8 @@ func main() { } setupApi(cfg, r, version) setupPages(cfg, r) - r.RedirectTrailingSlash = false + //r.RedirectTrailingSlash = false + r.SetRedirectTrailingSlash(false) r.GET("/github.com/:user/:repo/releases/*filepath", func(c *touka.Context) { c.Set("matcher", "releases") From 904a800eea735e73cfc0dbd2d14f84c1311e5b1b Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Mon, 30 Jun 2025 15:26:48 +0800 Subject: [PATCH 55/99] use touka uni httpc --- CHANGELOG.md | 6 +++ DEV-VERSION | 2 +- VERSION | 2 +- go.mod | 2 +- go.sum | 4 +- main.go | 7 ++- proxy/docker.go | 2 + proxy/gitreq.go | 23 --------- proxy/handler.go | 4 -- proxy/httpc.go | 122 +++++++++++++---------------------------------- proxy/routing.go | 5 -- 11 files changed, 51 insertions(+), 128 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08a9c5b..b360998 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # 更新日志 +4.1.4-rc.0 - 2025-06-30 +--- +- PRE-RELEASE: v4.1.4-rc.0是v4.1.4预发布版本,请勿在生产环境中使用; +- CHANGE: 使用`touka`框架的内建httpc统一管理, 同时对httpc相关初始化进行改进 +- CHANGE: 更新`json/v2`版本 + 4.1.3 - 2025-06-25 --- - CHANGE: 更新`touka`版本, 使用新的方式配置slash重定向功能 diff --git a/DEV-VERSION b/DEV-VERSION index 2be11c0..4e32678 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -4.1.3-rc.0 \ No newline at end of file +4.1.4-rc.0 \ No newline at end of file diff --git a/VERSION b/VERSION index 8c7fafd..9d086c6 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.1.3 \ No newline at end of file +4.1.4 \ No newline at end of file diff --git a/go.mod b/go.mod index 4019854..d28869e 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,6 @@ require ( require ( github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4 // indirect - github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8 // 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 261f233..9986b6b 100644 --- a/go.sum +++ b/go.sum @@ -14,8 +14,8 @@ 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-20250517221953-25912455fbc8 h1:o8UqXPI6SVwQt04RGsqKp3qqmbOfTNMqDrWsc4O47kk= -github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8/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.2.8 h1:PH4oR0fUjNr6t+Q3xkpqK+Q+kOFk7LN3xvy81xydu7Y= diff --git a/main.go b/main.go index 5910078..4fa4019 100644 --- a/main.go +++ b/main.go @@ -15,6 +15,7 @@ import ( "ghproxy/config" "ghproxy/proxy" + "github.com/WJQSERVER-STUDIO/httpc" "github.com/fenthope/bauth" "ghproxy/weakcache" @@ -33,7 +34,7 @@ var ( cfg *config.Config r *touka.Engine configfile = "/data/ghproxy/config/config.toml" - hertZfile *os.File + httpClient *httpc.Client cfgfile string version string runMode string @@ -165,7 +166,8 @@ func setupApi(cfg *config.Config, r *touka.Engine, version string) { } func InitReq(cfg *config.Config) { - err := proxy.InitReq(cfg) + var err error + httpClient, err = proxy.InitReq(cfg) if err != nil { fmt.Printf("Failed to initialize request: %v\n", err) os.Exit(1) @@ -335,6 +337,7 @@ func main() { r.Use(touka.Recovery()) // Recovery中间件 r.SetLogger(logger) + r.SetHTTPClient(httpClient) r.Use(record.Middleware()) // log中间件 r.Use(viaHeader()) /* diff --git a/proxy/docker.go b/proxy/docker.go index 44e4a72..cdcff70 100644 --- a/proxy/docker.go +++ b/proxy/docker.go @@ -119,6 +119,7 @@ func GhcrRequest(ctx context.Context, c *touka.Context, u string, image *imageIn }() method = c.Request.Method + ghcrclient := c.GetHTTPC() rb := ghcrclient.NewRequestBuilder(method, u) rb.NoDefaultHeaders() @@ -267,6 +268,7 @@ func ChallengeReq(target string, image *imageInfo, ctx context.Context, c *touka var resp401 *http.Response var req401 *http.Request var err error + ghcrclient := c.GetHTTPC() rb401 := ghcrclient.NewRequestBuilder("GET", "https://"+target+"/v2/") rb401.NoDefaultHeaders() diff --git a/proxy/gitreq.go b/proxy/gitreq.go index e4f0d95..f007290 100644 --- a/proxy/gitreq.go +++ b/proxy/gitreq.go @@ -17,23 +17,12 @@ func GitReq(ctx context.Context, c *touka.Context, u string, cfg *config.Config, resp *http.Response ) - /* - fullBody, err := c.GetReqBodyFull() - if err != nil { - HandleError(c, fmt.Sprintf("Failed to read request body: %v", err)) - return - } - reqBodyReader := bytes.NewBuffer(fullBody) - */ - reqBodyReader, err := c.GetReqBodyBuffer() if err != nil { HandleError(c, fmt.Sprintf("Failed to read request body: %v", err)) return } - //bodyReader := c.Request.BodyStream() // 不可替换为此实现 - if cfg.GitClone.Mode == "cache" { userPath, repoPath, remainingPath, queryParams, err := extractParts(u) if err != nil { @@ -103,14 +92,6 @@ func GitReq(ctx context.Context, c *touka.Context, u string, cfg *config.Config, } } - /* - for key, values := range resp.Header { - for _, value := range values { - c.Response.Header.Add(key, value) - } - } - */ - //copyHeader( resp.Header) c.SetHeaders(resp.Header) headersToRemove := map[string]struct{}{ @@ -143,10 +124,6 @@ func GitReq(ctx context.Context, c *touka.Context, u string, cfg *config.Config, bodyReader := resp.Body - // 读取body内容 - //bodyContent, _ := io.ReadAll(bodyReader) - // c.Infof("%s", bodyContent) - if cfg.RateLimit.BandwidthLimit.Enabled { bodyReader = limitreader.NewRateLimitedReader(bodyReader, bandwidthLimit, int(bandwidthBurst), ctx) } diff --git a/proxy/handler.go b/proxy/handler.go index 48d8c25..b15f1b5 100644 --- a/proxy/handler.go +++ b/proxy/handler.go @@ -15,10 +15,6 @@ func NoRouteHandler(cfg *config.Config) touka.HandlerFunc { return func(c *touka.Context) { var ctx = c.Request.Context() var shoudBreak bool - // shoudBreak = rateCheck(cfg, c, limiter, iplimiter) - // if shoudBreak { - // return - // } var ( rawPath string diff --git a/proxy/httpc.go b/proxy/httpc.go index 1cd9100..857f3f0 100644 --- a/proxy/httpc.go +++ b/proxy/httpc.go @@ -1,7 +1,6 @@ package proxy import ( - "fmt" "ghproxy/config" "net/http" "time" @@ -12,42 +11,40 @@ import ( var BufferSize int = 32 * 1024 // 32KB var ( - tr *http.Transport - gittr *http.Transport - client *httpc.Client - gitclient *httpc.Client - ghcrtr *http.Transport - ghcrclient *httpc.Client + tr *http.Transport + gittr *http.Transport + client *httpc.Client + gitclient *httpc.Client ) -func InitReq(cfg *config.Config) error { - initHTTPClient(cfg) +func InitReq(cfg *config.Config) (*httpc.Client, error) { + client := initHTTPClient(cfg) if cfg.GitClone.Mode == "cache" { initGitHTTPClient(cfg) } - initGhcrHTTPClient(cfg) err := SetGlobalRateLimit(cfg) if err != nil { - return err + return nil, err } - return nil + return client, nil } -func initHTTPClient(cfg *config.Config) { +func initHTTPClient(cfg *config.Config) *httpc.Client { var proTolcols = new(http.Protocols) proTolcols.SetHTTP1(true) proTolcols.SetHTTP2(true) proTolcols.SetUnencryptedHTTP2(true) - if cfg.Httpc.Mode == "auto" || cfg.Httpc.Mode == "" { + switch cfg.Httpc.Mode { + case "auto", "": tr = &http.Transport{ IdleConnTimeout: 30 * time.Second, WriteBufferSize: 32 * 1024, // 32KB ReadBufferSize: 32 * 1024, // 32KB Protocols: proTolcols, } - } else if cfg.Httpc.Mode == "advanced" { + case "advanced": tr = &http.Transport{ MaxIdleConns: cfg.Httpc.MaxIdleConns, MaxConnsPerHost: cfg.Httpc.MaxConnsPerHost, @@ -56,9 +53,10 @@ func initHTTPClient(cfg *config.Config) { ReadBufferSize: 32 * 1024, // 32KB Protocols: proTolcols, } - } else { + default: panic("unknown httpc mode: " + cfg.Httpc.Mode) } + if cfg.Outbound.Enabled { initTransport(cfg, tr) } @@ -72,18 +70,18 @@ func initHTTPClient(cfg *config.Config) { httpc.WithTransport(tr), ) } - + return client } func initGitHTTPClient(cfg *config.Config) { - - if cfg.Httpc.Mode == "auto" || cfg.Httpc.Mode == "" { + switch cfg.Httpc.Mode { + case "auto", "": gittr = &http.Transport{ IdleConnTimeout: 30 * time.Second, WriteBufferSize: 32 * 1024, // 32KB ReadBufferSize: 32 * 1024, // 32KB } - } else if cfg.Httpc.Mode == "advanced" { + case "advanced": gittr = &http.Transport{ MaxIdleConns: cfg.Httpc.MaxIdleConns, MaxConnsPerHost: cfg.Httpc.MaxConnsPerHost, @@ -91,84 +89,30 @@ func initGitHTTPClient(cfg *config.Config) { WriteBufferSize: 32 * 1024, // 32KB ReadBufferSize: 32 * 1024, // 32KB } - } else { + default: panic("unknown httpc mode: " + cfg.Httpc.Mode) } + if cfg.Outbound.Enabled { initTransport(cfg, gittr) } - 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, - }), - ) - } -} -func initGhcrHTTPClient(cfg *config.Config) { - var proTolcols = new(http.Protocols) - proTolcols.SetHTTP1(true) - proTolcols.SetHTTP2(true) - if cfg.Httpc.Mode == "auto" || cfg.Httpc.Mode == "" { + var opts []httpc.Option // 使用切片来收集选项 + opts = append(opts, httpc.WithTransport(gittr)) + var protocolsConfig httpc.ProtocolsConfig - ghcrtr = &http.Transport{ - IdleConnTimeout: 30 * time.Second, - WriteBufferSize: 32 * 1024, // 32KB - ReadBufferSize: 32 * 1024, // 32KB - Protocols: proTolcols, - } - } else if cfg.Httpc.Mode == "advanced" { - ghcrtr = &http.Transport{ - MaxIdleConns: cfg.Httpc.MaxIdleConns, - MaxConnsPerHost: cfg.Httpc.MaxConnsPerHost, - MaxIdleConnsPerHost: cfg.Httpc.MaxIdleConnsPerHost, - WriteBufferSize: 32 * 1024, // 32KB - ReadBufferSize: 32 * 1024, // 32KB - Protocols: proTolcols, - } + if cfg.GitClone.ForceH2C { + protocolsConfig.ForceH2C = true } else { - panic(fmt.Sprintf("unknown httpc mode: %s", cfg.Httpc.Mode)) - } - if cfg.Outbound.Enabled { - initTransport(cfg, ghcrtr) + protocolsConfig.Http1 = true + protocolsConfig.Http2 = true + protocolsConfig.Http2_Cleartext = true } + opts = append(opts, httpc.WithProtocols(protocolsConfig)) + if cfg.Server.Debug { - ghcrclient = httpc.New( - httpc.WithTransport(ghcrtr), - httpc.WithDumpLog(), - ) - } else { - ghcrclient = httpc.New( - httpc.WithTransport(ghcrtr), - ) + opts = append(opts, httpc.WithDumpLog()) } + + gitclient = httpc.New(opts...) } diff --git a/proxy/routing.go b/proxy/routing.go index 6c68b9c..7a5748f 100644 --- a/proxy/routing.go +++ b/proxy/routing.go @@ -12,11 +12,6 @@ func RoutingHandler(cfg *config.Config) touka.HandlerFunc { var shoudBreak bool - // shoudBreak = rateCheck(cfg, c, limiter, iplimiter) - // if shoudBreak { - // return - //} - var ( rawPath string ) From 4ea5a875feaab43cf80400033bd779b4de233b08 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Mon, 30 Jun 2025 15:27:17 +0800 Subject: [PATCH 56/99] 4.1.4 --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b360998..719ff85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 更新日志 +4.1.4 - 2025-06-30 +--- +- CHANGE: 使用`touka`框架的内建httpc统一管理, 同时对httpc相关初始化进行改进 +- CHANGE: 更新`json/v2`版本 + 4.1.4-rc.0 - 2025-06-30 --- - PRE-RELEASE: v4.1.4-rc.0是v4.1.4预发布版本,请勿在生产环境中使用; From ad4d55bc39a615ed8d218793aece3150e2bdcf42 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Thu, 3 Jul 2025 11:37:21 +0800 Subject: [PATCH 57/99] 4.1.5 --- CHANGELOG.md | 10 ++++++++++ DEV-VERSION | 2 +- README.md | 2 +- VERSION | 2 +- go.mod | 2 +- go.sum | 4 ++-- 6 files changed, 16 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 719ff85..972f44e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # 更新日志 +4.1.5 - 2025-07-03 +--- +- CHANGE: 使用`touka`框架的内建httpc统一管理, 同时对httpc相关初始化进行改进 +- CHANGE: 更新`json/v2`版本 + +4.1.5-rc.0 - 2025-07-03 +--- +- PRE-RELEASE: v4.1.5-rc.0是v4.1.5预发布版本,请勿在生产环境中使用; +- CHANGE: 更新`httpc`依赖以修正一些问题 + 4.1.4 - 2025-06-30 --- - CHANGE: 使用`touka`框架的内建httpc统一管理, 同时对httpc相关初始化进行改进 diff --git a/DEV-VERSION b/DEV-VERSION index 4e32678..a5f5ade 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -4.1.4-rc.0 \ No newline at end of file +4.1.5-rc.0 \ No newline at end of file diff --git a/README.md b/README.md index 4364c51..f3994ed 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ![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) -GHProxy是一个基于Go的支持代理Github仓库资源和API的项目, 同时支持Docker镜像代理与脚本嵌套加速等多种功能 +一个基于Go的高性能Github资源代理程序, 同时支持Docker镜像代理与脚本嵌套加速等多种功能 ## 项目说明 diff --git a/VERSION b/VERSION index 9d086c6..b673f6a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.1.4 \ No newline at end of file +4.1.5 \ No newline at end of file diff --git a/go.mod b/go.mod index d28869e..d4acf3c 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.24.4 require ( github.com/BurntSushi/toml v1.5.0 - github.com/WJQSERVER-STUDIO/httpc v0.7.1 + github.com/WJQSERVER-STUDIO/httpc v0.7.2 golang.org/x/net v0.41.0 golang.org/x/time v0.12.0 ) diff --git a/go.sum b/go.sum index 9986b6b..78dff75 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4 h1:JLtFd00AdFg/TP+dtvIzLkdHwKU github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4/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.7.1 h1:D3NlfY52pwKIOSzkdRrLinUynyKELrcPZEO8QjlBq2M= -github.com/WJQSERVER-STUDIO/httpc v0.7.1/go.mod h1:M7KNUZjjhCkzzcg9lBPs9YfkImI+7vqjAyjdA19+joE= +github.com/WJQSERVER-STUDIO/httpc v0.7.2 h1:ObEw1zCWBOVwhFTd2bE4BweOnEhSRJX/1qgCgt4hpf0= +github.com/WJQSERVER-STUDIO/httpc v0.7.2/go.mod h1:M7KNUZjjhCkzzcg9lBPs9YfkImI+7vqjAyjdA19+joE= 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= From 00513f689dbd4dae1da47fc6266fef7ce31377b0 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Thu, 3 Jul 2025 11:41:40 +0800 Subject: [PATCH 58/99] fix changelog --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 972f44e..17010c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,7 @@ 4.1.5 - 2025-07-03 --- -- CHANGE: 使用`touka`框架的内建httpc统一管理, 同时对httpc相关初始化进行改进 -- CHANGE: 更新`json/v2`版本 +- CHANGE: 更新`httpc`依赖以修正一些问题 4.1.5-rc.0 - 2025-07-03 --- From 4ee7f56ec5f58a79aea19e9aa3fc3fa0321ddf26 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sun, 6 Jul 2025 18:19:36 +0800 Subject: [PATCH 59/99] update deps optimize performance --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index d4acf3c..942ed72 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.24.4 require ( github.com/BurntSushi/toml v1.5.0 - github.com/WJQSERVER-STUDIO/httpc v0.7.2 + github.com/WJQSERVER-STUDIO/httpc v0.8.0 golang.org/x/net v0.41.0 golang.org/x/time v0.12.0 ) @@ -21,7 +21,7 @@ require ( ) require ( - github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4 // indirect + github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.6 // 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 78dff75..d4b3aec 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,11 @@ github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4 h1:JLtFd00AdFg/TP+dtvIzLkdHwKUGPOAijN1sMtEYoFg= -github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4/go.mod h1:FZ6XE+4TKy4MOfX1xWKe6Rwsg0ucYFCdNh1KLvyKTfc= +github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.6 h1:/50VJYXd6jcu+p5BnEBDyiX0nAyGxas1W3DCnrYMxMY= +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.7.2 h1:ObEw1zCWBOVwhFTd2bE4BweOnEhSRJX/1qgCgt4hpf0= -github.com/WJQSERVER-STUDIO/httpc v0.7.2/go.mod h1:M7KNUZjjhCkzzcg9lBPs9YfkImI+7vqjAyjdA19+joE= +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= From c19a0e9af9a9eb4e254ee8a32e9834b6e4be56c9 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Mon, 7 Jul 2025 15:28:16 +0800 Subject: [PATCH 60/99] 4.1.6 --- CHANGELOG.md | 11 +++++++++++ DEV-VERSION | 2 +- README.md | 15 +++++---------- VERSION | 2 +- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17010c4..f6392b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # 更新日志 +4.1.6 - 2025-07-07 +--- +- CHANGE: 更新[Touka框架]()版本到`v0.2.9`, 提升`io`相关方式的性能并降低分配 +- CHANGE: 更新[Touka HTTPC]()版本到`v0.8.0`, 使用`json/v2`的同时, 提升`io`相关操作性能并降低分配, 优化`debug`模式下打印输出性能 + +4.1.6-rc.0 - 2025-07-07 +--- +- PRE-RELEASE: v4.1.6-rc.0是v4.1.6预发布版本,请勿在生产环境中使用; +- CHANGE: 更新[Touka框架]()版本到`v0.2.9`, 提升`io`相关方式的性能并降低分配 +- CHANGE: 更新[Touka HTTPC]()版本到`v0.8.0`, 使用`json/v2`的同时, 提升`io`相关操作性能并降低分配, 优化`debug`模式下打印输出性能 + 4.1.5 - 2025-07-03 --- - CHANGE: 更新`httpc`依赖以修正一些问题 diff --git a/DEV-VERSION b/DEV-VERSION index a5f5ade..b56a9d3 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -4.1.5-rc.0 \ No newline at end of file +4.1.6-rc.0 \ No newline at end of file diff --git a/README.md b/README.md index f3994ed..c977c51 100644 --- a/README.md +++ b/README.md @@ -32,10 +32,10 @@ [TG讨论群组](https://t.me/ghproxy_go) -[相关文章](https://blog.wjqserver.com/categories/my-program/) - [GHProxy项目文档](https://wjqserver-docs.pages.dev/docs/ghproxy/) 感谢 [@redbunnys](https://github.com/redbunnys)的维护 +[相关文章](https://blog.wjqserver.com/categories/my-program/) + ### 使用示例 ```bash @@ -95,16 +95,11 @@ wget -O install-dev.sh https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghprox 参看[GHProxy-Frontend](https://github.com/WJQSERVER-STUDIO/GHProxy-Frontend) -## 项目简史 +## 文档 -本项目旨在于构建一个高效且功能多样的GHProxy +* [GHProxy项目文档](https://wjqserver-docs.pages.dev/docs/ghproxy/) 感谢 [@redbunnys](https://github.com/redbunnys)的维护 -- v4.0.0 迁移到[Touka框架](https://github.com/infinite-iroha/touka) -- v3.0.0 迁移到HertZ框架, 进一步提升效率 -- v2.4.1 对路径匹配进行优化 -- v2.0.0 对`proxy`核心模块进行了重构,大幅优化内存占用 -- v1.0.0 迁移至本仓库,并再次重构内容实现 -- v0.2.0 重构项目实现 +* [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/WJQSERVER-STUDIO/ghproxy) 可供参考, AI生成存在幻觉, 不完全可靠, 请注意辨别 ## LICENSE diff --git a/VERSION b/VERSION index b673f6a..00abb79 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.1.5 \ No newline at end of file +4.1.6 \ No newline at end of file From 90eca70eb170ac7950429adb36859535ee436545 Mon Sep 17 00:00:00 2001 From: WJQSERVER <114663932+WJQSERVER@users.noreply.github.com> Date: Mon, 7 Jul 2025 15:31:39 +0800 Subject: [PATCH 61/99] Update CHANGELOG.md Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6392b1..55ea9e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,8 @@ 4.1.6 - 2025-07-07 --- -- CHANGE: 更新[Touka框架]()版本到`v0.2.9`, 提升`io`相关方式的性能并降低分配 -- CHANGE: 更新[Touka HTTPC]()版本到`v0.8.0`, 使用`json/v2`的同时, 提升`io`相关操作性能并降低分配, 优化`debug`模式下打印输出性能 +- CHANGE: 更新[Touka框架](https://github.com/infinite-iroha/touka)版本到`v0.2.9`, 提升`io`相关方式的性能并降低分配 +- CHANGE: 更新[Touka HTTPC](https://github.com/WJQSERVER-STUDIO/httpc)版本到`v0.8.0`, 使用`json/v2`的同时, 提升`io`相关操作性能并降低分配, 优化`debug`模式下打印输出性能 4.1.6-rc.0 - 2025-07-07 --- From 7e153d2b51ca7eef3dfa8ae24af15d43f80b597d Mon Sep 17 00:00:00 2001 From: WJQSERVER <114663932+WJQSERVER@users.noreply.github.com> Date: Mon, 7 Jul 2025 15:32:58 +0800 Subject: [PATCH 62/99] Update CHANGELOG.md Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55ea9e9..7e9a3ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,8 @@ 4.1.6-rc.0 - 2025-07-07 --- - PRE-RELEASE: v4.1.6-rc.0是v4.1.6预发布版本,请勿在生产环境中使用; -- CHANGE: 更新[Touka框架]()版本到`v0.2.9`, 提升`io`相关方式的性能并降低分配 -- CHANGE: 更新[Touka HTTPC]()版本到`v0.8.0`, 使用`json/v2`的同时, 提升`io`相关操作性能并降低分配, 优化`debug`模式下打印输出性能 +- CHANGE: 更新[Touka框架](https://github.com/infinite-iroha/touka)版本到`v0.2.9`, 提升`io`相关方式的性能并降低分配 +- CHANGE: 更新[Touka HTTPC](https://github.com/WJQSERVER-STUDIO/httpc)版本到`v0.8.0`, 使用`json/v2`的同时, 提升`io`相关操作性能并降低分配, 优化`debug`模式下打印输出性能 4.1.5 - 2025-07-03 --- From b033079553b03db9f3b8b5a8b511462aa43b71a3 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Mon, 7 Jul 2025 15:44:38 +0800 Subject: [PATCH 63/99] update deps --- VERSION | 2 +- go.mod | 2 +- go.sum | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/VERSION b/VERSION index 00abb79..561ad33 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.1.6 \ No newline at end of file +4.1.6 diff --git a/go.mod b/go.mod index 942ed72..23f27ac 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ 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.2.8 + github.com/infinite-iroha/touka v0.2.9 github.com/wjqserver/modembed v0.0.1 ) diff --git a/go.sum b/go.sum index d4b3aec..e93fac2 100644 --- a/go.sum +++ b/go.sum @@ -18,8 +18,8 @@ github.com/go-json-experiment/json v0.0.0-20250626171732-1a886bd29d1b h1:ooF9/Nz 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.2.8 h1:PH4oR0fUjNr6t+Q3xkpqK+Q+kOFk7LN3xvy81xydu7Y= -github.com/infinite-iroha/touka v0.2.8/go.mod h1:e2LRc8FoSU8qjxSlyh3J8gGsBGKQ2VN9bQMU4sIrqnE= +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= From 1f3a03626746fd92d934e575fb5d11b11b0817f8 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sun, 20 Jul 2025 22:13:05 +0800 Subject: [PATCH 64/99] 4.1.7-rc.0 --- CHANGELOG.md | 6 +++ DEV-VERSION | 2 +- config/config.go | 13 +++--- go.mod | 10 ++--- go.sum | 16 ++++---- main.go | 8 +--- proxy/docker.go | 47 ++++++++++------------ proxy/error.go | 52 ++++++++++++++++++++---- proxy/gitreq.go | 6 ++- proxy/match.go | 100 ----------------------------------------------- 10 files changed, 98 insertions(+), 162 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e9a3ce..4e5d057 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # 更新日志 +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 b56a9d3..0f725cf 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -4.1.6-rc.0 \ No newline at end of file +4.1.7-rc.0 \ No newline at end of file diff --git a/config/config.go b/config/config.go index d88daa0..0e59f0e 100644 --- a/config/config.go +++ b/config/config.go @@ -60,12 +60,14 @@ 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"` - ForceH2C bool `toml:"ForceH2C"` + //CacheTimeout int `toml:"cacheTimeout"` + ForceH2C bool `toml:"ForceH2C"` } /* @@ -175,10 +177,11 @@ 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"` + Enabled bool `toml:"enabled"` + Target string `toml:"target"` + Auth bool `toml:"auth"` + Credentials map[string]string `toml:"credentials"` + AuthPassThrough bool `toml:"authPassThrough"` } // LoadConfig 从 TOML 配置文件加载配置 diff --git a/go.mod b/go.mod index 23f27ac..1d5ff59 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,11 @@ module ghproxy -go 1.24.4 +go 1.24.5 require ( github.com/BurntSushi/toml v1.5.0 - github.com/WJQSERVER-STUDIO/httpc v0.8.0 - golang.org/x/net v0.41.0 + github.com/WJQSERVER-STUDIO/httpc v0.8.1 + golang.org/x/net v0.42.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.2.9 + github.com/infinite-iroha/touka v0.3.1 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-20250626171732-1a886bd29d1b // indirect + github.com/go-json-experiment/json v0.0.0-20250714165856-be8212f5270d // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect ) diff --git a/go.sum b/go.sum index e93fac2..3347216 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.0 h1:G7inJ5EEsg5+BkeFiNIo/6+Mj7Ygiq85yMT3Ld7frJY= -github.com/WJQSERVER-STUDIO/httpc v0.8.0/go.mod h1:50297rvgppmgPbZEtWzTWgkomoqPREkGy9T3Y/NqN7o= +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/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-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/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/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.2.9 h1:Ugu0H3Zdip/ZnDbaCXquxsWnntByCUDBONez1oZANaU= -github.com/infinite-iroha/touka v0.2.9/go.mod h1:Cmok9Xs8yNRNEUSqiZfi3xtdO1UZYw/yP+phf+zjH2Y= +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/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.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= -golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= 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 4fa4019..fac8fb4 100644 --- a/main.go +++ b/main.go @@ -337,6 +337,7 @@ 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()) @@ -367,7 +368,6 @@ 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,12 +443,6 @@ 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 cdcff70..1f707db 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) { - - if reqTarget == "docker.io" { + switch reqTarget { + case "docker.io": target = dockerhubTarget - } else if reqTarget == "ghcr.io" { + case "ghcr.io": target = ghcrTarget - } else { + default: target = reqTarget } } else { @@ -132,11 +132,6 @@ 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) @@ -154,8 +149,9 @@ func GhcrRequest(ctx context.Context, c *touka.Context, u string, image *imageIn return } - // 处理状态码 - if resp.StatusCode == 401 { + switch resp.StatusCode { + + case 401: // 请求target /v2/路径 if string(c.GetRequestURIPath()) != "/v2/" { resp.Body.Close() @@ -181,13 +177,7 @@ 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) @@ -202,9 +192,20 @@ func GhcrRequest(ctx context.Context, c *touka.Context, u string, image *imageIn } } - } else if resp.StatusCode == 404 { // 错误处理(404) + 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) + return + } } var ( @@ -234,14 +235,6 @@ 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 be4c578..72a6b40 100644 --- a/proxy/error.go +++ b/proxy/error.go @@ -20,6 +20,19 @@ 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 @@ -65,6 +78,25 @@ 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 @@ -169,11 +201,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[string, []byte](10000, func(key string, value []byte) { + c.cache, err = lru.NewWithEvict(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 @@ -195,7 +227,6 @@ 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 } @@ -203,23 +234,19 @@ 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 @@ -231,7 +258,6 @@ func init() { var err error errorPageCache, err = NewSizedLRUCache(maxErrorPageCacheBytes) if err != nil { - // logError("Failed to initialize error page LRU cache: %v", err) panic(err) } } @@ -283,6 +309,16 @@ 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 f007290..af8e6bc 100644 --- a/proxy/gitreq.go +++ b/proxy/gitreq.go @@ -30,7 +30,11 @@ func GitReq(ctx context.Context, c *touka.Context, u string, cfg *config.Config, return } // 构建新url - u = cfg.GitClone.SmartGitAddr + userPath + repoPath + remainingPath + "?" + queryParams.Encode() + var paramStr string + if len(queryParams) > 0 { + paramStr = "?" + queryParams.Encode() + } + u = cfg.GitClone.SmartGitAddr + userPath + repoPath + remainingPath + paramStr } if cfg.GitClone.Mode == "cache" { diff --git a/proxy/match.go b/proxy/match.go index 8050779..a50d018 100644 --- a/proxy/match.go +++ b/proxy/match.go @@ -28,7 +28,6 @@ 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路径中高效地解析并匹配代理规则. @@ -159,105 +158,6 @@ 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 From 95dd34a45650fc8968816ca66a59800210633026 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sun, 20 Jul 2025 22:29:27 +0800 Subject: [PATCH 65/99] 4.1.7 --- CHANGELOG.md | 5 +++++ VERSION | 2 +- proxy/error.go | 3 +++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e5d057..90c5047 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 更新日志 +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预发布版本,请勿在生产环境中使用; diff --git a/VERSION b/VERSION index 561ad33..a4428cf 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.1.6 +4.1.7 \ No newline at end of file diff --git a/proxy/error.go b/proxy/error.go index 72a6b40..222f382 100644 --- a/proxy/error.go +++ b/proxy/error.go @@ -109,6 +109,9 @@ func init() { ErrNotFound.StatusCode: ErrNotFound, ErrTooManyRequests.StatusCode: ErrTooManyRequests, ErrInternalServerError.StatusCode: ErrInternalServerError, + ErrBadGateway.StatusCode: ErrBadGateway, + ErrServiceUnavailable.StatusCode: ErrServiceUnavailable, + ErrGatewayTimeout.StatusCode: ErrGatewayTimeout, } } From d2d9ad1db7693d5681588ea8c887ca385372d45b Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Tue, 22 Jul 2025 16:37:59 +0800 Subject: [PATCH 66/99] 4.2.0-rc.0 --- .gitignore | 1 + CHANGELOG.md | 6 +++++ DEV-VERSION | 2 +- auth/blacklist.go | 2 +- auth/ipfilter.go | 60 ++++++++++++++++++++++++++++++++++++++++++++ auth/whitelist.go | 3 ++- config/config.go | 38 +++++++++++++++++++--------- config/config.toml | 6 +++++ config/ipfilter.json | 11 ++++++++ go.mod | 1 + go.sum | 2 ++ main.go | 22 ++++++++++++++++ 12 files changed, 139 insertions(+), 15 deletions(-) create mode 100644 auth/ipfilter.go create mode 100644 config/ipfilter.json diff --git a/.gitignore b/.gitignore index 0ad54a9..6358c7d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ demo.toml *.log *.bak list.json +iplist.json repos pages *_test \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 90c5047..f2a972d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # 更新日志 +4.2.0-rc.0 - 2025-07-22 +--- +- PRE-RELEASE: v4.2.0-rc.0是v4.2.0预发布版本,请勿在生产环境中使用; +- CHANGE: 支持根据IP(CDIR)进行白名单与屏蔽 +- CHANGE: 深化json/v2改革, 预备go1.25 json/v2 + 4.1.7 - 2025-07-20 --- - CHANGE: 更新相关依赖 diff --git a/DEV-VERSION b/DEV-VERSION index 0f725cf..5cff3f3 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -4.1.7-rc.0 \ No newline at end of file +4.2.0-rc.0 \ No newline at end of file diff --git a/auth/blacklist.go b/auth/blacklist.go index 5ccc73c..014a41d 100644 --- a/auth/blacklist.go +++ b/auth/blacklist.go @@ -7,7 +7,7 @@ import ( "strings" "sync" - "encoding/json" + "github.com/go-json-experiment/json" ) type Blacklist struct { diff --git a/auth/ipfilter.go b/auth/ipfilter.go new file mode 100644 index 0000000..45fe884 --- /dev/null +++ b/auth/ipfilter.go @@ -0,0 +1,60 @@ +package auth + +import ( + "fmt" + "ghproxy/config" + "os" + + "github.com/go-json-experiment/json" + "github.com/go-json-experiment/json/jsontext" +) + +func ReadIPFilterList(cfg *config.Config) (whitelist []string, blacklist []string, err error) { + if cfg.IPFilter.IPFilterFile == "" { + return nil, nil, nil + } + + // 检查文件是否存在, 不存在则创建空json + if _, err := os.Stat(cfg.IPFilter.IPFilterFile); os.IsNotExist(err) { + if err := CreateEmptyIPFilterFile(cfg.IPFilter.IPFilterFile); err != nil { + return nil, nil, fmt.Errorf("failed to create empty IP filter file: %w", err) + } + } + + data, err := os.ReadFile(cfg.IPFilter.IPFilterFile) + if err != nil { + return nil, nil, fmt.Errorf("failed to read IP filter file: %w", err) + } + + var ipFilterData struct { + AllowList []string `json:"allow"` + BlockList []string `json:"block"` + } + if err := json.Unmarshal(data, &ipFilterData); err != nil { + return nil, nil, fmt.Errorf("invalid IP filter file format: %w", err) + } + + return ipFilterData.AllowList, ipFilterData.BlockList, nil +} + +// 创建空列表json +func CreateEmptyIPFilterFile(filePath string) error { + emptyData := struct { + AllowList []string `json:"allow"` + BlockList []string `json:"block"` + }{ + AllowList: []string{}, + BlockList: []string{}, + } + + jsonData, err := json.Marshal(emptyData, jsontext.Multiline(true), jsontext.WithIndent(" ")) + if err != nil { + return fmt.Errorf("failed to marshal empty IP filter data: %w", err) + } + + err = os.WriteFile(filePath, jsonData, 0644) + if err != nil { + return fmt.Errorf("failed to write empty IP filter file: %w", err) + } + return nil +} diff --git a/auth/whitelist.go b/auth/whitelist.go index ee93c20..1218307 100644 --- a/auth/whitelist.go +++ b/auth/whitelist.go @@ -1,12 +1,13 @@ package auth import ( - "encoding/json" "fmt" "ghproxy/config" "os" "strings" "sync" + + "github.com/go-json-experiment/json" ) // Whitelist 用于存储白名单信息 diff --git a/config/config.go b/config/config.go index 0e59f0e..48d43fc 100644 --- a/config/config.go +++ b/config/config.go @@ -7,18 +7,19 @@ import ( ) type Config struct { - Server ServerConfig - Httpc HttpcConfig - GitClone GitCloneConfig - Shell ShellConfig - Pages PagesConfig - Log LogConfig - Auth AuthConfig - Blacklist BlacklistConfig - Whitelist WhitelistConfig - RateLimit RateLimitConfig - Outbound OutboundConfig - Docker DockerConfig + Server ServerConfig `toml:"server"` + Httpc HttpcConfig `toml:"httpc"` + GitClone GitCloneConfig `toml:"gitclone"` + Shell ShellConfig `toml:"shell"` + Pages PagesConfig `toml:"pages"` + Log LogConfig `toml:"log"` + Auth AuthConfig `toml:"auth"` + Blacklist BlacklistConfig `toml:"blacklist"` + Whitelist WhitelistConfig `toml:"whitelist"` + IPFilter IPFilterConfig `toml:"ipFilter"` + RateLimit RateLimitConfig `toml:"rateLimit"` + Outbound OutboundConfig `toml:"outbound"` + Docker DockerConfig `toml:"docker"` } /* @@ -128,6 +129,13 @@ type WhitelistConfig struct { WhitelistFile string `toml:"whitelistFile"` } +type IPFilterConfig struct { + Enabled bool `toml:"enabled"` + EnableAllowList bool `toml:"enableAllowList"` + EnableBlockList bool `toml:"enableBlockList"` + IPFilterFile string +} + /* [rateLimit] enabled = false @@ -273,6 +281,12 @@ func DefaultConfig() *Config { Enabled: false, WhitelistFile: "/data/ghproxy/config/whitelist.json", }, + IPFilter: IPFilterConfig{ + Enabled: false, + IPFilterFile: "/data/ghproxy/config/ipfilter.json", + EnableAllowList: false, + EnableBlockList: false, + }, RateLimit: RateLimitConfig{ Enabled: false, RatePerMinute: 100, diff --git a/config/config.toml b/config/config.toml index ecf4959..ce490ca 100644 --- a/config/config.toml +++ b/config/config.toml @@ -49,6 +49,12 @@ enabled = false enabled = false whitelistFile = "/data/ghproxy/config/whitelist.json" +[ipFilter] +enabled = false +enableAllowList = false +enableBlockList = false +ipFilterFile = "/data/ghproxy/config/ipfilter.json" + [rateLimit] enabled = false ratePerMinute = 180 diff --git a/config/ipfilter.json b/config/ipfilter.json new file mode 100644 index 0000000..283bbd8 --- /dev/null +++ b/config/ipfilter.json @@ -0,0 +1,11 @@ +{ + "allow": [ + "127.0.0.1", + "192.168.1.0/24", + "::1" + ], + "block": [ + "10.0.0.0/8", + "192.168.1.0/24" + ] +} \ No newline at end of file diff --git a/go.mod b/go.mod index 1d5ff59..7d21b88 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/WJQSERVER-STUDIO/go-utils/limitreader v0.0.2 github.com/fenthope/bauth v0.0.1 github.com/fenthope/ikumi v0.0.2 + github.com/fenthope/ipfilter v0.0.1 github.com/fenthope/reco v0.0.3 github.com/fenthope/record v0.0.3 github.com/hashicorp/golang-lru/v2 v2.0.7 diff --git a/go.sum b/go.sum index 3347216..81d8fbc 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ 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= github.com/fenthope/ikumi v0.0.2/go.mod h1:IYbxzOGndZv/yRrbVMyV6dxh06X2wXCbfxrTRM1IruU= +github.com/fenthope/ipfilter v0.0.1 h1:HrYAyixCMvsDAz36GRyFfyCNtrgYwzrhMcY0XV7fGcM= +github.com/fenthope/ipfilter v0.0.1/go.mod h1:QfY0GrpG0D82HROgdH4c9eog4js42ghLIfl/iM4MvvY= 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= diff --git a/main.go b/main.go index fac8fb4..52f9fca 100644 --- a/main.go +++ b/main.go @@ -21,6 +21,7 @@ import ( "ghproxy/weakcache" "github.com/fenthope/ikumi" + "github.com/fenthope/ipfilter" "github.com/fenthope/reco" "github.com/fenthope/record" "github.com/infinite-iroha/touka" @@ -366,6 +367,27 @@ func main() { Burst: cfg.RateLimit.Burst, })) } + + if cfg.IPFilter.Enabled { + var err error + ipAllowList, ipBlockList, err := auth.ReadIPFilterList(cfg) + if err != nil { + fmt.Printf("Failed to read IP filter list: %v\n", err) + os.Exit(1) + } + ipBlockFilter, err := ipfilter.NewIPFilter(ipfilter.IPFilterConfig{ + EnableAllowList: cfg.IPFilter.EnableAllowList, + EnableBlockList: cfg.IPFilter.EnableBlockList, + AllowList: ipAllowList, + BlockList: ipBlockList, + }) + if err != nil { + fmt.Printf("Failed to initialize IP filter: %v\n", err) + os.Exit(1) + } else { + r.Use(ipBlockFilter) + } + } setupApi(cfg, r, version) setupPages(cfg, r) r.SetRedirectTrailingSlash(false) From 3abe4419d67e1c128657917620de6389dc7b650a Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Tue, 22 Jul 2025 16:38:24 +0800 Subject: [PATCH 67/99] add Thordata ads --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index c977c51..cc0847e 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,8 @@ [相关文章](https://blog.wjqserver.com/categories/my-program/) +代理相关推广: [Thordata](https://www.thordata.com/?ls=github&lk=WJQserver),市面上最具性价比的代理服务商,便宜好用,来自全球195个国家城市的6000万IP,轮换住宅/原生ISP/无限量仅从$0.65/GB 起,新用户$1=5GB .联系客户可获得免费测试. + ### 使用示例 ```bash From cc4b04ede213f83f34a7c5def3742ff7ea1fd690 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Tue, 22 Jul 2025 17:23:33 +0800 Subject: [PATCH 68/99] 4.2.0 --- CHANGELOG.md | 5 +++++ VERSION | 2 +- go.mod | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2a972d..b3446ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 更新日志 +4.2.0 - 2025-07-22 +--- +- CHANGE: 支持根据IP(CDIR)进行白名单与屏蔽 +- CHANGE: 进一步推进`json/v2`支持 + 4.2.0-rc.0 - 2025-07-22 --- - PRE-RELEASE: v4.2.0-rc.0是v4.2.0预发布版本,请勿在生产环境中使用; diff --git a/VERSION b/VERSION index a4428cf..ef8d756 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.1.7 \ No newline at end of file +4.2.0 \ No newline at end of file diff --git a/go.mod b/go.mod index 7d21b88..a477615 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/fenthope/ipfilter v0.0.1 github.com/fenthope/reco v0.0.3 github.com/fenthope/record v0.0.3 + github.com/go-json-experiment/json v0.0.0-20250714165856-be8212f5270d github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/infinite-iroha/touka v0.3.1 github.com/wjqserver/modembed v0.0.1 @@ -23,6 +24,5 @@ require ( 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/valyala/bytebufferpool v1.0.0 // indirect ) From 33bb588c363046859de097ab29bacfa10e96d183 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Tue, 22 Jul 2025 17:29:38 +0800 Subject: [PATCH 69/99] fix typo --- config/config.go | 8 ++++---- main.go | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/config/config.go b/config/config.go index 48d43fc..3ea330d 100644 --- a/config/config.go +++ b/config/config.go @@ -130,10 +130,10 @@ type WhitelistConfig struct { } type IPFilterConfig struct { - Enabled bool `toml:"enabled"` - EnableAllowList bool `toml:"enableAllowList"` - EnableBlockList bool `toml:"enableBlockList"` - IPFilterFile string + Enabled bool `toml:"enabled"` + EnableAllowList bool `toml:"enableAllowList"` + EnableBlockList bool `toml:"enableBlockList"` + IPFilterFile string `toml:"ipFilterFile"` } /* diff --git a/main.go b/main.go index 52f9fca..6a888ac 100644 --- a/main.go +++ b/main.go @@ -373,6 +373,7 @@ func main() { ipAllowList, ipBlockList, err := auth.ReadIPFilterList(cfg) if err != nil { fmt.Printf("Failed to read IP filter list: %v\n", err) + logger.Errorf("Failed to read IP filter list: %v", err) os.Exit(1) } ipBlockFilter, err := ipfilter.NewIPFilter(ipfilter.IPFilterConfig{ @@ -383,6 +384,7 @@ func main() { }) if err != nil { fmt.Printf("Failed to initialize IP filter: %v\n", err) + logger.Errorf("Failed to initialize IP filter: %v", err) os.Exit(1) } else { r.Use(ipBlockFilter) From 3f802a0ed357f90ec0a37e4687414e2137e6f462 Mon Sep 17 00:00:00 2001 From: WJQSERVER <114663932+WJQSERVER@users.noreply.github.com> Date: Thu, 24 Jul 2025 16:37:41 +0000 Subject: [PATCH 70/99] update deps --- go.mod | 3 ++- go.sum | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index a477615..6a9d201 100644 --- a/go.mod +++ b/go.mod @@ -18,11 +18,12 @@ require ( github.com/fenthope/record v0.0.3 github.com/go-json-experiment/json v0.0.0-20250714165856-be8212f5270d github.com/hashicorp/golang-lru/v2 v2.0.7 - github.com/infinite-iroha/touka v0.3.1 + github.com/infinite-iroha/touka v0.3.3 github.com/wjqserver/modembed v0.0.1 ) 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 ) diff --git a/go.sum b/go.sum index 81d8fbc..9a5bf1d 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.6 h1:/50VJYXd6jcu+p5BnEBDyiX0nAyGxas1W3DCnrYMxMY= github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.6/go.mod h1:FZ6XE+4TKy4MOfX1xWKe6Rwsg0ucYFCdNh1KLvyKTfc= +github.com/WJQSERVER-STUDIO/go-utils/iox v0.0.2 h1:AiIHXP21LpK7pFfqUlUstgQEWzjbekZgxOuvVwiMfyM= +github.com/WJQSERVER-STUDIO/go-utils/iox v0.0.2/go.mod h1:mCLqYU32bTmEE6dpj37MKKiZgz70Jh/xyK9vVbq6pok= 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= @@ -20,8 +22,8 @@ github.com/go-json-experiment/json v0.0.0-20250714165856-be8212f5270d h1:+d6m5Bj github.com/go-json-experiment/json v0.0.0-20250714165856-be8212f5270d/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.3.3 h1:6Vy36bYjtbGKaBNiZBRcTne9Lcx8QTE6rpHqyMb3oiA= +github.com/infinite-iroha/touka v0.3.3/go.mod h1:9Y/MWlvlBL/8cqA+2ZUsnBr4h3f7yo3nOxsegIcBduw= 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= From 98fdd6167358e797276332a089088b821ae937f2 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Fri, 25 Jul 2025 14:18:21 +0800 Subject: [PATCH 71/99] 4.2.1 --- CHANGELOG.md | 4 ++++ VERSION | 2 +- main.go | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3446ae..0ad0f46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 更新日志 +4.2.1 - 2025-07-25 +--- +- CHANGE: 更新主题样式, 新增`free`主题, `design`与`hub`主题样式更新 + 4.2.0 - 2025-07-22 --- - CHANGE: 支持根据IP(CDIR)进行白名单与屏蔽 diff --git a/VERSION b/VERSION index ef8d756..d87edbf 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.2.0 \ No newline at end of file +4.2.1 \ No newline at end of file diff --git a/main.go b/main.go index 6a888ac..e09426e 100644 --- a/main.go +++ b/main.go @@ -195,6 +195,8 @@ func loadEmbeddedPages(cfg *config.Config) (fs.FS, fs.FS, error) { pages, err = fs.Sub(pageFS, "pages/mino") case "hub": pages, err = fs.Sub(pageFS, "pages/hub") + case "free": + pages, err = fs.Sub(pageFS, "pages/free") default: pages, err = fs.Sub(pageFS, "pages/design") // 默认主题 logWarning("Invalid Pages Theme: %s, using default theme 'design'", cfg.Pages.Theme) 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 72/99] 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 73/99] 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 74/99] 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 75/99] 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 76/99] 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 77/99] 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) From afa2115b0d58397e049aa7e22693ddd04533c367 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sun, 27 Jul 2025 15:08:57 +0800 Subject: [PATCH 78/99] update err page loader --- .github/workflows/build-dev.yml | 2 +- .github/workflows/build.yml | 2 +- main.go | 110 ++++++++++++++++---------------- 3 files changed, 56 insertions(+), 58 deletions(-) diff --git a/.github/workflows/build-dev.yml b/.github/workflows/build-dev.yml index c1cb9d1..24e61c7 100644 --- a/.github/workflows/build-dev.yml +++ b/.github/workflows/build-dev.yml @@ -73,7 +73,7 @@ jobs: GOOS: ${{ matrix.goos }} GOARCH: ${{ matrix.goarch }} run: | - CGO_ENABLED=0 go build -ldflags "-X main.version=${{ env.VERSION }} -X main.dev=true" -o ${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}} ./main.go + CGO_ENABLED=0 go build -ldflags "-X main.version=${{ env.VERSION }} -X main.dev=true" -o ${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}} . - name: 打包 run: | mkdir ghproxyd diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 57bda5f..f847575 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -74,7 +74,7 @@ jobs: GOOS: ${{ matrix.goos }} GOARCH: ${{ matrix.goarch }} run: | - CGO_ENABLED=0 go build -ldflags "-s -w -X main.version=${{ env.VERSION }}" -o ${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}} ./main.go + CGO_ENABLED=0 go build -ldflags "-s -w -X main.version=${{ env.VERSION }}" -o ${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}} . - name: 打包 run: | mkdir ghproxyd diff --git a/main.go b/main.go index e09426e..65a6e31 100644 --- a/main.go +++ b/main.go @@ -53,12 +53,21 @@ var ( ) var ( - logger *reco.Logger - logDump = logger.Debugf - logDebug = logger.Debugf - logInfo = logger.Infof - logWarning = logger.Warnf - logError = logger.Errorf + // supportedThemes 定义了所有支持的主题, 用于验证配置和动态加载 + supportedThemes = map[string]struct{}{ + "bootstrap": {}, + "nebula": {}, + "design": {}, + "metro": {}, + "classic": {}, + "mino": {}, + "hub": {}, + "free": {}, + } +) + +var ( + logger *reco.Logger ) func readFlag() { @@ -111,7 +120,7 @@ func loadConfig() { cfg, err = config.LoadConfig(cfgfile) if err != nil { fmt.Printf("Failed to load config: %v\n", err) - // 如果配置文件加载失败,也显示帮助信息并退出 + // 如果配置文件加载失败, 也显示帮助信息并退出 flag.Usage() os.Exit(1) } @@ -150,7 +159,7 @@ func setupLogger(cfg *config.Config) { func setMemLimit(cfg *config.Config) { if cfg.Server.MemLimit > 0 { debug.SetMemoryLimit((cfg.Server.MemLimit) * 1024 * 1024) - logInfo("Set Memory Limit to %d MB", cfg.Server.MemLimit) + logger.Infof("Set Memory Limit to %d MB", cfg.Server.MemLimit) } } @@ -175,60 +184,52 @@ func InitReq(cfg *config.Config) { } } -// loadEmbeddedPages 加载嵌入式页面资源 +// initializeErrorPages 初始化嵌入的错误页面资源 +// 无论页面模式(internal/external)如何, 都应执行此操作, 以确保统一的错误页面处理 +func initializeErrorPages() { + pageFS := modembed.NewModTimeFS(pagesFS, time.Now()) + if err := proxy.InitErrPagesFS(pageFS); err != nil { + // 这是一个警告而不是致命错误, 因为即使没有自定义错误页面, 服务器也能运行 + logger.Warnf("failed to initialize embedded error pages: %v", err) + } +} + +// loadEmbeddedPages 使用 map 替代 switch, 动态加载嵌入式页面和资源文件系统 func loadEmbeddedPages(cfg *config.Config) (fs.FS, fs.FS, error) { pageFS := modembed.NewModTimeFS(pagesFS, time.Now()) - var pages fs.FS - var err error - switch cfg.Pages.Theme { - case "bootstrap": - pages, err = fs.Sub(pageFS, "pages/bootstrap") - case "nebula": - pages, err = fs.Sub(pageFS, "pages/nebula") - case "design": - pages, err = fs.Sub(pageFS, "pages/design") - case "metro": - pages, err = fs.Sub(pageFS, "pages/metro") - case "classic": - pages, err = fs.Sub(pageFS, "pages/classic") - case "mino": - pages, err = fs.Sub(pageFS, "pages/mino") - case "hub": - pages, err = fs.Sub(pageFS, "pages/hub") - case "free": - pages, err = fs.Sub(pageFS, "pages/free") - default: - pages, err = fs.Sub(pageFS, "pages/design") // 默认主题 - logWarning("Invalid Pages Theme: %s, using default theme 'design'", cfg.Pages.Theme) + theme := cfg.Pages.Theme + + // 检查主题是否受支持, 如果不支持则使用默认主题 + if _, ok := supportedThemes[theme]; !ok { + logger.Warnf("Invalid Pages Theme: %s, using default theme 'design'", theme) + theme = "design" // 默认主题 } + // 从嵌入式文件系统中获取主题子目录 + themePath := fmt.Sprintf("pages/%s", theme) + pages, err := fs.Sub(pageFS, themePath) if err != nil { - return nil, nil, fmt.Errorf("failed to load embedded pages: %w", err) + return nil, nil, fmt.Errorf("failed to load embedded theme '%s': %w", theme, err) } - // 初始化errPagesFs - errPagesInitErr := proxy.InitErrPagesFS(pageFS) - if errPagesInitErr != nil { - logWarning("errPagesInitErr: %s", errPagesInitErr) - } - - var assets fs.FS - assets, err = fs.Sub(pageFS, "pages/assets") + // 加载共享资源文件 + assets, err := fs.Sub(pageFS, "pages/assets") if err != nil { return nil, nil, fmt.Errorf("failed to load embedded assets: %w", err) } + return pages, assets, nil } -// setupPages 设置页面路由 +// setupPages 设置页面路由, 增强了错误处理 func setupPages(cfg *config.Config, r *touka.Engine) { switch cfg.Pages.Mode { case "internal": err := setInternalRoute(cfg, r) if err != nil { - logError("Failed when processing internal pages: %s", err) - fmt.Println(err.Error()) - return + logger.Errorf("Failed to set up internal pages, server cannot start: %s", err) + fmt.Printf("Failed to set up internal pages, server cannot start: %s", err) + os.Exit(1) } case "external": @@ -236,15 +237,13 @@ func setupPages(cfg *config.Config, r *touka.Engine) { default: // 处理无效的Pages Mode - logWarning("Invalid Pages Mode: %s, using default embedded theme", cfg.Pages.Mode) - + logger.Warnf("Invalid Pages Mode: %s, using default embedded theme", cfg.Pages.Mode) err := setInternalRoute(cfg, r) if err != nil { - logError("Failed when processing internal pages: %s", err) - fmt.Println(err.Error()) - return + logger.Errorf("Failed to set up internal pages, server cannot start: %s", err) + fmt.Printf("Failed to set up internal pages, server cannot start: %s", err) + os.Exit(1) } - } } @@ -266,11 +265,9 @@ func viaHeader() func(c *touka.Context) { } func setInternalRoute(cfg *config.Config, r *touka.Engine) error { - // 加载嵌入式资源 pages, assets, err := loadEmbeddedPages(cfg) if err != nil { - logError("Failed when processing pages: %s", err) return err } @@ -288,13 +285,13 @@ func init() { readFlag() flag.Parse() - // 如果设置了 -h,则显示帮助信息并退出 + // 如果设置了 -h, 则显示帮助信息并退出 if showHelp { flag.Usage() os.Exit(0) } - // 如果设置了 -v,则显示版本号并退出 + // 如果设置了 -v, 则显示版本号并退出 if showVersion { fmt.Printf("GHProxy Version: %s \n", version) os.Exit(0) @@ -303,6 +300,7 @@ func init() { loadConfig() if cfg != nil { // 在setupLogger前添加空值检查 setupLogger(cfg) + initializeErrorPages() InitReq(cfg) setMemLimit(cfg) loadlist(cfg) @@ -317,7 +315,7 @@ func init() { } if cfg.Server.Debug { - version = "Dev" // 如果是Debug模式,版本设置为"Dev" + version = "Dev" // 如果是Debug模式, 版本设置为"Dev" } } } @@ -492,7 +490,7 @@ func main() { addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port) err := r.RunShutdown(addr) if err != nil { - logError("Server Run Error: %v", err) + logger.Errorf("Server Run Error: %v", err) fmt.Printf("Server Run Error: %v\n", err) } From a9b3f6b9723d142b605fb9730075946184755884 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sun, 27 Jul 2025 15:33:03 +0800 Subject: [PATCH 79/99] refine oci image proxy default target --- proxy/docker.go | 107 ++++++++++++++++++++++-------------------------- 1 file changed, 50 insertions(+), 57 deletions(-) diff --git a/proxy/docker.go b/proxy/docker.go index 4712ae3..3287342 100644 --- a/proxy/docker.go +++ b/proxy/docker.go @@ -43,42 +43,57 @@ func InitWeakCache() *weakcache.Cache[string] { // 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") // 镜像文件路径 + // 从 main.go 中固定的路由 "/v2/:target/:user/:repo/*filepath" 获取参数 + 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 + var upstreamTarget string + var requestPath string + var imageNameForAuth string - // 根据 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 - } + // 关键逻辑: 判断 reqTarget 是真实主机名还是镜像名的一部分 + // 依据: 真实主机名/IP通常包含'.'或':' + if strings.Contains(reqTarget, ".") || strings.Contains(reqTarget, ":") { + // 情况 A: reqTarget 是一个显式指定的主机名 (例如 "ghcr.io", "my-registry.com", "127.0.0.1:5000") + c.Debugf("Request target '%s' identified as an explicit hostname.", reqTarget) + upstreamTarget = reqTarget + // 上游请求的路径是主机名之后的部分 + requestPath = fmt.Sprintf("%s/%s%s", reqImageUser, reqImageName, reqFilePath) + // 用于认证的镜像名是 user/repo + imageNameForAuth = fmt.Sprintf("%s/%s", reqImageUser, reqImageName) + } else { + // 情况 B: reqTarget 是镜像名的一部分 (例如 "wjqserver", "library") + c.Debugf("Request target '%s' identified as part of an image name. Using default registry.", reqTarget) + // 使用配置文件中的默认目标 + switch cfg.Docker.Target { + case "ghcr": + upstreamTarget = ghcrTarget + case "dockerhub": + upstreamTarget = dockerhubTarget + case "": + ErrorPage(c, NewErrorWithStatusLookup(500, "Default Docker Target is not configured in config file")) + return + default: + upstreamTarget = cfg.Docker.Target } + // 必须将路由错误分割的所有部分重新组合成完整的镜像路径 + requestPath = fmt.Sprintf("%s/%s/%s%s", reqTarget, reqImageUser, reqImageName, reqFilePath) + // 用于认证的镜像名是 target/user (例如 "wjqserver/ghproxy", "library/ubuntu") + imageNameForAuth = fmt.Sprintf("%s/%s", reqTarget, reqImageUser) } - // 封装镜像信息 + // 清理路径, 防止出现 "//" + requestPath = strings.TrimPrefix(requestPath, "/") + + // 为认证和缓存准备镜像信息 image := &imageInfo{ - User: reqImageUser, - Repo: reqImageName, - Image: fmt.Sprintf("%s/%s", reqImageUser, reqImageName), + Image: imageNameForAuth, } // 调用 GhcrToTarget 处理实际的代理请求 - GhcrToTarget(c, cfg, target, path, image) + GhcrToTarget(c, cfg, upstreamTarget, requestPath, image) } } @@ -90,39 +105,17 @@ func GhcrToTarget(c *touka.Context, cfg *config.Config, target string, path stri 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) + // 构造目标URL. 这里的target和path都是由GhcrWithImageRouting正确解析得来的. + destUrl := "https://" + target + "/v2/" + path + if query := c.GetReqQueryString(); query != "" { + destUrl += "?" + query } + c.Debugf("Proxying to target '%s' with path '%s'. Final URL: %s", target, path, destUrl) // 执行实际的代理请求 - GhcrRequest(ctx, c, destUrl, image, cfg, upstreamTarget) + GhcrRequest(ctx, c, destUrl, image, cfg, target) } // GhcrRequest 执行对Docker注册表的HTTP请求, 处理认证和重定向 @@ -166,7 +159,7 @@ func GhcrRequest(ctx context.Context, c *touka.Context, u string, image *imageIn req.Header.Set("Host", target) // 尝试从缓存中获取并使用认证令牌 - if image != nil { + if image != nil && image.Image != "" { token, exist := cache.Get(image.Image) if exist { req.Header.Set("Authorization", "Bearer "+token) @@ -188,7 +181,7 @@ func GhcrRequest(ctx context.Context, c *touka.Context, u string, image *imageIn c.Debugf("Initial request failed with status %d. Retry eligibility: %t", originalStatusCode, shouldRetry) if shouldRetry { - if image == nil { + if image == nil || image.Image == "" { _ = resp.Body.Close() // 终止流程, 关闭当前响应体 ErrorPage(c, NewErrorWithStatusLookup(originalStatusCode, "Unauthorized")) return From 08bae46742d37c2a06d2f241869c37fbd02b189e Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sun, 27 Jul 2025 15:35:18 +0800 Subject: [PATCH 80/99] 4.2.3-rc.0 --- CHANGELOG.md | 6 ++++++ DEV-VERSION | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 525bb62..8d94ef5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # 更新日志 +4.2.3-rc.0 - 2025-07-27 +--- +- PRE-RELEASE: v4.2.2-rc.0是v4.2.2预发布版本,请勿在生产环境中使用; +- CHANGE: 改进错误页面加载器, 避免在选择`external`模式时错误页面渲染回退到json输出 +- CHANGE: 完善OCI(Docker)镜像代理默认target逻辑 + 4.2.2 - 2025-07-25 --- - CHANGE: 重构OCI镜像代理部分, 完善对`ghcr`,`gcr`,`k8s.gcr`等上游源特殊处理的适配 diff --git a/DEV-VERSION b/DEV-VERSION index c6cdc65..f435fb6 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -4.2.2-rc.0 \ No newline at end of file +4.2.3-rc.0 \ No newline at end of file From 4df21fd258b1e7865d4f114ffdd88d7f9edbba91 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sun, 27 Jul 2025 15:46:30 +0800 Subject: [PATCH 81/99] 4.2.3 --- CHANGELOG.md | 7 ++++++- VERSION | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d94ef5..031cc7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,13 @@ # 更新日志 +4.2.3 - 2025-07-27 +--- +- CHANGE: 改进错误页面加载器, 避免在选择`external`模式时错误页面渲染回退到json输出 +- CHANGE: 完善OCI(Docker)镜像代理默认target逻辑 + 4.2.3-rc.0 - 2025-07-27 --- -- PRE-RELEASE: v4.2.2-rc.0是v4.2.2预发布版本,请勿在生产环境中使用; +- PRE-RELEASE: v4.2.3-rc.0是v4.2.3预发布版本,请勿在生产环境中使用; - CHANGE: 改进错误页面加载器, 避免在选择`external`模式时错误页面渲染回退到json输出 - CHANGE: 完善OCI(Docker)镜像代理默认target逻辑 diff --git a/VERSION b/VERSION index 078bf8b..ec87108 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.2.2 \ No newline at end of file +4.2.3 \ No newline at end of file From d232d1cf12f11126f8577adb4fb5218d79cdc2d7 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Tue, 29 Jul 2025 23:27:42 +0800 Subject: [PATCH 82/99] refine matcher: will not match releases page --- .github/workflows/build-dev.yml | 6 ++++++ .github/workflows/build.yml | 6 ++++++ CHANGELOG.md | 5 +++++ DEV-VERSION | 2 +- go.mod | 15 ++++++--------- go.sum | 22 ++++++++++------------ main.go | 2 +- proxy/error.go | 1 - proxy/match.go | 12 +++++++++++- proxy/matcher_test.go | 12 ++++++++++++ 10 files changed, 58 insertions(+), 25 deletions(-) diff --git a/.github/workflows/build-dev.yml b/.github/workflows/build-dev.yml index 24e61c7..6ad8d27 100644 --- a/.github/workflows/build-dev.yml +++ b/.github/workflows/build-dev.yml @@ -68,6 +68,12 @@ jobs: uses: actions/setup-go@v3 with: go-version: ${{ env.GO_VERSION }} + - name: 测试 + env: + GOOS: ${{ matrix.goos }} + GOARCH: ${{ matrix.goarch }} + run: | + go test . - name: 编译 env: GOOS: ${{ matrix.goos }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f847575..2d0228e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -69,6 +69,12 @@ jobs: uses: actions/setup-go@v3 with: go-version: ${{ env.GO_VERSION }} + - name: 测试 + env: + GOOS: ${{ matrix.goos }} + GOARCH: ${{ matrix.goarch }} + run: | + go test . - name: 编译 env: GOOS: ${{ matrix.goos }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 031cc7c..0fb46d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 更新日志 +4.2.4-rc.0 - 2025-07-29 +--- +- PRE-RELEASE: v4.2.4-rc.0是v4.2.4预发布版本,请勿在生产环境中使用; +- CHANGE: 改进匹配器, 防止匹配不应匹配的内容 + 4.2.3 - 2025-07-27 --- - CHANGE: 改进错误页面加载器, 避免在选择`external`模式时错误页面渲染回退到json输出 diff --git a/DEV-VERSION b/DEV-VERSION index f435fb6..4c573d6 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -4.2.3-rc.0 \ No newline at end of file +4.2.4-rc.0 \ No newline at end of file diff --git a/go.mod b/go.mod index 2e73766..2635310 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.24.5 require ( github.com/BurntSushi/toml v1.5.0 - github.com/WJQSERVER-STUDIO/httpc v0.8.1 + github.com/WJQSERVER-STUDIO/httpc v0.8.2 golang.org/x/net v0.42.0 golang.org/x/time v0.12.0 ) @@ -15,15 +15,12 @@ require ( github.com/fenthope/bauth v0.0.1 github.com/fenthope/ikumi v0.0.2 github.com/fenthope/ipfilter v0.0.1 - github.com/fenthope/reco v0.0.3 - github.com/fenthope/record v0.0.3 - github.com/go-json-experiment/json v0.0.0-20250714165856-be8212f5270d + github.com/fenthope/reco v0.0.4 + github.com/fenthope/record v0.0.4 + github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2 github.com/hashicorp/golang-lru/v2 v2.0.7 - github.com/infinite-iroha/touka v0.3.3 + github.com/infinite-iroha/touka v0.3.4 github.com/wjqserver/modembed v0.0.1 ) -require ( - github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.6 // indirect - github.com/valyala/bytebufferpool v1.0.0 // indirect -) +require github.com/valyala/bytebufferpool v1.0.0 // indirect diff --git a/go.sum b/go.sum index 9a5bf1d..f6f4f93 100644 --- a/go.sum +++ b/go.sum @@ -1,29 +1,27 @@ github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.6 h1:/50VJYXd6jcu+p5BnEBDyiX0nAyGxas1W3DCnrYMxMY= -github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.6/go.mod h1:FZ6XE+4TKy4MOfX1xWKe6Rwsg0ucYFCdNh1KLvyKTfc= github.com/WJQSERVER-STUDIO/go-utils/iox v0.0.2 h1:AiIHXP21LpK7pFfqUlUstgQEWzjbekZgxOuvVwiMfyM= github.com/WJQSERVER-STUDIO/go-utils/iox v0.0.2/go.mod h1:mCLqYU32bTmEE6dpj37MKKiZgz70Jh/xyK9vVbq6pok= 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.2 h1:PFPLodV0QAfGEP6915J57vIqoKu9cGuuiXG/7C9TNUk= +github.com/WJQSERVER-STUDIO/httpc v0.8.2/go.mod h1:8WhHVRO+olDFBSvL5PC/bdMkb6U3vRdPJ4p4pnguV5Y= 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= github.com/fenthope/ikumi v0.0.2/go.mod h1:IYbxzOGndZv/yRrbVMyV6dxh06X2wXCbfxrTRM1IruU= github.com/fenthope/ipfilter v0.0.1 h1:HrYAyixCMvsDAz36GRyFfyCNtrgYwzrhMcY0XV7fGcM= github.com/fenthope/ipfilter v0.0.1/go.mod h1:QfY0GrpG0D82HROgdH4c9eog4js42ghLIfl/iM4MvvY= -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/fenthope/reco v0.0.4 h1:yo2g3aWwdoMpaZWZX4SdZOW7mCK82RQIU/YI8ZUQThM= +github.com/fenthope/reco v0.0.4/go.mod h1:eMyS8HpdMVdJ/2WJt6Cvt8P1EH9Igzj5lSJrgc+0jeg= +github.com/fenthope/record v0.0.4 h1:/1JHNCxiXGLL/qCh4LEGaAvhj4CcKsb6siTxjLmjdO4= +github.com/fenthope/record v0.0.4/go.mod h1:G0a6KCiCDyX2SsC3nfzSN651fJKxH482AyJvzlnvAJU= +github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2 h1:iizUGZ9pEquQS5jTGkh4AqeeHCMbfbjeb0zMt0aEFzs= +github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2/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.3 h1:6Vy36bYjtbGKaBNiZBRcTne9Lcx8QTE6rpHqyMb3oiA= -github.com/infinite-iroha/touka v0.3.3/go.mod h1:9Y/MWlvlBL/8cqA+2ZUsnBr4h3f7yo3nOxsegIcBduw= +github.com/infinite-iroha/touka v0.3.4 h1:vYDjfXTkjpTe7tasSVbPeVAPSXzd/wS1T2tkiMx/Wwk= +github.com/infinite-iroha/touka v0.3.4/go.mod h1:xOKkEKTWYLHIBW6qbL2O6nSAO0RyDLsVXPtJxFYg/YM= 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= diff --git a/main.go b/main.go index 65a6e31..269727a 100644 --- a/main.go +++ b/main.go @@ -394,7 +394,7 @@ func main() { setupPages(cfg, r) r.SetRedirectTrailingSlash(false) - r.GET("/github.com/:user/:repo/releases/*filepath", func(c *touka.Context) { + r.GET("/github.com/:user/:repo/releases/download/*filepath", func(c *touka.Context) { c.Set("matcher", "releases") proxy.RoutingHandler(cfg)(c) }) diff --git a/proxy/error.go b/proxy/error.go index 222f382..79f38ab 100644 --- a/proxy/error.go +++ b/proxy/error.go @@ -21,7 +21,6 @@ func HandleError(c *touka.Context, message string) { } func UnifiedToukaErrorHandler(c *touka.Context, code int, err error) { - errMsg := "" if err != nil { errMsg = err.Error() diff --git a/proxy/match.go b/proxy/match.go index a50d018..b342f88 100644 --- a/proxy/match.go +++ b/proxy/match.go @@ -22,6 +22,10 @@ var ( apiPrefixLen int ) +var ( + releasesDownloadSinnipt = "releases/download/" +) + func init() { githubPrefixLen = len(githubPrefix) rawPrefixLen = len(rawPrefix) @@ -61,7 +65,13 @@ func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHPro } var matcher string switch action { - case "releases", "archive": + case "releases": + if strings.HasPrefix(remaining, releasesDownloadSinnipt) { + matcher = "releases" + } else { + return "", "", "", NewErrorWithStatusLookup(400, "malformed github path: not a releases download url") + } + case "archive": matcher = "releases" case "blob": matcher = "blob" diff --git a/proxy/matcher_test.go b/proxy/matcher_test.go index 0c35381..4b260c2 100644 --- a/proxy/matcher_test.go +++ b/proxy/matcher_test.go @@ -38,6 +38,18 @@ func TestMatcher_Compatibility(t *testing.T) { config: cfgWithAuth, expectedUser: "owner", expectedRepo: "repo", expectedMatcher: "releases", }, + { + name: "GH Releases Path Page", + rawPath: "https://github.com/owner/repo/releases", + config: cfgWithAuth, + expectError: true, expectedErrCode: 400, + }, + { + name: "GH Releases Path Tag Page", + rawPath: "https://github.com/owner/repo/releases/tag/v0.0.1", + config: cfgWithAuth, + expectError: true, expectedErrCode: 400, + }, { name: "GH Archive Path", rawPath: "https://github.com/owner/repo.git/archive/main.zip", From 0c04bb1355617bbe76bd01d61947a8e90c8fa847 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Tue, 29 Jul 2025 23:40:48 +0800 Subject: [PATCH 83/99] fix typo & update test workflow --- .github/workflows/build-dev.yml | 2 +- .github/workflows/build.yml | 2 +- proxy/match.go | 16 ++++++++-------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build-dev.yml b/.github/workflows/build-dev.yml index 6ad8d27..56c7a0f 100644 --- a/.github/workflows/build-dev.yml +++ b/.github/workflows/build-dev.yml @@ -73,7 +73,7 @@ jobs: GOOS: ${{ matrix.goos }} GOARCH: ${{ matrix.goarch }} run: | - go test . + go test ./.. - name: 编译 env: GOOS: ${{ matrix.goos }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2d0228e..071114f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -74,7 +74,7 @@ jobs: GOOS: ${{ matrix.goos }} GOARCH: ${{ matrix.goarch }} run: | - go test . + go test ./.. - name: 编译 env: GOOS: ${{ matrix.goos }} diff --git a/proxy/match.go b/proxy/match.go index b342f88..3f5f87f 100644 --- a/proxy/match.go +++ b/proxy/match.go @@ -10,11 +10,6 @@ import ( ) var ( - githubPrefix = "https://github.com/" - rawPrefix = "https://raw.githubusercontent.com/" - gistPrefix = "https://gist.github.com/" - gistContentPrefix = "https://gist.githubusercontent.com/" - apiPrefix = "https://api.github.com/" githubPrefixLen int rawPrefixLen int gistPrefixLen int @@ -22,8 +17,13 @@ var ( apiPrefixLen int ) -var ( - releasesDownloadSinnipt = "releases/download/" +const ( + githubPrefix = "https://github.com/" + rawPrefix = "https://raw.githubusercontent.com/" + gistPrefix = "https://gist.github.com/" + gistContentPrefix = "https://gist.githubusercontent.com/" + apiPrefix = "https://api.github.com/" + releasesDownloadSnippet = "releases/download/" ) func init() { @@ -66,7 +66,7 @@ func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHPro var matcher string switch action { case "releases": - if strings.HasPrefix(remaining, releasesDownloadSinnipt) { + if strings.HasPrefix(remaining, releasesDownloadSnippet) { matcher = "releases" } else { return "", "", "", NewErrorWithStatusLookup(400, "malformed github path: not a releases download url") From d7d3e1ca6532cd312faad752ec0c18047a2a94ac Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Tue, 29 Jul 2025 23:42:19 +0800 Subject: [PATCH 84/99] 4.2.4 --- CHANGELOG.md | 4 ++++ VERSION | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fb46d0..7eb963c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 更新日志 +4.2.4 - 2025-07-29 +--- +- CHANGE: 改进匹配器, 防止匹配不应匹配的内容 + 4.2.4-rc.0 - 2025-07-29 --- - PRE-RELEASE: v4.2.4-rc.0是v4.2.4预发布版本,请勿在生产环境中使用; diff --git a/VERSION b/VERSION index ec87108..74ecad8 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.2.3 \ No newline at end of file +4.2.4 \ No newline at end of file From 44f28e593a587fa64c249e214672989b51492233 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Tue, 29 Jul 2025 23:45:26 +0800 Subject: [PATCH 85/99] remove test --- .github/workflows/build-dev.yml | 6 ------ .github/workflows/build.yml | 6 ------ VERSION | 2 +- 3 files changed, 1 insertion(+), 13 deletions(-) diff --git a/.github/workflows/build-dev.yml b/.github/workflows/build-dev.yml index 56c7a0f..24e61c7 100644 --- a/.github/workflows/build-dev.yml +++ b/.github/workflows/build-dev.yml @@ -68,12 +68,6 @@ jobs: uses: actions/setup-go@v3 with: go-version: ${{ env.GO_VERSION }} - - name: 测试 - env: - GOOS: ${{ matrix.goos }} - GOARCH: ${{ matrix.goarch }} - run: | - go test ./.. - name: 编译 env: GOOS: ${{ matrix.goos }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 071114f..f847575 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -69,12 +69,6 @@ jobs: uses: actions/setup-go@v3 with: go-version: ${{ env.GO_VERSION }} - - name: 测试 - env: - GOOS: ${{ matrix.goos }} - GOARCH: ${{ matrix.goarch }} - run: | - go test ./.. - name: 编译 env: GOOS: ${{ matrix.goos }} diff --git a/VERSION b/VERSION index 74ecad8..cf78d5b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.2.4 \ No newline at end of file +4.2.4 From 208ce8a4f94d3d2470bef58896ee283fbcc5fe32 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Thu, 31 Jul 2025 20:01:03 +0800 Subject: [PATCH 86/99] 4.2.5 --- CHANGELOG.md | 4 ++ VERSION | 2 +- main.go | 5 ++ proxy/match.go | 133 ++++++++++++++++++++---------------------- proxy/matcher_test.go | 8 ++- 5 files changed, 80 insertions(+), 72 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7eb963c..ff99f7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 更新日志 +4.2.5 - 2025-07-31 +--- +- CHANGE: 进一步完善匹配器, 兼容更多情况 + 4.2.4 - 2025-07-29 --- - CHANGE: 改进匹配器, 防止匹配不应匹配的内容 diff --git a/VERSION b/VERSION index cf78d5b..ad35fe0 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.2.4 +4.2.5 \ No newline at end of file diff --git a/main.go b/main.go index 269727a..3313829 100644 --- a/main.go +++ b/main.go @@ -399,6 +399,11 @@ func main() { proxy.RoutingHandler(cfg)(c) }) + r.GET("/github.com/:user/:repo/releases/:tag/download/*filepath", func(c *touka.Context) { + c.Set("matcher", "releases") + proxy.RoutingHandler(cfg)(c) + }) + r.GET("/github.com/:user/:repo/archive/*filepath", func(c *touka.Context) { c.Set("matcher", "releases") proxy.RoutingHandler(cfg)(c) diff --git a/proxy/match.go b/proxy/match.go index 3f5f87f..9a37f0a 100644 --- a/proxy/match.go +++ b/proxy/match.go @@ -42,37 +42,62 @@ func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHPro // 匹配 "https://github.com/" if strings.HasPrefix(rawPath, githubPrefix) { - remaining := rawPath[githubPrefixLen:] - i := strings.IndexByte(remaining, '/') + pathAfterDomain := rawPath[githubPrefixLen:] + + // 解析 user + i := strings.IndexByte(pathAfterDomain, '/') if i <= 0 { return "", "", "", NewErrorWithStatusLookup(400, "malformed github path: missing user") } - user := remaining[:i] - remaining = remaining[i+1:] - i = strings.IndexByte(remaining, '/') + user := pathAfterDomain[:i] + pathAfterUser := pathAfterDomain[i+1:] + + // 解析 repo + i = strings.IndexByte(pathAfterUser, '/') if i <= 0 { - return "", "", "", NewErrorWithStatusLookup(400, "malformed github path: missing repo") - } - repo := remaining[:i] - remaining = remaining[i+1:] - if len(remaining) == 0 { return "", "", "", NewErrorWithStatusLookup(400, "malformed github path: missing action") } - i = strings.IndexByte(remaining, '/') - action := remaining - if i != -1 { - action = remaining[:i] + repo := pathAfterUser[:i] + pathAfterRepo := pathAfterUser[i+1:] + + if len(pathAfterRepo) == 0 { + return "", "", "", NewErrorWithStatusLookup(400, "malformed github path: missing action") } + + // 优先处理所有 "releases" 相关的下载路径 + if strings.HasPrefix(pathAfterRepo, "releases/") { + // 情况 A: "releases/download/..." + if strings.HasPrefix(pathAfterRepo, "releases/download/") { + return user, repo, "releases", nil + } + // 情况 B: "releases/:tag/download/..." + pathAfterReleases := pathAfterRepo[len("releases/"):] + slashIndex := strings.IndexByte(pathAfterReleases, '/') + if slashIndex > 0 { // 确保tag不为空 + pathAfterTag := pathAfterReleases[slashIndex+1:] + if strings.HasPrefix(pathAfterTag, "download/") { + return user, repo, "releases", nil + } + } + // 如果不满足上述下载链接的结构, 则为网页浏览路径, 予以拒绝 + return "", "", "", NewErrorWithStatusLookup(400, "unsupported releases page, only download links are allowed") + } + + // 检查 "archive/" 路径 + if strings.HasPrefix(pathAfterRepo, "archive/") { + // 根据测试用例, archive路径的matcher也应为releases + return user, repo, "releases", nil + } + + // 如果不是下载路径, 则解析action并进行分类 + i = strings.IndexByte(pathAfterRepo, '/') + action := pathAfterRepo + if i != -1 { + action = pathAfterRepo[:i] + } + var matcher string switch action { - case "releases": - if strings.HasPrefix(remaining, releasesDownloadSnippet) { - matcher = "releases" - } else { - return "", "", "", NewErrorWithStatusLookup(400, "malformed github path: not a releases download url") - } - case "archive": - matcher = "releases" case "blob": matcher = "blob" case "raw": @@ -88,59 +113,27 @@ func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHPro // 匹配 "https://raw.githubusercontent.com/" if strings.HasPrefix(rawPath, rawPrefix) { remaining := rawPath[rawPrefixLen:] - // 这里的逻辑与 github.com 的类似, 需要提取 user, repo, branch, file... - // 我们只需要 user 和 repo - i := strings.IndexByte(remaining, '/') - if i <= 0 { - return "", "", "", NewErrorWithStatusLookup(400, "malformed raw url: missing user") + parts := strings.SplitN(remaining, "/", 3) + if len(parts) < 3 { + return "", "", "", NewErrorWithStatusLookup(400, "malformed raw url: path too short") } - user := remaining[:i] - remaining = remaining[i+1:] - i = strings.IndexByte(remaining, '/') - if i <= 0 { - return "", "", "", NewErrorWithStatusLookup(400, "malformed raw url: missing repo") - } - repo := remaining[:i] - // raw 链接至少需要 user/repo/branch 三部分 - remaining = remaining[i+1:] - if len(remaining) == 0 { - return "", "", "", NewErrorWithStatusLookup(400, "malformed raw url: missing branch/commit") - } - return user, repo, "raw", nil + return parts[0], parts[1], "raw", nil } - // 匹配 "https://gist.github.com/" - if strings.HasPrefix(rawPath, gistPrefix) { - remaining := rawPath[gistPrefixLen:] - i := strings.IndexByte(remaining, '/') - if i <= 0 { - // case: https://gist.github.com/user - // 这种情况下, gist_id 缺失, 但我们仍然可以认为 user 是有效的 - if len(remaining) > 0 { - return remaining, "", "gist", nil - } + // 匹配 "https://gist.github.com/" 或 "https://gist.githubusercontent.com/" + isGist := strings.HasPrefix(rawPath, gistPrefix) + if isGist || strings.HasPrefix(rawPath, gistContentPrefix) { + var remaining string + if isGist { + remaining = rawPath[gistPrefixLen:] + } else { + remaining = rawPath[gistContentPrefixLen:] + } + parts := strings.SplitN(remaining, "/", 2) + if len(parts) == 0 || parts[0] == "" { return "", "", "", NewErrorWithStatusLookup(400, "malformed gist url: missing user") } - // case: https://gist.github.com/user/gist_id... - user := remaining[:i] - return user, "", "gist", nil - } - - // 匹配 "https://gist.githubusercontent.com/" - if strings.HasPrefix(rawPath, gistContentPrefix) { - remaining := rawPath[gistContentPrefixLen:] - i := strings.IndexByte(remaining, '/') - if i <= 0 { - // case: https://gist.githubusercontent.com/user - // 这种情况下, gist_id 缺失, 但我们仍然可以认为 user 是有效的 - if len(remaining) > 0 { - return remaining, "", "gist", nil - } - return "", "", "", NewErrorWithStatusLookup(400, "malformed gist url: missing user") - } - // case: https://gist.githubusercontent.com/user/gist_id... - user := remaining[:i] - return user, "", "gist", nil + return parts[0], "", "gist", nil } // 匹配 "https://api.github.com/" diff --git a/proxy/matcher_test.go b/proxy/matcher_test.go index 4b260c2..07f3e4a 100644 --- a/proxy/matcher_test.go +++ b/proxy/matcher_test.go @@ -33,11 +33,17 @@ func TestMatcher_Compatibility(t *testing.T) { expectedErrCode int }{ { - name: "GH Releases Path", + name: "GH Releases Path 1", rawPath: "https://github.com/owner/repo/releases/download/v1.0/asset.zip", config: cfgWithAuth, expectedUser: "owner", expectedRepo: "repo", expectedMatcher: "releases", }, + { + name: "GH Releases Path 2", + rawPath: "https://github.com/owner/repo/releases/v1.0/download/asset.zip", + config: cfgWithAuth, + expectedUser: "owner", expectedRepo: "repo", expectedMatcher: "releases", + }, { name: "GH Releases Path Page", rawPath: "https://github.com/owner/repo/releases", From 97ee25b65dffd1468ceb03a4e7f21b1367fb1805 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Fri, 1 Aug 2025 08:42:40 +0800 Subject: [PATCH 87/99] fix matcher(4.2.6) --- CHANGELOG.md | 4 ++++ VERSION | 2 +- main.go | 41 ++++++++++++++++++++++++++++++++++------- 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff99f7b..a0c8786 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 更新日志 +4.2.6 - 2025-08-01 +--- +- CHANGE: 修正匹配器 + 4.2.5 - 2025-07-31 --- - CHANGE: 进一步完善匹配器, 兼容更多情况 diff --git a/VERSION b/VERSION index ad35fe0..0ce756d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.2.5 \ No newline at end of file +4.2.6 \ No newline at end of file diff --git a/main.go b/main.go index 3313829..078a1b5 100644 --- a/main.go +++ b/main.go @@ -8,6 +8,7 @@ import ( "net/http" "os" "runtime/debug" + "strings" "time" "ghproxy/api" @@ -394,14 +395,40 @@ func main() { setupPages(cfg, r) r.SetRedirectTrailingSlash(false) - r.GET("/github.com/:user/:repo/releases/download/*filepath", func(c *touka.Context) { - c.Set("matcher", "releases") - proxy.RoutingHandler(cfg)(c) - }) + r.GET("/github.com/:user/:repo/releases/*filepath", func(c *touka.Context) { + // 规范化路径: 移除前导斜杠, 简化后续处理 + filepath := c.Param("filepath") + if len(filepath) > 0 && filepath[0] == '/' { + filepath = filepath[1:] + } - r.GET("/github.com/:user/:repo/releases/:tag/download/*filepath", func(c *touka.Context) { - c.Set("matcher", "releases") - proxy.RoutingHandler(cfg)(c) + isValidDownload := false + + // 检查两种合法的下载链接格式 + // 情况 A: "download/..." + if strings.HasPrefix(filepath, "download/") { + isValidDownload = true + } else { + // 情况 B: ":tag/download/..." + slashIndex := strings.IndexByte(filepath, '/') + // 确保 tag 部分存在 (slashIndex > 0) + if slashIndex > 0 { + pathAfterTag := filepath[slashIndex+1:] + if strings.HasPrefix(pathAfterTag, "download/") { + isValidDownload = true + } + } + } + + // 根据匹配结果执行最终操作 + if isValidDownload { + c.Set("matcher", "releases") + proxy.RoutingHandler(cfg)(c) + } else { + // 任何不符合下载链接格式的 'releases' 路径都被视为浏览页面并拒绝 + proxy.ErrorPage(c, proxy.NewErrorWithStatusLookup(400, "unsupported releases page, only download links are allowed")) + return + } }) r.GET("/github.com/:user/:repo/archive/*filepath", func(c *touka.Context) { From 8dca51b897a425a24340cedbc99e433f4660a2bd Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Mon, 4 Aug 2025 12:12:32 +0800 Subject: [PATCH 88/99] 4.2.7 --- CHANGELOG.md | 5 +++++ VERSION | 2 +- go.mod | 2 +- go.sum | 4 ++-- proxy/docker.go | 8 +++++++- 5 files changed, 16 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0c8786..9a9638b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 更新日志 +4.2.7 - 2025-08-04 +--- +- CHANGE: 在OCI镜像(docker)代理部分增加特殊处理, 保证可用性 参看[#159](https://github.com/WJQSERVER-STUDIO/ghproxy/issues/159) +- CHANGE: 更新Touka框架, 同步解决部分日志过多问题 + 4.2.6 - 2025-08-01 --- - CHANGE: 修正匹配器 diff --git a/VERSION b/VERSION index 0ce756d..c30a815 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.2.6 \ No newline at end of file +4.2.7 \ No newline at end of file diff --git a/go.mod b/go.mod index 2635310..a267aaf 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/fenthope/record v0.0.4 github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2 github.com/hashicorp/golang-lru/v2 v2.0.7 - github.com/infinite-iroha/touka v0.3.4 + github.com/infinite-iroha/touka v0.3.6 github.com/wjqserver/modembed v0.0.1 ) diff --git a/go.sum b/go.sum index f6f4f93..d84ed19 100644 --- a/go.sum +++ b/go.sum @@ -20,8 +20,8 @@ github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2 h1:iizUGZ9 github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2/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.4 h1:vYDjfXTkjpTe7tasSVbPeVAPSXzd/wS1T2tkiMx/Wwk= -github.com/infinite-iroha/touka v0.3.4/go.mod h1:xOKkEKTWYLHIBW6qbL2O6nSAO0RyDLsVXPtJxFYg/YM= +github.com/infinite-iroha/touka v0.3.6 h1:SkpM/VFGCWOFQP3RRuoWdX/Q4zafPngG1VMwkrLwtkw= +github.com/infinite-iroha/touka v0.3.6/go.mod h1:XW7a3fpLAjJfylSmdNuDQ8wGKkKmLVi9V/89sT1d7uw= 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= diff --git a/proxy/docker.go b/proxy/docker.go index 3287342..955f7e0 100644 --- a/proxy/docker.go +++ b/proxy/docker.go @@ -58,7 +58,13 @@ func GhcrWithImageRouting(cfg *config.Config) touka.HandlerFunc { if strings.Contains(reqTarget, ".") || strings.Contains(reqTarget, ":") { // 情况 A: reqTarget 是一个显式指定的主机名 (例如 "ghcr.io", "my-registry.com", "127.0.0.1:5000") c.Debugf("Request target '%s' identified as an explicit hostname.", reqTarget) - upstreamTarget = reqTarget + + // https://github.com/WJQSERVER-STUDIO/ghproxy/issues/159 + if reqTarget == "docker.io" { + upstreamTarget = dockerhubTarget + } else { + upstreamTarget = reqTarget + } // 上游请求的路径是主机名之后的部分 requestPath = fmt.Sprintf("%s/%s%s", reqImageUser, reqImageName, reqFilePath) // 用于认证的镜像名是 user/repo From d389a61f094795f7326b1d66fe694775b091fa26 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Mon, 11 Aug 2025 18:28:03 +0800 Subject: [PATCH 89/99] 4.3.0 --- .gitignore | 1 + CHANGELOG.md | 7 ++ DEV-VERSION | 2 +- VERSION | 2 +- config/config.go | 180 +++++++++++++++++++++++++----------------- go.mod | 5 +- go.sum | 6 +- main.go | 17 +--- proxy/docker.go | 199 +++++++++++++++++++++++++++++++++-------------- proxy/match.go | 9 ++- 10 files changed, 273 insertions(+), 155 deletions(-) diff --git a/.gitignore b/.gitignore index 6358c7d..b5c0aee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ demo demo.toml +demo.wanf *.log *.bak list.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a9638b..b3b7690 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # 更新日志 +4.3.0-rc.0 - 2025-08-11 +--- +- PRE-RELEASE: v4.3.0-rc.0是v4.3.0发布版本,请勿在生产环境中使用; +- CHANGE: 为OCI镜像(Docker)代理带来自动library附加功能 +- CHANGE(refactor): 改进OCI镜像(Docker)代理路径组成流程 +- ADD: 新增[WANF](https://github.com/WJQSERVER/wanf)配置文件格式支持 + 4.2.7 - 2025-08-04 --- - CHANGE: 在OCI镜像(docker)代理部分增加特殊处理, 保证可用性 参看[#159](https://github.com/WJQSERVER-STUDIO/ghproxy/issues/159) diff --git a/DEV-VERSION b/DEV-VERSION index 4c573d6..51ff7c7 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -4.2.4-rc.0 \ No newline at end of file +4.3.0-rc.0 \ No newline at end of file diff --git a/VERSION b/VERSION index c30a815..8191138 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.2.7 \ No newline at end of file +4.3.0 \ No newline at end of file diff --git a/config/config.go b/config/config.go index 3ea330d..035760e 100644 --- a/config/config.go +++ b/config/config.go @@ -1,25 +1,31 @@ package config import ( + "fmt" + "log" "os" + "path/filepath" "github.com/BurntSushi/toml" + + "github.com/WJQSERVER/wanf" ) +// Config 结构体定义了整个应用程序的配置 type Config struct { - Server ServerConfig `toml:"server"` - Httpc HttpcConfig `toml:"httpc"` - GitClone GitCloneConfig `toml:"gitclone"` - Shell ShellConfig `toml:"shell"` - Pages PagesConfig `toml:"pages"` - Log LogConfig `toml:"log"` - Auth AuthConfig `toml:"auth"` - Blacklist BlacklistConfig `toml:"blacklist"` - Whitelist WhitelistConfig `toml:"whitelist"` - IPFilter IPFilterConfig `toml:"ipFilter"` - RateLimit RateLimitConfig `toml:"rateLimit"` - Outbound OutboundConfig `toml:"outbound"` - Docker DockerConfig `toml:"docker"` + Server ServerConfig `toml:"server" wanf:"server"` + Httpc HttpcConfig `toml:"httpc" wanf:"httpc"` + GitClone GitCloneConfig `toml:"gitclone" wanf:"gitclone"` + Shell ShellConfig `toml:"shell" wanf:"shell"` + Pages PagesConfig `toml:"pages" wanf:"pages"` + Log LogConfig `toml:"log" wanf:"log"` + Auth AuthConfig `toml:"auth" wanf:"auth"` + Blacklist BlacklistConfig `toml:"blacklist" wanf:"blacklist"` + Whitelist WhitelistConfig `toml:"whitelist" wanf:"whitelist"` + IPFilter IPFilterConfig `toml:"ipFilter" wanf:"ipFilter"` + RateLimit RateLimitConfig `toml:"rateLimit" wanf:"rateLimit"` + Outbound OutboundConfig `toml:"outbound" wanf:"outbound"` + Docker DockerConfig `toml:"docker" wanf:"docker"` } /* @@ -32,13 +38,14 @@ cors = "*" # "*"/"" -> "*" ; "nil" -> "" ; debug = false */ +// ServerConfig 定义服务器相关的配置 type ServerConfig struct { - Port int `toml:"port"` - Host string `toml:"host"` - SizeLimit int `toml:"sizeLimit"` - MemLimit int64 `toml:"memLimit"` - Cors string `toml:"cors"` - Debug bool `toml:"debug"` + Port int `toml:"port" wanf:"port"` + Host string `toml:"host" wanf:"host"` + SizeLimit int `toml:"sizeLimit" wanf:"sizeLimit"` + MemLimit int64 `toml:"memLimit" wanf:"memLimit"` + Cors string `toml:"cors" wanf:"cors"` + Debug bool `toml:"debug" wanf:"debug"` } /* @@ -49,12 +56,13 @@ maxIdleConnsPerHost = 60 # only for advanced mode maxConnsPerHost = 0 # only for advanced mode useCustomRawHeaders = false */ +// HttpcConfig 定义 HTTP 客户端相关的配置 type HttpcConfig struct { - Mode string `toml:"mode"` - MaxIdleConns int `toml:"maxIdleConns"` - MaxIdleConnsPerHost int `toml:"maxIdleConnsPerHost"` - MaxConnsPerHost int `toml:"maxConnsPerHost"` - UseCustomRawHeaders bool `toml:"useCustomRawHeaders"` + Mode string `toml:"mode" wanf:"mode"` + MaxIdleConns int `toml:"maxIdleConns" wanf:"maxIdleConns"` + MaxIdleConnsPerHost int `toml:"maxIdleConnsPerHost" wanf:"maxIdleConnsPerHost"` + MaxConnsPerHost int `toml:"maxConnsPerHost" wanf:"maxConnsPerHost"` + UseCustomRawHeaders bool `toml:"useCustomRawHeaders" wanf:"useCustomRawHeaders"` } /* @@ -64,11 +72,12 @@ smartGitAddr = "http://127.0.0.1:8080" //cacheTimeout = 10 ForceH2C = true */ +// GitCloneConfig 定义 Git 克隆相关的配置 type GitCloneConfig struct { - Mode string `toml:"mode"` - SmartGitAddr string `toml:"smartGitAddr"` + Mode string `toml:"mode" wanf:"mode"` + SmartGitAddr string `toml:"smartGitAddr" wanf:"smartGitAddr"` //CacheTimeout int `toml:"cacheTimeout"` - ForceH2C bool `toml:"ForceH2C"` + ForceH2C bool `toml:"ForceH2C" wanf:"ForceH2C"` } /* @@ -76,9 +85,10 @@ type GitCloneConfig struct { editor = true rewriteAPI = false */ +// ShellConfig 定义 Shell 相关的配置 type ShellConfig struct { - Editor bool `toml:"editor"` - RewriteAPI bool `toml:"rewriteAPI"` + Editor bool `toml:"editor" wanf:"editor"` + RewriteAPI bool `toml:"rewriteAPI" wanf:"rewriteAPI"` } /* @@ -87,16 +97,18 @@ mode = "internal" # "internal" or "external" theme = "bootstrap" # "bootstrap" or "nebula" staticDir = "/data/www" */ +// PagesConfig 定义静态页面相关的配置 type PagesConfig struct { - Mode string `toml:"mode"` - Theme string `toml:"theme"` - StaticDir string `toml:"staticDir"` + Mode string `toml:"mode" wanf:"mode"` + Theme string `toml:"theme" wanf:"theme"` + StaticDir string `toml:"staticDir" wanf:"staticDir"` } +// LogConfig 定义日志相关的配置 type LogConfig struct { - LogFilePath string `toml:"logFilePath"` - MaxLogSize int64 `toml:"maxLogSize"` - Level string `toml:"level"` + LogFilePath string `toml:"logFilePath" wanf:"logFilePath"` + MaxLogSize int64 `toml:"maxLogSize" wanf:"maxLogSize"` + Level string `toml:"level" wanf:"level"` } /* @@ -109,31 +121,35 @@ passThrough = false ForceAllowApi = false ForceAllowApiPassList = false */ +// AuthConfig 定义认证相关的配置 type AuthConfig struct { - Enabled bool `toml:"enabled"` - Method string `toml:"method"` - Key string `toml:"key"` - Token string `toml:"token"` - PassThrough bool `toml:"passThrough"` - ForceAllowApi bool `toml:"ForceAllowApi"` - ForceAllowApiPassList bool `toml:"ForceAllowApiPassList"` + Enabled bool `toml:"enabled" wanf:"enabled"` + Method string `toml:"method" wanf:"method"` + Key string `toml:"key" wanf:"key"` + Token string `toml:"token" wanf:"token"` + PassThrough bool `toml:"passThrough" wanf:"passThrough"` + ForceAllowApi bool `toml:"ForceAllowApi" wanf:"ForceAllowApi"` + ForceAllowApiPassList bool `toml:"ForceAllowApiPassList" wanf:"ForceAllowApiPassList"` } +// BlacklistConfig 定义黑名单相关的配置 type BlacklistConfig struct { - Enabled bool `toml:"enabled"` - BlacklistFile string `toml:"blacklistFile"` + Enabled bool `toml:"enabled" wanf:"enabled"` + BlacklistFile string `toml:"blacklistFile" wanf:"blacklistFile"` } +// WhitelistConfig 定义白名单相关的配置 type WhitelistConfig struct { - Enabled bool `toml:"enabled"` - WhitelistFile string `toml:"whitelistFile"` + Enabled bool `toml:"enabled" wanf:"enabled"` + WhitelistFile string `toml:"whitelistFile" wanf:"whitelistFile"` } +// IPFilterConfig 定义 IP 过滤相关的配置 type IPFilterConfig struct { - Enabled bool `toml:"enabled"` - EnableAllowList bool `toml:"enableAllowList"` - EnableBlockList bool `toml:"enableBlockList"` - IPFilterFile string `toml:"ipFilterFile"` + Enabled bool `toml:"enabled" wanf:"enabled"` + EnableAllowList bool `toml:"enableAllowList" wanf:"enableAllowList"` + EnableBlockList bool `toml:"enableBlockList" wanf:"enableBlockList"` + IPFilterFile string `toml:"ipFilterFile" wanf:"ipFilterFile"` } /* @@ -150,19 +166,21 @@ burst = 10 singleBurst = "10mbps" */ +// RateLimitConfig 定义限速相关的配置 type RateLimitConfig struct { - Enabled bool `toml:"enabled"` - RatePerMinute int `toml:"ratePerMinute"` - Burst int `toml:"burst"` - BandwidthLimit BandwidthLimitConfig + Enabled bool `toml:"enabled" wanf:"enabled"` + RatePerMinute int `toml:"ratePerMinute" wanf:"ratePerMinute"` + Burst int `toml:"burst" wanf:"burst"` + BandwidthLimit BandwidthLimitConfig `toml:"bandwidthLimit" wanf:"bandwidthLimit"` } +// BandwidthLimitConfig 定义带宽限制相关的配置 type BandwidthLimitConfig struct { - Enabled bool `toml:"enabled"` - TotalLimit string `toml:"totalLimit"` - TotalBurst string `toml:"totalBurst"` - SingleLimit string `toml:"singleLimit"` - SingleBurst string `toml:"singleBurst"` + Enabled bool `toml:"enabled" wanf:"enabled"` + TotalLimit string `toml:"totalLimit" wanf:"totalLimit"` + TotalBurst string `toml:"totalBurst" wanf:"totalBurst"` + SingleLimit string `toml:"singleLimit" wanf:"singleLimit"` + SingleBurst string `toml:"singleBurst" wanf:"singleBurst"` } /* @@ -170,9 +188,10 @@ type BandwidthLimitConfig struct { enabled = false url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890" */ +// OutboundConfig 定义出站代理相关的配置 type OutboundConfig struct { - Enabled bool `toml:"enabled"` - Url string `toml:"url"` + Enabled bool `toml:"enabled" wanf:"enabled"` + Url string `toml:"url" wanf:"url"` } /* @@ -184,15 +203,16 @@ auth = false user1 = "testpass" test = "test123" */ +// DockerConfig 定义 Docker 相关的配置 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" wanf:"enabled"` + Target string `toml:"target" wanf:"target"` + Auth bool `toml:"auth" wanf:"auth"` + Credentials map[string]string `toml:"credentials" wanf:"credentials"` + AuthPassThrough bool `toml:"authPassThrough" wanf:"authPassThrough"` } -// LoadConfig 从 TOML 配置文件加载配置 +// LoadConfig 从配置文件加载配置 func LoadConfig(filePath string) (*Config, error) { if !FileExists(filePath) { // 楔入配置文件 @@ -202,15 +222,23 @@ func LoadConfig(filePath string) (*Config, error) { } return DefaultConfig(), nil } - var config Config + ext := filepath.Ext(filePath) + log.Printf("Loading config from %s with extension %s", filePath, ext) + if ext == ".wanf" { + if err := wanf.DecodeFile(filePath, &config); err != nil { + return nil, err + } + return &config, nil + } + if _, err := toml.DecodeFile(filePath, &config); err != nil { return nil, err } return &config, nil } -// 写入配置文件 +// WriteConfig 写入配置文件 func (c *Config) WriteConfig(filePath string) error { file, err := os.Create(filePath) if err != nil { @@ -218,17 +246,27 @@ func (c *Config) WriteConfig(filePath string) error { } defer file.Close() + ext := filepath.Ext(filePath) + fmt.Printf("%s", ext) + if ext == ".wanf" { + err := wanf.NewStreamEncoder(file).Encode(c) + if err != nil { + return err + } + return nil + } + encoder := toml.NewEncoder(file) return encoder.Encode(c) } -// 检测文件是否存在 +// FileExists 检测文件是否存在 func FileExists(filename string) bool { _, err := os.Stat(filename) return !os.IsNotExist(err) } -// 默认配置结构体 +// DefaultConfig 返回默认配置结构体 func DefaultConfig() *Config { return &Config{ Server: ServerConfig{ diff --git a/go.mod b/go.mod index a267aaf..84ecf8e 100644 --- a/go.mod +++ b/go.mod @@ -1,17 +1,18 @@ module ghproxy -go 1.24.5 +go 1.24.6 require ( github.com/BurntSushi/toml v1.5.0 github.com/WJQSERVER-STUDIO/httpc v0.8.2 - golang.org/x/net v0.42.0 + golang.org/x/net v0.43.0 golang.org/x/time v0.12.0 ) require ( github.com/WJQSERVER-STUDIO/go-utils/iox v0.0.2 github.com/WJQSERVER-STUDIO/go-utils/limitreader v0.0.2 + github.com/WJQSERVER/wanf v0.0.0-20250810023226-e51d9d0737ee github.com/fenthope/bauth v0.0.1 github.com/fenthope/ikumi v0.0.2 github.com/fenthope/ipfilter v0.0.1 diff --git a/go.sum b/go.sum index d84ed19..08e3e56 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/WJQSERVER-STUDIO/go-utils/limitreader v0.0.2 h1:8bBkKk6E2Zr+I5szL7gyc 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.2 h1:PFPLodV0QAfGEP6915J57vIqoKu9cGuuiXG/7C9TNUk= github.com/WJQSERVER-STUDIO/httpc v0.8.2/go.mod h1:8WhHVRO+olDFBSvL5PC/bdMkb6U3vRdPJ4p4pnguV5Y= +github.com/WJQSERVER/wanf v0.0.0-20250810023226-e51d9d0737ee h1:tJ31DNBn6UhWkk8fiikAQWqULODM+yBcGAEar1tzdZc= +github.com/WJQSERVER/wanf v0.0.0-20250810023226-e51d9d0737ee/go.mod h1:q2Pyg+G+s1acMWxrbI4CwS/Yk76/BzLREEdZ8iFwUNE= 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= @@ -26,7 +28,7 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw 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.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= 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 078a1b5..48c10fe 100644 --- a/main.go +++ b/main.go @@ -474,20 +474,11 @@ func main() { proxy.RoutingHandler(cfg)(c) }) - r.GET("/v2/", + r.ANY("/v2/*path", r.UseIf(cfg.Docker.Auth, func() touka.HandlerFunc { return bauth.BasicAuthForStatic(cfg.Docker.Credentials, "GHProxy Docker Proxy") }), - func(c *touka.Context) { - emptyJSON := "{}" - c.Header("Content-Type", "application/json") - c.Header("Content-Length", fmt.Sprint(len(emptyJSON))) - - c.Header("Docker-Distribution-API-Version", "registry/2.0") - - c.Status(200) - c.Writer.Write([]byte(emptyJSON)) - }, + proxy.OciWithImageRouting(cfg), ) r.GET("/v2", func(c *touka.Context) { @@ -495,10 +486,6 @@ func main() { c.Redirect(http.StatusMovedPermanently, "/v2/") }) - r.ANY("/v2/:target/:user/:repo/*filepath", func(c *touka.Context) { - proxy.GhcrWithImageRouting(cfg)(c) - }) - r.NoRoute(func(c *touka.Context) { proxy.NoRouteHandler(cfg)(c) }) diff --git a/proxy/docker.go b/proxy/docker.go index 955f7e0..4db7dec 100644 --- a/proxy/docker.go +++ b/proxy/docker.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + "log" "net/http" "net/url" "strconv" @@ -40,90 +41,168 @@ func InitWeakCache() *weakcache.Cache[string] { return cache } -// GhcrWithImageRouting 处理带有镜像路由的请求, 根据目标路由到不同的Docker注册表 -func GhcrWithImageRouting(cfg *config.Config) touka.HandlerFunc { +var ( + authEndpoint = "/" + passTypeMap = map[string]struct{}{ + "manifests": {}, + "blobs": {}, + "tags": {}, + "index": {}, + } +) + +// 处理路径各种情况 +func OciWithImageRouting(cfg *config.Config) touka.HandlerFunc { return func(c *touka.Context) { - // 从 main.go 中固定的路由 "/v2/:target/:user/:repo/*filepath" 获取参数 - reqTarget := c.Param("target") - reqImageUser := c.Param("user") - reqImageName := c.Param("repo") - reqFilePath := c.Param("filepath") + var ( + p1 string + p2 string + p3 string + p4 string + target string + user string + repo string + extpath string + p1IsTarget bool + ignorep3 bool + imageNameForAuth string + finalreqUrl string + iInfo *imageInfo + ) + ociPath := c.Param("path") + if ociPath == authEndpoint { + emptyJSON := "{}" + c.Header("Content-Type", "application/json") + c.Header("Content-Length", fmt.Sprint(len(emptyJSON))) - var upstreamTarget string - var requestPath string - var imageNameForAuth string + c.Header("Docker-Distribution-API-Version", "registry/2.0") - // 关键逻辑: 判断 reqTarget 是真实主机名还是镜像名的一部分 - // 依据: 真实主机名/IP通常包含'.'或':' - if strings.Contains(reqTarget, ".") || strings.Contains(reqTarget, ":") { - // 情况 A: reqTarget 是一个显式指定的主机名 (例如 "ghcr.io", "my-registry.com", "127.0.0.1:5000") - c.Debugf("Request target '%s' identified as an explicit hostname.", reqTarget) + c.Status(200) + c.Writer.Write([]byte(emptyJSON)) + return + } - // https://github.com/WJQSERVER-STUDIO/ghproxy/issues/159 - if reqTarget == "docker.io" { - upstreamTarget = dockerhubTarget + // 根据/分割 /:target/:user/:repo/*ext + log.Print(ociPath) + + ociPath = ociPath[1:] + i := strings.IndexByte(ociPath, '/') + if i <= 0 { + ErrorPage(c, NewErrorWithStatusLookup(404, "Not Found")) + log.Print(1) + return + } + p1 = ociPath[:i] + + // 开始判断p1是否为target + if strings.Contains(p1, ".") || strings.Contains(p1, ":") { + p1IsTarget = true + if p1 == "docker.io" { + target = dockerhubTarget } else { - upstreamTarget = reqTarget + target = p1 } - // 上游请求的路径是主机名之后的部分 - requestPath = fmt.Sprintf("%s/%s%s", reqImageUser, reqImageName, reqFilePath) - // 用于认证的镜像名是 user/repo - imageNameForAuth = fmt.Sprintf("%s/%s", reqImageUser, reqImageName) } else { - // 情况 B: reqTarget 是镜像名的一部分 (例如 "wjqserver", "library") - c.Debugf("Request target '%s' identified as part of an image name. Using default registry.", reqTarget) - // 使用配置文件中的默认目标 switch cfg.Docker.Target { case "ghcr": - upstreamTarget = ghcrTarget + target = ghcrTarget case "dockerhub": - upstreamTarget = dockerhubTarget + target = dockerhubTarget case "": ErrorPage(c, NewErrorWithStatusLookup(500, "Default Docker Target is not configured in config file")) return default: - upstreamTarget = cfg.Docker.Target + target = cfg.Docker.Target } - // 必须将路由错误分割的所有部分重新组合成完整的镜像路径 - requestPath = fmt.Sprintf("%s/%s/%s%s", reqTarget, reqImageUser, reqImageName, reqFilePath) - // 用于认证的镜像名是 target/user (例如 "wjqserver/ghproxy", "library/ubuntu") - imageNameForAuth = fmt.Sprintf("%s/%s", reqTarget, reqImageUser) } - // 清理路径, 防止出现 "//" - requestPath = strings.TrimPrefix(requestPath, "/") + ociPath = ociPath[i+1:] + i = strings.IndexByte(ociPath, '/') + if i <= 0 { + ErrorPage(c, NewErrorWithStatusLookup(404, "Not Found")) + log.Print(2) + return + } + p2 = ociPath[:i] + ociPath = ociPath[i+1:] - // 为认证和缓存准备镜像信息 - image := &imageInfo{ + // 若p2和passTypeMap匹配 + if !p1IsTarget { + if _, ok := passTypeMap[p2]; ok { + ignorep3 = true + switch cfg.Docker.Target { + case "ghcr": + target = ghcrTarget + case "dockerhub": + target = dockerhubTarget + case "": + ErrorPage(c, NewErrorWithStatusLookup(500, "Default Docker Target is not configured in config file")) + return + default: + target = cfg.Docker.Target + } + user = "library" + repo = p1 + extpath = "/" + p2 + "/" + ociPath + } + } + + if !ignorep3 { + i = strings.IndexByte(ociPath, '/') + if i <= 0 { + ErrorPage(c, NewErrorWithStatusLookup(404, "Not Found")) + log.Print(3) + return + } + p3 = ociPath[:i] + + ociPath = ociPath[i+1:] + p4 = ociPath + + if p1IsTarget { + if _, ok := passTypeMap[p3]; ok { + user = "library" + repo = p2 + extpath = "/" + p3 + "/" + p4 + } else { + user = p2 + repo = p3 + extpath = "/" + p4 + } + } else { + switch cfg.Docker.Target { + case "ghcr": + target = ghcrTarget + case "dockerhub": + target = dockerhubTarget + case "": + ErrorPage(c, NewErrorWithStatusLookup(500, "Default Docker Target is not configured in config file")) + return + default: + target = cfg.Docker.Target + } + user = p1 + repo = p2 + extpath = "/" + p3 + "/" + p4 + } + } + + imageNameForAuth = user + "/" + repo + finalreqUrl = "https://" + target + "/v2/" + imageNameForAuth + extpath + if query := c.GetReqQueryString(); query != "" { + finalreqUrl += "?" + query + } + + iInfo = &imageInfo{ + User: user, + Repo: repo, Image: imageNameForAuth, } - // 调用 GhcrToTarget 处理实际的代理请求 - GhcrToTarget(c, cfg, upstreamTarget, requestPath, image) + GhcrRequest(c.Request.Context(), c, finalreqUrl, iInfo, cfg, target) } } -// GhcrToTarget 根据配置和目标信息将请求代理到上游Docker注册表 -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")) - return - } - - var ctx = c.Request.Context() - - // 构造目标URL. 这里的target和path都是由GhcrWithImageRouting正确解析得来的. - destUrl := "https://" + target + "/v2/" + path - if query := c.GetReqQueryString(); query != "" { - destUrl += "?" + query - } - c.Debugf("Proxying to target '%s' with path '%s'. Final URL: %s", target, path, destUrl) - - // 执行实际的代理请求 - GhcrRequest(ctx, c, destUrl, image, cfg, target) -} - // GhcrRequest 执行对Docker注册表的HTTP请求, 处理认证和重定向 func GhcrRequest(ctx context.Context, c *touka.Context, u string, image *imageInfo, cfg *config.Config, target string) { var ( diff --git a/proxy/match.go b/proxy/match.go index 9a37f0a..9353c8b 100644 --- a/proxy/match.go +++ b/proxy/match.go @@ -23,6 +23,7 @@ const ( gistPrefix = "https://gist.github.com/" gistContentPrefix = "https://gist.githubusercontent.com/" apiPrefix = "https://api.github.com/" + ociv2Prefix = "https://v2/" releasesDownloadSnippet = "releases/download/" ) @@ -36,9 +37,11 @@ func init() { // Matcher 从原始URL路径中高效地解析并匹配代理规则. func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHProxyErrors) { - if len(rawPath) < 18 { - return "", "", "", NewErrorWithStatusLookup(404, "path too short") - } + /* + if len(rawPath) < 18 { + return "", "", "", NewErrorWithStatusLookup(404, "path too short") + } + */ // 匹配 "https://github.com/" if strings.HasPrefix(rawPath, githubPrefix) { From 5fc6f7ab6ff948fd1da78e677b84f9911bc09b70 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Mon, 11 Aug 2025 18:35:36 +0800 Subject: [PATCH 90/99] remove dev log --- config/config.go | 4 ---- proxy/docker.go | 10 ++++------ 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/config/config.go b/config/config.go index 035760e..6a6b039 100644 --- a/config/config.go +++ b/config/config.go @@ -1,8 +1,6 @@ package config import ( - "fmt" - "log" "os" "path/filepath" @@ -224,7 +222,6 @@ func LoadConfig(filePath string) (*Config, error) { } var config Config ext := filepath.Ext(filePath) - log.Printf("Loading config from %s with extension %s", filePath, ext) if ext == ".wanf" { if err := wanf.DecodeFile(filePath, &config); err != nil { return nil, err @@ -247,7 +244,6 @@ func (c *Config) WriteConfig(filePath string) error { defer file.Close() ext := filepath.Ext(filePath) - fmt.Printf("%s", ext) if ext == ".wanf" { err := wanf.NewStreamEncoder(file).Encode(c) if err != nil { diff --git a/proxy/docker.go b/proxy/docker.go index 4db7dec..fb0c309 100644 --- a/proxy/docker.go +++ b/proxy/docker.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "fmt" - "log" "net/http" "net/url" "strconv" @@ -54,6 +53,10 @@ var ( // 处理路径各种情况 func OciWithImageRouting(cfg *config.Config) touka.HandlerFunc { return func(c *touka.Context) { + if !cfg.Docker.Enabled { + ErrorPage(c, NewErrorWithStatusLookup(403, "Docker proxy is not enabled")) + return + } var ( p1 string p2 string @@ -83,13 +86,10 @@ func OciWithImageRouting(cfg *config.Config) touka.HandlerFunc { } // 根据/分割 /:target/:user/:repo/*ext - log.Print(ociPath) - ociPath = ociPath[1:] i := strings.IndexByte(ociPath, '/') if i <= 0 { ErrorPage(c, NewErrorWithStatusLookup(404, "Not Found")) - log.Print(1) return } p1 = ociPath[:i] @@ -120,7 +120,6 @@ func OciWithImageRouting(cfg *config.Config) touka.HandlerFunc { i = strings.IndexByte(ociPath, '/') if i <= 0 { ErrorPage(c, NewErrorWithStatusLookup(404, "Not Found")) - log.Print(2) return } p2 = ociPath[:i] @@ -151,7 +150,6 @@ func OciWithImageRouting(cfg *config.Config) touka.HandlerFunc { i = strings.IndexByte(ociPath, '/') if i <= 0 { ErrorPage(c, NewErrorWithStatusLookup(404, "Not Found")) - log.Print(3) return } p3 = ociPath[:i] From 972a37b497da1168d6d58599d47f81a32f427107 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Mon, 11 Aug 2025 18:39:24 +0800 Subject: [PATCH 91/99] 4.3.0 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3b7690..1a2cb2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # 更新日志 +4.3.0 - 2025-08-11 +--- +- CHANGE: 为OCI镜像(Docker)代理带来自动library附加功能 +- CHANGE(refactor): 改进OCI镜像(Docker)代理路径组成流程 +- ADD: 新增[WANF](https://github.com/WJQSERVER/wanf)配置文件格式支持 + 4.3.0-rc.0 - 2025-08-11 --- - PRE-RELEASE: v4.3.0-rc.0是v4.3.0发布版本,请勿在生产环境中使用; From 74a22be16c8ec539f4ce6991f93949c2bd464893 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Wed, 13 Aug 2025 20:54:33 +0800 Subject: [PATCH 92/99] 4.3.1 --- .github/workflows/build-dev.yml | 2 +- .github/workflows/build.yml | 2 +- CHANGELOG.md | 4 ++++ VERSION | 2 +- go.mod | 4 ++-- go.sum | 4 ++-- 6 files changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-dev.yml b/.github/workflows/build-dev.yml index 24e61c7..5906659 100644 --- a/.github/workflows/build-dev.yml +++ b/.github/workflows/build-dev.yml @@ -46,7 +46,7 @@ jobs: goarch: [amd64, arm64] env: OUTPUT_BINARY: ghproxy - GO_VERSION: 1.24 + GO_VERSION: 1.25 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f847575..c814dfb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -47,7 +47,7 @@ jobs: goarch: [amd64, arm64] env: OUTPUT_BINARY: ghproxy - GO_VERSION: 1.24 + GO_VERSION: 1.25 steps: - uses: actions/checkout@v3 diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a2cb2a..fb222e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 更新日志 +4.3.1 - 2025-08-13 +--- +- CHANGE: 更新至[Go 1.25](https://tip.golang.org/doc/go1.25) + 4.3.0 - 2025-08-11 --- - CHANGE: 为OCI镜像(Docker)代理带来自动library附加功能 diff --git a/VERSION b/VERSION index 8191138..ecedc98 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.3.0 \ No newline at end of file +4.3.1 \ No newline at end of file diff --git a/go.mod b/go.mod index 84ecf8e..b1b88a6 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module ghproxy -go 1.24.6 +go 1.25 require ( github.com/BurntSushi/toml v1.5.0 @@ -18,7 +18,7 @@ require ( github.com/fenthope/ipfilter v0.0.1 github.com/fenthope/reco v0.0.4 github.com/fenthope/record v0.0.4 - github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2 + github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/infinite-iroha/touka v0.3.6 github.com/wjqserver/modembed v0.0.1 diff --git a/go.sum b/go.sum index 08e3e56..a9c3c71 100644 --- a/go.sum +++ b/go.sum @@ -18,8 +18,8 @@ github.com/fenthope/reco v0.0.4 h1:yo2g3aWwdoMpaZWZX4SdZOW7mCK82RQIU/YI8ZUQThM= github.com/fenthope/reco v0.0.4/go.mod h1:eMyS8HpdMVdJ/2WJt6Cvt8P1EH9Igzj5lSJrgc+0jeg= github.com/fenthope/record v0.0.4 h1:/1JHNCxiXGLL/qCh4LEGaAvhj4CcKsb6siTxjLmjdO4= github.com/fenthope/record v0.0.4/go.mod h1:G0a6KCiCDyX2SsC3nfzSN651fJKxH482AyJvzlnvAJU= -github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2 h1:iizUGZ9pEquQS5jTGkh4AqeeHCMbfbjeb0zMt0aEFzs= -github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= +github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced h1:Q311OHjMh/u5E2TITc++WlTP5We0xNseRMkHDyvhW7I= +github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced/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.6 h1:SkpM/VFGCWOFQP3RRuoWdX/Q4zafPngG1VMwkrLwtkw= From 44cc5d5677bb6c4f6dfea3424326047d355370f2 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Wed, 20 Aug 2025 15:48:00 +0800 Subject: [PATCH 93/99] fix if cfg.Pages.StaticDir is "" issue --- main.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index 48c10fe..5f5b3ed 100644 --- a/main.go +++ b/main.go @@ -234,8 +234,18 @@ func setupPages(cfg *config.Config, r *touka.Engine) { } case "external": - r.SetUnMatchFS(http.Dir(cfg.Pages.StaticDir)) - + if cfg.Pages.StaticDir == "" { + logger.Errorf("Pages Mode is 'external' but StaticDir is empty. Using embedded pages instead.") + err := setInternalRoute(cfg, r) + if err != nil { + logger.Errorf("Failed to load embedded pages: %s", err) + fmt.Printf("Failed to load embedded pages: %s", err) + os.Exit(1) + } + } else { + extPageFS := os.DirFS(cfg.Pages.StaticDir) + r.SetUnMatchFS(http.FS(extPageFS)) + } default: // 处理无效的Pages Mode logger.Warnf("Invalid Pages Mode: %s, using default embedded theme", cfg.Pages.Mode) From a2857772174c9de535c069a38e150fb1190d8fd1 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Wed, 20 Aug 2025 15:53:09 +0800 Subject: [PATCH 94/99] 4.3.2 --- CHANGELOG.md | 4 ++++ VERSION | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb222e3..c46007d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 更新日志 +4.3.2 - 2025-08-20 +--- +- FIX: 修正`cfg.Pages.StaticDir`为空时的处置 + 4.3.1 - 2025-08-13 --- - CHANGE: 更新至[Go 1.25](https://tip.golang.org/doc/go1.25) diff --git a/VERSION b/VERSION index ecedc98..7e961f9 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.3.1 \ No newline at end of file +4.3.2 \ No newline at end of file From 4a7ad2ec751ea4eaeba56c2b4947f76a3ac320cd Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Wed, 10 Sep 2025 03:21:14 +0800 Subject: [PATCH 95/99] 4.3.3 --- CHANGELOG.md | 5 +++++ VERSION | 2 +- config/config.go | 43 +++++++++++++++++++++++++++++++++++++------ go.mod | 10 +++++----- go.sum | 16 ++++++++-------- 5 files changed, 56 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c46007d..29e7720 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 更新日志 +4.3.3 - 2025-09-10 +--- +- CHANGE: 增强对[wanf](https://github.com/WJQSERVER/wanf)的支持 +- CHANGE: 更新包括Touka框架在内的各个依赖版本 + 4.3.2 - 2025-08-20 --- - FIX: 修正`cfg.Pages.StaticDir`为空时的处置 diff --git a/VERSION b/VERSION index 7e961f9..2533cac 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.3.2 \ No newline at end of file +4.3.3 \ No newline at end of file diff --git a/config/config.go b/config/config.go index 6a6b039..9e1e026 100644 --- a/config/config.go +++ b/config/config.go @@ -1,8 +1,10 @@ package config import ( + "fmt" "os" "path/filepath" + "strings" "github.com/BurntSushi/toml" @@ -212,7 +214,8 @@ type DockerConfig struct { // LoadConfig 从配置文件加载配置 func LoadConfig(filePath string) (*Config, error) { - if !FileExists(filePath) { + exist, filePath2read := FileExists(filePath) + if !exist { // 楔入配置文件 err := DefaultConfig().WriteConfig(filePath) if err != nil { @@ -221,15 +224,15 @@ func LoadConfig(filePath string) (*Config, error) { return DefaultConfig(), nil } var config Config - ext := filepath.Ext(filePath) + ext := filepath.Ext(filePath2read) if ext == ".wanf" { - if err := wanf.DecodeFile(filePath, &config); err != nil { + if err := wanf.DecodeFile(filePath2read, &config); err != nil { return nil, err } return &config, nil } - if _, err := toml.DecodeFile(filePath, &config); err != nil { + if _, err := toml.DecodeFile(filePath2read, &config); err != nil { return nil, err } return &config, nil @@ -257,9 +260,37 @@ func (c *Config) WriteConfig(filePath string) error { } // FileExists 检测文件是否存在 -func FileExists(filename string) bool { +func FileExists(filename string) (bool, string) { _, err := os.Stat(filename) - return !os.IsNotExist(err) + if err == nil { + return true, filename + } + if os.IsNotExist(err) { + // 获取文件名(不包含路径) + base := filepath.Base(filename) + dir := filepath.Dir(filename) + + // 获取扩展名 + fileNameBody := strings.TrimSuffix(base, filepath.Ext(base)) + + // 重新组合路径, 扩展名改为.wanf, 确认是否存在 + wanfFilename := filepath.Join(dir, fileNameBody+".wanf") + + _, err = os.Stat(wanfFilename) + if err == nil { + // .wanf 文件存在 + fmt.Printf("\n Found .wanf file: %s\n", wanfFilename) + return true, wanfFilename + } else if os.IsNotExist(err) { + // .wanf 文件不存在 + return false, "" + } else { + // 其他错误 + return false, "" + } + } else { + return true, filename + } } // DefaultConfig 返回默认配置结构体 diff --git a/go.mod b/go.mod index b1b88a6..d43cb00 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,12 @@ module ghproxy -go 1.25 +go 1.25.1 require ( github.com/BurntSushi/toml v1.5.0 github.com/WJQSERVER-STUDIO/httpc v0.8.2 - golang.org/x/net v0.43.0 - golang.org/x/time v0.12.0 + golang.org/x/net v0.44.0 + golang.org/x/time v0.13.0 ) require ( @@ -18,9 +18,9 @@ require ( github.com/fenthope/ipfilter v0.0.1 github.com/fenthope/reco v0.0.4 github.com/fenthope/record v0.0.4 - github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced + github.com/go-json-experiment/json v0.0.0-20250813233538-9b1f9ea2e11b github.com/hashicorp/golang-lru/v2 v2.0.7 - github.com/infinite-iroha/touka v0.3.6 + github.com/infinite-iroha/touka v0.3.7 github.com/wjqserver/modembed v0.0.1 ) diff --git a/go.sum b/go.sum index a9c3c71..97337f6 100644 --- a/go.sum +++ b/go.sum @@ -18,17 +18,17 @@ github.com/fenthope/reco v0.0.4 h1:yo2g3aWwdoMpaZWZX4SdZOW7mCK82RQIU/YI8ZUQThM= github.com/fenthope/reco v0.0.4/go.mod h1:eMyS8HpdMVdJ/2WJt6Cvt8P1EH9Igzj5lSJrgc+0jeg= github.com/fenthope/record v0.0.4 h1:/1JHNCxiXGLL/qCh4LEGaAvhj4CcKsb6siTxjLmjdO4= github.com/fenthope/record v0.0.4/go.mod h1:G0a6KCiCDyX2SsC3nfzSN651fJKxH482AyJvzlnvAJU= -github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced h1:Q311OHjMh/u5E2TITc++WlTP5We0xNseRMkHDyvhW7I= -github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= +github.com/go-json-experiment/json v0.0.0-20250813233538-9b1f9ea2e11b h1:6Q4zRHXS/YLOl9Ng1b1OOOBWMidAQZR3Gel0UKPC/KU= +github.com/go-json-experiment/json v0.0.0-20250813233538-9b1f9ea2e11b/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.6 h1:SkpM/VFGCWOFQP3RRuoWdX/Q4zafPngG1VMwkrLwtkw= -github.com/infinite-iroha/touka v0.3.6/go.mod h1:XW7a3fpLAjJfylSmdNuDQ8wGKkKmLVi9V/89sT1d7uw= +github.com/infinite-iroha/touka v0.3.7 h1:bIIZW5Weh7lVpyOWh4FmyR9UOfb5FOt+cR9yQ30FJLA= +github.com/infinite-iroha/touka v0.3.7/go.mod h1:uwkF1gTrNEgQ4P/Gwtk6WLbERehq3lzB8x1FMedyrfE= 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.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= -golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= -golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= -golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= +golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= +golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= From e3f84f4c175f020a47fe458584d61337544afe6f Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Wed, 10 Sep 2025 03:36:15 +0800 Subject: [PATCH 96/99] fix retrun, change to false --- config/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.go b/config/config.go index 9e1e026..e098e72 100644 --- a/config/config.go +++ b/config/config.go @@ -289,7 +289,7 @@ func FileExists(filename string) (bool, string) { return false, "" } } else { - return true, filename + return false, filename } } From bd9f590b0ac6a3d94a320f0bf7db76526d0761ea Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sun, 14 Sep 2025 07:31:41 +0800 Subject: [PATCH 97/99] 4.3.4 --- CHANGELOG.md | 4 ++++ proxy/chunkreq.go | 11 +++-------- proxy/nest.go | 36 +++--------------------------------- proxy/reqheader.go | 3 ++- 4 files changed, 12 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29e7720..5987eb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 更新日志 +4.3.4 - 2025-09-10 +--- +- CHANGE: 改进嵌套加速实现, 增强稳定性 + 4.3.3 - 2025-09-10 --- - CHANGE: 增强对[wanf](https://github.com/WJQSERVER/wanf)的支持 diff --git a/proxy/chunkreq.go b/proxy/chunkreq.go index 9227b78..7e3725e 100644 --- a/proxy/chunkreq.go +++ b/proxy/chunkreq.go @@ -127,18 +127,14 @@ func ChunkedProxyRequest(ctx context.Context, c *touka.Context, u string, cfg *c defer bodyReader.Close() if MatcherShell(u) && matchString(matcher) && cfg.Shell.Editor { - // 判断body是不是gzip - var compress string - if resp.Header.Get("Content-Encoding") == "gzip" { - compress = "gzip" - } c.Debugf("Use Shell Editor: %s %s %s %s %s", c.ClientIP(), c.Request.Method, u, c.UserAgent(), c.Request.Proto) - c.Header("Content-Length", "") + c.DelHeader("Content-Length") + c.DelHeader("Content-Encoding") var reader io.Reader - reader, _, err = processLinks(bodyReader, compress, c.Request.Host, cfg, c) + reader, _, err = processLinks(bodyReader, c.Request.Host, cfg, c) c.WriteStream(reader) if err != nil { c.Errorf("%s %s %s %s %s Failed to copy response body: %v", c.ClientIP(), c.Request.Method, u, c.UserAgent(), c.Request.Proto, err) @@ -146,7 +142,6 @@ func ChunkedProxyRequest(ctx context.Context, c *touka.Context, u string, cfg *c return } } else { - if contentLength != "" { c.SetHeader("Content-Length", contentLength) c.WriteStream(bodyReader) diff --git a/proxy/nest.go b/proxy/nest.go index 4f93f20..675e4a3 100644 --- a/proxy/nest.go +++ b/proxy/nest.go @@ -2,7 +2,6 @@ package proxy import ( "bufio" - "compress/gzip" "fmt" "ghproxy/config" "io" @@ -66,7 +65,7 @@ func modifyURL(url string, host string, cfg *config.Config) string { } // processLinks 处理链接,返回包含处理后数据的 io.Reader -func processLinks(input io.ReadCloser, compress string, host string, cfg *config.Config, c *touka.Context) (readerOut io.Reader, written int64, err error) { +func processLinks(input io.ReadCloser, host string, cfg *config.Config, c *touka.Context) (readerOut io.Reader, written int64, err error) { pipeReader, pipeWriter := io.Pipe() // 创建 io.Pipe readerOut = pipeReader @@ -97,43 +96,14 @@ func processLinks(input io.ReadCloser, compress string, host string, cfg *config var bufReader *bufio.Reader - if compress == "gzip" { - // 解压gzip - gzipReader, gzipErr := gzip.NewReader(input) - if gzipErr != nil { - err = fmt.Errorf("gzip解压错误: %v", gzipErr) - return // Goroutine 中使用 return 返回错误 - } - defer gzipReader.Close() - bufReader = bufio.NewReader(gzipReader) - } else { - bufReader = bufio.NewReader(input) - } + bufReader = bufio.NewReader(input) var bufWriter *bufio.Writer - var gzipWriter *gzip.Writer - // 根据是否gzip确定 writer 的创建 - if compress == "gzip" { - gzipWriter = gzip.NewWriter(pipeWriter) // 使用 pipeWriter - bufWriter = bufio.NewWriterSize(gzipWriter, 4096) //设置缓冲区大小 - } else { - bufWriter = bufio.NewWriterSize(pipeWriter, 4096) // 使用 pipeWriter - } + bufWriter = bufio.NewWriterSize(pipeWriter, 4096) // 使用 pipeWriter //确保writer关闭 defer func() { - var closeErr error // 局部变量,用于保存defer中可能发生的错误 - - if gzipWriter != nil { - if closeErr = gzipWriter.Close(); closeErr != nil { - c.Errorf("gzipWriter close failed %v", closeErr) - // 如果已经存在错误,则保留。否则,记录此错误。 - if err == nil { - err = closeErr - } - } - } if flushErr := bufWriter.Flush(); flushErr != nil { c.Errorf("writer flush failed %v", flushErr) // 如果已经存在错误,则保留。否则,记录此错误。 diff --git a/proxy/reqheader.go b/proxy/reqheader.go index c89dc76..57d8542 100644 --- a/proxy/reqheader.go +++ b/proxy/reqheader.go @@ -27,6 +27,7 @@ var ( "CDN-Loop": {}, "Upgrade": {}, "Connection": {}, + "Accept-Encoding": {}, } cloneHeadersToRemove = map[string]struct{}{ @@ -43,7 +44,7 @@ var ( var ( defaultHeaders = map[string]string{ "Accept": "*/*", - "Accept-Encoding": "gzip", + "Accept-Encoding": "", "Transfer-Encoding": "chunked", "User-Agent": "GHProxy/1.0", } From ba33d5743f2108bae1b51e5e0939fc8049f9e993 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sun, 14 Sep 2025 07:44:46 +0800 Subject: [PATCH 98/99] 4.3.4 --- CHANGELOG.md | 2 +- VERSION | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5987eb6..77c5a26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # 更新日志 -4.3.4 - 2025-09-10 +4.3.4 - 2025-09-14 --- - CHANGE: 改进嵌套加速实现, 增强稳定性 diff --git a/VERSION b/VERSION index 2533cac..a6695ff 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.3.3 \ No newline at end of file +4.3.4 \ No newline at end of file From 32baca85db60a33b51a9a3edf5861b6fa422f784 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sun, 12 Oct 2025 15:46:36 +0800 Subject: [PATCH 99/99] remove --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index cc0847e..c977c51 100644 --- a/README.md +++ b/README.md @@ -36,8 +36,6 @@ [相关文章](https://blog.wjqserver.com/categories/my-program/) -代理相关推广: [Thordata](https://www.thordata.com/?ls=github&lk=WJQserver),市面上最具性价比的代理服务商,便宜好用,来自全球195个国家城市的6000万IP,轮换住宅/原生ISP/无限量仅从$0.65/GB 起,新用户$1=5GB .联系客户可获得免费测试. - ### 使用示例 ```bash