From 8ffceb7f2b6f4f9ea47833d64aa952018f70f4d9 Mon Sep 17 00:00:00 2001 From: user123456 Date: Fri, 13 Jun 2025 13:59:06 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E9=A1=B9=E7=9B=AE=E7=BB=86?= =?UTF-8?q?=E8=8A=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++-- src/access_control.go | 25 +++--------------------- src/config.go | 19 ------------------- src/docker.go | 14 +------------- src/imagetar.go | 4 ++-- src/main.go | 27 +++++++++----------------- src/proxysh.go | 15 --------------- src/public/images.html | 34 +++++---------------------------- src/public/index.html | 14 +------------- src/public/search.html | 43 +++--------------------------------------- src/ratelimiter.go | 24 ++++++----------------- src/search.go | 7 +------ src/smart_ratelimit.go | 29 ++++++---------------------- src/token_cache.go | 22 +++------------------ 14 files changed, 42 insertions(+), 239 deletions(-) diff --git a/README.md b/README.md index 5161547..27596dd 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,13 @@ ## ✨ 特性 - 🐳 **Docker 镜像加速** - 单域名实现 Docker Hub、GHCR、Quay 等多个镜像仓库加速,以及优化拉取速度。 -- 🐳 **离线镜像包** - 支持批量下载离线镜像包。 +- 🐳 **离线镜像包** - 支持下载离线镜像包,流式传输加防抖设计。 - 📁 **GitHub 文件加速** - 加速 GitHub Release、Raw 文件下载,支持`api.github.com`,脚本嵌套加速等等 - 🤖 **AI 模型库支持** - 支持 Hugging Face 模型下载加速 - 🛡️ **智能限流** - IP 限流保护,防止滥用 - 🚫 **仓库审计** - 强大的自定义黑名单,白名单,同时审计镜像仓库,和GitHub仓库 - 🔍 **镜像搜索** - 在线搜索 Docker 镜像 -- ⚡ **轻量高效** - 基于 Go 语言,单二进制文件运行,资源占用低 +- ⚡ **轻量高效** - 基于 Go 语言,单二进制文件运行,资源占用低,优雅的内存清理机制。 - 🔧 **配置热重载** - 统一配置管理,部分配置项支持热重载,无需重启服务 ## 🚀 快速开始 diff --git a/src/access_control.go b/src/access_control.go index c589c86..c2a3dee 100644 --- a/src/access_control.go +++ b/src/access_control.go @@ -31,13 +31,10 @@ var GlobalAccessController = &AccessController{} // ParseDockerImage 解析Docker镜像名称 func (ac *AccessController) ParseDockerImage(image string) DockerImageInfo { - // 移除可能的协议前缀 image = strings.TrimPrefix(image, "docker://") - // 分离标签 var tag string if idx := strings.LastIndex(image, ":"); idx != -1 { - // 检查是否是端口号而不是标签(包含斜杠) part := image[idx+1:] if !strings.Contains(part, "/") { tag = part @@ -48,15 +45,11 @@ func (ac *AccessController) ParseDockerImage(image string) DockerImageInfo { tag = "latest" } - // 分离命名空间和仓库名 var namespace, repository string if strings.Contains(image, "/") { - // 处理自定义registry的情况,如 registry.com/user/repo parts := strings.Split(image, "/") if len(parts) >= 2 { - // 检查第一部分是否是域名(包含.) if strings.Contains(parts[0], ".") { - // 跳过registry域名,取用户名和仓库名 if len(parts) >= 3 { namespace = parts[1] repository = parts[2] @@ -65,13 +58,11 @@ func (ac *AccessController) ParseDockerImage(image string) DockerImageInfo { repository = parts[1] } } else { - // 标准格式:user/repo namespace = parts[0] repository = parts[1] } } } else { - // 官方镜像,如 nginx namespace = "library" repository = image } @@ -171,7 +162,6 @@ func (ac *AccessController) matchImageInList(imageInfo DockerImageInfo, list []s } } - // 5. 子仓库匹配(防止 user/repo 匹配到 user/repo-fork) if strings.HasPrefix(fullName, item+"/") { return true } @@ -185,7 +175,6 @@ func (ac *AccessController) checkList(matches, list []string) bool { return false } - // 组合用户名和仓库名,处理.git后缀 username := strings.ToLower(strings.TrimSpace(matches[0])) repoName := strings.ToLower(strings.TrimSpace(strings.TrimSuffix(matches[1], ".git"))) fullRepo := username + "/" + repoName @@ -196,10 +185,7 @@ func (ac *AccessController) checkList(matches, list []string) bool { continue } - // 支持多种匹配模式: - // 1. 精确匹配: "vaxilu/x-ui" - // 2. 用户级匹配: "vaxilu/*" 或 "vaxilu" - // 3. 前缀匹配: "vaxilu/x-ui-*" + // 支持多种匹配模式 if fullRepo == item { return true } @@ -225,15 +211,10 @@ func (ac *AccessController) checkList(matches, list []string) bool { return false } -// 🔥 Reload 热重载访问控制规则 +// Reload 热重载访问控制规则 func (ac *AccessController) Reload() { ac.mu.Lock() defer ac.mu.Unlock() - // 访问控制器本身不缓存配置,每次检查时都会调用GetConfig() - // 所以这里只需要确保锁的原子性,实际的重载在GetConfig()中完成 - // 可以在这里添加一些初始化逻辑,比如预编译正则表达式等 - - // 目前访问控制器设计为无状态的,每次检查都读取最新配置 - // 这样设计的好处是配置更新后无需额外处理,自动生效 + // 访问控制器本身不缓存配置 } \ No newline at end of file diff --git a/src/config.go b/src/config.go index 3a205a0..0d20aa9 100644 --- a/src/config.go +++ b/src/config.go @@ -48,10 +48,8 @@ type AppConfig struct { MaxImages int `toml:"maxImages"` // 单次下载最大镜像数量限制 } `toml:"download"` - // 新增:Registry映射配置 Registries map[string]RegistryMapping `toml:"registries"` - // Token缓存配置 TokenCache struct { Enabled bool `toml:"enabled"` // 是否启用token缓存 DefaultTTL string `toml:"defaultTTL"` // 默认缓存时间 @@ -64,7 +62,6 @@ var ( isViperEnabled bool viperInstance *viper.Viper - // ✅ 配置缓存变量 cachedConfig *AppConfig configCacheTime time.Time configCacheTTL = 5 * time.Second @@ -147,7 +144,6 @@ func DefaultConfig() *AppConfig { // GetConfig 安全地获取配置副本 func GetConfig() *AppConfig { - // ✅ 快速缓存检查,减少深拷贝开销 configCacheMutex.RLock() if cachedConfig != nil && time.Since(configCacheTime) < configCacheTTL { config := cachedConfig @@ -194,7 +190,6 @@ func setConfig(cfg *AppConfig) { defer appConfigLock.Unlock() appConfig = cfg - // ✅ 配置更新时清除缓存 configCacheMutex.Lock() cachedConfig = nil configCacheMutex.Unlock() @@ -220,17 +215,13 @@ func LoadConfig() error { // 设置配置 setConfig(cfg) - // 🔥 首次加载后启用Viper热重载 if !isViperEnabled { go enableViperHotReload() } - // 配置加载成功,详细信息在启动时统一显示 - return nil } -// 🔥 启用Viper自动热重载 func enableViperHotReload() { if isViperEnabled { return @@ -251,9 +242,7 @@ func enableViperHotReload() { } isViperEnabled = true - // 热重载已启用,不显示额外信息 - // 🚀 启用文件监听 viperInstance.WatchConfig() viperInstance.OnConfigChange(func(e fsnotify.Event) { fmt.Printf("检测到配置文件变化: %s\n", e.Name) @@ -261,7 +250,6 @@ func enableViperHotReload() { }) } -// 🔥 使用Viper进行热重载 func hotReloadWithViper() { start := time.Now() fmt.Println("🔄 自动热重载...") @@ -275,10 +263,8 @@ func hotReloadWithViper() { return } - // 从环境变量覆盖(保持原有功能) overrideFromEnv(cfg) - // 原子性更新配置 setConfig(cfg) // 异步更新受影响的组件 @@ -288,7 +274,6 @@ func hotReloadWithViper() { }() } -// 🔧 更新受配置影响的组件 func updateAffectedComponents() { // 重新初始化限流器 if globalLimiter != nil { @@ -302,7 +287,6 @@ func updateAffectedComponents() { GlobalAccessController.Reload() } - // 🔥 刷新Registry配置映射 fmt.Println("🌐 更新Registry配置映射...") reloadRegistryConfig() @@ -310,7 +294,6 @@ func updateAffectedComponents() { fmt.Println("🔧 组件更新完成") } -// 🔥 重新加载Registry配置 func reloadRegistryConfig() { cfg := GetConfig() enabledCount := 0 @@ -324,8 +307,6 @@ func reloadRegistryConfig() { fmt.Printf("🌐 Registry配置已更新: %d个启用\n", enabledCount) - // Registry配置是动态读取的,每次请求都会调用GetConfig() - // 所以这里只需要简单通知,实际生效是自动的 } // overrideFromEnv 从环境变量覆盖配置 diff --git a/src/docker.go b/src/docker.go index fd3c740..4b4ebf1 100644 --- a/src/docker.go +++ b/src/docker.go @@ -78,8 +78,6 @@ func initDockerProxy() { registry: registry, options: options, } - - // Docker代理初始化完成 } // ProxyDockerRegistryGin 标准Docker Registry API v2代理 @@ -105,7 +103,6 @@ func handleRegistryRequest(c *gin.Context, path string) { // 移除 /v2/ 前缀 pathWithoutV2 := strings.TrimPrefix(path, "/v2/") - // 🔍 新增:Registry域名检测和路由 if registryDomain, remainingPath := registryDetector.detectRegistryDomain(pathWithoutV2); registryDomain != "" { if registryDetector.isRegistryEnabled(registryDomain) { // 设置目标Registry信息到Context @@ -118,7 +115,6 @@ func handleRegistryRequest(c *gin.Context, path string) { } } - // 原有逻辑完全保持(零改动) imageName, apiType, reference := parseRegistryPath(pathWithoutV2) if imageName == "" || apiType == "" { c.String(http.StatusBadRequest, "Invalid path format") @@ -392,9 +388,7 @@ func (r *ResponseRecorder) Write(data []byte) (int, error) { return r.ResponseWriter.Write(data) } -// proxyDockerAuthOriginal Docker认证代理(原始逻辑,保持不变) func proxyDockerAuthOriginal(c *gin.Context) { - // 检查是否有目标Registry域名(来自Context) var authURL string if targetDomain, exists := c.Get("target_registry_domain"); exists { if mapping, found := registryDetector.getRegistryMapping(targetDomain.(string)); found { @@ -672,17 +666,11 @@ func createUpstreamOptions(mapping RegistryMapping) []remote.Option { remote.WithUserAgent("hubproxy/go-containerregistry"), } - // 根据Registry类型添加特定的认证选项 + // 根据Registry类型添加特定的认证选项(方便后续扩展) switch mapping.AuthType { case "github": - // GitHub Container Registry 通常使用匿名访问 - // 如需要认证,可在此处添加 case "google": - // Google Container Registry 配置 - // 如需要认证,可在此处添加 case "quay": - // Quay.io 配置 - // 如需要认证,可在此处添加 } return options diff --git a/src/imagetar.go b/src/imagetar.go index b5b0404..64c8fce 100644 --- a/src/imagetar.go +++ b/src/imagetar.go @@ -361,7 +361,7 @@ func (is *ImageStreamer) streamDockerFormatWithReturn(ctx context.Context, tarWr default: } - if err := func() error { // ✅ 匿名函数确保资源立即释放 + if err := func() error { digest, err := layer.Digest() if err != nil { return err @@ -859,7 +859,7 @@ func (is *ImageStreamer) StreamMultipleImages(ctx context.Context, imageRefs []s log.Printf("处理镜像 %d/%d: %s", i+1, len(imageRefs), imageRef) - // ✅ 添加超时保护,防止单个镜像处理时间过长 + // 防止单个镜像处理时间过长 timeoutCtx, cancel := context.WithTimeout(ctx, 15*time.Minute) manifest, repositories, err := is.streamSingleImageForBatch(timeoutCtx, tarWriter, imageRef, options) cancel() diff --git a/src/main.go b/src/main.go index 94022c6..3a186f0 100644 --- a/src/main.go +++ b/src/main.go @@ -46,7 +46,7 @@ var ( } globalLimiter *IPRateLimiter - // ✅ 服务启动时间追踪 + // 服务启动时间 serviceStartTime = time.Now() ) @@ -75,7 +75,7 @@ func main() { gin.SetMode(gin.ReleaseMode) router := gin.Default() - // ✅ 添加全局Panic恢复保护 + // 全局Panic恢复保护 router.Use(gin.CustomRecovery(func(c *gin.Context, recovered interface{}) { log.Printf("🚨 Panic recovered: %v", recovered) c.JSON(http.StatusInternalServerError, gin.H{ @@ -84,13 +84,13 @@ func main() { }) })) - // ✅ 初始化监控端点 (优先级最高,避免中间件影响) + // 初始化监控端点 initHealthRoutes(router) // 初始化镜像tar下载路由 initImageTarRoutes(router) - // 静态文件路由(使用嵌入文件) + // 静态文件路由 router.GET("/", func(c *gin.Context) { serveEmbedFile(c, "public/index.html") }) @@ -120,7 +120,7 @@ func main() { router.Any("/v2/*path", RateLimitMiddleware(globalLimiter), ProxyDockerRegistryGin) - // 注册NoRoute处理器,应用限流中间件 + // 注册NoRoute处理器 router.NoRoute(RateLimitMiddleware(globalLimiter), handler) cfg := GetConfig() @@ -226,7 +226,6 @@ func proxyWithRedirect(c *gin.Context, u string, redirectCount int) { resp.Header.Del("Referrer-Policy") resp.Header.Del("Strict-Transport-Security") - // 智能处理系统 - 自动识别需要加速的内容 // 获取真实域名 realHost := c.Request.Header.Get("X-Forwarded-Host") if realHost == "" { @@ -237,15 +236,11 @@ func proxyWithRedirect(c *gin.Context, u string, redirectCount int) { realHost = "https://" + realHost } - // 🚀 高性能预筛选:仅对.sh文件进行智能处理 if strings.HasSuffix(strings.ToLower(u), ".sh") { - // 检查是否为gzip压缩内容 isGzipCompressed := resp.Header.Get("Content-Encoding") == "gzip" - // 仅对shell脚本使用智能处理器 processedBody, processedSize, err := ProcessSmart(resp.Body, isGzipCompressed, realHost) if err != nil { - // 优雅降级 - 处理失败时使用直接代理模式 fmt.Printf("智能处理失败,回退到直接代理: %v\n", err) processedBody = resp.Body processedSize = 0 @@ -253,7 +248,6 @@ func proxyWithRedirect(c *gin.Context, u string, redirectCount int) { // 智能设置响应头 if processedSize > 0 { - // 内容被处理过,清理压缩相关头,使用chunked传输 resp.Header.Del("Content-Length") resp.Header.Del("Content-Encoding") resp.Header.Set("Transfer-Encoding", "chunked") @@ -282,8 +276,6 @@ func proxyWithRedirect(c *gin.Context, u string, redirectCount int) { return } } else { - // 🔥 非.sh文件:直接高性能流式代理,零内存消耗 - // 复制所有响应头 for key, values := range resp.Header { for _, value := range values { c.Header(key, value) @@ -302,7 +294,7 @@ func proxyWithRedirect(c *gin.Context, u string, redirectCount int) { c.Status(resp.StatusCode) - // 直接流式转发,零内存拷贝 + // 直接流式转发 if _, err := io.Copy(c.Writer, resp.Body); err != nil { fmt.Printf("直接代理失败: %v\n", err) } @@ -318,9 +310,9 @@ func checkURL(u string) []string { return nil } -// ✅ 初始化健康监控路由 +// 初始化健康监控路由 func initHealthRoutes(router *gin.Engine) { - // 健康检查端点 - 最轻量级,无依赖检查 + // 健康检查端点 router.GET("/health", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "status": "healthy", @@ -330,12 +322,11 @@ func initHealthRoutes(router *gin.Engine) { }) }) - // 就绪检查端点 - 检查关键组件状态 + // 就绪检查端点 router.GET("/ready", func(c *gin.Context) { checks := make(map[string]string) allReady := true - // 检查配置状态 if GetConfig() != nil { checks["config"] = "ok" } else { diff --git a/src/proxysh.go b/src/proxysh.go index 5f7e7bd..f3f7f58 100644 --- a/src/proxysh.go +++ b/src/proxysh.go @@ -16,7 +16,6 @@ var githubRegex = regexp.MustCompile(`https?://(?:github\.com|raw\.githubusercon func ProcessSmart(input io.ReadCloser, isCompressed bool, host string) (io.Reader, int64, error) { defer input.Close() - // 读取Shell脚本内容 content, err := readShellContent(input, isCompressed) if err != nil { return nil, 0, fmt.Errorf("内容读取失败: %v", err) @@ -26,38 +25,31 @@ func ProcessSmart(input io.ReadCloser, isCompressed bool, host string) (io.Reade return strings.NewReader(""), 0, nil } - // Shell脚本大小检查 (10MB限制) if len(content) > 10*1024*1024 { return strings.NewReader(content), int64(len(content)), nil } - // 快速检查是否包含GitHub URL if !strings.Contains(content, "github.com") && !strings.Contains(content, "githubusercontent.com") { return strings.NewReader(content), int64(len(content)), nil } - // 执行GitHub URL替换 processed := processGitHubURLs(content, host) return strings.NewReader(processed), int64(len(processed)), nil } -// readShellContent 读取Shell脚本内容 func readShellContent(input io.ReadCloser, isCompressed bool) (string, error) { var reader io.Reader = input // 处理gzip压缩 if isCompressed { - // 读取前2字节检查gzip魔数 peek := make([]byte, 2) n, err := input.Read(peek) if err != nil && err != io.EOF { return "", fmt.Errorf("读取数据失败: %v", err) } - // 检查gzip魔数 (0x1f, 0x8b) if n >= 2 && peek[0] == 0x1f && peek[1] == 0x8b { - // 合并peek数据和剩余流 combinedReader := io.MultiReader(bytes.NewReader(peek[:n]), input) gzReader, err := gzip.NewReader(combinedReader) if err != nil { @@ -66,12 +58,10 @@ func readShellContent(input io.ReadCloser, isCompressed bool) (string, error) { defer gzReader.Close() reader = gzReader } else { - // 不是gzip格式,合并peek数据 reader = io.MultiReader(bytes.NewReader(peek[:n]), input) } } - // 读取全部内容 data, err := io.ReadAll(reader) if err != nil { return "", fmt.Errorf("读取内容失败: %v", err) @@ -80,7 +70,6 @@ func readShellContent(input io.ReadCloser, isCompressed bool) (string, error) { return string(data), nil } -// processGitHubURLs 处理GitHub URL替换 func processGitHubURLs(content, host string) string { return githubRegex.ReplaceAllStringFunc(content, func(url string) string { return transformURL(url, host) @@ -89,19 +78,15 @@ func processGitHubURLs(content, host string) string { // transformURL URL转换函数 func transformURL(url, host string) string { - // 避免重复处理 if strings.Contains(url, host) { return url } - // 协议标准化为https if strings.HasPrefix(url, "http://") { url = "https" + url[4:] } else if !strings.HasPrefix(url, "https://") && !strings.HasPrefix(url, "//") { url = "https://" + url } - - // 清理host格式 cleanHost := strings.TrimPrefix(host, "https://") cleanHost = strings.TrimPrefix(cleanHost, "http://") cleanHost = strings.TrimSuffix(cleanHost, "/") diff --git a/src/public/images.html b/src/public/images.html index 8393d94..b82f010 100644 --- a/src/public/images.html +++ b/src/public/images.html @@ -348,7 +348,6 @@ margin-top: 0.25rem; } - /* 响应式设计 */ @media (max-width: 768px) { .navbar-container { padding: 0 0.5rem; @@ -385,7 +384,6 @@ } } - /* 加载动画 */ .loading { display: inline-block; width: 1rem; @@ -405,7 +403,6 @@ display: none; } - /* 移动端菜单样式 - 与其他页面完全一致 */ .mobile-menu-toggle { display: none; } @@ -480,7 +477,6 @@ -