diff --git a/src/config/config.go b/src/config/config.go index 1973d3a..700b4c9 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -78,7 +78,7 @@ func DefaultConfig() *AppConfig { Host: "0.0.0.0", Port: 5000, FileSize: 2 * 1024 * 1024 * 1024, // 2GB - EnableH2C: false, // 默认关闭H2C + EnableH2C: false, // 默认关闭H2C }, RateLimit: struct { RequestLimit int `toml:"requestLimit"` @@ -268,4 +268,4 @@ func CreateDefaultConfigFile() error { } return os.WriteFile("config.toml", data, 0644) -} \ No newline at end of file +} diff --git a/src/go.mod b/src/go.mod index 0eca90c..6e8a2f8 100644 --- a/src/go.mod +++ b/src/go.mod @@ -44,7 +44,6 @@ require ( github.com/vbatts/tar-split v0.12.1 // indirect golang.org/x/arch v0.8.0 // indirect golang.org/x/crypto v0.32.0 // indirect - golang.org/x/net v0.33.0 // indirect golang.org/x/sync v0.14.0 // indirect golang.org/x/sys v0.33.0 // indirect golang.org/x/text v0.21.0 // indirect diff --git a/src/handlers/docker.go b/src/handlers/docker.go index 33c5b2d..ffccc73 100644 --- a/src/handlers/docker.go +++ b/src/handlers/docker.go @@ -605,4 +605,4 @@ func createUpstreamOptions(mapping config.RegistryMapping) []remote.Option { } return options -} \ No newline at end of file +} diff --git a/src/handlers/github.go b/src/handlers/github.go index d3f64c6..e29f174 100644 --- a/src/handlers/github.go +++ b/src/handlers/github.go @@ -36,7 +36,7 @@ func GitHubProxyHandler(c *gin.Context) { for strings.HasPrefix(rawPath, "/") { rawPath = strings.TrimPrefix(rawPath, "/") } - + // 自动补全协议头 if !strings.HasPrefix(rawPath, "https://") { if strings.HasPrefix(rawPath, "http:/") || strings.HasPrefix(rawPath, "https:/") { @@ -47,7 +47,7 @@ func GitHubProxyHandler(c *gin.Context) { } rawPath = "https://" + rawPath } - + matches := CheckGitHubURL(rawPath) if matches != nil { if allowed, reason := utils.GlobalAccessController.CheckGitHubAccess(matches); !allowed { @@ -96,7 +96,7 @@ func proxyGitHubWithRedirect(c *gin.Context, u string, redirectCount int) { c.String(http.StatusLoopDetected, "重定向次数过多,可能存在循环重定向") return } - + req, err := http.NewRequest(c.Request.Method, u, c.Request.Body) if err != nil { c.String(http.StatusInternalServerError, fmt.Sprintf("server error %v", err)) @@ -210,4 +210,4 @@ func proxyGitHubWithRedirect(c *gin.Context, u string, redirectCount int) { // 直接流式转发 io.Copy(c.Writer, resp.Body) } -} \ No newline at end of file +} diff --git a/src/handlers/imagetar.go b/src/handlers/imagetar.go index 0d4a499..106d630 100644 --- a/src/handlers/imagetar.go +++ b/src/handlers/imagetar.go @@ -855,4 +855,4 @@ func (is *ImageStreamer) StreamMultipleImages(ctx context.Context, imageRefs []s log.Printf("批量下载完成,共处理 %d 个镜像", len(imageRefs)) return nil -} \ No newline at end of file +} diff --git a/src/handlers/search.go b/src/handlers/search.go index 428e41b..7c2ec2a 100644 --- a/src/handlers/search.go +++ b/src/handlers/search.go @@ -26,27 +26,27 @@ type SearchResult struct { // Repository 仓库信息 type Repository struct { - Name string `json:"repo_name"` - Description string `json:"short_description"` - IsOfficial bool `json:"is_official"` - IsAutomated bool `json:"is_automated"` - StarCount int `json:"star_count"` - PullCount int `json:"pull_count"` - RepoOwner string `json:"repo_owner"` - LastUpdated string `json:"last_updated"` - Status int `json:"status"` - Organization string `json:"affiliation"` - PullsLastWeek int `json:"pulls_last_week"` - Namespace string `json:"namespace"` + Name string `json:"repo_name"` + Description string `json:"short_description"` + IsOfficial bool `json:"is_official"` + IsAutomated bool `json:"is_automated"` + StarCount int `json:"star_count"` + PullCount int `json:"pull_count"` + RepoOwner string `json:"repo_owner"` + LastUpdated string `json:"last_updated"` + Status int `json:"status"` + Organization string `json:"affiliation"` + PullsLastWeek int `json:"pulls_last_week"` + Namespace string `json:"namespace"` } // TagInfo 标签信息 type TagInfo struct { - Name string `json:"name"` - FullSize int64 `json:"full_size"` - LastUpdated time.Time `json:"last_updated"` - LastPusher string `json:"last_pusher"` - Images []Image `json:"images"` + Name string `json:"name"` + FullSize int64 `json:"full_size"` + LastUpdated time.Time `json:"last_updated"` + LastPusher string `json:"last_pusher"` + Images []Image `json:"images"` Vulnerabilities struct { Critical int `json:"critical"` High int `json:"high"` @@ -79,15 +79,15 @@ type cacheEntry struct { } const ( - maxCacheSize = 1000 - maxPaginationCache = 200 - cacheTTL = 30 * time.Minute + maxCacheSize = 1000 + maxPaginationCache = 200 + cacheTTL = 30 * time.Minute ) type Cache struct { - data map[string]cacheEntry - mu sync.RWMutex - maxSize int + data map[string]cacheEntry + mu sync.RWMutex + maxSize int } var ( @@ -101,18 +101,18 @@ func (c *Cache) Get(key string) (interface{}, bool) { c.mu.RLock() entry, exists := c.data[key] c.mu.RUnlock() - + if !exists { return nil, false } - + if time.Now().After(entry.expiresAt) { c.mu.Lock() delete(c.data, key) c.mu.Unlock() return nil, false } - + return entry.data, true } @@ -123,11 +123,11 @@ func (c *Cache) Set(key string, data interface{}) { func (c *Cache) SetWithTTL(key string, data interface{}, ttl time.Duration) { c.mu.Lock() defer c.mu.Unlock() - + if len(c.data) >= c.maxSize { c.cleanupExpiredLocked() } - + c.data[key] = cacheEntry{ data: data, expiresAt: time.Now().Add(ttl), @@ -153,7 +153,7 @@ func init() { go func() { ticker := time.NewTicker(5 * time.Minute) defer ticker.Stop() - + for range ticker.C { searchCache.Cleanup() } @@ -163,45 +163,45 @@ func init() { func filterSearchResults(results []Repository, query string) []Repository { searchTerm := strings.ToLower(strings.TrimPrefix(query, "library/")) filtered := make([]Repository, 0) - + for _, repo := range results { repoName := strings.ToLower(repo.Name) repoDesc := strings.ToLower(repo.Description) - + score := 0 - + if repoName == searchTerm { score += 100 } - + if strings.HasPrefix(repoName, searchTerm) { score += 50 } - + if strings.Contains(repoName, searchTerm) { score += 30 } - + if strings.Contains(repoDesc, searchTerm) { score += 10 } - + if repo.IsOfficial { score += 20 } - + if score > 0 { filtered = append(filtered, repo) } } - + sort.Slice(filtered, func(i, j int) bool { if filtered[i].IsOfficial != filtered[j].IsOfficial { return filtered[i].IsOfficial } return filtered[i].PullCount > filtered[j].PullCount }) - + return filtered } @@ -216,7 +216,7 @@ func normalizeRepository(repo *Repository) { if repo.Namespace == "" && repo.RepoOwner != "" { repo.Namespace = repo.RepoOwner } - + if strings.Contains(repo.Name, "/") { parts := strings.Split(repo.Name, "/") if len(parts) > 1 { @@ -239,14 +239,14 @@ func searchDockerHubWithDepth(ctx context.Context, query string, page, pageSize return nil, fmt.Errorf("搜索请求过于复杂,请尝试更具体的关键词") } cacheKey := fmt.Sprintf("search:%s:%d:%d", query, page, pageSize) - + if cached, ok := searchCache.Get(cacheKey); ok { return cached.(*SearchResult), nil } - + isUserRepo := strings.Contains(query, "/") var namespace, repoName string - + if isUserRepo { parts := strings.Split(query, "/") if len(parts) == 2 { @@ -254,11 +254,11 @@ func searchDockerHubWithDepth(ctx context.Context, query string, page, pageSize repoName = parts[1] } } - + baseURL := "https://registry.hub.docker.com/v2" var fullURL string var params url.Values - + if isUserRepo && namespace != "" { fullURL = fmt.Sprintf("%s/repositories/%s/", baseURL, namespace) params = url.Values{ @@ -273,20 +273,20 @@ func searchDockerHubWithDepth(ctx context.Context, query string, page, pageSize "page_size": {fmt.Sprintf("%d", pageSize)}, } } - + fullURL = fullURL + "?" + params.Encode() - + resp, err := utils.GetSearchHTTPClient().Get(fullURL) if err != nil { return nil, fmt.Errorf("请求Docker Hub API失败: %v", err) } defer safeCloseResponseBody(resp.Body, "搜索响应体") - + body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("读取响应失败: %v", err) } - + if resp.StatusCode != http.StatusOK { switch resp.StatusCode { case http.StatusTooManyRequests: @@ -302,7 +302,7 @@ func searchDockerHubWithDepth(ctx context.Context, query string, page, pageSize return nil, fmt.Errorf("请求失败: 状态码=%d, 响应=%s", resp.StatusCode, string(body)) } } - + var result *SearchResult if isUserRepo && namespace != "" { var userRepos struct { @@ -314,14 +314,14 @@ func searchDockerHubWithDepth(ctx context.Context, query string, page, pageSize if err := json.Unmarshal(body, &userRepos); err != nil { return nil, fmt.Errorf("解析响应失败: %v", err) } - + result = &SearchResult{ Count: userRepos.Count, Next: userRepos.Next, Previous: userRepos.Previous, Results: make([]Repository, 0), } - + for _, repo := range userRepos.Results { if repoName == "" || strings.Contains(strings.ToLower(repo.Name), strings.ToLower(repoName)) { repo.Namespace = namespace @@ -329,22 +329,22 @@ func searchDockerHubWithDepth(ctx context.Context, query string, page, pageSize result.Results = append(result.Results, repo) } } - + if len(result.Results) == 0 { return searchDockerHubWithDepth(ctx, repoName, page, pageSize, depth+1) } - + result.Count = len(result.Results) } else { result = &SearchResult{} if err := json.Unmarshal(body, &result); err != nil { return nil, fmt.Errorf("解析响应失败: %v", err) } - + for i := range result.Results { normalizeRepository(&result.Results[i]) } - + if isUserRepo && namespace != "" { filteredResults := make([]Repository, 0) for _, repo := range result.Results { @@ -356,7 +356,7 @@ func searchDockerHubWithDepth(ctx context.Context, query string, page, pageSize result.Count = len(filteredResults) } } - + searchCache.Set(cacheKey, result) return result, nil } @@ -365,14 +365,14 @@ func isRetryableError(err error) bool { if err == nil { return false } - + if strings.Contains(err.Error(), "timeout") || - strings.Contains(err.Error(), "connection refused") || - strings.Contains(err.Error(), "no such host") || - strings.Contains(err.Error(), "too many requests") { + strings.Contains(err.Error(), "connection refused") || + strings.Contains(err.Error(), "no such host") || + strings.Contains(err.Error(), "too many requests") { return true } - + return false } @@ -423,7 +423,7 @@ func fetchTagPage(ctx context.Context, url string, maxRetries int) (*struct { Results []TagInfo `json:"results"` }, error) { var lastErr error - + for retry := 0; retry < maxRetries; retry++ { if retry > 0 { time.Sleep(time.Duration(retry) * 500 * time.Millisecond) @@ -442,7 +442,7 @@ func fetchTagPage(ctx context.Context, url string, maxRetries int) (*struct { defer safeCloseResponseBody(resp.Body, "标签响应体") return io.ReadAll(resp.Body) }() - + if err != nil { lastErr = err if retry < maxRetries-1 { @@ -478,21 +478,21 @@ func fetchTagPage(ctx context.Context, url string, maxRetries int) (*struct { return &result, nil } - + return nil, lastErr } func parsePaginationParams(c *gin.Context, defaultPageSize int) (page, pageSize int) { page = 1 pageSize = defaultPageSize - + if p := c.Query("page"); p != "" { fmt.Sscanf(p, "%d", &page) } if ps := c.Query("page_size"); ps != "" { fmt.Sscanf(ps, "%d", &pageSize) } - + return page, pageSize } @@ -556,4 +556,4 @@ func RegisterSearchRoute(r *gin.Engine) { c.JSON(http.StatusOK, tags) } }) -} \ No newline at end of file +} diff --git a/src/main.go b/src/main.go index ff0d8bf..e1bf45c 100644 --- a/src/main.go +++ b/src/main.go @@ -119,12 +119,12 @@ func main() { fmt.Printf("🚀 HubProxy 启动成功\n") fmt.Printf("📡 监听地址: %s:%d\n", cfg.Server.Host, cfg.Server.Port) fmt.Printf("⚡ 限流配置: %d请求/%g小时\n", cfg.RateLimit.RequestLimit, cfg.RateLimit.PeriodHours) - + // 显示HTTP/2支持状态 if cfg.Server.EnableH2C { fmt.Printf("H2c: 已启用\n") } - + fmt.Printf("🔗 项目地址: https://github.com/sky22333/hubproxy\n") // 创建HTTP2服务器 @@ -155,17 +155,7 @@ func main() { } } - - // 简单的健康检查 -func formatBeijingTime(t time.Time) string { - loc, err := time.LoadLocation("Asia/Shanghai") - if err != nil { - loc = time.FixedZone("CST", 8*3600) - } - return t.In(loc).Format("2006-01-02 15:04:05") -} - func formatDuration(d time.Duration) string { if d < time.Minute { return fmt.Sprintf("%d秒", int(d.Seconds())) @@ -180,26 +170,20 @@ func formatDuration(d time.Duration) string { } } -func initHealthRoutes(router *gin.Engine) { - router.GET("/health", func(c *gin.Context) { - uptime := time.Since(serviceStartTime) - c.JSON(http.StatusOK, gin.H{ - "status": "healthy", - "timestamp_unix": serviceStartTime.Unix(), - "uptime_sec": uptime.Seconds(), - "service": "hubproxy", - "start_time_bj": formatBeijingTime(serviceStartTime), - "uptime_human": formatDuration(uptime), - }) - }) +func getUptimeInfo() (time.Duration, float64, string) { + uptime := time.Since(serviceStartTime) + return uptime, uptime.Seconds(), formatDuration(uptime) +} +func initHealthRoutes(router *gin.Engine) { router.GET("/ready", func(c *gin.Context) { - uptime := time.Since(serviceStartTime) + _, uptimeSec, uptimeHuman := getUptimeInfo() c.JSON(http.StatusOK, gin.H{ - "ready": true, - "timestamp_unix": time.Now().Unix(), - "uptime_sec": uptime.Seconds(), - "uptime_human": formatDuration(uptime), + "ready": true, + "service": "hubproxy", + "start_time_unix": serviceStartTime.Unix(), + "uptime_sec": uptimeSec, + "uptime_human": uptimeHuman, }) }) } diff --git a/src/utils/access_control.go b/src/utils/access_control.go index aaf01ca..48a685b 100644 --- a/src/utils/access_control.go +++ b/src/utils/access_control.go @@ -202,4 +202,4 @@ func (ac *AccessController) checkList(matches, list []string) bool { } } return false -} \ No newline at end of file +} diff --git a/src/utils/cache.go b/src/utils/cache.go index ec4a232..488ce80 100644 --- a/src/utils/cache.go +++ b/src/utils/cache.go @@ -161,4 +161,4 @@ func init() { } } }() -} \ No newline at end of file +} diff --git a/src/utils/http_client.go b/src/utils/http_client.go index 6834213..9bb250f 100644 --- a/src/utils/http_client.go +++ b/src/utils/http_client.go @@ -64,4 +64,4 @@ func GetGlobalHTTPClient() *http.Client { // GetSearchHTTPClient 获取搜索HTTP客户端 func GetSearchHTTPClient() *http.Client { return searchHTTPClient -} \ No newline at end of file +} diff --git a/src/utils/proxy_shell.go b/src/utils/proxy_shell.go index d670c02..d83313f 100644 --- a/src/utils/proxy_shell.go +++ b/src/utils/proxy_shell.go @@ -91,4 +91,4 @@ func transformURL(url, host string) string { cleanHost = strings.TrimSuffix(cleanHost, "/") return cleanHost + "/" + url -} \ No newline at end of file +} diff --git a/src/utils/ratelimiter.go b/src/utils/ratelimiter.go index 2734b30..bc6f2f2 100644 --- a/src/utils/ratelimiter.go +++ b/src/utils/ratelimiter.go @@ -269,4 +269,4 @@ func RateLimitMiddleware(limiter *IPRateLimiter) gin.HandlerFunc { c.Next() } -} \ No newline at end of file +}