package main import ( "fmt" "io" "log" "net/http" "net/url" "regexp" "strings" ) // ======================== // 配置区域 // ======================== const ( sizeLimit = int64(1024 * 1024 * 1024 * 1) // 允许的文件大小, 1GB host = "127.0.0.1" port = "2005" ) // 先生效白名单再匹配黑名单 // 每行一个规则,示例: // user1 # 封禁user1的所有仓库 // user1/repo1 # 封禁user1的repo1 // */repo1 # 封禁所有叫做repo1的仓库 var ( whiteListStr = `` blackListStr = `` ) // ======================== // 全局变量与预编译正则 // ======================== var ( whiteList [][]string blackList [][]string exp1 = regexp.MustCompile(`^(?:https?://)?github\.com/(?P[^/]+)/(?P[^/]+)/(?:releases|archive)/.*$`) exp2 = regexp.MustCompile(`^(?:https?://)?github\.com/(?P[^/]+)/(?P[^/]+)/(?:blob|raw)/.*$`) exp3 = regexp.MustCompile(`^(?:https?://)?github\.com/(?P[^/]+)/(?P[^/]+)/(?:info|git-).*$`) exp4 = regexp.MustCompile(`^(?:https?://)?raw\.(?:githubusercontent|github)\.com/(?P[^/]+)/(?P[^/]+)/.+?/.+$`) exp5 = regexp.MustCompile(`^(?:https?://)?gist\.(?:githubusercontent|github)\.com/(?P[^/]+)/.+?/.+$`) httpClient *http.Client ) func init() { // 1. 初始化列表 whiteList = parseList(whiteListStr) blackList = parseList(blackListStr) httpClient = &http.Client{ CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, Timeout: 0, } } func main() { http.HandleFunc("/", routeHandler) addr := fmt.Sprintf("%s:%s", host, port) log.Printf("服务器启动成功,正在监听 %s", addr) if err := http.ListenAndServe(addr, nil); err != nil { log.Fatal(err) } } func routeHandler(w http.ResponseWriter, r *http.Request) { u := strings.TrimPrefix(r.URL.RequestURI(), "/") if strings.HasPrefix(u, "https:/") && !strings.HasPrefix(u, "https://") { u = "https://" + strings.TrimPrefix(u, "https:/") } else if strings.HasPrefix(u, "http:/") && !strings.HasPrefix(u, "http://") { u = "http://" + strings.TrimPrefix(u, "http:/") } if !strings.HasPrefix(u, "http") { u = "https://" + u } m := checkURL(u) if m == nil { http.Error(w, "Invalid input.", http.StatusForbidden) return } if len(whiteList) > 0 { allowed := false for _, i := range whiteList { if matchRule(m, i) { allowed = true break } } if !allowed { http.Error(w, "Forbidden by white list.", http.StatusForbidden) return } } for _, i := range blackList { if matchRule(m, i) { http.Error(w, "Forbidden by black list.", http.StatusForbidden) return } } // 将网页浏览的blob链接统一替换为raw下载链接 if exp2.MatchString(u) { u = strings.Replace(u, "/blob/", "/raw/", 1) } // 发起代理请求 parsedURL, err := url.Parse(u) if err == nil { u = parsedURL.String() } proxy(w, r, u) } func proxy(w http.ResponseWriter, r *http.Request, targetURL string) { // 修正由于多重代理可能造成的 URL 格式错误 if strings.HasPrefix(targetURL, "https:/") && ! strings.HasPrefix(targetURL, "https://") { targetURL = "https://" + targetURL[7:] } req, err := http.NewRequest(r.Method, targetURL, r.Body) if err != nil { http.Error(w, "server error " + err.Error(), http.StatusInternalServerError) return } // 拷贝 Header, 但不包含 Host for k, vv := range r.Header { if strings.EqualFold(k, "Host") { continue } for _, v := range vv { req.Header.Add(k, v) } } resp, err := httpClient.Do(req) if err != nil { http.Error(w, "server error " + err.Error(), http.StatusInternalServerError) return } defer resp.Body.Close() // 校验大文件限制 if resp.ContentLength > sizeLimit { http.Redirect(w, r, targetURL, http.StatusFound) return } // 处理重定向 Location Header if loc := resp.Header.Get("Location"); loc != "" { if checkURL(loc) != nil { resp.Header.Set("Location", "/"+loc) } else { // 如果不符合 Github 下载格式,则递归代理目标地址 proxy(w, r, loc) return } } // 拷贝响应的 Header 并写入状态码 for k, vv := range resp.Header { for _, v := range vv { w.Header().Add(k, v) } } w.WriteHeader(resp.StatusCode) // Go 会原生以 Stream 方式拷贝数据给 Client 端 io.Copy(w, resp.Body) } func matchRule(m []string, i []string) bool { // m 通常为 [author, repo] 或 [author] if len(i) == 1 { return len(m) >= 1 && m[0] == i[0] } else if len(i) == 2 { if i[0] == "*" && len(m) >= 2 && m[1] == i[1] { return true } return len(m) >= 2 && m[0] == i[0] && m[1] == i[1] } return false } func checkURL(u string) []string { exps := []*regexp.Regexp {exp1, exp2, exp3, exp4, exp5} for _, exp := range exps { matches := exp.FindStringSubmatch(u) if matches != nil { var result []string for i, name := range exp.SubexpNames() { if i > 0 && name != "" { result = append(result, matches[i]) } } return result } } return nil } func parseList(s string) [][]string { var res [][]string lines := strings.Split(s, "\n") for _, line := range lines { line = strings.TrimSpace(line) if line != "" { parts := strings.Split(line, "/") var cleaned []string for _, p := range parts { cleaned = append(cleaned, strings.ReplaceAll(p, " ", "")) } res = append(res, cleaned) } } return res }