diff --git a/config/cmd/cmd.go b/config/cmd/cmd.go index 6df5b9b1..a752f719 100644 --- a/config/cmd/cmd.go +++ b/config/cmd/cmd.go @@ -469,10 +469,15 @@ func buildServiceConfig(url *url.URL) ([]*config.ServiceConfig, error) { var auth *config.AuthConfig if url.User != nil { - auth = &config.AuthConfig{ - Username: url.User.Username(), + if strings.HasPrefix(url.Scheme, "ss") { + auth = decodeSIP002Auth(url) + } + if auth == nil { + auth = &config.AuthConfig{ + Username: url.User.Username(), + } + auth.Password, _ = url.User.Password() } - auth.Password, _ = url.User.Password() } m := map[string]any{} @@ -614,10 +619,15 @@ func buildNodeConfig(url *url.URL, m map[string]any) (*config.NodeConfig, error) var auth *config.AuthConfig if url.User != nil { - auth = &config.AuthConfig{ - Username: url.User.Username(), + if strings.HasPrefix(url.Scheme, "ss") { + auth = decodeSIP002Auth(url) + } + if auth == nil { + auth = &config.AuthConfig{ + Username: url.User.Username(), + } + auth.Password, _ = url.User.Password() } - auth.Password, _ = url.User.Password() } if sauth := mdutil.GetString(md, "auth"); sauth != "" && auth == nil { @@ -742,10 +752,46 @@ func Norm(s string) (*url.URL, error) { return url, nil } -func parseAuthFromCmd(sa string) (*config.AuthConfig, error) { - v, err := base64.StdEncoding.DecodeString(sa) +func decodeSIP002Auth(u *url.URL) *config.AuthConfig { + if u.User == nil { + return nil + } + _, hasPassword := u.User.Password() + if hasPassword { + return nil + } + username := u.User.Username() + if username == "" { + return nil + } + + decoded, err := base64.RawURLEncoding.DecodeString(username) if err != nil { - return nil, err + decoded, err = base64.StdEncoding.DecodeString(username) + if err != nil { + return nil + } + } + + cs := string(decoded) + n := strings.IndexByte(cs, ':') + if n < 0 { + return nil + } + + return &config.AuthConfig{ + Username: cs[:n], + Password: cs[n+1:], + } +} + +func parseAuthFromCmd(sa string) (*config.AuthConfig, error) { + v, err := base64.RawURLEncoding.DecodeString(sa) + if err != nil { + v, err = base64.StdEncoding.DecodeString(sa) + if err != nil { + return nil, err + } } cs := string(v) n := strings.IndexByte(cs, ':') diff --git a/config/cmd/cmd_test.go b/config/cmd/cmd_test.go new file mode 100644 index 00000000..a2ca9f4f --- /dev/null +++ b/config/cmd/cmd_test.go @@ -0,0 +1,229 @@ +package cmd + +import ( + "encoding/base64" + "net/url" + "testing" +) + +func TestDecodeSIP002Auth(t *testing.T) { + tests := []struct { + name string + rawURL string + wantUsername string + wantPassword string + wantNil bool + }{ + { + name: "SIP002 URL-safe base64 without padding", + rawURL: "ss://" + base64.RawURLEncoding.EncodeToString([]byte("aes-256-gcm:password")) + "@server:8388", + wantUsername: "aes-256-gcm", + wantPassword: "password", + }, + { + name: "SIP002 standard base64 with padding", + rawURL: "ss://" + base64.StdEncoding.EncodeToString([]byte("aes-256-gcm:pass")) + "@server:8388", + wantUsername: "aes-256-gcm", + wantPassword: "pass", + }, + { + name: "standard format with password (not SIP002)", + rawURL: "ss://aes-256-gcm:password@server:8388", + wantNil: true, + }, + { + name: "no userinfo", + rawURL: "ss://server:8388", + wantNil: true, + }, + { + name: "invalid base64", + rawURL: "ss://not-valid!!!@server:8388", + wantNil: true, + }, + { + name: "base64 without colon in decoded", + rawURL: "ss://" + base64.RawURLEncoding.EncodeToString([]byte("nocolon")) + "@server:8388", + wantNil: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + u, err := url.Parse(tt.rawURL) + if err != nil { + t.Fatalf("url.Parse(%q): %v", tt.rawURL, err) + } + + got := decodeSIP002Auth(u) + if tt.wantNil { + if got != nil { + t.Errorf("decodeSIP002Auth() = %+v, want nil", got) + } + return + } + if got == nil { + t.Fatal("decodeSIP002Auth() = nil, want non-nil") + } + if got.Username != tt.wantUsername { + t.Errorf("Username = %q, want %q", got.Username, tt.wantUsername) + } + if got.Password != tt.wantPassword { + t.Errorf("Password = %q, want %q", got.Password, tt.wantPassword) + } + }) + } +} + +func TestParseAuthFromCmd(t *testing.T) { + tests := []struct { + name string + input string + wantUsername string + wantPassword string + wantErr bool + }{ + { + name: "StdEncoding with padding", + input: base64.StdEncoding.EncodeToString([]byte("user:pass")), + wantUsername: "user", + wantPassword: "pass", + }, + { + name: "RawURLEncoding without padding", + input: base64.RawURLEncoding.EncodeToString([]byte("user:pass")), + wantUsername: "user", + wantPassword: "pass", + }, + { + name: "invalid base64", + input: "not-valid!!!", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseAuthFromCmd(tt.input) + if tt.wantErr { + if err == nil { + t.Error("parseAuthFromCmd() error = nil, want error") + } + return + } + if err != nil { + t.Fatalf("parseAuthFromCmd() error = %v", err) + } + if got.Username != tt.wantUsername { + t.Errorf("Username = %q, want %q", got.Username, tt.wantUsername) + } + if got.Password != tt.wantPassword { + t.Errorf("Password = %q, want %q", got.Password, tt.wantPassword) + } + }) + } +} + +func TestBuildNodeConfigSIP002(t *testing.T) { + encoded := base64.RawURLEncoding.EncodeToString([]byte("aes-256-gcm:password")) + rawURL := "ss://" + encoded + "@server:8388" + + u, err := url.Parse(rawURL) + if err != nil { + t.Fatalf("url.Parse: %v", err) + } + + node, err := buildNodeConfig(u, map[string]any{}) + if err != nil { + t.Fatalf("buildNodeConfig: %v", err) + } + + if node.Connector == nil || node.Connector.Auth == nil { + t.Fatal("node.Connector.Auth is nil") + } + if node.Connector.Auth.Username != "aes-256-gcm" { + t.Errorf("Username = %q, want %q", node.Connector.Auth.Username, "aes-256-gcm") + } + if node.Connector.Auth.Password != "password" { + t.Errorf("Password = %q, want %q", node.Connector.Auth.Password, "password") + } +} + +func TestBuildNodeConfigStandardAuth(t *testing.T) { + rawURL := "http://user:pass@server:8080" + u, err := url.Parse(rawURL) + if err != nil { + t.Fatalf("url.Parse: %v", err) + } + + node, err := buildNodeConfig(u, map[string]any{}) + if err != nil { + t.Fatalf("buildNodeConfig: %v", err) + } + + if node.Connector == nil || node.Connector.Auth == nil { + t.Fatal("node.Connector.Auth is nil") + } + if node.Connector.Auth.Username != "user" { + t.Errorf("Username = %q, want %q", node.Connector.Auth.Username, "user") + } + if node.Connector.Auth.Password != "pass" { + t.Errorf("Password = %q, want %q", node.Connector.Auth.Password, "pass") + } +} + +func TestBuildServiceConfigSIP002(t *testing.T) { + encoded := base64.RawURLEncoding.EncodeToString([]byte("aes-256-gcm:password")) + rawURL := "ss://" + encoded + "@:8388" + + u, err := url.Parse(rawURL) + if err != nil { + t.Fatalf("url.Parse: %v", err) + } + + svcs, err := buildServiceConfig(u) + if err != nil { + t.Fatalf("buildServiceConfig: %v", err) + } + if len(svcs) == 0 { + t.Fatal("no services returned") + } + + auth := svcs[0].Handler.Auth + if auth == nil { + t.Fatal("Handler.Auth is nil") + } + if auth.Username != "aes-256-gcm" { + t.Errorf("Username = %q, want %q", auth.Username, "aes-256-gcm") + } + if auth.Password != "password" { + t.Errorf("Password = %q, want %q", auth.Password, "password") + } +} + +func TestBuildServiceConfigStandardAuth(t *testing.T) { + rawURL := "http://user:pass@:8080" + u, err := url.Parse(rawURL) + if err != nil { + t.Fatalf("url.Parse: %v", err) + } + + svcs, err := buildServiceConfig(u) + if err != nil { + t.Fatalf("buildServiceConfig: %v", err) + } + if len(svcs) == 0 { + t.Fatal("no services returned") + } + + auth := svcs[0].Handler.Auth + if auth == nil { + t.Fatal("Handler.Auth is nil") + } + if auth.Username != "user" { + t.Errorf("Username = %q, want %q", auth.Username, "user") + } + if auth.Password != "pass" { + t.Errorf("Password = %q, want %q", auth.Password, "pass") + } +} diff --git a/connector/ss/connector.go b/connector/ss/connector.go index d721d52b..85a79be9 100644 --- a/connector/ss/connector.go +++ b/connector/ss/connector.go @@ -49,7 +49,7 @@ func (c *ssConnector) Init(md md.Metadata) (err error) { clientConfig, err := utils.NewClientConfig(method, password) if err != nil { - return nil + return err } c.client = core.NewTCPClient(clientConfig) diff --git a/connector/ss/udp/connector.go b/connector/ss/udp/connector.go index db558fbd..cce3b3c7 100644 --- a/connector/ss/udp/connector.go +++ b/connector/ss/udp/connector.go @@ -51,9 +51,10 @@ func (c *ssuConnector) Init(md md.Metadata) (err error) { password, _ := c.options.Auth.Password() clientConfig, err := utils.NewClientConfig(method, password) if err != nil { - return nil + return err } c.client = core.NewUDPClient(clientConfig, 60) + c.tcpClient = core.NewTCPClient(clientConfig) } return