fix: query params for websocket
Hee Sung Son committed
Nov 12, 2025 at 13:47 UTC
4c5ed619687942f8ec4bfa242d4e898ed27c76d6
1 file changed
+184
-116
cmd/webclient/polyfill.js
+184
-116
@@ -7,6 +7,17 @@
7
// Save original WebSocket
8
const NativeWebSocket = window.WebSocket;
9
10
+ // Conditional logging helper - only log when localhost is in URL
11
+ const isLocalhost = window.location.hostname === 'localhost' ||
12
+ window.location.hostname === '127.0.0.1' ||
13
+ window.location.hostname.endsWith('.localhost');
14
+
15
+ function debugLog(...args) {
16
+ if (isLocalhost) {
17
+ console.log(...args);
18
+ }
19
+ }
20
+
21
// Generate unique client ID
22
function generateClientId() {
23
return `client-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
@@ -19,37 +30,135 @@
30
return btoa(String.fromCharCode(...bytes));
31
}
32
22
- // WebSocket polyfill using Service Worker E2EE
23
- class WebSocketPolyfill {
33
+ // Helper to validate WebSocket constructor arguments (mimics native behavior)
34
+ function validateWebSocketArgs(url, protocols) {
35
+ if (!url) {
36
+ throw new DOMException("Failed to construct 'WebSocket': 1 argument required, but only 0 present.");
37
+ }
38
+
39
+ let parsedUrl;
40
+ try {
41
+ parsedUrl = new URL(url, window.location.href);
42
+ } catch (e) {
43
+ throw new DOMException(`Failed to construct 'WebSocket': The URL '${url}' is invalid.`);
44
+ }
45
+
46
+ if (parsedUrl.protocol !== 'ws:' && parsedUrl.protocol !== 'wss:') {
47
+ throw new DOMException(`Failed to construct 'WebSocket': The URL's scheme must be either 'ws' or 'wss'. '${parsedUrl.protocol.slice(0, -1)}' is not allowed.`);
48
+ }
49
+
50
+ let normalizedProtocols = protocols;
51
+ if (protocols !== undefined && protocols !== null) {
52
+ if (typeof protocols === 'string') {
53
+ normalizedProtocols = [protocols];
54
+ } else if (Array.isArray(protocols)) {
55
+ normalizedProtocols = protocols;
56
+ } else {
57
+ throw new DOMException("Failed to construct 'WebSocket': The subprotocol '" + protocols + "' is invalid.");
58
+ }
59
+
60
+ const seen = new Set();
61
+ for (const protocol of normalizedProtocols) {
62
+ if (typeof protocol !== 'string') {
63
+ throw new DOMException("Failed to construct 'WebSocket': The subprotocol '" + protocol + "' is invalid.");
64
+ }
65
+ if (protocol === '') {
66
+ throw new DOMException("Failed to construct 'WebSocket': The subprotocol '' is invalid.");
67
+ }
68
+ if (seen.has(protocol)) {
69
+ throw new DOMException(`Failed to construct 'WebSocket': The subprotocol '${protocol}' is duplicated.`);
70
+ }
71
+ seen.add(protocol);
72
+ }
73
+ }
74
+
75
+ return { parsedUrl, normalizedProtocols };
76
+ }
77
+
78
+ // WebSocket polyfill using Service Worker E2EE - extends EventTarget for native event handling
79
+ class WebSocketPolyfill extends EventTarget {
80
+ // WebSocket ready state constants (matching native WebSocket)
81
+ static CONNECTING = 0;
82
+ static OPEN = 1;
83
+ static CLOSING = 2;
84
+ static CLOSED = 3;
85
+
86
constructor(url, protocols) {
25
- this.url = url;
26
- this.protocols = protocols;
27
- this.readyState = WebSocket.CONNECTING;
28
- this.bufferedAmount = 0;
29
- this.extensions = "";
30
- this.protocol = "";
87
+ super(); // Initialize EventTarget
88
+
89
+ // Validate arguments using native-like validation
90
+ const { parsedUrl, normalizedProtocols } = validateWebSocketArgs(url, protocols);
91
+
92
+ // Store original URL and protocols
93
+ this._url = parsedUrl.href;
94
+ this._protocols = normalizedProtocols;
95
+ this._parsedUrl = parsedUrl;
96
+
97
+ // Define read-only properties to match native WebSocket
98
+ Object.defineProperty(this, 'url', {
99
+ get: () => this._url,
100
+ enumerable: true,
101
+ configurable: true
102
+ });
103
+
104
+ Object.defineProperty(this, 'readyState', {
105
+ get: () => this._readyState,
106
+ enumerable: true,
107
+ configurable: true
108
+ });
109
+
110
+ Object.defineProperty(this, 'bufferedAmount', {
111
+ get: () => this._bufferedAmount,
112
+ enumerable: true,
113
+ configurable: true
114
+ });
115
+
116
+ Object.defineProperty(this, 'extensions', {
117
+ get: () => this._extensions,
118
+ enumerable: true,
119
+ configurable: true
120
+ });
121
+
122
+ Object.defineProperty(this, 'protocol', {
123
+ get: () => this._protocol,
124
+ enumerable: true,
125
+ configurable: true
126
+ });
127
+
128
+ // Internal state
129
+ this._readyState = WebSocketPolyfill.CONNECTING;
130
+ this._bufferedAmount = 0;
131
+ this._extensions = "";
132
+ this._protocol = "";
133
this.binaryType = "blob";
134
33
- // Event handlers
135
+ // Event handlers (use native-like pattern)
136
this.onopen = null;
137
this.onmessage = null;
138
this.onerror = null;
139
this.onclose = null;
140
39
- // Internal state
141
+ // Internal connection state
142
this._clientId = generateClientId();
143
this._connId = null;
144
this._isClosed = false;
145
this._wsKey = generateWebSocketKey();
146
this._frameBuffer = new Uint8Array(0);
147
46
- // Setup Service Worker message listener
148
+ // Setup and connect
149
this._setupMessageListener();
48
-
49
- // Initialize connection
150
this._connect();
151
}
152
153
+ // Helper to send messages to Service Worker
154
+ _postToServiceWorker(message) {
155
+ navigator.serviceWorker.controller.postMessage({
156
+ clientId: this._clientId,
157
+ connId: this._connId,
158
+ ...message
159
+ });
160
+ }
161
+
162
_setupMessageListener() {
163
navigator.serviceWorker.addEventListener("message", (event) => {
164
const data = event.data;
@@ -80,11 +189,10 @@
189
}
190
191
async _connect() {
83
- console.log("[WebSocket Polyfill] Connecting via Service Worker SDK to:", this.url);
192
+ debugLog("[WebSocket Polyfill] Connecting via Service Worker SDK to:", this._url);
193
try {
85
- // Extract hostname from URL
86
- const urlObj = new URL(this.url);
87
- let hostname = urlObj.hostname;
194
+ // Extract and normalize hostname (already parsed in constructor)
195
+ let hostname = this._parsedUrl.hostname;
196
197
// Normalize punycode to lowercase (punycode is case-insensitive per RFC 3492)
198
// This ensures XN--CW4B85OB9G becomes xn--cw4b85ob9g before processing
@@ -94,12 +202,12 @@
202
// Go backend will convert punycode->unicode and then uppercase
203
const leaseName = hostname.split('.')[0];
204
97
- console.log("[WebSocket Polyfill] Lease name:", leaseName);
205
+ debugLog("[WebSocket Polyfill] Lease name:", leaseName);
206
207
// Wait for Service Worker to be ready
208
await navigator.serviceWorker.ready;
209
102
- // Send connect message to Service Worker
210
+ // Send connect message to Service Worker (note: connId not set yet, so we manually include clientId)
211
navigator.serviceWorker.controller.postMessage({
212
type: "SDK_CONNECT",
213
clientId: this._clientId,
@@ -115,17 +223,16 @@
223
_handleConnectSuccess(data) {
224
this._connId = data.connId;
225
118
- console.log("[WebSocket Polyfill] E2EE connection established, sending WebSocket upgrade");
226
+ debugLog("[WebSocket Polyfill] E2EE connection established, sending WebSocket upgrade");
227
228
// Send WebSocket HTTP Upgrade request
229
this._sendWebSocketUpgrade();
230
}
231
232
_sendWebSocketUpgrade() {
125
- // Parse URL to get path
126
- const urlObj = new URL(this.url);
127
- const path = urlObj.pathname || "/";
128
- const host = urlObj.host;
233
+ // Parse URL to get path with query parameters
234
+ const path = (this._parsedUrl.pathname || "/") + (this._parsedUrl.search || "");
235
+ const host = this._parsedUrl.host;
236
237
// Build HTTP Upgrade request
238
let upgradeRequest = `GET ${path} HTTP/1.1\r\n`;
@@ -135,25 +242,23 @@
242
upgradeRequest += `Sec-WebSocket-Key: ${this._wsKey}\r\n`;
243
upgradeRequest += `Sec-WebSocket-Version: 13\r\n`;
244
138
- if (this.protocols) {
139
- const protocolStr = Array.isArray(this.protocols)
140
- ? this.protocols.join(', ')
141
- : this.protocols;
245
+ if (this._protocols) {
246
+ const protocolStr = Array.isArray(this._protocols)
247
+ ? this._protocols.join(', ')
248
+ : this._protocols;
249
upgradeRequest += `Sec-WebSocket-Protocol: ${protocolStr}\r\n`;
250
}
251
252
upgradeRequest += `\r\n`;
253
147
- console.log("[WebSocket Polyfill] Sending upgrade request:", upgradeRequest);
254
+ debugLog("[WebSocket Polyfill] Sending upgrade request:", upgradeRequest);
255
256
// Convert to bytes and send
257
const encoder = new TextEncoder();
258
const bytes = encoder.encode(upgradeRequest);
259
153
- navigator.serviceWorker.controller.postMessage({
260
+ this._postToServiceWorker({
261
type: "SDK_SEND",
155
- clientId: this._clientId,
156
- connId: this._connId,
262
data: bytes,
263
});
264
@@ -191,7 +296,7 @@
296
297
// Parse HTTP response
298
const headers = text.substring(0, headerEndIndex);
194
- console.log("[WebSocket Polyfill] Received upgrade response:", headers);
299
+ debugLog("[WebSocket Polyfill] Received upgrade response:", headers);
300
301
// Check if upgrade was successful
302
if (!headers.includes('HTTP/1.1 101') && !headers.includes('HTTP/1.0 101')) {
@@ -202,19 +307,16 @@
307
// Extract protocol if present
308
const protocolMatch = headers.match(/Sec-WebSocket-Protocol:\s*(\S+)/i);
309
if (protocolMatch) {
205
- this.protocol = protocolMatch[1];
310
+ this._protocol = protocolMatch[1];
311
}
312
313
// Upgrade successful!
314
this._waitingForUpgrade = false;
210
- this.readyState = WebSocket.OPEN;
315
+ this._readyState = WebSocketPolyfill.OPEN;
316
212
- console.log("[WebSocket Polyfill] WebSocket connection established");
317
+ debugLog("[WebSocket Polyfill] WebSocket connection established");
318
214
- // Fire onopen event
215
- if (this.onopen) {
216
- this.onopen(new Event("open"));
217
- }
319
+ // Fire onopen event (dispatchEvent will handle both onopen and listeners)
320
this.dispatchEvent(new Event("open"));
321
322
// If there's any data after the headers, process it as WebSocket frames
@@ -235,7 +337,7 @@
337
// For now, assume data is the payload (we'll implement frame parsing if needed)
338
// WebSocket frames from server are not masked
339
238
- if (this.readyState !== WebSocket.OPEN) return;
340
+ if (this._readyState !== WebSocketPolyfill.OPEN) return;
341
342
// Append incoming data to frame buffer
343
const newBuffer = new Uint8Array(this._frameBuffer.length + data.length);
@@ -288,11 +390,8 @@
390
const text = new TextDecoder().decode(payload);
391
const event = new MessageEvent("message", {
392
data: text,
291
- origin: new URL(this.url).origin,
393
+ origin: this._parsedUrl.origin,
394
});
293
- if (this.onmessage) {
294
- this.onmessage(event);
295
- }
395
this.dispatchEvent(event);
396
} else if (opcode === 0x02) {
397
// Binary frame
@@ -304,11 +403,8 @@
403
}
404
const event = new MessageEvent("message", {
405
data: eventData,
307
- origin: new URL(this.url).origin,
406
+ origin: this._parsedUrl.origin,
407
});
309
- if (this.onmessage) {
310
- this.onmessage(event);
311
- }
408
this.dispatchEvent(event);
409
} else if (opcode === 0x08) {
410
// Close frame
@@ -333,10 +429,8 @@
429
_sendPong(payload) {
430
// Send pong frame
431
const frame = this._createWebSocketFrame(0x0A, payload);
336
- navigator.serviceWorker.controller.postMessage({
432
+ this._postToServiceWorker({
433
type: "SDK_SEND",
338
- clientId: this._clientId,
339
- connId: this._connId,
434
data: frame,
435
});
436
}
@@ -396,7 +490,7 @@
490
const code = data.code || 1000;
491
const reason = data.reason || "";
492
399
- console.log(
493
+ debugLog(
494
"[WebSocket Polyfill] Connection closed, code:",
495
code,
496
"reason:",
@@ -404,7 +498,7 @@
498
);
499
500
this._isClosed = true;
407
- this.readyState = WebSocket.CLOSED;
501
+ this._readyState = WebSocketPolyfill.CLOSED;
502
503
const event = new CloseEvent("close", {
504
code: code,
@@ -412,9 +506,7 @@
506
wasClean: code === 1000,
507
});
508
415
- if (this.onclose) {
416
- this.onclose(event);
417
- }
509
+ // dispatchEvent will handle both onclose and event listeners
510
this.dispatchEvent(event);
511
}
512
@@ -429,9 +521,7 @@
521
const event = new Event("error");
522
event.error = error;
523
432
- if (this.onerror) {
433
- this.onerror(event);
434
- }
524
+ // dispatchEvent will handle both onerror and event listeners
525
this.dispatchEvent(event);
526
527
// Close connection after error
@@ -441,8 +531,8 @@
531
}
532
533
send(data) {
444
- if (this.readyState !== WebSocket.OPEN) {
445
- throw new Error("WebSocket is not open");
534
+ if (this._readyState !== WebSocketPolyfill.OPEN) {
535
+ throw new DOMException("Failed to execute 'send' on 'WebSocket': Still in CONNECTING state.");
536
}
537
538
if (!this._connId) {
@@ -469,10 +559,8 @@
559
data.arrayBuffer().then(arrayBuffer => {
560
const bytes = new Uint8Array(arrayBuffer);
561
const frame = this._createWebSocketFrame(0x02, bytes);
472
- navigator.serviceWorker.controller.postMessage({
562
+ this._postToServiceWorker({
563
type: "SDK_SEND",
474
- clientId: this._clientId,
475
- connId: this._connId,
564
data: frame,
565
});
566
});
@@ -485,10 +573,8 @@
573
const frame = this._createWebSocketFrame(opcode, bytes);
574
575
// Send to Service Worker
488
- navigator.serviceWorker.controller.postMessage({
576
+ this._postToServiceWorker({
577
type: "SDK_SEND",
490
- clientId: this._clientId,
491
- connId: this._connId,
578
data: frame,
579
});
580
} catch (error) {
@@ -498,20 +584,20 @@
584
}
585
586
close(code = 1000, reason = "") {
501
- if (this._isClosed || this.readyState === WebSocket.CLOSING) {
587
+ if (this._isClosed || this._readyState === WebSocketPolyfill.CLOSING) {
588
return;
589
}
590
505
- console.log(
591
+ debugLog(
592
"[WebSocket Polyfill] Client initiated close, code:",
593
code,
594
"reason:",
595
reason
596
);
597
512
- this.readyState = WebSocket.CLOSING;
598
+ this._readyState = WebSocketPolyfill.CLOSING;
599
514
- if (this._connId && this.readyState === WebSocket.OPEN) {
600
+ if (this._connId && this._readyState === WebSocketPolyfill.OPEN) {
601
// Send WebSocket close frame
602
const reasonBytes = new TextEncoder().encode(reason);
603
const payload = new Uint8Array(2 + reasonBytes.length);
@@ -521,20 +607,16 @@
607
608
const frame = this._createWebSocketFrame(0x08, payload);
609
524
- navigator.serviceWorker.controller.postMessage({
610
+ this._postToServiceWorker({
611
type: "SDK_SEND",
526
- clientId: this._clientId,
527
- connId: this._connId,
612
data: frame,
613
});
614
}
615
616
// Close SDK connection
617
if (this._connId) {
534
- navigator.serviceWorker.controller.postMessage({
618
+ this._postToServiceWorker({
619
type: "SDK_CLOSE",
536
- clientId: this._clientId,
537
- connId: this._connId,
620
});
621
}
622
@@ -542,35 +624,20 @@
624
this._handleDataClose({ code, reason });
625
}
626
545
- // EventTarget implementation
546
- addEventListener(type, listener) {
547
- if (!this._listeners) {
548
- this._listeners = {};
549
- }
550
- if (!this._listeners[type]) {
551
- this._listeners[type] = [];
552
- }
553
- this._listeners[type].push(listener);
554
- }
555
-
556
- removeEventListener(type, listener) {
557
- if (!this._listeners || !this._listeners[type]) {
558
- return;
559
- }
560
- const index = this._listeners[type].indexOf(listener);
561
- if (index !== -1) {
562
- this._listeners[type].splice(index, 1);
563
- }
564
- }
565
-
627
+ // Override dispatchEvent to handle onXXX handlers (EventTarget provides addEventListener/removeEventListener)
628
dispatchEvent(event) {
567
- if (!this._listeners || !this._listeners[event.type]) {
568
- return true;
629
+ // Call onXXX handler first (matches native WebSocket behavior)
630
+ const handlerName = 'on' + event.type;
631
+ if (typeof this[handlerName] === 'function') {
632
+ try {
633
+ this[handlerName].call(this, event);
634
+ } catch (e) {
635
+ console.error('Error in event handler:', e);
636
+ }
637
}
570
- this._listeners[event.type].forEach((listener) => {
571
- listener.call(this, event);
572
- });
573
- return true;
638
+
639
+ // Use native EventTarget.dispatchEvent for event listeners
640
+ return super.dispatchEvent(event);
641
}
642
}
643
@@ -594,17 +661,17 @@
661
}
662
}
663
597
- // Replace WebSocket with polyfill
598
- window.WebSocket = function (url, protocols) {
664
+ // Replace WebSocket with a simple factory function (zero overhead after construction)
665
+ window.WebSocket = function WebSocket(url, protocols) {
666
// Use polyfill for same-origin, native for cross-origin
667
if (isSameOrigin(url)) {
601
- console.log(
668
+ debugLog(
669
"[WebSocket Polyfill] Using E2EE polyfill for same-origin connection:",
670
url
671
);
672
return new WebSocketPolyfill(url, protocols);
673
} else {
607
- console.log(
674
+ debugLog(
675
"[WebSocket Polyfill] Using native WebSocket for cross-origin connection:",
676
url
677
);
@@ -612,17 +679,18 @@
679
}
680
};
681
615
- // Copy static properties
616
- window.WebSocket.CONNECTING = NativeWebSocket.CONNECTING;
617
- window.WebSocket.OPEN = NativeWebSocket.OPEN;
618
- window.WebSocket.CLOSING = NativeWebSocket.CLOSING;
619
- window.WebSocket.CLOSED = NativeWebSocket.CLOSED;
682
+ // Copy static constants from WebSocketPolyfill
683
+ // Essential for: WebSocket.CONNECTING, WebSocket.OPEN, WebSocket.CLOSING, WebSocket.CLOSED
684
+ window.WebSocket.CONNECTING = WebSocketPolyfill.CONNECTING; // 0
685
+ window.WebSocket.OPEN = WebSocketPolyfill.OPEN; // 1
686
+ window.WebSocket.CLOSING = WebSocketPolyfill.CLOSING; // 2
687
+ window.WebSocket.CLOSED = WebSocketPolyfill.CLOSED; // 3
688
621
- console.log("[WebSocket Polyfill] Initialized with E2EE and WebSocket protocol support");
689
+ debugLog("[WebSocket Polyfill] Initialized with E2EE and WebSocket protocol support");
690
691
// Remove the polyfill script tag after initialization
692
if (currentScript && currentScript.parentNode) {
693
currentScript.parentNode.removeChild(currentScript);
626
- console.log("[WebSocket Polyfill] Script tag removed");
694
+ debugLog("[WebSocket Polyfill] Script tag removed");
695
}
696
})();