mirror of
https://github.com/WJQSERVER-STUDIO/ghproxy.git
synced 2026-02-03 00:01:10 +08:00
Merge pull request #119 from WJQSERVER-STUDIO/dev
Some checks failed
Build / prepare (push) Has been cancelled
Build / build (amd64, darwin) (push) Has been cancelled
Build / build (amd64, freebsd) (push) Has been cancelled
Build / build (amd64, linux) (push) Has been cancelled
Build / build (arm64, darwin) (push) Has been cancelled
Build / build (arm64, freebsd) (push) Has been cancelled
Build / build (arm64, linux) (push) Has been cancelled
Build / docker (push) Has been cancelled
Some checks failed
Build / prepare (push) Has been cancelled
Build / build (amd64, darwin) (push) Has been cancelled
Build / build (amd64, freebsd) (push) Has been cancelled
Build / build (amd64, linux) (push) Has been cancelled
Build / build (arm64, darwin) (push) Has been cancelled
Build / build (arm64, freebsd) (push) Has been cancelled
Build / build (arm64, linux) (push) Has been cancelled
Build / docker (push) Has been cancelled
optimize matcher performance
This commit is contained in:
commit
65769975b6
8 changed files with 505 additions and 43 deletions
|
|
@ -1,5 +1,14 @@
|
|||
# 更新日志
|
||||
|
||||
3.5.1 - 2025-06-09
|
||||
---
|
||||
- CHANGE: 大幅优化`Matcher`的性能, 实现零分配, 大幅提升性能; 单次操作时间: `254.3 ns/op` => `29.59 ns/op`
|
||||
|
||||
25w45a - 2025-06-09
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v3.5.1预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 大幅优化`Matcher`的性能, 实现零分配, 大幅提升性能; 单次操作时间: `254.3 ns/op` => `29.59 ns/op`
|
||||
|
||||
3.5.0 - 2025-06-05
|
||||
---
|
||||
- CHANGE: 更新许可证 v2.0 => v2.1
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
25w44a
|
||||
25w45a
|
||||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
3.5.0
|
||||
3.5.1
|
||||
10
go.mod
10
go.mod
|
|
@ -8,8 +8,8 @@ require (
|
|||
github.com/WJQSERVER-STUDIO/logger v1.7.3
|
||||
github.com/cloudwego/hertz v0.10.0
|
||||
github.com/hertz-contrib/http2 v0.1.8
|
||||
golang.org/x/net v0.40.0
|
||||
golang.org/x/time v0.11.0
|
||||
golang.org/x/net v0.41.0
|
||||
golang.org/x/time v0.12.0
|
||||
)
|
||||
|
||||
require (
|
||||
|
|
@ -36,10 +36,10 @@ require (
|
|||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
golang.org/x/arch v0.17.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b // indirect
|
||||
golang.org/x/arch v0.18.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
golang.org/x/text v0.26.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
)
|
||||
|
||||
|
|
|
|||
10
go.sum
10
go.sum
|
|
@ -89,12 +89,16 @@ github.com/wjqserver/modembed v0.0.1/go.mod h1:sYbQJMAjSBsdYQrUsuHY380XXE1CuRh8g
|
|||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/arch v0.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU=
|
||||
golang.org/x/arch v0.17.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
|
||||
golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||
golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b h1:QoALfVG9rhQ/M7vYDScfPdWjGL9dlsVVM5VGh7aKoAA=
|
||||
golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
|
||||
golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 h1:bsqhLWFR6G6xiQcb+JoGqdKdRU6WzPWmK8E0jxTjzo4=
|
||||
golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
|
|
@ -106,6 +110,8 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
|||
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
|
@ -135,8 +141,12 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
|||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c
|
|||
bodyReader = limitreader.NewRateLimitedReader(bodyReader, bandwidthLimit, int(bandwidthBurst), ctx)
|
||||
}
|
||||
|
||||
if MatcherShell(u) && matchString(matcher, matchedMatchers) && cfg.Shell.Editor {
|
||||
if MatcherShell(u) && matchString(matcher) && cfg.Shell.Editor {
|
||||
// 判断body是不是gzip
|
||||
var compress string
|
||||
if resp.Header.Get("Content-Encoding") == "gzip" {
|
||||
|
|
|
|||
204
proxy/match.go
204
proxy/match.go
|
|
@ -6,8 +6,141 @@ import (
|
|||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
githubPrefix = "https://github.com/"
|
||||
rawPrefix = "https://raw.githubusercontent.com/"
|
||||
gistPrefix = "https://gist.github.com/"
|
||||
apiPrefix = "https://api.github.com/"
|
||||
githubPrefixLen int
|
||||
rawPrefixLen int
|
||||
gistPrefixLen int
|
||||
apiPrefixLen int
|
||||
)
|
||||
|
||||
func init() {
|
||||
githubPrefixLen = len(githubPrefix)
|
||||
rawPrefixLen = len(rawPrefix)
|
||||
gistPrefixLen = len(gistPrefix)
|
||||
apiPrefixLen = len(apiPrefix)
|
||||
//log.Printf("githubPrefixLen: %d, rawPrefixLen: %d, gistPrefixLen: %d, apiPrefixLen: %d", githubPrefixLen, rawPrefixLen, gistPrefixLen, apiPrefixLen)
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
// 原实现
|
||||
/*
|
||||
func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHProxyErrors) {
|
||||
var (
|
||||
user string
|
||||
|
|
@ -17,11 +150,11 @@ func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHPro
|
|||
// 匹配 "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, "/")
|
||||
}
|
||||
*/
|
||||
|
||||
//if strings.HasPrefix(remainingPath, "/") {
|
||||
// remainingPath = strings.TrimPrefix(remainingPath, "/")
|
||||
//}
|
||||
|
||||
remainingPath = strings.TrimPrefix(remainingPath, "/")
|
||||
// 预期格式/user/repo/more...
|
||||
// 取出user和repo和最后部分
|
||||
|
|
@ -103,55 +236,56 @@ func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHPro
|
|||
errMsg := "Didn't match any matcher"
|
||||
return "", "", "", NewErrorWithStatusLookup(404, errMsg)
|
||||
}
|
||||
*/
|
||||
|
||||
var (
|
||||
matchedMatchers = []string{
|
||||
"blob",
|
||||
"raw",
|
||||
"gist",
|
||||
}
|
||||
proxyableMatchersMap map[string]struct{}
|
||||
initMatchersOnce sync.Once
|
||||
)
|
||||
|
||||
// matchString 检查目标字符串是否在给定的字符串集合中
|
||||
func matchString(target string, stringsToMatch []string) bool {
|
||||
matchMap := make(map[string]struct{}, len(stringsToMatch))
|
||||
for _, str := range stringsToMatch {
|
||||
matchMap[str] = struct{}{}
|
||||
}
|
||||
_, exists := matchMap[target]
|
||||
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 从给定的 URL 中提取所需的部分
|
||||
// extractParts 与原始版本签名兼容
|
||||
func extractParts(rawURL string) (string, string, string, url.Values, error) {
|
||||
// 解析 URL
|
||||
parsedURL, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return "", "", "", nil, err
|
||||
}
|
||||
|
||||
// 获取路径部分并分割
|
||||
pathParts := strings.Split(parsedURL.Path, "/")
|
||||
path := parsedURL.Path
|
||||
if len(path) > 0 && path[0] == '/' {
|
||||
path = path[1:]
|
||||
}
|
||||
|
||||
// 提取所需的部分
|
||||
if len(pathParts) < 3 {
|
||||
parts := strings.SplitN(path, "/", 3)
|
||||
|
||||
if len(parts) < 2 {
|
||||
return "", "", "", nil, fmt.Errorf("URL path is too short")
|
||||
}
|
||||
|
||||
// 提取 /WJQSERVER-STUDIO 和 /go-utils.git
|
||||
repoOwner := "/" + pathParts[1]
|
||||
repoName := "/" + pathParts[2]
|
||||
|
||||
// 剩余部分
|
||||
remainingPath := strings.Join(pathParts[3:], "/")
|
||||
if remainingPath != "" {
|
||||
remainingPath = "/" + remainingPath
|
||||
repoOwner := "/" + parts[0]
|
||||
repoName := "/" + parts[1]
|
||||
var remainingPath string
|
||||
if len(parts) > 2 {
|
||||
remainingPath = "/" + parts[2]
|
||||
}
|
||||
|
||||
// 查询参数
|
||||
queryParams := parsedURL.Query()
|
||||
|
||||
return repoOwner, repoName, remainingPath, queryParams, nil
|
||||
return repoOwner, repoName, remainingPath, parsedURL.Query(), nil
|
||||
}
|
||||
|
||||
var urlPattern = regexp.MustCompile(`https?://[^\s'"]+`)
|
||||
|
|
|
|||
309
proxy/matcher_test.go
Normal file
309
proxy/matcher_test.go
Normal file
|
|
@ -0,0 +1,309 @@
|
|||
package proxy
|
||||
|
||||
import (
|
||||
"ghproxy/config"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMatcher_Compatibility(t *testing.T) {
|
||||
// --- 准备各种配置用于测试 ---
|
||||
cfgWithAuth := &config.Config{
|
||||
Auth: config.AuthConfig{Enabled: true, Method: "header", ForceAllowApi: false},
|
||||
}
|
||||
cfgNoAuth := &config.Config{
|
||||
Auth: config.AuthConfig{Enabled: false},
|
||||
}
|
||||
cfgApiForceAllowed := &config.Config{
|
||||
Auth: config.AuthConfig{ForceAllowApi: true},
|
||||
}
|
||||
cfgWrongAuthMethod := &config.Config{
|
||||
Auth: config.AuthConfig{Enabled: true, Method: "none"},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
rawPath string
|
||||
config *config.Config
|
||||
expectedUser string
|
||||
expectedRepo string
|
||||
expectedMatcher string
|
||||
expectError bool
|
||||
expectedErrCode int
|
||||
}{
|
||||
{
|
||||
name: "GH Releases Path",
|
||||
rawPath: "https://github.com/owner/repo/releases/download/v1.0/asset.zip",
|
||||
config: cfgWithAuth,
|
||||
expectedUser: "owner", expectedRepo: "repo", expectedMatcher: "releases",
|
||||
},
|
||||
{
|
||||
name: "GH Archive Path",
|
||||
rawPath: "https://github.com/owner/repo.git/archive/main.zip",
|
||||
config: cfgWithAuth,
|
||||
expectedUser: "owner", expectedRepo: "repo.git", expectedMatcher: "releases",
|
||||
},
|
||||
{
|
||||
name: "GH Blob Path",
|
||||
rawPath: "https://github.com/owner/repo/blob/main/path/to/file.go",
|
||||
config: cfgWithAuth,
|
||||
expectedUser: "owner", expectedRepo: "repo", expectedMatcher: "blob",
|
||||
},
|
||||
{
|
||||
name: "GH Raw Path",
|
||||
rawPath: "https://github.com/owner/repo/raw/main/image.png",
|
||||
config: cfgWithAuth,
|
||||
expectedUser: "owner", expectedRepo: "repo", expectedMatcher: "raw",
|
||||
},
|
||||
{
|
||||
name: "GH Clone Info Refs",
|
||||
rawPath: "https://github.com/owner/repo.git/info/refs?service=git-upload-pack",
|
||||
config: cfgWithAuth,
|
||||
expectedUser: "owner", expectedRepo: "repo.git", expectedMatcher: "clone",
|
||||
},
|
||||
{
|
||||
name: "GH Clone Git Upload Pack",
|
||||
rawPath: "https://github.com/owner/repo/git-upload-pack",
|
||||
config: cfgWithAuth,
|
||||
expectedUser: "owner", expectedRepo: "repo", expectedMatcher: "clone",
|
||||
},
|
||||
{
|
||||
name: "Girhub Broken Path",
|
||||
rawPath: "https://github.com/owner",
|
||||
config: cfgWithAuth,
|
||||
expectError: true, expectedErrCode: 400,
|
||||
},
|
||||
|
||||
{
|
||||
name: "RawGHUserContent Path",
|
||||
rawPath: "https://raw.githubusercontent.com/owner/repo/branch/file.sh",
|
||||
config: cfgWithAuth,
|
||||
expectedUser: "owner", expectedRepo: "repo", expectedMatcher: "raw",
|
||||
},
|
||||
{
|
||||
name: "Gist Path",
|
||||
rawPath: "https://gist.github.com/user/abcdef1234567890",
|
||||
config: cfgWithAuth,
|
||||
expectedUser: "user", expectedRepo: "", expectedMatcher: "gist",
|
||||
},
|
||||
{
|
||||
name: "API Repos Path (with Auth)",
|
||||
rawPath: "https://api.github.com/repos/owner/repo/pulls",
|
||||
config: cfgWithAuth,
|
||||
expectedUser: "owner", expectedRepo: "repo", expectedMatcher: "api",
|
||||
},
|
||||
{
|
||||
name: "API Users Path (with Auth)",
|
||||
rawPath: "https://api.github.com/users/someuser/repos",
|
||||
config: cfgWithAuth,
|
||||
expectedUser: "someuser", expectedRepo: "", expectedMatcher: "api",
|
||||
},
|
||||
{
|
||||
name: "API Other Path (with Auth)",
|
||||
rawPath: "https://api.github.com/octocat",
|
||||
config: cfgWithAuth,
|
||||
expectedUser: "", expectedRepo: "", expectedMatcher: "api",
|
||||
},
|
||||
{
|
||||
name: "API Path (Force Allowed)",
|
||||
rawPath: "https://api.github.com/repos/owner/repo",
|
||||
config: cfgApiForceAllowed, // Auth disabled, but force allowed
|
||||
expectedUser: "owner", expectedRepo: "repo", expectedMatcher: "api",
|
||||
},
|
||||
{
|
||||
name: "Malformed GH Path (no repo)",
|
||||
rawPath: "https://github.com/owner/",
|
||||
config: cfgWithAuth,
|
||||
expectError: true, expectedErrCode: 400,
|
||||
},
|
||||
{
|
||||
name: "Malformed GH Path (no action)",
|
||||
rawPath: "https://github.com/owner/repo",
|
||||
config: cfgWithAuth,
|
||||
expectError: true, expectedErrCode: 400,
|
||||
},
|
||||
{
|
||||
name: "Malformed GH Path (empty user)",
|
||||
rawPath: "https://github.com//repo/blob/main/file.go",
|
||||
config: cfgWithAuth,
|
||||
expectError: true, expectedErrCode: 400,
|
||||
},
|
||||
{
|
||||
name: "Malformed Raw Path (no repo)",
|
||||
rawPath: "https://raw.githubusercontent.com/owner/",
|
||||
config: cfgWithAuth,
|
||||
expectError: true, expectedErrCode: 400,
|
||||
},
|
||||
{
|
||||
name: "Malformed Gist Path (no user)",
|
||||
rawPath: "https://gist.github.com/",
|
||||
config: cfgWithAuth,
|
||||
expectError: true, expectedErrCode: 400,
|
||||
},
|
||||
{
|
||||
name: "Unsupported GH Action",
|
||||
rawPath: "https://github.com/owner/repo/issues/123",
|
||||
config: cfgWithAuth,
|
||||
expectError: true, expectedErrCode: 400,
|
||||
},
|
||||
{
|
||||
name: "API Path (No Auth)",
|
||||
rawPath: "https://api.github.com/user",
|
||||
config: cfgNoAuth,
|
||||
expectError: true, expectedErrCode: 403,
|
||||
},
|
||||
{
|
||||
name: "API Path (Wrong Auth Method)",
|
||||
rawPath: "https://api.github.com/user",
|
||||
config: cfgWrongAuthMethod,
|
||||
expectError: true, expectedErrCode: 403,
|
||||
},
|
||||
{
|
||||
name: "No Matcher Found (other domain)",
|
||||
rawPath: "https://bitbucket.org/owner/repo",
|
||||
config: cfgWithAuth,
|
||||
expectError: true, expectedErrCode: 404,
|
||||
},
|
||||
{
|
||||
name: "No Matcher Found (path too short)",
|
||||
rawPath: "https://a.co",
|
||||
config: cfgWithAuth,
|
||||
expectError: true, expectedErrCode: 404,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
user, repo, matcher, ghErr := Matcher(tc.rawPath, tc.config)
|
||||
|
||||
if tc.expectError {
|
||||
if ghErr == nil {
|
||||
t.Fatalf("Expected a GHProxyErrors error, but got nil")
|
||||
}
|
||||
if ghErr.StatusCode != tc.expectedErrCode {
|
||||
t.Errorf("Expected error code %d, but got %d (msg: %s)",
|
||||
tc.expectedErrCode, ghErr.StatusCode, ghErr.ErrorMessage)
|
||||
}
|
||||
} else {
|
||||
if ghErr != nil {
|
||||
t.Fatalf("Expected no error, but got: %s", ghErr.ErrorMessage)
|
||||
}
|
||||
if user != tc.expectedUser {
|
||||
t.Errorf("user: got %q, want %q", user, tc.expectedUser)
|
||||
}
|
||||
if repo != tc.expectedRepo {
|
||||
t.Errorf("repo: got %q, want %q", repo, tc.expectedRepo)
|
||||
}
|
||||
if matcher != tc.expectedMatcher {
|
||||
t.Errorf("matcher: got %q, want %q", matcher, tc.expectedMatcher)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractParts_Compatibility(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
rawURL string
|
||||
expectedOwner string
|
||||
expectedRepo string
|
||||
expectedRem string
|
||||
expectedQuery url.Values
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "Standard git clone URL",
|
||||
rawURL: "https://github.com/WJQSERVER-STUDIO/go-utils.git/info/refs?service=git-upload-pack",
|
||||
expectedOwner: "/WJQSERVER-STUDIO",
|
||||
expectedRepo: "/go-utils.git",
|
||||
expectedRem: "/info/refs",
|
||||
expectedQuery: url.Values{"service": []string{"git-upload-pack"}},
|
||||
},
|
||||
{
|
||||
name: "No remaining path",
|
||||
rawURL: "https://example.com/owner/repo",
|
||||
expectedOwner: "/owner",
|
||||
expectedRepo: "/repo",
|
||||
expectedRem: "",
|
||||
expectedQuery: url.Values{},
|
||||
},
|
||||
{
|
||||
name: "Root path only",
|
||||
rawURL: "https://example.com/",
|
||||
expectError: true, // Path is too short
|
||||
},
|
||||
{
|
||||
name: "One level path",
|
||||
rawURL: "https://example.com/owner",
|
||||
expectError: true, // Path is too short
|
||||
},
|
||||
{
|
||||
name: "Empty path segments",
|
||||
rawURL: "https://example.com//repo/a", // Will be treated as /repo/a
|
||||
expectedOwner: "", // First part is empty
|
||||
expectedRepo: "/repo",
|
||||
expectedRem: "/a",
|
||||
},
|
||||
{
|
||||
name: "Invalid URL format",
|
||||
rawURL: "://invalid",
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
owner, repo, rem, query, err := extractParts(tc.rawURL)
|
||||
|
||||
if (err != nil) != tc.expectError {
|
||||
t.Fatalf("extractParts() error = %v, expectError %v", err, tc.expectError)
|
||||
}
|
||||
|
||||
if !tc.expectError {
|
||||
if owner != tc.expectedOwner {
|
||||
t.Errorf("owner: got %q, want %q", owner, tc.expectedOwner)
|
||||
}
|
||||
if repo != tc.expectedRepo {
|
||||
t.Errorf("repo: got %q, want %q", repo, tc.expectedRepo)
|
||||
}
|
||||
if rem != tc.expectedRem {
|
||||
t.Errorf("remaining path: got %q, want %q", rem, tc.expectedRem)
|
||||
}
|
||||
if !reflect.DeepEqual(query, tc.expectedQuery) {
|
||||
t.Errorf("query: got %v, want %v", query, tc.expectedQuery)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchString_Compatibility(t *testing.T) {
|
||||
testCases := []struct {
|
||||
target string
|
||||
expected bool
|
||||
}{
|
||||
{"blob", true}, {"raw", true}, {"gist", true},
|
||||
{"clone", false}, {"releases", false},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.target, func(t *testing.T) {
|
||||
if got := matchString(tc.target); got != tc.expected {
|
||||
t.Errorf("matchString('%s') = %v; want %v", tc.target, got, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMatcher(b *testing.B) {
|
||||
cfg := &config.Config{}
|
||||
path := "https://github.com/WJQSERVER/speedtest-ex/releases/download/v1.2.0/speedtest-linux-amd64.tar.gz"
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _, _, _ = Matcher(path, cfg)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue