From 6a780a5e277ff9b451b87995a0821bba4db4269b Mon Sep 17 00:00:00 2001 From: nite Date: Thu, 12 Jun 2025 15:41:56 +1000 Subject: [PATCH] Refactor Shadowsocks and Socks parsers to improve username and password handling --- .gitignore | 4 +- parser/shadowsocks.go | 30 ++++-- parser/socks.go | 49 +++++++--- test/socks_test.go | 221 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 280 insertions(+), 24 deletions(-) create mode 100644 test/socks_test.go diff --git a/.gitignore b/.gitignore index b607ca7..55d9fff 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,4 @@ subs logs data .env -.vscode/settings.json -test -*test.go \ No newline at end of file +.vscode/settings.json \ No newline at end of file diff --git a/parser/shadowsocks.go b/parser/shadowsocks.go index dd45acd..9e6b1f3 100644 --- a/parser/shadowsocks.go +++ b/parser/shadowsocks.go @@ -92,19 +92,35 @@ func (p *ShadowsocksParser) Parse(proxy string) (P.Proxy, error) { } method := link.User.Username() - password, _ := link.User.Password() + password, hasPassword := link.User.Password() - if password == "" { - user, err := DecodeBase64(method) + if !hasPassword && isLikelyBase64(method) { + decodedStr, err := DecodeBase64(method) if err == nil { - methodAndPass := strings.SplitN(user, ":", 2) + methodAndPass := strings.SplitN(decodedStr, ":", 2) if len(methodAndPass) == 2 { - method = methodAndPass[0] - password = methodAndPass[1] + method, err = url.QueryUnescape(methodAndPass[0]) + if err != nil { + return P.Proxy{}, &ParseError{ + Type: ErrInvalidStruct, + Message: "invalid method", + Raw: proxy, + } + } + password, err = url.QueryUnescape(methodAndPass[1]) + if err != nil { + return P.Proxy{}, &ParseError{ + Type: ErrInvalidStruct, + Message: "invalid password", + Raw: proxy, + } + } + } else { + method = decodedStr } } } - if isLikelyBase64(password) { + if password != "" && isLikelyBase64(password) { password, err = DecodeBase64(password) if err != nil { return P.Proxy{}, &ParseError{ diff --git a/parser/socks.go b/parser/socks.go index 41601e8..d0edfcb 100644 --- a/parser/socks.go +++ b/parser/socks.go @@ -68,23 +68,45 @@ func (p *SocksParser) Parse(proxy string) (P.Proxy, error) { } remarks = strings.TrimSpace(remarks) - encodeStr := link.User.Username() var username, password string - if encodeStr != "" { - decodeStr, err := DecodeBase64(encodeStr) - splitStr := strings.Split(decodeStr, ":") - if err != nil { - return P.Proxy{}, &ParseError{ - Type: ErrInvalidStruct, - Message: "url parse error", - Raw: proxy, + + username = link.User.Username() + password, hasPassword := link.User.Password() + + if !hasPassword && isLikelyBase64(username) { + decodedStr, err := DecodeBase64(username) + if err == nil { + usernameAndPassword := strings.SplitN(decodedStr, ":", 2) + if len(usernameAndPassword) == 2 { + username, err = url.QueryUnescape(usernameAndPassword[0]) + if err != nil { + return P.Proxy{}, &ParseError{ + Type: ErrInvalidStruct, + Message: "invalid username", + Raw: proxy, + } + } + password, err = url.QueryUnescape(usernameAndPassword[1]) + if err != nil { + return P.Proxy{}, &ParseError{ + Type: ErrInvalidStruct, + Message: "invalid password", + Raw: proxy, + } + } + } else { + username, err = url.QueryUnescape(decodedStr) + if err != nil { + return P.Proxy{}, &ParseError{ + Type: ErrInvalidStruct, + Message: "invalid username", + Raw: proxy, + } + } } } - username = splitStr[0] - if len(splitStr) == 2 { - password = splitStr[1] - } } + return P.Proxy{ Type: p.GetType(), Name: remarks, @@ -95,7 +117,6 @@ func (p *SocksParser) Parse(proxy string) (P.Proxy, error) { Password: password, }, }, nil - } func init() { diff --git a/test/socks_test.go b/test/socks_test.go new file mode 100644 index 0000000..3ac27cb --- /dev/null +++ b/test/socks_test.go @@ -0,0 +1,221 @@ +package test + +import ( + "testing" + + "github.com/bestnite/sub2clash/model/proxy" + "github.com/bestnite/sub2clash/parser" +) + +func TestSocksParser_Parse(t *testing.T) { + p := &parser.SocksParser{} + + tests := []struct { + name string + input string + expected proxy.Proxy + expectError bool + errorType string + }{ + { + name: "无认证SOCKS5代理 - IPv4", + input: "socks://127.0.0.1:1080", + expected: proxy.Proxy{ + Type: "socks5", + Name: "127.0.0.1:1080", + Socks: proxy.Socks{ + Server: "127.0.0.1", + Port: 1080, + UserName: "", + Password: "", + }, + }, + expectError: false, + }, + { + name: "无认证SOCKS5代理 - IPv6", + input: "socks://[2001:db8::1]:1080", + expected: proxy.Proxy{ + Type: "socks5", + Name: "2001:db8::1:1080", + Socks: proxy.Socks{ + Server: "2001:db8::1", + Port: 1080, + UserName: "", + Password: "", + }, + }, + expectError: false, + }, + { + name: "明文用户名密码认证 - IPv4", + input: "socks://user:pass@127.0.0.1:1080", + expected: proxy.Proxy{ + Type: "socks5", + Name: "127.0.0.1:1080", + Socks: proxy.Socks{ + Server: "127.0.0.1", + Port: 1080, + UserName: "user", + Password: "pass", + }, + }, + expectError: false, + }, + { + name: "明文用户名密码认证 - IPv6", + input: "socks://user:pass@[2001:db8::1]:1080", + expected: proxy.Proxy{ + Type: "socks5", + Name: "2001:db8::1:1080", + Socks: proxy.Socks{ + Server: "2001:db8::1", + Port: 1080, + UserName: "user", + Password: "pass", + }, + }, + expectError: false, + }, + { + name: "Base64编码的用户名密码", + input: "socks://dXNlcm5hbWU6cGFzc3dvcmQ=@127.0.0.1:1080", + expected: proxy.Proxy{ + Type: "socks5", + Name: "127.0.0.1:1080", + Socks: proxy.Socks{ + Server: "127.0.0.1", + Port: 1080, + UserName: "username", + Password: "password", + }, + }, + expectError: false, + }, + { + name: "带Fragment的URL", + input: "socks://username:password@proxy.example.com:1080#MyProxy", + expected: proxy.Proxy{ + Type: "socks5", + Name: "MyProxy", + Socks: proxy.Socks{ + Server: "proxy.example.com", + Port: 1080, + UserName: "username", + Password: "password", + }, + }, + expectError: false, + }, + { + name: "只有用户名没有密码", + input: "socks://username@127.0.0.1:1080", + expected: proxy.Proxy{ + Type: "socks5", + Name: "127.0.0.1:1080", + Socks: proxy.Socks{ + Server: "127.0.0.1", + Port: 1080, + UserName: "username", + Password: "", + }, + }, + expectError: false, + }, + { + name: "Base64编码只有用户名", + input: "socks://dXNlcm5hbWU=@127.0.0.1:1080", + expected: proxy.Proxy{ + Type: "socks5", + Name: "127.0.0.1:1080", + Socks: proxy.Socks{ + Server: "127.0.0.1", + Port: 1080, + UserName: "username", + Password: "", + }, + }, + expectError: false, + }, + { + name: "域名代理", + input: "socks://proxy.example.com:8080", + expected: proxy.Proxy{ + Type: "socks5", + Name: "proxy.example.com:8080", + Socks: proxy.Socks{ + Server: "proxy.example.com", + Port: 8080, + UserName: "", + Password: "", + }, + }, + expectError: false, + }, + { + name: "特殊字符用户名密码", + input: "socks://user%40domain:p%40ss%21@127.0.0.1:1080", + expected: proxy.Proxy{ + Type: "socks5", + Name: "127.0.0.1:1080", + Socks: proxy.Socks{ + Server: "127.0.0.1", + Port: 1080, + UserName: "user@domain", + Password: "p@ss!", + }, + }, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := p.Parse(tt.input) + + if tt.expectError { + if err == nil { + t.Errorf("期望出现错误,但没有错误") + return + } + + if parseErr, ok := err.(*parser.ParseError); ok { + if tt.errorType != "" && string(parseErr.Type) != tt.errorType { + t.Errorf("错误类型不匹配: 期望 %s, 实际 %s", tt.errorType, string(parseErr.Type)) + } + } + return + } + + if err != nil { + t.Errorf("意外的错误: %v", err) + return + } + + // 验证解析结果 + if result.Type != tt.expected.Type { + t.Errorf("Type不匹配: 期望 %s, 实际 %s", tt.expected.Type, result.Type) + } + + if result.Name != tt.expected.Name { + t.Errorf("Name不匹配: 期望 %s, 实际 %s", tt.expected.Name, result.Name) + } + + if result.Socks.Server != tt.expected.Socks.Server { + t.Errorf("Server不匹配: 期望 %s, 实际 %s", tt.expected.Socks.Server, result.Socks.Server) + } + + if result.Socks.Port != tt.expected.Socks.Port { + t.Errorf("Port不匹配: 期望 %d, 实际 %d", tt.expected.Socks.Port, result.Socks.Port) + } + + if result.Socks.UserName != tt.expected.Socks.UserName { + t.Errorf("UserName不匹配: 期望 %s, 实际 %s", tt.expected.Socks.UserName, result.Socks.UserName) + } + + if result.Socks.Password != tt.expected.Socks.Password { + t.Errorf("Password不匹配: 期望 %s, 实际 %s", tt.expected.Socks.Password, result.Socks.Password) + } + }) + } +}