3.5.x旨在于改善性能, 同时加入MPL 2.0许可证

主要更新

Matcher(匹配器)大幅性能改进

以下数据由matcher_test.go测试得出

单次操作时间: 254.3 ns/op => 29.59 ns/op 内存分配次数: 1/op => 0/op 内存分配大小: 96B/op => 0B/op

零分配实现

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
// 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) {
		remaining := rawPath[githubPrefixLen:]
		i := strings.IndexByte(remaining, '/')
		if i <= 0 {
			return "", "", "", NewErrorWithStatusLookup(400, "malformed github path: missing user")
		}
		user := remaining[:i]
		remaining = remaining[i+1:]
		i = strings.IndexByte(remaining, '/')
		if i <= 0 {
			return "", "", "", NewErrorWithStatusLookup(400, "malformed github path: missing repo")
		}
		repo := remaining[:i]
		remaining = remaining[i+1:]
		if len(remaining) == 0 {
			return "", "", "", NewErrorWithStatusLookup(400, "malformed github path: missing action")
		}
		i = strings.IndexByte(remaining, '/')
		action := remaining
		if i != -1 {
			action = remaining[:i]
		}
		var matcher string
		switch action {
		case "releases", "archive":
			matcher = "releases"
		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:]
		// 这里的逻辑与 github.com 的类似, 需要提取 user, repo, branch, file...
		// 我们只需要 user 和 repo
		i := strings.IndexByte(remaining, '/')
		if i <= 0 {
			return "", "", "", NewErrorWithStatusLookup(400, "malformed raw url: missing user")
		}
		user := remaining[:i]
		remaining = remaining[i+1:]
		i = strings.IndexByte(remaining, '/')
		if i <= 0 {
			return "", "", "", NewErrorWithStatusLookup(400, "malformed raw url: missing repo")
		}
		repo := remaining[:i]
		// raw 链接至少需要 user/repo/branch 三部分
		remaining = remaining[i+1:]
		if len(remaining) == 0 {
			return "", "", "", NewErrorWithStatusLookup(400, "malformed raw url: missing branch/commit")
		}
		return user, repo, "raw", nil
	}

	// 匹配 "https://gist.github.com/"
	if strings.HasPrefix(rawPath, gistPrefix) {
		remaining := rawPath[gistPrefixLen:]
		i := strings.IndexByte(remaining, '/')
		if i <= 0 {
			// case: https://gist.github.com/user
			// 这种情况下, gist_id 缺失, 但我们仍然可以认为 user 是有效的
			if len(remaining) > 0 {
				return remaining, "", "gist", nil
			}
			return "", "", "", NewErrorWithStatusLookup(400, "malformed gist url: missing user")
		}
		// case: https://gist.github.com/user/gist_id...
		user := remaining[:i]
		return user, "", "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")
}

原实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHProxyErrors) {
	var (
		user    string
		repo    string
		matcher string
	)
	// 匹配 "https://github.com"开头的链接
	if strings.HasPrefix(rawPath, "https://github.com") {
		remainingPath := strings.TrimPrefix(rawPath, "https://github.com")

		//if strings.HasPrefix(remainingPath, "/") {
		//		remainingPath = strings.TrimPrefix(remainingPath, "/")
		//}

		remainingPath = strings.TrimPrefix(remainingPath, "/")
		// 预期格式/user/repo/more...
		// 取出user和repo和最后部分
		parts := strings.Split(remainingPath, "/")
		if len(parts) <= 2 {
			errMsg := "Not enough parts in path after matching 'https://github.com*'"
			return "", "", "", NewErrorWithStatusLookup(400, errMsg)
		}
		user = parts[0]
		repo = parts[1]
		// 匹配 "https://github.com"开头的链接
		if len(parts) >= 3 {
			switch parts[2] {
			case "releases", "archive":
				matcher = "releases"
			case "blob":
				matcher = "blob"
			case "raw":
				matcher = "raw"
			case "info", "git-upload-pack":
				matcher = "clone"
			default:
				errMsg := "Url Matched 'https://github.com*', but didn't match the next matcher"
				return "", "", "", NewErrorWithStatusLookup(400, errMsg)
			}
		}
		return user, repo, matcher, nil
	}
	// 匹配 "https://raw"开头的链接
	if strings.HasPrefix(rawPath, "https://raw") {
		remainingPath := strings.TrimPrefix(rawPath, "https://")
		parts := strings.Split(remainingPath, "/")
		if len(parts) <= 3 {
			errMsg := "URL after matched 'https://raw*' should have at least 4 parts (user/repo/branch/file)."
			return "", "", "", NewErrorWithStatusLookup(400, errMsg)
		}
		user = parts[1]
		repo = parts[2]
		matcher = "raw"

		return user, repo, matcher, nil
	}
	// 匹配 "https://gist"开头的链接
	if strings.HasPrefix(rawPath, "https://gist") {
		remainingPath := strings.TrimPrefix(rawPath, "https://")
		parts := strings.Split(remainingPath, "/")
		if len(parts) <= 3 {
			errMsg := "URL after matched 'https://gist*' should have at least 4 parts (user/gist_id)."
			return "", "", "", NewErrorWithStatusLookup(400, errMsg)
		}
		user = parts[1]
		repo = ""
		matcher = "gist"
		return user, repo, matcher, nil
	}
	// 匹配 "https://api.github.com/"开头的链接
	if strings.HasPrefix(rawPath, "https://api.github.com/") {
		matcher = "api"
		remainingPath := strings.TrimPrefix(rawPath, "https://api.github.com/")

		parts := strings.Split(remainingPath, "/")
		if parts[0] == "repos" {
			user = parts[1]
			repo = parts[2]
		}
		if parts[0] == "users" {
			user = parts[1]
		}
		if !cfg.Auth.ForceAllowApi {
			if cfg.Auth.Method != "header" || !cfg.Auth.Enabled {
				//return "", "", "", ErrAuthHeaderUnavailable
				errMsg := "AuthHeader Unavailable, Need to open header auth to enable api proxy"
				return "", "", "", NewErrorWithStatusLookup(403, errMsg)
			}
		}
		return user, repo, matcher, nil
	}
	//return "", "", "", ErrNotFound
	errMsg := "Didn't match any matcher"
	return "", "", "", NewErrorWithStatusLookup(404, errMsg)
}

引入 MPL 2.0许可证

本项目使用 WJQserver Studio License 2.1Mozilla Public License Version 2.0 双重许可, 您可从中选择一个遵守

WJQserver Studio License 2.1 是一个 Source-available 许可证, 在公开分发的情况下其存在类似于AGPL 3.0的强Copyleft特性, 在非营利私人(未分发)使用场景下, 其类似于 宽松型开源许可证

Mozilla Public License Version 2.0 是一个 Open-Source 许可证, 是一个文件级Copyleft的弱Copyleft许可证

项目相关

Github仓库地址

GHProxy-Touka

DEMO

TG讨论群组

相关文章

GHProxy项目文档Next 感谢 @redbunnys的维护