package handlers import ( "fmt" "io" "net/http" "regexp" "strconv" "strings" "github.com/gin-gonic/gin" "hubproxy/config" "hubproxy/utils" ) var ( // GitHub URL匹配正则表达式 githubExps = []*regexp.Regexp{ regexp.MustCompile(`^(?:https?://)?github\.com/([^/]+)/([^/]+)/(?:releases|archive)/.*`), regexp.MustCompile(`^(?:https?://)?github\.com/([^/]+)/([^/]+)/(?:blob|raw)/.*`), regexp.MustCompile(`^(?:https?://)?github\.com/([^/]+)/([^/]+)/(?:info|git-).*`), regexp.MustCompile(`^(?:https?://)?raw\.github(?:usercontent|)\.com/([^/]+)/([^/]+)/.+?/.+`), regexp.MustCompile(`^(?:https?://)?gist\.(?:githubusercontent|github)\.com/(.+?)/(.+?)/.+\.[a-zA-Z0-9]+$`), regexp.MustCompile(`^(?:https?://)?api\.github\.com/repos/([^/]+)/([^/]+)/.*`), regexp.MustCompile(`^(?:https?://)?huggingface\.co(?:/spaces)?/([^/]+)/(.+)`), regexp.MustCompile(`^(?:https?://)?cdn-lfs\.hf\.co(?:/spaces)?/([^/]+)/([^/]+)(?:/(.*))?`), regexp.MustCompile(`^(?:https?://)?download\.docker\.com/([^/]+)/.*\.(tgz|zip)`), regexp.MustCompile(`^(?:https?://)?(github|opengraph)\.githubassets\.com/([^/]+)/.+?`), } ) // GitHubProxyHandler GitHub代理处理器 func GitHubProxyHandler(c *gin.Context) { rawPath := strings.TrimPrefix(c.Request.URL.RequestURI(), "/") for strings.HasPrefix(rawPath, "/") { rawPath = strings.TrimPrefix(rawPath, "/") } // 自动补全协议头 if !strings.HasPrefix(rawPath, "https://") { if strings.HasPrefix(rawPath, "http:/") || strings.HasPrefix(rawPath, "https:/") { rawPath = strings.Replace(rawPath, "http:/", "", 1) rawPath = strings.Replace(rawPath, "https:/", "", 1) } else if strings.HasPrefix(rawPath, "http://") { rawPath = strings.TrimPrefix(rawPath, "http://") } rawPath = "https://" + rawPath } matches := CheckGitHubURL(rawPath) if matches != nil { if allowed, reason := utils.GlobalAccessController.CheckGitHubAccess(matches); !allowed { var repoPath string if len(matches) >= 2 { username := matches[0] repoName := strings.TrimSuffix(matches[1], ".git") repoPath = username + "/" + repoName } fmt.Printf("GitHub仓库 %s 访问被拒绝: %s\n", repoPath, reason) c.String(http.StatusForbidden, reason) return } } else { c.String(http.StatusForbidden, "无效输入") return } // 将blob链接转换为raw链接 if githubExps[1].MatchString(rawPath) { rawPath = strings.Replace(rawPath, "/blob/", "/raw/", 1) } ProxyGitHubRequest(c, rawPath) } // CheckGitHubURL 检查URL是否匹配GitHub模式 func CheckGitHubURL(u string) []string { for _, exp := range githubExps { if matches := exp.FindStringSubmatch(u); matches != nil { return matches[1:] } } return nil } // ProxyGitHubRequest 代理GitHub请求 func ProxyGitHubRequest(c *gin.Context, u string) { proxyGitHubWithRedirect(c, u, 0) } // proxyGitHubWithRedirect 带重定向的GitHub代理请求 func proxyGitHubWithRedirect(c *gin.Context, u string, redirectCount int) { const maxRedirects = 20 if redirectCount > maxRedirects { 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)) return } // 复制请求头 for key, values := range c.Request.Header { for _, value := range values { req.Header.Add(key, value) } } req.Header.Del("Host") resp, err := utils.GetGlobalHTTPClient().Do(req) if err != nil { c.String(http.StatusInternalServerError, fmt.Sprintf("server error %v", err)) return } defer func() { if err := resp.Body.Close(); err != nil { fmt.Printf("关闭响应体失败: %v\n", err) } }() // 检查文件大小限制 cfg := config.GetConfig() if contentLength := resp.Header.Get("Content-Length"); contentLength != "" { if size, err := strconv.ParseInt(contentLength, 10, 64); err == nil && size > cfg.Server.FileSize { c.String(http.StatusRequestEntityTooLarge, fmt.Sprintf("文件过大,限制大小: %d MB", cfg.Server.FileSize/(1024*1024))) return } } // 清理安全相关的头 resp.Header.Del("Content-Security-Policy") resp.Header.Del("Referrer-Policy") resp.Header.Del("Strict-Transport-Security") // 获取真实域名 realHost := c.Request.Header.Get("X-Forwarded-Host") if realHost == "" { realHost = c.Request.Host } if !strings.HasPrefix(realHost, "http://") && !strings.HasPrefix(realHost, "https://") { realHost = "https://" + realHost } // 处理.sh文件的智能处理 if strings.HasSuffix(strings.ToLower(u), ".sh") { isGzipCompressed := resp.Header.Get("Content-Encoding") == "gzip" processedBody, processedSize, err := utils.ProcessSmart(resp.Body, isGzipCompressed, realHost) if err != nil { fmt.Printf("智能处理失败,回退到直接代理: %v\n", err) processedBody = resp.Body processedSize = 0 } // 智能设置响应头 if processedSize > 0 { resp.Header.Del("Content-Length") resp.Header.Del("Content-Encoding") resp.Header.Set("Transfer-Encoding", "chunked") } // 复制其他响应头 for key, values := range resp.Header { for _, value := range values { c.Header(key, value) } } // 处理重定向 if location := resp.Header.Get("Location"); location != "" { if CheckGitHubURL(location) != nil { c.Header("Location", "/"+location) } else { proxyGitHubWithRedirect(c, location, redirectCount+1) return } } c.Status(resp.StatusCode) // 输出处理后的内容 if _, err := io.Copy(c.Writer, processedBody); err != nil { return } } else { // 复制响应头 for key, values := range resp.Header { for _, value := range values { c.Header(key, value) } } // 处理重定向 if location := resp.Header.Get("Location"); location != "" { if CheckGitHubURL(location) != nil { c.Header("Location", "/"+location) } else { proxyGitHubWithRedirect(c, location, redirectCount+1) return } } c.Status(resp.StatusCode) // 直接流式转发 io.Copy(c.Writer, resp.Body) } }