feat: refactor relay URL handling to support comma-separated input

yoonhyunwoo committed Nov 17, 2025 at 17:49 UTC 337b72a8e4e0740309a421465bc36fa862ae8931
1 file changed +23 -58
cmd/portal-tunnel/main.go
+23 -58
@@ -19,66 +19,12 @@ import (
19
20 var (
21 flagConfigPath string
22 - flagRelayURLs = newRelayListFlag("ws://localhost:4017/relay")
22 + flagRelayURLs string
23 flagHost string
24 flagPort string
25 flagName string
26 )
27
28 -type relayListFlag struct {
29 - values []string
30 - explicit bool
31 -}
32 -
33 -func newRelayListFlag(defaults ...string) *relayListFlag {
34 - list := make([]string, 0, len(defaults))
35 - for _, value := range defaults {
36 - value = strings.TrimSpace(value)
37 - if value == "" {
38 - continue
39 - }
40 - list = append(list, value)
41 - }
42 - return &relayListFlag{values: list}
43 -}
44 -
45 -func (f *relayListFlag) Set(value string) error {
46 - if f == nil {
47 - return fmt.Errorf("relay list flag is not initialized")
48 - }
49 -
50 - if !f.explicit {
51 - f.values = nil
52 - f.explicit = true
53 - }
54 -
55 - parts := strings.Split(value, ",")
56 - for _, part := range parts {
57 - part = strings.TrimSpace(part)
58 - if part == "" {
59 - continue
60 - }
61 - f.values = append(f.values, part)
62 - }
63 - return nil
64 -}
65 -
66 -func (f *relayListFlag) String() string {
67 - if f == nil {
68 - return ""
69 - }
70 - return strings.Join(f.values, ",")
71 -}
72 -
73 -func (f *relayListFlag) Values() []string {
74 - if f == nil || len(f.values) == 0 {
75 - return nil
76 - }
77 - out := make([]string, len(f.values))
78 - copy(out, f.values)
79 - return out
80 -}
81 -
28 type serviceContext struct {
29 Name string
30 LocalAddr string
@@ -95,7 +41,7 @@ func main() {
41 case "expose":
42 fs := flag.NewFlagSet("expose", flag.ExitOnError)
43 fs.StringVar(&flagConfigPath, "config", "", "Path to portal-tunnel config file")
98 - fs.Var(flagRelayURLs, "relay", "Portal relay server URL. Repeat or comma-separate to supply multiple servers when config is not provided")
44 + fs.StringVar(&flagRelayURLs, "relay", "ws://localhost:4017/relay", "Portal relay server URLs when config is not provided (comma-separated)")
45 fs.StringVar(&flagHost, "host", "localhost", "Local host to proxy to when config is not provided")
46 fs.StringVar(&flagPort, "port", "4018", "Local port to proxy to when config is not provided")
47 fs.StringVar(&flagName, "name", "", "Service name when config is not provided (auto-generated if empty)")
@@ -118,7 +64,7 @@ func printTunnelUsage() {
64 fmt.Println()
65 fmt.Println("Usage:")
66 fmt.Println(" portal-tunnel expose --config <file>")
121 - fmt.Println(" portal-tunnel expose [--relay URL ...] [--host HOST] [--port PORT] [--name NAME]")
67 + fmt.Println(" portal-tunnel expose [--relay URL,URL...]] [--host HOST] [--port PORT] [--name NAME]")
68 }
69
70 func runExpose() error {
@@ -187,7 +133,7 @@ func runExposeWithConfig() error {
133 }
134
135 func runExposeWithFlags() error {
190 - relayURLs := flagRelayURLs.Values()
136 + relayURLs := parseCommaSeparatedURLs(flagRelayURLs)
137 if len(relayURLs) == 0 {
138 return fmt.Errorf("--relay must include at least one non-empty URL when --config is not provided")
139 }
@@ -390,6 +336,25 @@ func selectServices(cfg *TunnelConfig) ([]*ServiceConfig, error) {
336 return services, nil
337 }
338
339 +func parseCommaSeparatedURLs(raw string) []string {
340 + raw = strings.TrimSpace(raw)
341 + if raw == "" {
342 + return nil
343 + }
344 +
345 + parts := strings.Split(raw, ",")
346 + out := make([]string, 0, len(parts))
347 +
348 + for _, p := range parts {
349 + p = strings.TrimSpace(p)
350 + if p != "" {
351 + out = append(out, p)
352 + }
353 + }
354 +
355 + return out
356 +}
357 +
358 func extractHost(wsURL string) string {
359 // Simple extraction: ws://host:port/path -> host:port
360 // Remove ws:// or wss://