Files
ghproxy/proxy/match.go
wjqserver d389a61f09 4.3.0
2025-08-11 18:28:03 +08:00

218 lines
6.0 KiB
Go

package proxy
import (
"fmt"
"ghproxy/config"
"net/url"
"regexp"
"strings"
"sync"
)
var (
githubPrefixLen int
rawPrefixLen int
gistPrefixLen int
gistContentPrefixLen int
apiPrefixLen int
)
const (
githubPrefix = "https://github.com/"
rawPrefix = "https://raw.githubusercontent.com/"
gistPrefix = "https://gist.github.com/"
gistContentPrefix = "https://gist.githubusercontent.com/"
apiPrefix = "https://api.github.com/"
ociv2Prefix = "https://v2/"
releasesDownloadSnippet = "releases/download/"
)
func init() {
githubPrefixLen = len(githubPrefix)
rawPrefixLen = len(rawPrefix)
gistPrefixLen = len(gistPrefix)
gistContentPrefixLen = len(gistContentPrefix)
apiPrefixLen = len(apiPrefix)
}
// Matcher 从原始URL路径中高效地解析并匹配代理规则.
func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHProxyErrors) {
/*
if len(rawPath) < 18 {
return "", "", "", NewErrorWithStatusLookup(404, "path too short")
}
*/
// 匹配 "https://github.com/"
if strings.HasPrefix(rawPath, githubPrefix) {
pathAfterDomain := rawPath[githubPrefixLen:]
// 解析 user
i := strings.IndexByte(pathAfterDomain, '/')
if i <= 0 {
return "", "", "", NewErrorWithStatusLookup(400, "malformed github path: missing user")
}
user := pathAfterDomain[:i]
pathAfterUser := pathAfterDomain[i+1:]
// 解析 repo
i = strings.IndexByte(pathAfterUser, '/')
if i <= 0 {
return "", "", "", NewErrorWithStatusLookup(400, "malformed github path: missing action")
}
repo := pathAfterUser[:i]
pathAfterRepo := pathAfterUser[i+1:]
if len(pathAfterRepo) == 0 {
return "", "", "", NewErrorWithStatusLookup(400, "malformed github path: missing action")
}
// 优先处理所有 "releases" 相关的下载路径
if strings.HasPrefix(pathAfterRepo, "releases/") {
// 情况 A: "releases/download/..."
if strings.HasPrefix(pathAfterRepo, "releases/download/") {
return user, repo, "releases", nil
}
// 情况 B: "releases/:tag/download/..."
pathAfterReleases := pathAfterRepo[len("releases/"):]
slashIndex := strings.IndexByte(pathAfterReleases, '/')
if slashIndex > 0 { // 确保tag不为空
pathAfterTag := pathAfterReleases[slashIndex+1:]
if strings.HasPrefix(pathAfterTag, "download/") {
return user, repo, "releases", nil
}
}
// 如果不满足上述下载链接的结构, 则为网页浏览路径, 予以拒绝
return "", "", "", NewErrorWithStatusLookup(400, "unsupported releases page, only download links are allowed")
}
// 检查 "archive/" 路径
if strings.HasPrefix(pathAfterRepo, "archive/") {
// 根据测试用例, archive路径的matcher也应为releases
return user, repo, "releases", nil
}
// 如果不是下载路径, 则解析action并进行分类
i = strings.IndexByte(pathAfterRepo, '/')
action := pathAfterRepo
if i != -1 {
action = pathAfterRepo[:i]
}
var matcher string
switch action {
case "blob":
matcher = "blob"
case "raw":
matcher = "raw"
case "info", "git-upload-pack":
matcher = "clone"
default:
return "", "", "", NewErrorWithStatusLookup(400, fmt.Sprintf("unsupported github action: %s", action))
}
return user, repo, matcher, nil
}
// 匹配 "https://raw.githubusercontent.com/"
if strings.HasPrefix(rawPath, rawPrefix) {
remaining := rawPath[rawPrefixLen:]
parts := strings.SplitN(remaining, "/", 3)
if len(parts) < 3 {
return "", "", "", NewErrorWithStatusLookup(400, "malformed raw url: path too short")
}
return parts[0], parts[1], "raw", nil
}
// 匹配 "https://gist.github.com/" 或 "https://gist.githubusercontent.com/"
isGist := strings.HasPrefix(rawPath, gistPrefix)
if isGist || strings.HasPrefix(rawPath, gistContentPrefix) {
var remaining string
if isGist {
remaining = rawPath[gistPrefixLen:]
} else {
remaining = rawPath[gistContentPrefixLen:]
}
parts := strings.SplitN(remaining, "/", 2)
if len(parts) == 0 || parts[0] == "" {
return "", "", "", NewErrorWithStatusLookup(400, "malformed gist url: missing user")
}
return parts[0], "", "gist", nil
}
// 匹配 "https://api.github.com/"
if strings.HasPrefix(rawPath, apiPrefix) {
if !cfg.Auth.ForceAllowApi && (cfg.Auth.Method != "header" || !cfg.Auth.Enabled) {
return "", "", "", NewErrorWithStatusLookup(403, "API proxy requires header authentication")
}
remaining := rawPath[apiPrefixLen:]
var user, repo string
if strings.HasPrefix(remaining, "repos/") {
parts := strings.SplitN(remaining[6:], "/", 3)
if len(parts) >= 2 {
user = parts[0]
repo = parts[1]
}
} else if strings.HasPrefix(remaining, "users/") {
parts := strings.SplitN(remaining[6:], "/", 2)
if len(parts) >= 1 {
user = parts[0]
}
}
return user, repo, "api", nil
}
return "", "", "", NewErrorWithStatusLookup(404, "no matcher found for the given path")
}
var (
proxyableMatchersMap map[string]struct{}
initMatchersOnce sync.Once
)
func initMatchers() {
initMatchersOnce.Do(func() {
matchers := []string{"blob", "raw", "gist"}
proxyableMatchersMap = make(map[string]struct{}, len(matchers))
for _, m := range matchers {
proxyableMatchersMap[m] = struct{}{}
}
})
}
// matchString 与原始版本签名兼容
func matchString(target string) bool {
initMatchers()
_, exists := proxyableMatchersMap[target]
return exists
}
// extractParts 与原始版本签名兼容
func extractParts(rawURL string) (string, string, string, url.Values, error) {
parsedURL, err := url.Parse(rawURL)
if err != nil {
return "", "", "", nil, err
}
path := parsedURL.Path
if len(path) > 0 && path[0] == '/' {
path = path[1:]
}
parts := strings.SplitN(path, "/", 3)
if len(parts) < 2 {
return "", "", "", nil, fmt.Errorf("URL path is too short")
}
repoOwner := "/" + parts[0]
repoName := "/" + parts[1]
var remainingPath string
if len(parts) > 2 {
remainingPath = "/" + parts[2]
}
return repoOwner, repoName, remainingPath, parsedURL.Query(), nil
}
var urlPattern = regexp.MustCompile(`https?://[^\s'"]+`)