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 1/4] 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 2/4] 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 3/4] 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 4/4] 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