diff --git a/util/base64.go b/util/base64.go index c2327b0..cbafdde 100644 --- a/util/base64.go +++ b/util/base64.go @@ -1,10 +1,25 @@ package util -import "encoding/base64" +import ( + "encoding/base64" + "strings" +) + +func decodeBase64(str string) ([]byte, error) { + str = strings.TrimSpace(str) + var err error + for _, enc := range []*base64.Encoding{base64.StdEncoding, base64.RawStdEncoding, base64.URLEncoding, base64.RawURLEncoding} { + var decoded []byte + if decoded, err = enc.DecodeString(str); err == nil { + return decoded, nil + } + } + return nil, err +} // Function to return decoded bytes if a string is Base64 encoded func StrOrBase64Encoded(str string) string { - decoded, err := base64.StdEncoding.DecodeString(str) + decoded, err := decodeBase64(str) if err == nil { return string(decoded) } @@ -12,7 +27,7 @@ func StrOrBase64Encoded(str string) string { } func B64StrToByte(str string) ([]byte, error) { - return base64.StdEncoding.DecodeString(str) + return decodeBase64(str) } func ByteToB64Str(b []byte) string { diff --git a/util/linkToJson.go b/util/linkToJson.go index f98855b..ec546fb 100644 --- a/util/linkToJson.go +++ b/util/linkToJson.go @@ -351,6 +351,21 @@ func tuic(u *url.URL, i int) (*map[string]interface{}, string, error) { } func ss(u *url.URL, i int) (*map[string]interface{}, string, error) { + if u.User == nil { + decoded, err := decodeBase64(u.Host + strings.TrimSuffix(u.Path, "/")) + if err != nil { + return nil, "", common.NewError("Unsupported shadowsocks") + } + idx := strings.LastIndex(string(decoded), "@") + if idx == -1 { + return nil, "", common.NewError("Unsupported shadowsocks") + } + legacy := *u + legacy.User = url.UserPassword(splitMethod(string(decoded[:idx]))) + legacy.Host = string(decoded[idx+1:]) + legacy.Path = "" + u = &legacy + } query, _ := url.ParseQuery(u.RawQuery) host, portStr, _ := net.SplitHostPort(u.Host) port := 443 @@ -410,6 +425,11 @@ func ss(u *url.URL, i int) (*map[string]interface{}, string, error) { return &ss, tag, nil } +func splitMethod(userInfo string) (string, string) { + method, password, _ := strings.Cut(userInfo, ":") + return method, password +} + func parseNaiveLink(u *url.URL, i int) (*map[string]interface{}, string, error) { var host, portStr, username, password string var port int diff --git a/util/linkToJson_ss_test.go b/util/linkToJson_ss_test.go new file mode 100644 index 0000000..32274fa --- /dev/null +++ b/util/linkToJson_ss_test.go @@ -0,0 +1,48 @@ +package util + +import ( + "encoding/base64" + "testing" +) + +// Shadowsocks links come base64-encoded in several flavours; strict standard +// decoding rejected most of them and the node vanished from Clash output, +// while the link subscription passed the line through untouched (#1281). +func TestShadowsocksLinkFormats(t *testing.T) { + const method = "2022-blake3-aes-256-gcm" + const password = "bWFza2VkLXZhbHVlMDAwMDAwMDAwMDAwMDAwMDAwMDA=:bWFza2VkLXZhbHVlMDAwMDAwMDAwMDAwMDAwMDAwMDA=" + userInfo := method + ":" + password + + cases := map[string]string{ + "padded standard": base64.StdEncoding.EncodeToString([]byte(userInfo)) + "@[240e:1:2::1234]:22851", + "unpadded url-safe": base64.RawURLEncoding.EncodeToString([]byte(userInfo)) + "@[240e:1:2::1234]:22851", + "plugin path": base64.RawURLEncoding.EncodeToString([]byte(userInfo)) + "@[240e:1:2::1234]:22851/?plugin=none", + "legacy whole": base64.StdEncoding.EncodeToString([]byte(userInfo + "@[240e:1:2::1234]:22851")), + "legacy unpadded": base64.RawURLEncoding.EncodeToString([]byte(userInfo + "@[240e:1:2::1234]:22851")), + } + for name, body := range cases { + t.Run(name, func(t *testing.T) { + out, tag, err := GetOutbound("ss://"+body+"#node", 0) + if err != nil { + t.Fatal(err) + } + o := *out + if tag != "node" || o["method"] != method || o["password"] != password || + o["server"] != "240e:1:2::1234" || o["server_port"] != 22851 { + t.Errorf("got %v", o) + } + }) + } +} + +// vmess payloads are often unpadded too. +func TestVmessUnpaddedLink(t *testing.T) { + payload := base64.RawStdEncoding.EncodeToString([]byte(`{"v":"2","ps":"vm","add":"1.2.3.4","port":"443","id":"b831381d-6324-4d53-ad4f-8cda48b30811","net":"tcp"}`)) + out, _, err := GetOutbound("vmess://"+payload, 0) + if err != nil { + t.Fatal(err) + } + if (*out)["server"] != "1.2.3.4" { + t.Errorf("got %v", *out) + } +}