sdk: nomalize bootstrap server url
Kim committed
Nov 17, 2025 at 11:02 UTC
e3ca1f4c7b14eefe9b2eeb246d856aa761203c87
2 files changed
+136
-3
sdk/sdk.go
+73
-3
@@ -7,6 +7,7 @@ import (
7
"fmt"
8
"io"
9
"net"
10
+ "net/url"
11
"regexp"
12
"strings"
13
"sync"
@@ -217,6 +218,58 @@ func WithHide(hide bool) MetadataOption {
218
}
219
}
220
221
+// normalizeBootstrapServer takes various user-friendly server inputs and
222
+// converts them into a proper WebSocket URL.
223
+// Examples:
224
+// - "wss://localhost:4017/relay" -> unchanged
225
+// - "ws://localhost:4017/relay" -> unchanged
226
+// - "http://example.com" -> "ws://example.com/relay"
227
+// - "https://example.com" -> "wss://example.com/relay"
228
+// - "localhost:4017" -> "wss://localhost:4017/relay"
229
+// - "example.com" -> "wss://example.com/relay"
230
+func normalizeBootstrapServer(raw string) (string, error) {
231
+ server := strings.TrimSpace(raw)
232
+ if server == "" {
233
+ return "", fmt.Errorf("bootstrap server is empty")
234
+ }
235
+
236
+ // Already a WebSocket URL
237
+ if strings.HasPrefix(server, "ws://") || strings.HasPrefix(server, "wss://") {
238
+ return server, nil
239
+ }
240
+
241
+ // HTTP/HTTPS -> WS/WSS with default /relay path
242
+ if strings.HasPrefix(server, "http://") || strings.HasPrefix(server, "https://") {
243
+ u, err := url.Parse(server)
244
+ if err != nil {
245
+ return "", fmt.Errorf("invalid bootstrap server %q: %w", raw, err)
246
+ }
247
+ switch u.Scheme {
248
+ case "http":
249
+ u.Scheme = "ws"
250
+ case "https":
251
+ u.Scheme = "wss"
252
+ }
253
+ if u.Path == "" || u.Path == "/" {
254
+ u.Path = "/relay"
255
+ }
256
+ return u.String(), nil
257
+ }
258
+
259
+ // Bare host[:port][/path] -> assume WSS and /relay if no path
260
+ u, err := url.Parse("wss://" + server)
261
+ if err != nil {
262
+ return "", fmt.Errorf("invalid bootstrap server %q: %w", raw, err)
263
+ }
264
+ if u.Host == "" {
265
+ return "", fmt.Errorf("invalid bootstrap server %q: missing host", raw)
266
+ }
267
+ if u.Path == "" || u.Path == "/" {
268
+ u.Path = "/relay"
269
+ }
270
+ return u.String(), nil
271
+}
272
+
273
type RDClient struct {
274
mu sync.Mutex
275
@@ -264,12 +317,29 @@ func NewClient(opt ...Option) (*RDClient, error) {
317
// Initialize relays from bootstrap servers
318
var connectionErrors []error
319
for _, server := range config.BootstrapServers {
267
- err := client.AddRelay(server, config.Dialer)
320
+ normalized, err := normalizeBootstrapServer(server)
321
+ if err != nil {
322
+ log.Error().
323
+ Err(err).
324
+ Str("server", server).
325
+ Msg("[SDK] Invalid bootstrap server")
326
+ connectionErrors = append(connectionErrors, err)
327
+ continue
328
+ }
329
+
330
+ err = client.AddRelay(normalized, config.Dialer)
331
if err != nil {
269
- log.Error().Err(err).Str("server", server).Msg("[SDK] Failed to connect to bootstrap server")
332
+ log.Error().
333
+ Err(err).
334
+ Str("server", normalized).
335
+ Msg("[SDK] Failed to connect to bootstrap server")
336
connectionErrors = append(connectionErrors, err)
337
+ continue
338
}
272
- log.Debug().Str("server", server).Msg("[SDK] Successfully connected to bootstrap server")
339
+ log.Debug().
340
+ Str("server_raw", server).
341
+ Str("server", normalized).
342
+ Msg("[SDK] Successfully connected to bootstrap server")
343
}
344
345
// If no relays were successfully connected, return an error
sdk/validation_test.go
+63
@@ -68,3 +68,66 @@ func TestIsURLSafeName(t *testing.T) {
68
})
69
}
70
}
71
+
72
+func TestNormalizeBootstrapServer(t *testing.T) {
73
+ tests := []struct {
74
+ name string
75
+ input string
76
+ want string
77
+ shouldFail bool
78
+ }{
79
+ {
80
+ name: "already ws",
81
+ input: "ws://localhost:4017/relay",
82
+ want: "ws://localhost:4017/relay",
83
+ },
84
+ {
85
+ name: "already wss",
86
+ input: "wss://localhost:4017/relay",
87
+ want: "wss://localhost:4017/relay",
88
+ },
89
+ {
90
+ name: "localhost with port",
91
+ input: "localhost:4017",
92
+ want: "wss://localhost:4017/relay",
93
+ },
94
+ {
95
+ name: "domain without port",
96
+ input: "example.com",
97
+ want: "wss://example.com/relay",
98
+ },
99
+ {
100
+ name: "http scheme",
101
+ input: "http://example.com",
102
+ want: "ws://example.com/relay",
103
+ },
104
+ {
105
+ name: "https scheme",
106
+ input: "https://example.com",
107
+ want: "wss://example.com/relay",
108
+ },
109
+ {
110
+ name: "empty",
111
+ input: "",
112
+ shouldFail: true,
113
+ },
114
+ }
115
+
116
+ for _, tt := range tests {
117
+ t.Run(tt.name, func(t *testing.T) {
118
+ got, err := normalizeBootstrapServer(tt.input)
119
+ if tt.shouldFail {
120
+ if err == nil {
121
+ t.Fatalf("normalizeBootstrapServer(%q) expected error, got nil", tt.input)
122
+ }
123
+ return
124
+ }
125
+ if err != nil {
126
+ t.Fatalf("normalizeBootstrapServer(%q) unexpected error: %v", tt.input, err)
127
+ }
128
+ if got != tt.want {
129
+ t.Fatalf("normalizeBootstrapServer(%q) = %q, want %q", tt.input, got, tt.want)
130
+ }
131
+ })
132
+ }
133
+}