package client import ( "os" "path/filepath" "strings" "testing" ) func TestNormalizeAPI(t *testing.T) { cases := map[string]string{ "tunnel.tzgyn.dev": "https://tunnel.tzgyn.dev", "https://tunnel.tzgyn.dev/": "https://tunnel.tzgyn.dev", "http://127.0.0.1:8080": "http://127.0.0.1:8080", } for input, want := range cases { got, err := NormalizeAPI(input) if err != nil || got != want { t.Errorf("NormalizeAPI(%q) = %q, %v; want %q", input, got, err, want) } } if _, err := NormalizeAPI("ftp://example.com"); err == nil { t.Error("ftp should be rejected") } } func TestNormalizeTarget(t *testing.T) { cases := map[string]string{ "3000": "127.0.0.1:3000", ":3000": "127.0.0.1:3000", "127.0.0.1:4096": "127.0.0.1:4096", "localhost:8080": "localhost:8080", "[::1]:3000": "[::1]:3000", } for input, want := range cases { got, err := NormalizeTarget(input) if err != nil || got != want { t.Errorf("NormalizeTarget(%q) = %q, %v; want %q", input, got, err, want) } } for _, bad := range []string{"", "http://x", "localhost", "127.0.0.1:0", "127.0.0.1:99999"} { if _, err := NormalizeTarget(bad); err == nil { t.Errorf("NormalizeTarget(%q) should fail", bad) } } } func TestConfigRoundTrip(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "config.json") cfg := &Config{API: "https://tunnel.example", Routes: map[string]Route{}} if err := cfg.AddRoute("api", "4096", ""); err != nil { t.Fatal(err) } if err := cfg.Save(path); err != nil { t.Fatal(err) } info, err := os.Stat(path) if err != nil { t.Fatal(err) } if info.Mode().Perm() != 0o600 { t.Errorf("mode = %o, want 600", info.Mode().Perm()) } loaded, err := Load(path) if err != nil { t.Fatal(err) } if loaded.API != cfg.API || loaded.Routes["api"].Target != "127.0.0.1:4096" { t.Errorf("round trip: %+v", loaded) } } func TestEnsureIdentityReusesCSR(t *testing.T) { paths := ConfigPaths(filepath.Join(t.TempDir(), "config.json")) key1, csr1, err := EnsureIdentity(paths, "abc.tunnel.example") if err != nil { t.Fatal(err) } key2, csr2, err := EnsureIdentity(paths, "abc.tunnel.example") if err != nil { t.Fatal(err) } if string(key1) != string(key2) || string(csr1) != string(csr2) { t.Error("identity was regenerated on the second call") } if !csrMatches(csr1, "abc.tunnel.example") { t.Error("CSR does not match the hostname") } } func TestRouteForSNI(t *testing.T) { host := "abc.tunnel.example" cases := map[string]string{ host: "@", "api." + host: "api", "API." + host: "api", "api." + host + ".": "api", "a.b." + host: "", "other.example": "", "api.abc.tunnel.example.x": "", } for sni, want := range cases { if got := routeForSNI(host, sni); got != want { t.Errorf("routeForSNI(%q) = %q, want %q", sni, got, want) } } } func TestReadOfficialRoutes(t *testing.T) { path := filepath.Join(t.TempDir(), "coolify.toml") body := `[routes] api = "127.0.0.1:4096" web = { target = "localhost:3000", proxy_protocol = "v2" } ` if err := os.WriteFile(path, []byte(body), 0o600); err != nil { t.Fatal(err) } routes, err := readOfficialRoutes(path) if err != nil { t.Fatal(err) } if routes["api"].Target != "127.0.0.1:4096" { t.Errorf("api = %+v", routes["api"]) } if routes["web"].Target != "localhost:3000" || routes["web"].ProxyProtocol != "v2" { t.Errorf("web = %+v", routes["web"]) } } func TestImportOfficialProfile(t *testing.T) { root := t.TempDir() dataHome := filepath.Join(root, "data") configHome := filepath.Join(root, "config") t.Setenv("XDG_DATA_HOME", dataHome) t.Setenv("XDG_CONFIG_HOME", configHome) sourceDir := filepath.Join(dataHome, "opentunnel", "coolify") if err := os.MkdirAll(sourceDir, 0o700); err != nil { t.Fatal(err) } officialPaths := ConfigPaths(filepath.Join(root, "official", "config.json")) keyPEM, _, err := EnsureIdentity(officialPaths, "abc.tunnel.example") if err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(sourceDir, "tunnel.json"), []byte(`{"id":"abc","hostname":"abc.tunnel.example","certificateExpiry":"2027-01-01T00:00:00Z"}`), 0o600); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(sourceDir, "token"), []byte("tok\n"), 0o600); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(sourceDir, "private-key.pem"), keyPEM, 0o600); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(sourceDir, "certificate.pem"), []byte("dummy"), 0o600); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(sourceDir, "chain.pem"), []byte("dummy"), 0o600); err != nil { t.Fatal(err) } if err := os.MkdirAll(filepath.Join(configHome, "opentunnel"), 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(configHome, "opentunnel", "coolify.toml"), []byte("[routes]\napi = \"127.0.0.1:4096\"\n"), 0o600); err != nil { t.Fatal(err) } target := filepath.Join(root, "client", "config.json") result, err := Import(ImportOptions{Profile: "coolify", API: "https://tunnel.example", ConfigPath: target}) if err != nil { t.Fatal(err) } if result.TunnelID != "abc" || result.Hostname != "abc.tunnel.example" { t.Errorf("result = %+v", result) } cfg, err := Load(target) if err != nil { t.Fatal(err) } if cfg.Token != "tok" || cfg.API != "https://tunnel.example" || cfg.Routes["api"].Target != "127.0.0.1:4096" { t.Errorf("config = %+v", cfg) } if _, err := os.Stat(ConfigPaths(target).Key); err != nil { t.Errorf("key not copied: %v", err) } } func TestProxyHeaders(t *testing.T) { if got := string(proxyV1("203.0.113.9:51234")); got != "PROXY TCP4 203.0.113.9 0.0.0.0 51234 443\r\n" { t.Errorf("v1 = %q", got) } if got := string(proxyV1("nonsense")); got != "PROXY UNKNOWN\r\n" { t.Errorf("v1 unknown = %q", got) } v2 := proxyV2("203.0.113.9:51234", "api.example") sig := "\r\n\r\n\x00\r\nQUIT\n" if !strings.HasPrefix(string(v2), sig) { t.Fatalf("v2 signature missing") } if v2[12] != 0x21 || v2[13] != 0x11 { t.Errorf("v2 version/family = %x %x", v2[12], v2[13]) } if !strings.HasSuffix(string(v2), "api.example") { t.Errorf("v2 TLV missing") } unknown := proxyV2("nonsense", "") if unknown[12] != 0x20 { t.Errorf("v2 local command = %x", unknown[12]) } }