Files
x/config/cmd/cmd_test.go
T
Evsyukov Denis 802943bd28 fix(ss): propagate cipher errors and decode SIP002 base64 auth
- Return err instead of nil in SS TCP/UDP connector Init when
  NewClientConfig fails, preventing nil pointer dereference in WrapConn
- Initialize tcpClient in UDP connector for UDP-over-TCP path
- Add decodeSIP002Auth to handle ss://BASE64(method:password)@host:port
  URL format with RawURLEncoding and StdEncoding fallback
- Update buildServiceConfig and buildNodeConfig to decode SIP002 auth
  for ss* schemes before falling back to standard userinfo parsing
- Support RawURLEncoding in parseAuthFromCmd with StdEncoding fallback
- Add tests for decodeSIP002Auth, parseAuthFromCmd, and integration
  tests for buildNodeConfig/buildServiceConfig
2026-04-21 20:55:45 +08:00

230 lines
5.6 KiB
Go

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")
}
}