1952 lines
51 KiB
Go
1952 lines
51 KiB
Go
package cmd
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"net/url"
|
|
"testing"
|
|
|
|
"github.com/go-gost/x/config"
|
|
)
|
|
|
|
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 TestNorm(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
want string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "empty string",
|
|
input: "",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "whitespace only",
|
|
input: " ",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "host:port without scheme",
|
|
input: "host:8080",
|
|
want: "auto://host:8080",
|
|
},
|
|
{
|
|
name: "colon prefix",
|
|
input: ":8080",
|
|
want: "auto://:8080",
|
|
},
|
|
{
|
|
name: "https scheme rewrites to http+tls",
|
|
input: "https://host:443",
|
|
want: "http+tls://host:443",
|
|
},
|
|
{
|
|
name: "http scheme unchanged",
|
|
input: "http://host:8080",
|
|
want: "http://host:8080",
|
|
},
|
|
{
|
|
name: "with query params",
|
|
input: "http://host:8080?key=val",
|
|
want: "http://host:8080?key=val",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := Norm(tt.input)
|
|
if tt.wantErr {
|
|
if err == nil {
|
|
t.Error("Norm() error = nil, want error")
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("Norm() error = %v", err)
|
|
}
|
|
if got.String() != tt.want {
|
|
t.Errorf("Norm() = %q, want %q", got.String(), tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCutHost(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
wantHost string
|
|
wantRemain string
|
|
}{
|
|
{
|
|
name: "empty string",
|
|
input: "",
|
|
wantHost: "",
|
|
wantRemain: "",
|
|
},
|
|
{
|
|
name: "scheme://host:port",
|
|
input: "http://host:8080",
|
|
wantHost: "host:8080",
|
|
wantRemain: "http://",
|
|
},
|
|
{
|
|
name: "scheme://host:port/path",
|
|
input: "http://host:8080/path",
|
|
wantHost: "host:8080",
|
|
wantRemain: "http:///path",
|
|
},
|
|
{
|
|
name: "scheme://host:port?query",
|
|
input: "http://host:8080?key=val",
|
|
wantHost: "host:8080",
|
|
wantRemain: "http://?key=val",
|
|
},
|
|
{
|
|
name: "with user:pass@host",
|
|
input: "http://user:pass@host:8080",
|
|
wantHost: "host:8080",
|
|
wantRemain: "http://user:pass@",
|
|
},
|
|
{
|
|
name: "with user:pass@host/path?query",
|
|
input: "http://user:pass@host:8080/path?key=val",
|
|
wantHost: "host:8080",
|
|
wantRemain: "http://user:pass@/path?key=val",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
host, remain := cutHost(tt.input)
|
|
if host != tt.wantHost {
|
|
t.Errorf("host = %q, want %q", host, tt.wantHost)
|
|
}
|
|
if remain != tt.wantRemain {
|
|
t.Errorf("remain = %q, want %q", remain, tt.wantRemain)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseSelector(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input map[string]any
|
|
wantNil bool
|
|
wantStrat string
|
|
wantFails int
|
|
wantDurGt bool // failTimeout > 0
|
|
}{
|
|
{
|
|
name: "no params",
|
|
input: map[string]any{},
|
|
wantNil: true,
|
|
},
|
|
{
|
|
name: "only strategy",
|
|
input: map[string]any{
|
|
"strategy": "rr",
|
|
},
|
|
wantStrat: "rr",
|
|
wantFails: 1,
|
|
wantDurGt: true,
|
|
},
|
|
{
|
|
name: "only maxFails",
|
|
input: map[string]any{
|
|
"max_fails": 3,
|
|
},
|
|
wantStrat: "round",
|
|
wantFails: 3,
|
|
wantDurGt: true,
|
|
},
|
|
{
|
|
name: "all params with failTimeout",
|
|
input: map[string]any{
|
|
"strategy": "hash",
|
|
"maxFails": 5,
|
|
"failTimeout": "10s",
|
|
},
|
|
wantStrat: "hash",
|
|
wantFails: 5,
|
|
wantDurGt: true,
|
|
},
|
|
{
|
|
name: "fail_timeout with underscore",
|
|
input: map[string]any{
|
|
"strategy": "fifo",
|
|
"max_fails": 2,
|
|
"fail_timeout": "15s",
|
|
},
|
|
wantStrat: "fifo",
|
|
wantFails: 2,
|
|
wantDurGt: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := parseSelector(tt.input)
|
|
if tt.wantNil {
|
|
if got != nil {
|
|
t.Errorf("parseSelector() = %+v, want nil", got)
|
|
}
|
|
return
|
|
}
|
|
if got == nil {
|
|
t.Fatal("parseSelector() = nil, want non-nil")
|
|
}
|
|
if got.Strategy != tt.wantStrat {
|
|
t.Errorf("Strategy = %q, want %q", got.Strategy, tt.wantStrat)
|
|
}
|
|
if got.MaxFails != tt.wantFails {
|
|
t.Errorf("MaxFails = %d, want %d", got.MaxFails, tt.wantFails)
|
|
}
|
|
if tt.wantDurGt && got.FailTimeout <= 0 {
|
|
t.Errorf("FailTimeout = %v, want > 0", got.FailTimeout)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCopyMap(t *testing.T) {
|
|
t.Run("nil map", func(t *testing.T) {
|
|
if copyMap(nil) != nil {
|
|
t.Error("copyMap(nil) should be nil")
|
|
}
|
|
})
|
|
|
|
t.Run("non-nil map", func(t *testing.T) {
|
|
orig := map[string]any{"key": "val"}
|
|
c := copyMap(orig)
|
|
if c["key"] != "val" {
|
|
t.Errorf("value = %v, want %v", c["key"], "val")
|
|
}
|
|
// mutate copy, verify original unchanged
|
|
c["key"] = "newval"
|
|
if orig["key"] != "val" {
|
|
t.Error("original map was mutated")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCopyTLS(t *testing.T) {
|
|
t.Run("nil", func(t *testing.T) {
|
|
if copyTLS(nil) != nil {
|
|
t.Error("copyTLS(nil) should be nil")
|
|
}
|
|
})
|
|
|
|
t.Run("non-nil", func(t *testing.T) {
|
|
orig := &config.TLSConfig{CertFile: "cert.pem", KeyFile: "key.pem"}
|
|
c := copyTLS(orig)
|
|
if c.CertFile != "cert.pem" || c.KeyFile != "key.pem" {
|
|
t.Error("copyTLS did not copy fields")
|
|
}
|
|
c.CertFile = "other.pem"
|
|
if orig.CertFile != "cert.pem" {
|
|
t.Error("original TLSConfig was mutated")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCopyConnectorConfig(t *testing.T) {
|
|
t.Run("nil", func(t *testing.T) {
|
|
if copyConnectorConfig(nil) != nil {
|
|
t.Error("copyConnectorConfig(nil) should be nil")
|
|
}
|
|
})
|
|
|
|
t.Run("non-nil deep copy", func(t *testing.T) {
|
|
orig := &config.ConnectorConfig{
|
|
Type: "http",
|
|
Auth: &config.AuthConfig{Username: "u", Password: "p"},
|
|
Metadata: map[string]any{"k": "v"},
|
|
}
|
|
c := copyConnectorConfig(orig)
|
|
if c.Type != "http" {
|
|
t.Errorf("Type = %q, want %q", c.Type, "http")
|
|
}
|
|
if c.Auth.Username != "u" || c.Auth.Password != "p" {
|
|
t.Error("Auth not copied")
|
|
}
|
|
// mutate copy, verify original unchanged
|
|
c.Auth.Username = "x"
|
|
if orig.Auth.Username != "u" {
|
|
t.Error("original auth was mutated")
|
|
}
|
|
c.Metadata["k"] = "x"
|
|
if orig.Metadata["k"] != "v" {
|
|
t.Error("original metadata was mutated")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCopyDialerConfig(t *testing.T) {
|
|
t.Run("nil", func(t *testing.T) {
|
|
if copyDialerConfig(nil) != nil {
|
|
t.Error("copyDialerConfig(nil) should be nil")
|
|
}
|
|
})
|
|
|
|
t.Run("non-nil deep copy", func(t *testing.T) {
|
|
orig := &config.DialerConfig{
|
|
Type: "tls",
|
|
TLS: &config.TLSConfig{ServerName: "example.com"},
|
|
Auth: &config.AuthConfig{Username: "u"},
|
|
Metadata: map[string]any{"k": "v"},
|
|
}
|
|
c := copyDialerConfig(orig)
|
|
if c.TLS.ServerName != "example.com" {
|
|
t.Error("TLS not copied")
|
|
}
|
|
c.TLS.ServerName = "other.com"
|
|
if orig.TLS.ServerName != "example.com" {
|
|
t.Error("original TLS was mutated")
|
|
}
|
|
c.Auth.Username = "x"
|
|
if orig.Auth.Username != "u" {
|
|
t.Error("original auth was mutated")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestBuildNodeConfig_NilMap(t *testing.T) {
|
|
// nil m should not panic
|
|
rawURL := "http://server:8080"
|
|
u, _ := url.Parse(rawURL)
|
|
node, err := buildNodeConfig(u, nil)
|
|
if err != nil {
|
|
t.Fatalf("buildNodeConfig(nil m): %v", err)
|
|
}
|
|
if node.Addr != "server:8080" {
|
|
t.Errorf("Addr = %q, want %q", node.Addr, "server:8080")
|
|
}
|
|
if node.Connector.Type != "http" {
|
|
t.Errorf("Connector.Type = %q, want %q", node.Connector.Type, "http")
|
|
}
|
|
}
|
|
|
|
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")
|
|
}
|
|
}
|
|
|
|
func TestBuildNodeConfig_TLS(t *testing.T) {
|
|
u, _ := url.Parse("http+tls://server:443")
|
|
m := map[string]any{
|
|
"secure": true,
|
|
"serverName": "example.com",
|
|
}
|
|
|
|
node, err := buildNodeConfig(u, m)
|
|
if err != nil {
|
|
t.Fatalf("buildNodeConfig: %v", err)
|
|
}
|
|
|
|
if node.Dialer.TLS == nil {
|
|
t.Fatal("Dialer.TLS is nil")
|
|
}
|
|
if node.Dialer.TLS.Secure != true {
|
|
t.Error("TLS.Secure should be true")
|
|
}
|
|
if node.Dialer.TLS.ServerName != "example.com" {
|
|
t.Errorf("TLS.ServerName = %q, want %q", node.Dialer.TLS.ServerName, "example.com")
|
|
}
|
|
}
|
|
|
|
func TestBuildNodeConfig_TLSServerNameDefaultsToHost(t *testing.T) {
|
|
rawURL := "http+tls://example.com:443?secure=true"
|
|
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.Dialer.TLS.ServerName != "example.com" {
|
|
t.Errorf("TLS.ServerName = %q, want %q", node.Dialer.TLS.ServerName, "example.com")
|
|
}
|
|
}
|
|
|
|
func TestBuildNodeConfig_AuthFallbackForUnknownScheme(t *testing.T) {
|
|
// When dialer type is unknown, it defaults to tcp, and auth stays on connector
|
|
rawURL := "unknown://user:pass@server:8080"
|
|
u, _ := url.Parse(rawURL)
|
|
|
|
node, err := buildNodeConfig(u, map[string]any{})
|
|
if err != nil {
|
|
t.Fatalf("buildNodeConfig: %v", err)
|
|
}
|
|
|
|
// Connector defaults to "http", dialer defaults to "tcp"
|
|
if node.Connector.Auth == nil {
|
|
t.Fatal("Connector.Auth is nil")
|
|
}
|
|
if node.Connector.Auth.Username != "user" || node.Connector.Auth.Password != "pass" {
|
|
t.Errorf("Connector.Auth = %+v, want user:pass", node.Connector.Auth)
|
|
}
|
|
if node.Dialer.Auth != nil {
|
|
t.Error("Dialer.Auth should be nil for non-ssh dialer")
|
|
}
|
|
}
|
|
|
|
func TestBuildNodeConfig_AuthFromMetadata(t *testing.T) {
|
|
authB64 := base64.RawURLEncoding.EncodeToString([]byte("quser:qpass"))
|
|
u, _ := url.Parse("http://server:8080")
|
|
m := map[string]any{"auth": authB64}
|
|
|
|
node, err := buildNodeConfig(u, m)
|
|
if err != nil {
|
|
t.Fatalf("buildNodeConfig: %v", err)
|
|
}
|
|
|
|
if node.Connector.Auth == nil {
|
|
t.Fatal("Connector.Auth is nil")
|
|
}
|
|
if node.Connector.Auth.Username != "quser" || node.Connector.Auth.Password != "qpass" {
|
|
t.Errorf("Connector.Auth = %+v, want quser:qpass", node.Connector.Auth)
|
|
}
|
|
}
|
|
|
|
func TestBuildNodeConfig_InvalidAuthParam(t *testing.T) {
|
|
u, _ := url.Parse("http://server:8080")
|
|
m := map[string]any{"auth": "invalid!!!"}
|
|
|
|
_, err := buildNodeConfig(u, m)
|
|
if err == nil {
|
|
t.Error("buildNodeConfig should fail with invalid auth param")
|
|
}
|
|
}
|
|
|
|
func TestBuildNodeConfig_MetadataRouting(t *testing.T) {
|
|
m := map[string]any{
|
|
"node.key": "nodeval",
|
|
"connector.key": "connval",
|
|
"dialer.key": "dialval",
|
|
"common.key": "commonval",
|
|
}
|
|
|
|
rawURL := "http://server:8080"
|
|
u, _ := url.Parse(rawURL)
|
|
|
|
node, err := buildNodeConfig(u, m)
|
|
if err != nil {
|
|
t.Fatalf("buildNodeConfig: %v", err)
|
|
}
|
|
|
|
if node.Metadata["key"] != "nodeval" {
|
|
t.Errorf("node metadata: key = %v, want nodeval", node.Metadata["key"])
|
|
}
|
|
if node.Connector.Metadata["key"] != "connval" {
|
|
t.Errorf("connector metadata: key = %v, want connval", node.Connector.Metadata["key"])
|
|
}
|
|
if node.Dialer.Metadata["key"] != "dialval" {
|
|
t.Errorf("dialer metadata: key = %v, want dialval", node.Dialer.Metadata["key"])
|
|
}
|
|
// common keys should propagate to all
|
|
if node.Metadata["common.key"] != "commonval" {
|
|
t.Error("common key not in node metadata")
|
|
}
|
|
if node.Connector.Metadata["common.key"] != "commonval" {
|
|
t.Error("common key not in connector metadata")
|
|
}
|
|
if node.Dialer.Metadata["common.key"] != "commonval" {
|
|
t.Error("common key not in dialer metadata")
|
|
}
|
|
}
|
|
|
|
func TestBuildNodeConfig_UnknownConnectorUsesHTTP(t *testing.T) {
|
|
rawURL := "unknown://server:8080"
|
|
u, _ := url.Parse(rawURL)
|
|
|
|
node, err := buildNodeConfig(u, map[string]any{})
|
|
if err != nil {
|
|
t.Fatalf("buildNodeConfig: %v", err)
|
|
}
|
|
|
|
if node.Connector.Type != "http" {
|
|
t.Errorf("Connector.Type = %q, want http", node.Connector.Type)
|
|
}
|
|
if node.Dialer.Type != "tcp" {
|
|
t.Errorf("Dialer.Type = %q, want tcp", node.Dialer.Type)
|
|
}
|
|
}
|
|
|
|
func TestBuildNodeConfig_SsuConnector(t *testing.T) {
|
|
// ssu connector is not in registry, so it falls back to http.
|
|
// The dialer falls back to tcp (not udp) because the connector was
|
|
// already resolved to http before the ssu check.
|
|
rawURL := "ssu://server:8388"
|
|
u, _ := url.Parse(rawURL)
|
|
|
|
node, err := buildNodeConfig(u, map[string]any{})
|
|
if err != nil {
|
|
t.Fatalf("buildNodeConfig: %v", err)
|
|
}
|
|
|
|
// Connector falls back to http, dialer falls back to tcp
|
|
if node.Connector.Type != "http" {
|
|
t.Errorf("Connector.Type = %q, want http", node.Connector.Type)
|
|
}
|
|
if node.Dialer.Type != "tcp" {
|
|
t.Errorf("Dialer.Type = %q, want tcp", node.Dialer.Type)
|
|
}
|
|
}
|
|
|
|
func TestBuildServiceConfig_AuthOnHandlerByDefault(t *testing.T) {
|
|
// When listener type is unknown (defaults to tcp), auth stays on handler
|
|
rawURL := "http://user:pass@:8080"
|
|
u, _ := url.Parse(rawURL)
|
|
|
|
svcs, err := buildServiceConfig(u)
|
|
if err != nil {
|
|
t.Fatalf("buildServiceConfig: %v", err)
|
|
}
|
|
|
|
svc := svcs[0]
|
|
if svc.Handler.Auth == nil {
|
|
t.Fatal("Handler.Auth is nil")
|
|
}
|
|
if svc.Handler.Auth.Username != "user" || svc.Handler.Auth.Password != "pass" {
|
|
t.Errorf("Handler.Auth = %+v, want user:pass", svc.Handler.Auth)
|
|
}
|
|
if svc.Listener.Auth != nil {
|
|
t.Error("Listener.Auth should be nil for non-ssh listener")
|
|
}
|
|
}
|
|
|
|
func TestBuildServiceConfig_TLS(t *testing.T) {
|
|
rawURL := "http://:443?certFile=mycert.pem&keyFile=mykey.pem&caFile=myca.pem"
|
|
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)
|
|
}
|
|
|
|
tls := svcs[0].Listener.TLS
|
|
if tls == nil {
|
|
t.Fatal("Listener.TLS is nil")
|
|
}
|
|
if tls.CertFile != "mycert.pem" {
|
|
t.Errorf("CertFile = %q, want mycert.pem", tls.CertFile)
|
|
}
|
|
if tls.KeyFile != "mykey.pem" {
|
|
t.Errorf("KeyFile = %q, want mykey.pem", tls.KeyFile)
|
|
}
|
|
if tls.CAFile != "myca.pem" {
|
|
t.Errorf("CAFile = %q, want myca.pem", tls.CAFile)
|
|
}
|
|
}
|
|
|
|
func TestBuildServiceConfig_TLSShortKeys(t *testing.T) {
|
|
rawURL := "http://:443?cert=a.pem&key=b.pem&ca=c.pem"
|
|
u, _ := url.Parse(rawURL)
|
|
|
|
svcs, err := buildServiceConfig(u)
|
|
if err != nil {
|
|
t.Fatalf("buildServiceConfig: %v", err)
|
|
}
|
|
|
|
tls := svcs[0].Listener.TLS
|
|
if tls == nil {
|
|
t.Fatal("Listener.TLS is nil")
|
|
}
|
|
if tls.CertFile != "a.pem" || tls.KeyFile != "b.pem" || tls.CAFile != "c.pem" {
|
|
t.Errorf("TLS = %+v, want a.pem/b.pem/c.pem", tls)
|
|
}
|
|
}
|
|
|
|
func TestBuildServiceConfig_NoTLSWithoutCert(t *testing.T) {
|
|
rawURL := "http://:443?keyFile=key.pem&caFile=ca.pem"
|
|
u, _ := url.Parse(rawURL)
|
|
|
|
svcs, err := buildServiceConfig(u)
|
|
if err != nil {
|
|
t.Fatalf("buildServiceConfig: %v", err)
|
|
}
|
|
|
|
if svcs[0].Listener.TLS != nil {
|
|
t.Error("Listener.TLS should be nil without certFile")
|
|
}
|
|
}
|
|
|
|
func TestBuildServiceConfig_DNSParam(t *testing.T) {
|
|
rawURL := "http://:8080?dns=1.1.1.1,8.8.8.8"
|
|
u, _ := url.Parse(rawURL)
|
|
|
|
svcs, err := buildServiceConfig(u)
|
|
if err != nil {
|
|
t.Fatalf("buildServiceConfig: %v", err)
|
|
}
|
|
|
|
md := svcs[0].Handler.Metadata
|
|
val, ok := md["dns"]
|
|
if !ok {
|
|
t.Fatal("dns key missing from metadata")
|
|
}
|
|
strs, ok := val.([]string)
|
|
if !ok {
|
|
t.Fatalf("dns value type = %T, want []string", val)
|
|
}
|
|
if len(strs) != 2 || strs[0] != "1.1.1.1" || strs[1] != "8.8.8.8" {
|
|
t.Errorf("dns = %v, want [1.1.1.1 8.8.8.8]", strs)
|
|
}
|
|
}
|
|
|
|
func TestBuildServiceConfig_MetadataRouting(t *testing.T) {
|
|
rawURL := "http://:8080?service.key=svcval&handler.key=hval&listener.key=lval&common=val"
|
|
u, _ := url.Parse(rawURL)
|
|
|
|
svcs, err := buildServiceConfig(u)
|
|
if err != nil {
|
|
t.Fatalf("buildServiceConfig: %v", err)
|
|
}
|
|
|
|
svc := svcs[0]
|
|
if svc.Metadata["key"] != "svcval" {
|
|
t.Errorf("service metadata: key = %v, want svcval", svc.Metadata["key"])
|
|
}
|
|
if svc.Handler.Metadata["key"] != "hval" {
|
|
t.Errorf("handler metadata: key = %v, want hval", svc.Handler.Metadata["key"])
|
|
}
|
|
if svc.Listener.Metadata["key"] != "lval" {
|
|
t.Errorf("listener metadata: key = %v, want lval", svc.Listener.Metadata["key"])
|
|
}
|
|
// common keys propagate everywhere
|
|
if svc.Metadata["common"] != "val" {
|
|
t.Error("common key not in service metadata")
|
|
}
|
|
if svc.Handler.Metadata["common"] != "val" {
|
|
t.Error("common key not in handler metadata")
|
|
}
|
|
if svc.Listener.Metadata["common"] != "val" {
|
|
t.Error("common key not in listener metadata")
|
|
}
|
|
}
|
|
|
|
func TestBuildServiceConfig_NetParams(t *testing.T) {
|
|
rawURL := "http://:8080?interface=eth0&so_mark=42&proxyProtocol=2&netns=myns&netns.out=outns"
|
|
u, _ := url.Parse(rawURL)
|
|
|
|
svcs, err := buildServiceConfig(u)
|
|
if err != nil {
|
|
t.Fatalf("buildServiceConfig: %v", err)
|
|
}
|
|
|
|
md := svcs[0].Metadata
|
|
if md["interface"] != "eth0" {
|
|
t.Errorf("interface = %v, want eth0", md["interface"])
|
|
}
|
|
if md["so_mark"] != 42 {
|
|
t.Errorf("so_mark = %v, want 42", md["so_mark"])
|
|
}
|
|
if md["proxyProtocol"] != 2 {
|
|
t.Errorf("proxyProtocol = %v, want 2", md["proxyProtocol"])
|
|
}
|
|
if md["netns"] != "myns" {
|
|
t.Errorf("netns = %v, want myns", md["netns"])
|
|
}
|
|
if md["netns.out"] != "outns" {
|
|
t.Errorf("netns.out = %v, want outns", md["netns.out"])
|
|
}
|
|
}
|
|
|
|
func TestBuildServiceConfig_AuthFromQueryParam(t *testing.T) {
|
|
authB64 := base64.RawURLEncoding.EncodeToString([]byte("qu:qp"))
|
|
rawURL := "http://:8080?auth=" + authB64
|
|
u, _ := url.Parse(rawURL)
|
|
|
|
svcs, err := buildServiceConfig(u)
|
|
if err != nil {
|
|
t.Fatalf("buildServiceConfig: %v", err)
|
|
}
|
|
|
|
auth := svcs[0].Handler.Auth
|
|
if auth == nil {
|
|
t.Fatal("Handler.Auth is nil")
|
|
}
|
|
if auth.Username != "qu" || auth.Password != "qp" {
|
|
t.Errorf("Auth = %+v, want qu:qp", auth)
|
|
}
|
|
}
|
|
|
|
func TestBuildServiceConfig_InvalidAuthParam(t *testing.T) {
|
|
rawURL := "http://:8080?auth=bad!!!"
|
|
u, _ := url.Parse(rawURL)
|
|
|
|
_, err := buildServiceConfig(u)
|
|
if err == nil {
|
|
t.Error("buildServiceConfig should fail with invalid auth param")
|
|
}
|
|
}
|
|
|
|
func TestBuildServiceConfig_FullAuto(t *testing.T) {
|
|
// ":8080" should prepend "auto://"
|
|
rawURL := "auto://:8080"
|
|
u, _ := url.Parse(rawURL)
|
|
|
|
svcs, err := buildServiceConfig(u)
|
|
if err != nil {
|
|
t.Fatalf("buildServiceConfig: %v", err)
|
|
}
|
|
|
|
if len(svcs) != 1 {
|
|
t.Fatalf("len(svcs) = %d, want 1", len(svcs))
|
|
}
|
|
if svcs[0].Handler.Type != "auto" {
|
|
t.Errorf("Handler.Type = %q, want auto", svcs[0].Handler.Type)
|
|
}
|
|
if svcs[0].Listener.Type != "tcp" {
|
|
t.Errorf("Listener.Type = %q, want tcp", svcs[0].Listener.Type)
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigFromCmd_Empty(t *testing.T) {
|
|
cfg, err := BuildConfigFromCmd(nil, nil)
|
|
if err != nil {
|
|
t.Fatalf("BuildConfigFromCmd: %v", err)
|
|
}
|
|
if cfg == nil {
|
|
t.Fatal("cfg is nil")
|
|
}
|
|
if len(cfg.Services) != 0 {
|
|
t.Errorf("expected 0 services, got %d", len(cfg.Services))
|
|
}
|
|
if len(cfg.Chains) != 0 {
|
|
t.Errorf("expected 0 chains, got %d", len(cfg.Chains))
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigFromCmd_SingleService(t *testing.T) {
|
|
cfg, err := BuildConfigFromCmd([]string{":8080"}, nil)
|
|
if err != nil {
|
|
t.Fatalf("BuildConfigFromCmd: %v", err)
|
|
}
|
|
|
|
if len(cfg.Services) != 1 {
|
|
t.Fatalf("len(Services) = %d, want 1", len(cfg.Services))
|
|
}
|
|
svc := cfg.Services[0]
|
|
if svc.Addr != ":8080" {
|
|
t.Errorf("Addr = %q, want :8080", svc.Addr)
|
|
}
|
|
if svc.Handler.Type != "auto" {
|
|
t.Errorf("Handler.Type = %q, want auto", svc.Handler.Type)
|
|
}
|
|
if svc.Listener.Type != "tcp" {
|
|
t.Errorf("Listener.Type = %q, want tcp", svc.Listener.Type)
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigFromCmd_ServiceWithChain(t *testing.T) {
|
|
cfg, err := BuildConfigFromCmd(
|
|
[]string{":8080"},
|
|
[]string{"http://proxy:3128"},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("BuildConfigFromCmd: %v", err)
|
|
}
|
|
|
|
if len(cfg.Chains) != 1 {
|
|
t.Fatalf("len(Chains) = %d, want 1", len(cfg.Chains))
|
|
}
|
|
chain := cfg.Chains[0]
|
|
if len(chain.Hops) != 1 {
|
|
t.Fatalf("len(Hops) = %d, want 1", len(chain.Hops))
|
|
}
|
|
if chain.Hops[0].Nodes[0].Addr != "proxy:3128" {
|
|
t.Errorf("Node addr = %q, want proxy:3128", chain.Hops[0].Nodes[0].Addr)
|
|
}
|
|
|
|
svc := cfg.Services[0]
|
|
if svc.Handler.Chain != "chain-0" {
|
|
t.Errorf("Handler.Chain = %q, want chain-0", svc.Handler.Chain)
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigFromCmd_ServiceWithMultipleNodes(t *testing.T) {
|
|
cfg, err := BuildConfigFromCmd(
|
|
[]string{":8080"},
|
|
[]string{"http://proxy1:3128", "http://proxy2:3128"},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("BuildConfigFromCmd: %v", err)
|
|
}
|
|
|
|
chain := cfg.Chains[0]
|
|
if len(chain.Hops) != 2 {
|
|
t.Fatalf("len(Hops) = %d, want 2", len(chain.Hops))
|
|
}
|
|
if chain.Hops[0].Name != "hop-0" {
|
|
t.Errorf("Hops[0].Name = %q, want hop-0", chain.Hops[0].Name)
|
|
}
|
|
if chain.Hops[1].Name != "hop-1" {
|
|
t.Errorf("Hops[1].Name = %q, want hop-1", chain.Hops[1].Name)
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigFromCmd_NodeWithBypass(t *testing.T) {
|
|
cfg, err := BuildConfigFromCmd(
|
|
[]string{":8080"},
|
|
[]string{"http://proxy:3128?bypass=10.0.0.0/8,*.local"},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("BuildConfigFromCmd: %v", err)
|
|
}
|
|
|
|
if len(cfg.Bypasses) != 1 {
|
|
t.Fatalf("len(Bypasses) = %d, want 1", len(cfg.Bypasses))
|
|
}
|
|
bp := cfg.Bypasses[0]
|
|
if len(bp.Matchers) != 2 {
|
|
t.Errorf("len(Matchers) = %d, want 2", len(bp.Matchers))
|
|
}
|
|
if cfg.Chains[0].Hops[0].Bypass != bp.Name {
|
|
t.Errorf("Hop.Bypass = %q, want %q", cfg.Chains[0].Hops[0].Bypass, bp.Name)
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigFromCmd_NodeWithInvertBypass(t *testing.T) {
|
|
cfg, err := BuildConfigFromCmd(
|
|
[]string{":8080"},
|
|
[]string{"http://proxy:3128?bypass=~10.0.0.0/8"},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("BuildConfigFromCmd: %v", err)
|
|
}
|
|
|
|
bp := cfg.Bypasses[0]
|
|
if !bp.Whitelist {
|
|
t.Error("Bypass.Whitelist should be true for ~ prefix")
|
|
}
|
|
if len(bp.Matchers) != 1 || bp.Matchers[0] != "10.0.0.0/8" {
|
|
t.Errorf("Matchers = %v, want [10.0.0.0/8]", bp.Matchers)
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigFromCmd_NodeWithResolver(t *testing.T) {
|
|
cfg, err := BuildConfigFromCmd(
|
|
[]string{":8080"},
|
|
[]string{"http://proxy:3128?resolver=8.8.8.8,1.1.1.1"},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("BuildConfigFromCmd: %v", err)
|
|
}
|
|
|
|
if len(cfg.Resolvers) != 1 {
|
|
t.Fatalf("len(Resolvers) = %d, want 1", len(cfg.Resolvers))
|
|
}
|
|
rs := cfg.Resolvers[0]
|
|
if len(rs.Nameservers) != 2 {
|
|
t.Errorf("len(Nameservers) = %d, want 2", len(rs.Nameservers))
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigFromCmd_NodeResolverFamilyOptions(t *testing.T) {
|
|
cfg, err := BuildConfigFromCmd(
|
|
[]string{":8080"},
|
|
[]string{"http://proxy:3128?resolver=auto&prefer=ipv6&only=ipv4"},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("BuildConfigFromCmd: %v", err)
|
|
}
|
|
|
|
rs := cfg.Resolvers[0]
|
|
if len(rs.Nameservers) != 1 {
|
|
t.Fatalf("len(Nameservers) = %d, want 1", len(rs.Nameservers))
|
|
}
|
|
ns := rs.Nameservers[0]
|
|
if ns.Addr != "auto" {
|
|
t.Errorf("Addr = %q, want auto", ns.Addr)
|
|
}
|
|
if ns.Prefer != "ipv6" {
|
|
t.Errorf("Prefer = %q, want ipv6", ns.Prefer)
|
|
}
|
|
if ns.Only != "ipv4" {
|
|
t.Errorf("Only = %q, want ipv4", ns.Only)
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigFromCmd_NodeWithHosts(t *testing.T) {
|
|
cfg, err := BuildConfigFromCmd(
|
|
[]string{":8080"},
|
|
[]string{"http://proxy:3128?hosts=example.com:1.2.3.4,test.local:5.6.7.8"},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("BuildConfigFromCmd: %v", err)
|
|
}
|
|
|
|
if len(cfg.Hosts) != 1 {
|
|
t.Fatalf("len(Hosts) = %d, want 1", len(cfg.Hosts))
|
|
}
|
|
hs := cfg.Hosts[0]
|
|
if len(hs.Mappings) != 2 {
|
|
t.Errorf("len(Mappings) = %d, want 2", len(hs.Mappings))
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigFromCmd_NodeWithHopMetadata(t *testing.T) {
|
|
cfg, err := BuildConfigFromCmd(
|
|
[]string{":8080"},
|
|
[]string{"http://proxy:3128?hop.key=hval&interface=eth0&so_mark=1&proxyProtocol=1"},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("BuildConfigFromCmd: %v", err)
|
|
}
|
|
|
|
hop := cfg.Chains[0].Hops[0]
|
|
if hop.Metadata["key"] != "hval" {
|
|
t.Errorf("hop metadata key = %v, want hval", hop.Metadata["key"])
|
|
}
|
|
if hop.Metadata["so_mark"] != 1 {
|
|
t.Errorf("hop.so_mark = %v, want 1", hop.Metadata["so_mark"])
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigFromCmd_ServiceWithAdmission(t *testing.T) {
|
|
cfg, err := BuildConfigFromCmd(
|
|
[]string{":8080?admission=10.0.0.0/8,192.168.0.0/16"},
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("BuildConfigFromCmd: %v", err)
|
|
}
|
|
|
|
if len(cfg.Admissions) != 1 {
|
|
t.Fatalf("len(Admissions) = %d, want 1", len(cfg.Admissions))
|
|
}
|
|
adm := cfg.Admissions[0]
|
|
if len(adm.Matchers) != 2 {
|
|
t.Errorf("len(Matchers) = %d, want 2", len(adm.Matchers))
|
|
}
|
|
if cfg.Services[0].Admission != adm.Name {
|
|
t.Errorf("Service.Admission = %q, want %q", cfg.Services[0].Admission, adm.Name)
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigFromCmd_ServiceWithInvertAdmission(t *testing.T) {
|
|
cfg, err := BuildConfigFromCmd(
|
|
[]string{":8080?admission=~10.0.0.0/8"},
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("BuildConfigFromCmd: %v", err)
|
|
}
|
|
|
|
if !cfg.Admissions[0].Whitelist {
|
|
t.Error("Admission.Whitelist should be true for ~ prefix")
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigFromCmd_ServiceWithBypass(t *testing.T) {
|
|
cfg, err := BuildConfigFromCmd(
|
|
[]string{":8080?bypass=10.0.0.0/8"},
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("BuildConfigFromCmd: %v", err)
|
|
}
|
|
|
|
if len(cfg.Bypasses) != 1 {
|
|
t.Fatalf("len(Bypasses) = %d, want 1", len(cfg.Bypasses))
|
|
}
|
|
if cfg.Services[0].Bypass != cfg.Bypasses[0].Name {
|
|
t.Errorf("Service.Bypass = %q, want %q", cfg.Services[0].Bypass, cfg.Bypasses[0].Name)
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigFromCmd_ServiceWithResolver(t *testing.T) {
|
|
cfg, err := BuildConfigFromCmd(
|
|
[]string{":8080?resolver=8.8.8.8&prefer=go"},
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("BuildConfigFromCmd: %v", err)
|
|
}
|
|
|
|
rs := cfg.Resolvers[0]
|
|
if rs.Nameservers[0].Prefer != "go" {
|
|
t.Errorf("Prefer = %q, want go", rs.Nameservers[0].Prefer)
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigFromCmd_ServiceSystemResolverOnlyIPv4(t *testing.T) {
|
|
cfg, err := BuildConfigFromCmd(
|
|
[]string{"http://:8080?resolver=system&only=ipv4"},
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("BuildConfigFromCmd: %v", err)
|
|
}
|
|
|
|
if len(cfg.Resolvers) != 1 {
|
|
t.Fatalf("len(Resolvers) = %d, want 1", len(cfg.Resolvers))
|
|
}
|
|
rs := cfg.Resolvers[0]
|
|
if len(rs.Nameservers) != 1 {
|
|
t.Fatalf("len(Nameservers) = %d, want 1", len(rs.Nameservers))
|
|
}
|
|
ns := rs.Nameservers[0]
|
|
if ns.Addr != "system" {
|
|
t.Errorf("Addr = %q, want system", ns.Addr)
|
|
}
|
|
if ns.Only != "ipv4" {
|
|
t.Errorf("Only = %q, want ipv4", ns.Only)
|
|
}
|
|
if cfg.Services[0].Resolver != rs.Name {
|
|
t.Errorf("Service.Resolver = %q, want %q", cfg.Services[0].Resolver, rs.Name)
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigFromCmd_ServiceWithHosts(t *testing.T) {
|
|
cfg, err := BuildConfigFromCmd(
|
|
[]string{":8080?hosts=example.com:1.2.3.4"},
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("BuildConfigFromCmd: %v", err)
|
|
}
|
|
|
|
hs := cfg.Hosts[0]
|
|
if len(hs.Mappings) != 1 || hs.Mappings[0].Hostname != "example.com" || hs.Mappings[0].IP != "1.2.3.4" {
|
|
t.Errorf("Mappings = %+v, want example.com:1.2.3.4", hs.Mappings)
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigFromCmd_ServiceWithLimiters(t *testing.T) {
|
|
cfg, err := BuildConfigFromCmd(
|
|
[]string{":8080?limiter.in=1MB&limiter.out=2MB&limiter.conn.in=10&limiter.conn.out=20"},
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("BuildConfigFromCmd: %v", err)
|
|
}
|
|
|
|
if len(cfg.Limiters) != 1 {
|
|
t.Fatalf("len(Limiters) = %d, want 1", len(cfg.Limiters))
|
|
}
|
|
lim := cfg.Limiters[0]
|
|
if len(lim.Limits) != 2 {
|
|
t.Errorf("len(Limits) = %d, want 2", len(lim.Limits))
|
|
}
|
|
if cfg.Services[0].Limiter != lim.Name {
|
|
t.Errorf("Service.Limiter = %q, want %q", cfg.Services[0].Limiter, lim.Name)
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigFromCmd_ServiceWithCLimiter(t *testing.T) {
|
|
cfg, err := BuildConfigFromCmd(
|
|
[]string{":8080?climiter=100"},
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("BuildConfigFromCmd: %v", err)
|
|
}
|
|
|
|
if len(cfg.CLimiters) != 1 {
|
|
t.Fatalf("len(CLimiters) = %d, want 1", len(cfg.CLimiters))
|
|
}
|
|
if cfg.Services[0].CLimiter != cfg.CLimiters[0].Name {
|
|
t.Errorf("Service.CLimiter = %q, want %q", cfg.Services[0].CLimiter, cfg.CLimiters[0].Name)
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigFromCmd_ServiceWithRLimiter(t *testing.T) {
|
|
cfg, err := BuildConfigFromCmd(
|
|
[]string{":8080?rlimiter=0.5"},
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("BuildConfigFromCmd: %v", err)
|
|
}
|
|
|
|
if len(cfg.RLimiters) != 1 {
|
|
t.Fatalf("len(RLimiters) = %d, want 1", len(cfg.RLimiters))
|
|
}
|
|
if cfg.Services[0].RLimiter != cfg.RLimiters[0].Name {
|
|
t.Errorf("Service.RLimiter = %q, want %q", cfg.Services[0].RLimiter, cfg.RLimiters[0].Name)
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigFromCmd_ServiceWithRetries(t *testing.T) {
|
|
cfg, err := BuildConfigFromCmd(
|
|
[]string{":8080?retries=3"},
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("BuildConfigFromCmd: %v", err)
|
|
}
|
|
|
|
if cfg.Services[0].Handler.Retries != 3 {
|
|
t.Errorf("Retries = %d, want 3", cfg.Services[0].Handler.Retries)
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigFromCmd_MultipleServices(t *testing.T) {
|
|
cfg, err := BuildConfigFromCmd(
|
|
[]string{":8080", ":9090"},
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("BuildConfigFromCmd: %v", err)
|
|
}
|
|
|
|
if len(cfg.Services) != 2 {
|
|
t.Fatalf("len(Services) = %d, want 2", len(cfg.Services))
|
|
}
|
|
if cfg.Services[0].Name != "service-0" || cfg.Services[1].Name != "service-1" {
|
|
t.Errorf("names = %q, %q, want service-0, service-1",
|
|
cfg.Services[0].Name, cfg.Services[1].Name)
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigFromCmd_InvalidNode(t *testing.T) {
|
|
// Norm returns ErrInvalidCmd for empty strings
|
|
_, err := BuildConfigFromCmd(
|
|
nil,
|
|
[]string{""},
|
|
)
|
|
if err == nil {
|
|
t.Error("BuildConfigFromCmd should return error for empty node string")
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigFromCmd_NodeWithCommaSeparatedHosts(t *testing.T) {
|
|
// url.Parse accepts comma-separated hostnames without a port on the first entry
|
|
cfg, err := BuildConfigFromCmd(
|
|
[]string{":8080"},
|
|
[]string{"http://h1,h2:3128"},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("BuildConfigFromCmd: %v", err)
|
|
}
|
|
|
|
hop := cfg.Chains[0].Hops[0]
|
|
if len(hop.Nodes) != 2 {
|
|
t.Fatalf("len(Nodes) = %d, want 2", len(hop.Nodes))
|
|
}
|
|
if hop.Nodes[0].Addr != "h1" || hop.Nodes[1].Addr != "h2:3128" {
|
|
t.Errorf("Node addrs = %q, %q, want h1, h2:3128",
|
|
hop.Nodes[0].Addr, hop.Nodes[1].Addr)
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigFromCmd_NodeWithSelector(t *testing.T) {
|
|
cfg, err := BuildConfigFromCmd(
|
|
[]string{":8080"},
|
|
[]string{"http://proxy:3128?strategy=rr&maxFails=3&failTimeout=10s"},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("BuildConfigFromCmd: %v", err)
|
|
}
|
|
|
|
sel := cfg.Chains[0].Hops[0].Selector
|
|
if sel == nil {
|
|
t.Fatal("Selector is nil")
|
|
}
|
|
if sel.Strategy != "rr" {
|
|
t.Errorf("Strategy = %q, want rr", sel.Strategy)
|
|
}
|
|
if sel.MaxFails != 3 {
|
|
t.Errorf("MaxFails = %d, want 3", sel.MaxFails)
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigFromCmd_HttpsRewritesToHTTPTLS(t *testing.T) {
|
|
cfg, err := BuildConfigFromCmd(
|
|
[]string{"https://:443"},
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("BuildConfigFromCmd: %v", err)
|
|
}
|
|
|
|
// https is rewritten to http+tls by Norm; buildServiceConfig splits into handler=http, listener=tls
|
|
svc := cfg.Services[0]
|
|
if svc.Handler.Type != "http" && svc.Handler.Type != "auto" {
|
|
t.Errorf("Handler.Type = %q, want http or auto", svc.Handler.Type)
|
|
}
|
|
if svc.Listener.Type != "tls" && svc.Listener.Type != "tcp" {
|
|
t.Errorf("Listener.Type = %q, want tls or tcp", svc.Listener.Type)
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigFromCmd_EmptyServiceString(t *testing.T) {
|
|
cfg, err := BuildConfigFromCmd(
|
|
[]string{"", " ", ":8080"},
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("BuildConfigFromCmd: %v", err)
|
|
}
|
|
|
|
if len(cfg.Services) != 1 {
|
|
t.Fatalf("len(Services) = %d, want 1 (empty strings skipped)", len(cfg.Services))
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigFromCmd_NodeMetadataNotMixedWithServiceMetadata(t *testing.T) {
|
|
cfg, err := BuildConfigFromCmd(
|
|
[]string{":8080?hop.something=svcval"},
|
|
[]string{"http://proxy:3128?hop.something=nodeval"},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("BuildConfigFromCmd: %v", err)
|
|
}
|
|
|
|
hop := cfg.Chains[0].Hops[0]
|
|
if hop.Metadata["something"] != "nodeval" {
|
|
t.Errorf("hop metadata = %v, want nodeval", hop.Metadata["something"])
|
|
}
|
|
// service's hop.something shouldn't interfere with node's hop metadata
|
|
svc := cfg.Services[0]
|
|
if svc.Metadata["something"] == "svcval" {
|
|
t.Error("service metadata should not contain hop.something")
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigFromCmd_NodeWithConnectorAndDialerMetadata(t *testing.T) {
|
|
cfg, err := BuildConfigFromCmd(
|
|
[]string{":8080"},
|
|
[]string{"http://proxy:3128?connector.timeout=5s&dialer.keepAlive=true&node.weight=10"},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("BuildConfigFromCmd: %v", err)
|
|
}
|
|
|
|
nodeCfg := cfg.Chains[0].Hops[0].Nodes[0]
|
|
if nodeCfg.Connector.Metadata["timeout"] != "5s" {
|
|
t.Errorf("connector timeout = %v, want 5s", nodeCfg.Connector.Metadata["timeout"])
|
|
}
|
|
if nodeCfg.Dialer.Metadata["keepAlive"] != "true" {
|
|
t.Errorf("dialer keepAlive = %v, want true", nodeCfg.Dialer.Metadata["keepAlive"])
|
|
}
|
|
if nodeCfg.Metadata["weight"] != "10" {
|
|
t.Errorf("node weight = %v, want \"10\"", nodeCfg.Metadata["weight"])
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigFromCmd_ServiceWithHandlerAndListenerMetadata(t *testing.T) {
|
|
cfg, err := BuildConfigFromCmd(
|
|
[]string{":8080?handler.timeout=10s&listener.backlog=128&service.note=test"},
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("BuildConfigFromCmd: %v", err)
|
|
}
|
|
|
|
svc := cfg.Services[0]
|
|
if svc.Handler.Metadata["timeout"] != "10s" {
|
|
t.Errorf("handler timeout = %v, want 10s", svc.Handler.Metadata["timeout"])
|
|
}
|
|
if svc.Listener.Metadata["backlog"] != "128" {
|
|
t.Errorf("listener backlog = %v, want 128", svc.Listener.Metadata["backlog"])
|
|
}
|
|
if svc.Metadata["note"] != "test" {
|
|
t.Errorf("service note = %v, want test", svc.Metadata["note"])
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigFromCmd_ForwardService(t *testing.T) {
|
|
cfg, err := BuildConfigFromCmd(
|
|
[]string{"tcp://:8080/1.2.3.4:80"},
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("BuildConfigFromCmd: %v", err)
|
|
}
|
|
|
|
svc := cfg.Services[0]
|
|
if svc.Forwarder == nil {
|
|
t.Fatal("Forwarder is nil")
|
|
}
|
|
if len(svc.Forwarder.Nodes) != 1 {
|
|
t.Fatalf("len(Nodes) = %d, want 1", len(svc.Forwarder.Nodes))
|
|
}
|
|
if svc.Forwarder.Nodes[0].Addr != "1.2.3.4:80" {
|
|
t.Errorf("Forward addr = %q, want 1.2.3.4:80", svc.Forwarder.Nodes[0].Addr)
|
|
}
|
|
}
|
|
|
|
func TestParseAuthFromCmd_UsernameOnly(t *testing.T) {
|
|
authB64 := base64.RawURLEncoding.EncodeToString([]byte("useronly"))
|
|
got, err := parseAuthFromCmd(authB64)
|
|
if err != nil {
|
|
t.Fatalf("parseAuthFromCmd: %v", err)
|
|
}
|
|
if got.Username != "useronly" {
|
|
t.Errorf("Username = %q, want useronly", got.Username)
|
|
}
|
|
if got.Password != "" {
|
|
t.Errorf("Password = %q, want empty", got.Password)
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigFromCmd_ServiceWithInvertBypass(t *testing.T) {
|
|
cfg, err := BuildConfigFromCmd(
|
|
[]string{":8080?bypass=~10.0.0.0/8"},
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("BuildConfigFromCmd: %v", err)
|
|
}
|
|
|
|
bp := cfg.Bypasses[0]
|
|
if !bp.Whitelist {
|
|
t.Error("service Bypass.Whitelist should be true for ~ prefix")
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigFromCmd_NodeBypassEmptyMatcher(t *testing.T) {
|
|
// Empty entries after comma split should be skipped
|
|
cfg, err := BuildConfigFromCmd(
|
|
[]string{":8080"},
|
|
[]string{"http://proxy:3128?bypass=10.0.0.0/8,,192.168.0.0/16"},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("BuildConfigFromCmd: %v", err)
|
|
}
|
|
|
|
bp := cfg.Bypasses[0]
|
|
if len(bp.Matchers) != 2 {
|
|
t.Errorf("len(Matchers) = %d, want 2 (empty entry skipped)", len(bp.Matchers))
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigFromCmd_NodeResolverEmptyEntry(t *testing.T) {
|
|
cfg, err := BuildConfigFromCmd(
|
|
[]string{":8080"},
|
|
[]string{"http://proxy:3128?resolver=8.8.8.8,,1.1.1.1"},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("BuildConfigFromCmd: %v", err)
|
|
}
|
|
|
|
if len(cfg.Resolvers[0].Nameservers) != 2 {
|
|
t.Errorf("len(Nameservers) = %d, want 2 (empty entry skipped)", len(cfg.Resolvers[0].Nameservers))
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigFromCmd_NodeHostsInvalidMapping(t *testing.T) {
|
|
// entries without a colon are skipped; entries with empty IP after colon are kept
|
|
cfg, err := BuildConfigFromCmd(
|
|
[]string{":8080"},
|
|
[]string{"http://proxy:3128?hosts=valid.com:1.2.3.4,badentry,another.com:5.6.7.8"},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("BuildConfigFromCmd: %v", err)
|
|
}
|
|
|
|
hs := cfg.Hosts[0]
|
|
if len(hs.Mappings) != 2 {
|
|
t.Errorf("len(Mappings) = %d, want 2 (badentry without colon skipped)", len(hs.Mappings))
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigFromCmd_ServiceHostsInvalidMapping(t *testing.T) {
|
|
// entries without colon are skipped; entries with only colon+empty IP are kept
|
|
cfg, err := BuildConfigFromCmd(
|
|
[]string{":8080?hosts=good.com:1.2.3.4,badonly,other.com:5.6.7.8"},
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("BuildConfigFromCmd: %v", err)
|
|
}
|
|
|
|
hs := cfg.Hosts[0]
|
|
if len(hs.Mappings) != 2 {
|
|
t.Errorf("len(Mappings) = %d, want 2 (badonly without colon skipped)", len(hs.Mappings))
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigFromCmd_ServiceResolverEmptyEntry(t *testing.T) {
|
|
cfg, err := BuildConfigFromCmd(
|
|
[]string{":8080?resolver=8.8.8.8,,1.1.1.1"},
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("BuildConfigFromCmd: %v", err)
|
|
}
|
|
|
|
if len(cfg.Resolvers[0].Nameservers) != 2 {
|
|
t.Errorf("len(Nameservers) = %d, want 2", len(cfg.Resolvers[0].Nameservers))
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigFromCmd_ServiceAdmissionEmptyMatcher(t *testing.T) {
|
|
cfg, err := BuildConfigFromCmd(
|
|
[]string{":8080?admission=10.0.0.0/8,,192.168.0.0/16"},
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("BuildConfigFromCmd: %v", err)
|
|
}
|
|
|
|
if len(cfg.Admissions[0].Matchers) != 2 {
|
|
t.Errorf("len(Matchers) = %d, want 2", len(cfg.Admissions[0].Matchers))
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigFromCmd_ServiceBypassEmptyMatcher(t *testing.T) {
|
|
cfg, err := BuildConfigFromCmd(
|
|
[]string{":8080?bypass=10.0.0.0/8,,192.168.0.0/16"},
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("BuildConfigFromCmd: %v", err)
|
|
}
|
|
|
|
if len(cfg.Bypasses[0].Matchers) != 2 {
|
|
t.Errorf("len(Matchers) = %d, want 2", len(cfg.Bypasses[0].Matchers))
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigFromCmd_MultipleNodesCommaHost(t *testing.T) {
|
|
// Tests comma-separated hosts in node addr that result in multiple nodes with same hop
|
|
cfg, err := BuildConfigFromCmd(
|
|
[]string{":8080"},
|
|
[]string{"http://h1,h2,h3:3128"},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("BuildConfigFromCmd: %v", err)
|
|
}
|
|
|
|
hop := cfg.Chains[0].Hops[0]
|
|
if len(hop.Nodes) != 3 {
|
|
t.Fatalf("len(Nodes) = %d, want 3", len(hop.Nodes))
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigFromCmd_NodeHostEmptySkipped(t *testing.T) {
|
|
// url.Parse("http://h1,,h2:3128") is valid (comma in hostname).
|
|
// Split by comma yields ["h1", "", "h2:3128"] — the empty string is skipped.
|
|
cfg, err := BuildConfigFromCmd(
|
|
[]string{":8080"},
|
|
[]string{"http://h1,,h2:3128"},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("BuildConfigFromCmd: %v", err)
|
|
}
|
|
|
|
hop := cfg.Chains[0].Hops[0]
|
|
if len(hop.Nodes) != 2 {
|
|
t.Errorf("len(Nodes) = %d, want 2 (empty entry skipped)", len(hop.Nodes))
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigFromCmd_NoChainForMissingNode(t *testing.T) {
|
|
cfg, err := BuildConfigFromCmd(
|
|
[]string{":8080"},
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("BuildConfigFromCmd: %v", err)
|
|
}
|
|
|
|
// No nodes = no chain = handler should not have chain
|
|
if cfg.Services[0].Handler.Chain != "" {
|
|
t.Errorf("Handler.Chain = %q, want empty (no chain created)", cfg.Services[0].Handler.Chain)
|
|
}
|
|
}
|
|
|
|
func TestBuildServiceConfig_FullAutoScheme(t *testing.T) {
|
|
// auto:// scheme triggers handler=auto, listener=tcp fallback
|
|
rawURL := "auto://:8080"
|
|
u, _ := url.Parse(rawURL)
|
|
|
|
svcs, err := buildServiceConfig(u)
|
|
if err != nil {
|
|
t.Fatalf("buildServiceConfig: %v", err)
|
|
}
|
|
|
|
if len(svcs) != 1 {
|
|
t.Fatalf("len(svcs) = %d, want 1", len(svcs))
|
|
}
|
|
if svcs[0].Handler.Type != "auto" {
|
|
t.Errorf("Handler.Type = %q, want auto", svcs[0].Handler.Type)
|
|
}
|
|
}
|
|
|
|
func TestBuildServiceConfig_SSUHandlerUDPListener(t *testing.T) {
|
|
// ssu handler auto-detects to udp listener (handler stays "ssu" if in registry)
|
|
rawURL := "ssu://:8388"
|
|
u, _ := url.Parse(rawURL)
|
|
|
|
svcs, err := buildServiceConfig(u)
|
|
if err != nil {
|
|
t.Fatalf("buildServiceConfig: %v", err)
|
|
}
|
|
|
|
// ssu handler not in registry → falls back to auto
|
|
// listener defaults to tcp, but if handler was "ssu" it would be udp
|
|
if svcs[0].Listener.Type != "tcp" {
|
|
t.Errorf("Listener.Type = %q, want tcp (ssu not in test registry)", svcs[0].Listener.Type)
|
|
}
|
|
}
|
|
|
|
func TestBuildServiceConfig_RelayHandler(t *testing.T) {
|
|
// relay handler stays "relay" when forwarding (the relay!=relay check
|
|
// in buildServiceConfig preserves the handler type). When relay is not
|
|
// in the registry it falls back to auto, but we still get a forwarder.
|
|
rawURL := "relay://:8421/1.2.3.4:80"
|
|
u, _ := url.Parse(rawURL)
|
|
|
|
svcs, err := buildServiceConfig(u)
|
|
if err != nil {
|
|
t.Fatalf("buildServiceConfig: %v", err)
|
|
}
|
|
|
|
svc := svcs[0]
|
|
if svc.Forwarder == nil {
|
|
t.Fatal("Forwarder is nil for relay with forward path")
|
|
}
|
|
if svc.Handler.Type != "relay" && svc.Handler.Type != "auto" && svc.Handler.Type != "tcp" {
|
|
t.Errorf("Handler.Type = %q, want relay, auto, or tcp", svc.Handler.Type)
|
|
}
|
|
}
|
|
|
|
func TestBuildServiceConfig_ForwardMode(t *testing.T) {
|
|
// Non-relay handler with forward path → handler becomes listener type (tcp by default)
|
|
rawURL := "http://:8080/1.2.3.4:80,5.6.7.8:443"
|
|
u, _ := url.Parse(rawURL)
|
|
|
|
svcs, err := buildServiceConfig(u)
|
|
if err != nil {
|
|
t.Fatalf("buildServiceConfig: %v", err)
|
|
}
|
|
|
|
svc := svcs[0]
|
|
if svc.Forwarder == nil {
|
|
t.Fatal("Forwarder is nil")
|
|
}
|
|
if len(svc.Forwarder.Nodes) != 2 {
|
|
t.Errorf("len(Forwarder.Nodes) = %d, want 2", len(svc.Forwarder.Nodes))
|
|
}
|
|
// handler becomes listener type (tcp) for non-relay forward mode, or "auto" if not found
|
|
if svc.Handler.Type != "tcp" && svc.Handler.Type != "auto" && svc.Handler.Type != "forward" {
|
|
t.Errorf("Handler.Type = %q", svc.Handler.Type)
|
|
}
|
|
}
|
|
|
|
func TestBuildServiceConfig_DNSListener(t *testing.T) {
|
|
// dns handler+listener with forward path → handler becomes listener type
|
|
rawURL := "dns://:53/8.8.8.8"
|
|
u, _ := url.Parse(rawURL)
|
|
|
|
svcs, err := buildServiceConfig(u)
|
|
if err != nil {
|
|
t.Fatalf("buildServiceConfig: %v", err)
|
|
}
|
|
|
|
svc := svcs[0]
|
|
// dns not in registry → handler falls back to auto, then becomes listener type (tcp) for forward mode
|
|
// or dns IS in registry → handler stays "dns"
|
|
if svc.Forwarder == nil {
|
|
t.Fatal("Forwarder is nil")
|
|
}
|
|
if svc.Handler.Type != "dns" && svc.Handler.Type != "tcp" && svc.Handler.Type != "auto" {
|
|
t.Errorf("Handler.Type = %q", svc.Handler.Type)
|
|
}
|
|
}
|
|
|
|
func TestBuildNodeConfig_SSH_StandardAuth(t *testing.T) {
|
|
// SSH scheme with unknown registries: connector→http, dialer→tcp.
|
|
// Since dialer != "ssh"/"sshd" after fallback, auth stays on connector.
|
|
rawURL := "ssh://user:pass@server:22"
|
|
u, _ := url.Parse(rawURL)
|
|
|
|
node, err := buildNodeConfig(u, map[string]any{})
|
|
if err != nil {
|
|
t.Fatalf("buildNodeConfig: %v", err)
|
|
}
|
|
|
|
if node.Connector.Auth == nil {
|
|
t.Fatal("Connector.Auth is nil")
|
|
}
|
|
if node.Connector.Auth.Username != "user" || node.Connector.Auth.Password != "pass" {
|
|
t.Errorf("Connector.Auth = %+v, want user:pass", node.Connector.Auth)
|
|
}
|
|
if node.Dialer.Auth != nil {
|
|
t.Error("Dialer.Auth should be nil (dialer fell back to tcp)")
|
|
}
|
|
}
|
|
|
|
func TestBuildNodeConfig_TLSWithCert(t *testing.T) {
|
|
// TLS with certFile and keyFile (but no secure)
|
|
u, _ := url.Parse("http+tls://server:443")
|
|
m := map[string]any{
|
|
"cert": "cert.pem",
|
|
"key": "key.pem",
|
|
}
|
|
|
|
node, err := buildNodeConfig(u, m)
|
|
if err != nil {
|
|
t.Fatalf("buildNodeConfig: %v", err)
|
|
}
|
|
|
|
if node.Dialer.TLS == nil {
|
|
t.Fatal("Dialer.TLS is nil")
|
|
}
|
|
if node.Dialer.TLS.CertFile != "cert.pem" || node.Dialer.TLS.KeyFile != "key.pem" {
|
|
t.Errorf("TLS = %+v, want cert.pem/key.pem", node.Dialer.TLS)
|
|
}
|
|
}
|
|
|
|
func TestBuildNodeConfig_UnixFallsBackToTCP(t *testing.T) {
|
|
// http+unix scheme: connector=http, dialer=unix.
|
|
// Since unix is not in the test registry, dialer falls back to tcp,
|
|
// and the path-based address logic (dialer=="unix") is skipped.
|
|
rawURL := "http+unix:///var/run/socket"
|
|
u, _ := url.Parse(rawURL)
|
|
|
|
node, err := buildNodeConfig(u, map[string]any{})
|
|
if err != nil {
|
|
t.Fatalf("buildNodeConfig: %v", err)
|
|
}
|
|
|
|
if node.Dialer.Type != "tcp" {
|
|
t.Errorf("Dialer.Type = %q, want tcp (unix not in test registry)", node.Dialer.Type)
|
|
}
|
|
// Addr is url.Host ("") since no host component in three-slash unix URL
|
|
if node.Addr != "" {
|
|
t.Errorf("Addr = %q, want empty", node.Addr)
|
|
}
|
|
}
|
|
|
|
func TestBuildServiceConfig_RelayTLSForward(t *testing.T) {
|
|
// relay+tls scheme: handler=relay, listener=tls.
|
|
// relay not in test registry → handler falls back to auto, listener falls back to tcp.
|
|
// With forward path, handler becomes listener type (tcp) since handler != "relay" after fallback.
|
|
u, _ := url.Parse("relay+tls://:8443/backend:443")
|
|
svcs, err := buildServiceConfig(u)
|
|
if err != nil {
|
|
t.Fatalf("buildServiceConfig: %v", err)
|
|
}
|
|
if len(svcs) != 1 {
|
|
t.Errorf("len(svcs) = %d, want 1", len(svcs))
|
|
}
|
|
if svcs[0].Forwarder == nil {
|
|
t.Fatal("Forwarder is nil for relay+tls with path")
|
|
}
|
|
// Handler falls back to tcp due to registry fallback + forward mode reassignment
|
|
if svcs[0].Handler.Type != "tcp" && svcs[0].Handler.Type != "auto" && svcs[0].Handler.Type != "relay" {
|
|
t.Errorf("Handler.Type = %q", svcs[0].Handler.Type)
|
|
}
|
|
}
|
|
|
|
func TestBuildNodeConfig_SSUDefaultAuth(t *testing.T) {
|
|
// SIP002 auth for ssu scheme
|
|
encoded := base64.RawURLEncoding.EncodeToString([]byte("method:secret"))
|
|
rawURL := "ssu://" + encoded + "@server:8388"
|
|
u, _ := url.Parse(rawURL)
|
|
|
|
node, err := buildNodeConfig(u, map[string]any{})
|
|
if err != nil {
|
|
t.Fatalf("buildNodeConfig: %v", err)
|
|
}
|
|
|
|
// Connector falls back to http, so auth is on connector
|
|
if node.Connector.Auth == nil {
|
|
t.Fatal("Connector.Auth is nil")
|
|
}
|
|
if node.Connector.Auth.Username != "method" || node.Connector.Auth.Password != "secret" {
|
|
t.Errorf("Auth = %+v, want method:secret", node.Connector.Auth)
|
|
}
|
|
}
|
|
|
|
func TestCopyHandlerConfig_Nil(t *testing.T) {
|
|
if copyHandlerConfig(nil) != nil {
|
|
t.Error("copyHandlerConfig(nil) should be nil")
|
|
}
|
|
}
|
|
|
|
func TestCopyListenerConfig_Nil(t *testing.T) {
|
|
if copyListenerConfig(nil) != nil {
|
|
t.Error("copyListenerConfig(nil) should be nil")
|
|
}
|
|
}
|
|
|
|
func TestBuildConfigFromCmd_ServiceAuthError(t *testing.T) {
|
|
// Invalid base64 auth in service query should fail in buildServiceConfig
|
|
_, err := BuildConfigFromCmd(
|
|
[]string{"http://:8080?auth=!!!bad!!!"},
|
|
nil,
|
|
)
|
|
if err == nil {
|
|
t.Error("BuildConfigFromCmd should return error for invalid service auth")
|
|
}
|
|
}
|
|
|
|
func TestNorm_InvalidURL(t *testing.T) {
|
|
// Test with a URL containing control characters that url.Parse might reject
|
|
_, err := Norm("http://host:8080\x00")
|
|
if err == nil {
|
|
t.Error("Norm should return error for invalid URL")
|
|
}
|
|
}
|
|
|
|
func TestParseSelector_FailTimeoutAsInt(t *testing.T) {
|
|
// failTimeout can be specified as an integer (treated as seconds).
|
|
// GetDuration treats int values as seconds.
|
|
m := map[string]any{
|
|
"strategy": "random",
|
|
"failTimeout": 10,
|
|
}
|
|
got := parseSelector(m)
|
|
if got == nil {
|
|
t.Fatal("parseSelector() = nil")
|
|
}
|
|
if got.FailTimeout <= 0 {
|
|
t.Errorf("FailTimeout = %v, want > 0", got.FailTimeout)
|
|
}
|
|
// keys should be cleaned up
|
|
if _, ok := m["failTimeout"]; ok {
|
|
t.Error("failTimeout key should be deleted from m")
|
|
}
|
|
}
|
|
|
|
func TestBuildNodeConfig_TLSOnlySecure(t *testing.T) {
|
|
// TLS with only Secure=true should still create a TLSConfig (Secure alone is enough)
|
|
u, _ := url.Parse("http://server:8080")
|
|
m := map[string]any{"secure": true}
|
|
|
|
node, err := buildNodeConfig(u, m)
|
|
if err != nil {
|
|
t.Fatalf("buildNodeConfig: %v", err)
|
|
}
|
|
|
|
// TLS should be set because Secure=true, even without cert/ca/servername
|
|
if node.Dialer.TLS == nil {
|
|
t.Fatal("Dialer.TLS is nil — Secure alone should keep TLS config")
|
|
}
|
|
if node.Dialer.TLS.Secure != true {
|
|
t.Error("TLS.Secure should be true")
|
|
}
|
|
}
|