change polyfill for mocking websocket.
Hee Sung Son committed
Nov 7, 2025 at 12:11 UTC
9c331ffa535b1a88129ece5b1fa5098957aa5a12
3 files changed
+634
-210
cmd/webclient/main_js.go
+213
@@ -29,6 +29,10 @@ import (
29
30
var (
31
rdClient *sdk.RDClient
32
+
33
+ // SDK connection manager for Service Worker messaging
34
+ sdkConnections = make(map[string]io.ReadWriteCloser)
35
+ sdkConnectionsMu sync.RWMutex
36
)
37
38
// getBootstrapServers retrieves bootstrap servers from global JavaScript variable
@@ -604,6 +608,190 @@ func (p *Proxy) handleDisconnect(w http.ResponseWriter, r *http.Request, connID
608
w.WriteHeader(http.StatusOK)
609
}
610
611
+// SDK Connection handlers for Service Worker messaging
612
+
613
+func handleSDKConnect(data js.Value) {
614
+ leaseName := data.Get("leaseName").String()
615
+ clientId := data.Get("clientId").String()
616
+
617
+ log.Info().Str("leaseName", leaseName).Str("clientId", clientId).Msg("[SDK Connect] Connecting")
618
+
619
+ go func() {
620
+ // Lookup lease by name to get lease ID
621
+ lease, err := rdClient.LookupName(leaseName)
622
+ if err != nil {
623
+ log.Error().Err(err).Str("leaseName", leaseName).Msg("[SDK Connect] Lease lookup failed")
624
+ js.Global().Call("__sdk_post_message", map[string]interface{}{
625
+ "type": "SDK_CONNECT_ERROR",
626
+ "clientId": clientId,
627
+ "error": err.Error(),
628
+ })
629
+ return
630
+ }
631
+
632
+ leaseID := lease.GetIdentity().GetId()
633
+ log.Info().Str("leaseName", leaseName).Str("leaseID", leaseID).Msg("[SDK Connect] Lease found")
634
+
635
+ // Create E2EE connection using SDK with lease ID
636
+ cred := sdk.NewCredential()
637
+ conn, err := rdClient.Dial(cred, leaseID, "http/1.1")
638
+ if err != nil {
639
+ log.Error().Err(err).Str("leaseID", leaseID).Msg("[SDK Connect] Failed")
640
+
641
+ // Send error to client
642
+ js.Global().Call("__sdk_post_message", map[string]interface{}{
643
+ "type": "SDK_CONNECT_ERROR",
644
+ "clientId": clientId,
645
+ "error": err.Error(),
646
+ })
647
+ return
648
+ }
649
+
650
+ // Generate connection ID
651
+ connID := generateConnID()
652
+
653
+ // Store connection
654
+ sdkConnectionsMu.Lock()
655
+ sdkConnections[connID] = conn
656
+ sdkConnectionsMu.Unlock()
657
+
658
+ log.Info().Str("leaseName", leaseName).Str("connId", connID).Msg("[SDK Connect] Connected")
659
+
660
+ // Send success to client
661
+ js.Global().Call("__sdk_post_message", map[string]interface{}{
662
+ "type": "SDK_CONNECT_SUCCESS",
663
+ "clientId": clientId,
664
+ "connId": connID,
665
+ })
666
+
667
+ // Start reading from connection
668
+ go func() {
669
+ buffer := make([]byte, 32*1024)
670
+ for {
671
+ n, err := conn.Read(buffer)
672
+ if err != nil {
673
+ if err != io.EOF {
674
+ log.Error().Err(err).Str("connId", connID).Msg("[SDK Connect] Read error")
675
+ }
676
+
677
+ // Remove connection
678
+ sdkConnectionsMu.Lock()
679
+ delete(sdkConnections, connID)
680
+ sdkConnectionsMu.Unlock()
681
+
682
+ // Send close to client
683
+ code := 1000
684
+ if err != io.EOF {
685
+ code = 1006
686
+ }
687
+ js.Global().Call("__sdk_post_message", map[string]interface{}{
688
+ "type": "SDK_DATA_CLOSE",
689
+ "clientId": clientId,
690
+ "connId": connID,
691
+ "code": code,
692
+ })
693
+ return
694
+ }
695
+
696
+ // Copy data to JavaScript Uint8Array
697
+ data := make([]byte, n)
698
+ copy(data, buffer[:n])
699
+
700
+ uint8Array := js.Global().Get("Uint8Array").New(n)
701
+ js.CopyBytesToJS(uint8Array, data)
702
+
703
+ // Send data to client
704
+ js.Global().Call("__sdk_post_message", map[string]interface{}{
705
+ "type": "SDK_DATA",
706
+ "clientId": clientId,
707
+ "connId": connID,
708
+ "data": uint8Array,
709
+ })
710
+ }
711
+ }()
712
+ }()
713
+}
714
+
715
+func handleSDKSend(data js.Value) {
716
+ connID := data.Get("connId").String()
717
+ clientId := data.Get("clientId").String()
718
+ payload := data.Get("data")
719
+
720
+ // Get connection
721
+ sdkConnectionsMu.RLock()
722
+ conn, ok := sdkConnections[connID]
723
+ sdkConnectionsMu.RUnlock()
724
+
725
+ if !ok {
726
+ log.Warn().Str("connId", connID).Msg("[SDK Send] Connection not found")
727
+ js.Global().Call("__sdk_post_message", map[string]interface{}{
728
+ "type": "SDK_SEND_ERROR",
729
+ "clientId": clientId,
730
+ "connId": connID,
731
+ "error": "connection not found",
732
+ })
733
+ return
734
+ }
735
+
736
+ // Convert payload to bytes
737
+ var bytes []byte
738
+ if payload.InstanceOf(js.Global().Get("Uint8Array")) {
739
+ length := payload.Get("length").Int()
740
+ bytes = make([]byte, length)
741
+ js.CopyBytesToGo(bytes, payload)
742
+ } else if payload.InstanceOf(js.Global().Get("ArrayBuffer")) {
743
+ uint8Array := js.Global().Get("Uint8Array").New(payload)
744
+ length := uint8Array.Get("length").Int()
745
+ bytes = make([]byte, length)
746
+ js.CopyBytesToGo(bytes, uint8Array)
747
+ } else {
748
+ log.Warn().Str("connId", connID).Msg("[SDK Send] Unsupported data type")
749
+ return
750
+ }
751
+
752
+ go func() {
753
+ _, err := conn.Write(bytes)
754
+ if err != nil {
755
+ log.Error().Err(err).Str("connId", connID).Msg("[SDK Send] Write failed")
756
+ js.Global().Call("__sdk_post_message", map[string]interface{}{
757
+ "type": "SDK_SEND_ERROR",
758
+ "clientId": clientId,
759
+ "connId": connID,
760
+ "error": err.Error(),
761
+ })
762
+ }
763
+ }()
764
+}
765
+
766
+func handleSDKClose(data js.Value) {
767
+ connID := data.Get("connId").String()
768
+ clientId := data.Get("clientId").String()
769
+
770
+ // Get and remove connection
771
+ sdkConnectionsMu.Lock()
772
+ conn, ok := sdkConnections[connID]
773
+ if ok {
774
+ delete(sdkConnections, connID)
775
+ }
776
+ sdkConnectionsMu.Unlock()
777
+
778
+ if !ok {
779
+ log.Warn().Str("connId", connID).Msg("[SDK Close] Connection not found")
780
+ return
781
+ }
782
+
783
+ log.Info().Str("connId", connID).Msg("[SDK Close] Closing connection")
784
+ conn.Close()
785
+
786
+ // Send close confirmation to client
787
+ js.Global().Call("__sdk_post_message", map[string]interface{}{
788
+ "type": "SDK_DATA_CLOSE",
789
+ "clientId": clientId,
790
+ "connId": connID,
791
+ "code": 1000,
792
+ })
793
+}
794
+
795
func main() {
796
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339})
797
var err error
@@ -640,6 +828,31 @@ func main() {
828
}))
829
log.Info().Msg("Portal proxy handler registered as __go_jshttp")
830
831
+ // Expose SDK connection handler for Service Worker messaging
832
+ js.Global().Set("__sdk_message_handler", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
833
+ if len(args) < 2 {
834
+ log.Warn().Msg("[SDK Message] Invalid arguments")
835
+ return nil
836
+ }
837
+
838
+ messageType := args[0].String()
839
+ data := args[1]
840
+
841
+ switch messageType {
842
+ case "SDK_CONNECT":
843
+ handleSDKConnect(data)
844
+ case "SDK_SEND":
845
+ handleSDKSend(data)
846
+ case "SDK_CLOSE":
847
+ handleSDKClose(data)
848
+ default:
849
+ log.Warn().Str("type", messageType).Msg("[SDK Message] Unknown message type")
850
+ }
851
+
852
+ return nil
853
+ }))
854
+ log.Info().Msg("SDK message handler registered as __sdk_message_handler")
855
+
856
if runtime.Compiler == "tinygo" {
857
return
858
}
cmd/webclient/polyfill.js
+398
-210
@@ -7,7 +7,19 @@
7
// Save original WebSocket
8
const NativeWebSocket = window.WebSocket;
9
10
- // WebSocket polyfill using HTTP
10
+ // Generate unique client ID
11
+ function generateClientId() {
12
+ return `client-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
13
+ }
14
+
15
+ // Generate WebSocket key for handshake
16
+ function generateWebSocketKey() {
17
+ const bytes = new Uint8Array(16);
18
+ crypto.getRandomValues(bytes);
19
+ return btoa(String.fromCharCode(...bytes));
20
+ }
21
+
22
+ // WebSocket polyfill using Service Worker E2EE
23
class WebSocketPolyfill {
24
constructor(url, protocols) {
25
this.url = url;
@@ -25,184 +37,351 @@
37
this.onclose = null;
38
39
// Internal state
40
+ this._clientId = generateClientId();
41
this._connId = null;
29
- this._sendQueue = [];
30
- this._isSending = false;
31
- this._pollInterval = null;
42
this._isClosed = false;
43
+ this._wsKey = generateWebSocketKey();
44
+
45
+ // Setup Service Worker message listener
46
+ this._setupMessageListener();
47
48
// Initialize connection
49
this._connect();
50
}
51
52
+ _setupMessageListener() {
53
+ navigator.serviceWorker.addEventListener("message", (event) => {
54
+ const data = event.data;
55
+
56
+ // Only handle messages for this client
57
+ if (data.clientId !== this._clientId) {
58
+ return;
59
+ }
60
+
61
+ switch (data.type) {
62
+ case "SDK_CONNECT_SUCCESS":
63
+ this._handleConnectSuccess(data);
64
+ break;
65
+ case "SDK_CONNECT_ERROR":
66
+ this._handleConnectError(data);
67
+ break;
68
+ case "SDK_DATA":
69
+ this._handleData(data);
70
+ break;
71
+ case "SDK_DATA_CLOSE":
72
+ this._handleDataClose(data);
73
+ break;
74
+ case "SDK_SEND_ERROR":
75
+ this._handleSendError(data);
76
+ break;
77
+ }
78
+ });
79
+ }
80
+
81
async _connect() {
39
- console.log("[WebSocket Polyfill] Connecting to:", this.url);
82
+ console.log("[WebSocket Polyfill] Connecting via Service Worker SDK to:", this.url);
83
try {
41
- // Send connect request
42
- const response = await fetch("/sw-cgi/websocket/connect", {
43
- method: "POST",
44
- headers: {
45
- "Content-Type": "application/json",
46
- },
47
- body: JSON.stringify({
48
- url: this.url,
49
- protocols: Array.isArray(this.protocols)
50
- ? this.protocols
51
- : this.protocols
52
- ? [this.protocols]
53
- : [],
54
- }),
84
+ // Extract hostname from URL
85
+ const urlObj = new URL(this.url);
86
+ const hostname = urlObj.hostname;
87
+ const leaseName = hostname.split('.')[0].toUpperCase();
88
+
89
+ console.log("[WebSocket Polyfill] Lease name:", leaseName);
90
+
91
+ // Wait for Service Worker to be ready
92
+ await navigator.serviceWorker.ready;
93
+
94
+ // Send connect message to Service Worker
95
+ navigator.serviceWorker.controller.postMessage({
96
+ type: "SDK_CONNECT",
97
+ clientId: this._clientId,
98
+ leaseName: leaseName,
99
});
100
57
- if (!response.ok) {
58
- throw new Error(
59
- `Connection failed: ${response.status} ${response.statusText}`
60
- );
101
+ } catch (error) {
102
+ console.error("[WebSocket Polyfill] Failed to connect:", error);
103
+ this._handleError(new Error(error));
104
+ }
105
+ }
106
+
107
+ _handleConnectSuccess(data) {
108
+ this._connId = data.connId;
109
+
110
+ console.log("[WebSocket Polyfill] E2EE connection established, sending WebSocket upgrade");
111
+
112
+ // Send WebSocket HTTP Upgrade request
113
+ this._sendWebSocketUpgrade();
114
+ }
115
+
116
+ _sendWebSocketUpgrade() {
117
+ // Parse URL to get path
118
+ const urlObj = new URL(this.url);
119
+ const path = urlObj.pathname || "/";
120
+ const host = urlObj.host;
121
+
122
+ // Build HTTP Upgrade request
123
+ let upgradeRequest = `GET ${path} HTTP/1.1\r\n`;
124
+ upgradeRequest += `Host: ${host}\r\n`;
125
+ upgradeRequest += `Upgrade: websocket\r\n`;
126
+ upgradeRequest += `Connection: Upgrade\r\n`;
127
+ upgradeRequest += `Sec-WebSocket-Key: ${this._wsKey}\r\n`;
128
+ upgradeRequest += `Sec-WebSocket-Version: 13\r\n`;
129
+
130
+ if (this.protocols) {
131
+ const protocolStr = Array.isArray(this.protocols)
132
+ ? this.protocols.join(', ')
133
+ : this.protocols;
134
+ upgradeRequest += `Sec-WebSocket-Protocol: ${protocolStr}\r\n`;
135
+ }
136
+
137
+ upgradeRequest += `\r\n`;
138
+
139
+ console.log("[WebSocket Polyfill] Sending upgrade request:", upgradeRequest);
140
+
141
+ // Convert to bytes and send
142
+ const encoder = new TextEncoder();
143
+ const bytes = encoder.encode(upgradeRequest);
144
+
145
+ navigator.serviceWorker.controller.postMessage({
146
+ type: "SDK_SEND",
147
+ clientId: this._clientId,
148
+ connId: this._connId,
149
+ data: bytes,
150
+ });
151
+
152
+ // Wait for upgrade response in _handleData
153
+ this._waitingForUpgrade = true;
154
+ this._upgradeBuffer = new Uint8Array(0);
155
+ }
156
+
157
+ _handleConnectError(data) {
158
+ console.error("[WebSocket Polyfill] Connection error:", data.error);
159
+ this._handleError(new Error(data.error));
160
+ }
161
+
162
+ _handleData(data) {
163
+ const uint8Array = data.data;
164
+
165
+ // If waiting for upgrade response, buffer and parse HTTP response
166
+ if (this._waitingForUpgrade) {
167
+ // Append to buffer
168
+ const newBuffer = new Uint8Array(this._upgradeBuffer.length + uint8Array.length);
169
+ newBuffer.set(this._upgradeBuffer);
170
+ newBuffer.set(uint8Array, this._upgradeBuffer.length);
171
+ this._upgradeBuffer = newBuffer;
172
+
173
+ // Try to parse HTTP response
174
+ const decoder = new TextDecoder();
175
+ const text = decoder.decode(this._upgradeBuffer);
176
+
177
+ // Look for end of HTTP headers (\r\n\r\n)
178
+ const headerEndIndex = text.indexOf('\r\n\r\n');
179
+ if (headerEndIndex === -1) {
180
+ // Not complete yet, keep buffering
181
+ return;
182
}
183
63
- const result = await response.json();
64
- this._connId = result.connId;
65
- this.protocol = result.protocol || "";
184
+ // Parse HTTP response
185
+ const headers = text.substring(0, headerEndIndex);
186
+ console.log("[WebSocket Polyfill] Received upgrade response:", headers);
187
67
- console.log(
68
- "[WebSocket Polyfill] Connected successfully, connId:",
69
- this._connId
70
- );
188
+ // Check if upgrade was successful
189
+ if (!headers.includes('HTTP/1.1 101') && !headers.includes('HTTP/1.0 101')) {
190
+ this._handleError(new Error("WebSocket upgrade failed: " + headers.split('\r\n')[0]));
191
+ return;
192
+ }
193
+
194
+ // Extract protocol if present
195
+ const protocolMatch = headers.match(/Sec-WebSocket-Protocol:\s*(\S+)/i);
196
+ if (protocolMatch) {
197
+ this.protocol = protocolMatch[1];
198
+ }
199
72
- // Update state
200
+ // Upgrade successful!
201
+ this._waitingForUpgrade = false;
202
this.readyState = WebSocket.OPEN;
203
204
+ console.log("[WebSocket Polyfill] WebSocket connection established");
205
+
206
// Fire onopen event
76
- console.log("[WebSocket Polyfill] Dispatching open event");
207
if (this.onopen) {
208
this.onopen(new Event("open"));
209
}
210
this.dispatchEvent(new Event("open"));
211
82
- // Start polling for messages
83
- this._startPolling();
84
- } catch (error) {
85
- this._handleError(error);
212
+ // If there's any data after the headers, process it as WebSocket frames
213
+ const remainingBytes = this._upgradeBuffer.slice(headerEndIndex + 4);
214
+ if (remainingBytes.length > 0) {
215
+ this._processWebSocketFrames(remainingBytes);
216
+ }
217
+ this._upgradeBuffer = null;
218
+
219
+ return;
220
}
87
- }
221
89
- async _startPolling() {
90
- console.log("[WebSocket Polyfill] Starting polling loop");
91
- // Long polling: continuously fetch messages
92
- // Server will wait up to 5 seconds before responding
93
- while (!this._isClosed) {
94
- try {
95
- const response = await fetch(
96
- `/sw-cgi/websocket/poll/${this._connId}`,
97
- {
98
- method: "GET",
99
- }
100
- );
101
-
102
- if (!response.ok) {
103
- this._handleClose(1006, "abnormal closure");
104
- throw new Error(`Poll failed: ${response.status}`);
105
- }
222
+ // Normal WebSocket data - process frames
223
+ this._processWebSocketFrames(uint8Array);
224
+ }
225
107
- const result = await response.json();
226
+ _processWebSocketFrames(data) {
227
+ // For now, assume data is the payload (we'll implement frame parsing if needed)
228
+ // WebSocket frames from server are not masked
229
109
- if (result.messages && Array.isArray(result.messages)) {
110
- for (const message of result.messages) {
111
- this._handleMessage(message);
112
- // Stop processing messages after close
113
- if (this._isClosed) {
114
- break;
115
- }
116
- }
117
- }
230
+ if (this.readyState !== WebSocket.OPEN) return;
231
119
- // Exit polling loop if connection was closed
120
- if (this._isClosed) {
121
- break;
122
- }
123
- } catch (error) {
124
- if (!this._isClosed) {
125
- console.error("Polling error:", error);
126
- this._handleError(error);
127
- }
128
- break;
129
- }
232
+ // Simple frame parsing - check if this is a complete frame
233
+ if (data.length < 2) return;
234
+
235
+ const byte1 = data[0];
236
+ const byte2 = data[1];
237
+
238
+ const fin = (byte1 & 0x80) !== 0;
239
+ const opcode = byte1 & 0x0F;
240
+ const masked = (byte2 & 0x80) !== 0;
241
+ let payloadLen = byte2 & 0x7F;
242
+
243
+ let offset = 2;
244
+
245
+ // Handle extended payload length
246
+ if (payloadLen === 126) {
247
+ if (data.length < 4) return; // Need more data
248
+ payloadLen = (data[2] << 8) | data[3];
249
+ offset = 4;
250
+ } else if (payloadLen === 127) {
251
+ if (data.length < 10) return; // Need more data
252
+ // For simplicity, assuming payload < 2^32
253
+ payloadLen = (data[6] << 24) | (data[7] << 16) | (data[8] << 8) | data[9];
254
+ offset = 10;
255
}
131
- }
256
133
- _handleMessage(message) {
134
- if (this.readyState !== WebSocket.OPEN) return;
257
+ // Server messages should not be masked
258
+ if (masked) {
259
+ offset += 4; // Skip mask key
260
+ }
261
136
- if (message.type === "close") {
137
- console.log(
138
- "[WebSocket Polyfill] Received close message, code:",
139
- message.code,
140
- "reason:",
141
- message.reason
142
- );
143
- this._handleClose(message.code || 1000, message.reason || "");
262
+ if (data.length < offset + payloadLen) {
263
+ // Incomplete frame, buffer it
264
+ // TODO: Implement frame buffering
265
return;
266
}
267
147
- // Decode data from base64
148
- let data;
149
- try {
150
- const binaryString = atob(message.data);
151
- const bytes = new Uint8Array(binaryString.length);
152
- for (let i = 0; i < binaryString.length; i++) {
153
- bytes[i] = binaryString.charCodeAt(i);
154
- }
268
+ const payload = data.slice(offset, offset + payloadLen);
269
156
- // Use messageType from server to determine if text or binary
157
- if (message.messageType === "text") {
158
- // Decode as text
159
- const decoder = new TextDecoder("utf-8");
160
- data = decoder.decode(bytes);
270
+ // Handle different opcodes
271
+ if (opcode === 0x01) {
272
+ // Text frame
273
+ const text = new TextDecoder().decode(payload);
274
+ const event = new MessageEvent("message", {
275
+ data: text,
276
+ origin: new URL(this.url).origin,
277
+ });
278
+ if (this.onmessage) {
279
+ this.onmessage(event);
280
+ }
281
+ this.dispatchEvent(event);
282
+ } else if (opcode === 0x02) {
283
+ // Binary frame
284
+ let eventData;
285
+ if (this.binaryType === "blob") {
286
+ eventData = new Blob([payload]);
287
} else {
162
- // Binary message - respect binaryType setting
163
- if (this.binaryType === "blob") {
164
- data = new Blob([bytes]);
165
- } else {
166
- data = bytes.buffer;
288
+ eventData = payload.buffer;
289
+ }
290
+ const event = new MessageEvent("message", {
291
+ data: eventData,
292
+ origin: new URL(this.url).origin,
293
+ });
294
+ if (this.onmessage) {
295
+ this.onmessage(event);
296
+ }
297
+ this.dispatchEvent(event);
298
+ } else if (opcode === 0x08) {
299
+ // Close frame
300
+ let code = 1000;
301
+ let reason = "";
302
+ if (payload.length >= 2) {
303
+ code = (payload[0] << 8) | payload[1];
304
+ if (payload.length > 2) {
305
+ reason = new TextDecoder().decode(payload.slice(2));
306
}
307
}
169
- } catch (e) {
170
- console.error("Failed to decode message:", e);
171
- return;
308
+ this._handleDataClose({ code, reason });
309
+ } else if (opcode === 0x09) {
310
+ // Ping - send pong
311
+ this._sendPong(payload);
312
+ } else if (opcode === 0x0A) {
313
+ // Pong - ignore
314
}
315
+ }
316
174
- // Create MessageEvent
175
- const event = new MessageEvent("message", {
176
- data: data,
177
- origin: new URL(this.url).origin,
317
+ _sendPong(payload) {
318
+ // Send pong frame
319
+ const frame = this._createWebSocketFrame(0x0A, payload);
320
+ navigator.serviceWorker.controller.postMessage({
321
+ type: "SDK_SEND",
322
+ clientId: this._clientId,
323
+ connId: this._connId,
324
+ data: frame,
325
});
179
-
180
- if (this.onmessage) {
181
- this.onmessage(event);
182
- }
183
- this.dispatchEvent(event);
326
}
327
186
- _handleError(error) {
187
- console.error("[WebSocket Polyfill] Error occurred:", error);
328
+ _createWebSocketFrame(opcode, payload) {
329
+ // Create WebSocket frame (client to server, must be masked)
330
+ const payloadLen = payload.length;
331
+ let frameHeader;
332
+ let offset;
333
+
334
+ if (payloadLen < 126) {
335
+ frameHeader = new Uint8Array(2 + 4 + payloadLen);
336
+ frameHeader[0] = 0x80 | opcode; // FIN + opcode
337
+ frameHeader[1] = 0x80 | payloadLen; // MASK + length
338
+ offset = 2;
339
+ } else if (payloadLen < 65536) {
340
+ frameHeader = new Uint8Array(4 + 4 + payloadLen);
341
+ frameHeader[0] = 0x80 | opcode;
342
+ frameHeader[1] = 0x80 | 126;
343
+ frameHeader[2] = (payloadLen >> 8) & 0xFF;
344
+ frameHeader[3] = payloadLen & 0xFF;
345
+ offset = 4;
346
+ } else {
347
+ frameHeader = new Uint8Array(10 + 4 + payloadLen);
348
+ frameHeader[0] = 0x80 | opcode;
349
+ frameHeader[1] = 0x80 | 127;
350
+ // Simplified: assuming payload < 2^32
351
+ frameHeader[2] = 0;
352
+ frameHeader[3] = 0;
353
+ frameHeader[4] = 0;
354
+ frameHeader[5] = 0;
355
+ frameHeader[6] = (payloadLen >> 24) & 0xFF;
356
+ frameHeader[7] = (payloadLen >> 16) & 0xFF;
357
+ frameHeader[8] = (payloadLen >> 8) & 0xFF;
358
+ frameHeader[9] = payloadLen & 0xFF;
359
+ offset = 10;
360
+ }
361
189
- const event = new Event("error");
190
- event.error = error;
362
+ // Generate masking key
363
+ const maskKey = new Uint8Array(4);
364
+ crypto.getRandomValues(maskKey);
365
+ frameHeader.set(maskKey, offset);
366
192
- if (this.onerror) {
193
- this.onerror(event);
367
+ // Mask payload
368
+ const maskedPayload = new Uint8Array(payloadLen);
369
+ for (let i = 0; i < payloadLen; i++) {
370
+ maskedPayload[i] = payload[i] ^ maskKey[i % 4];
371
}
195
- this.dispatchEvent(event);
372
197
- // Close connection after error
198
- this._handleClose(1006, error.message);
373
+ frameHeader.set(maskedPayload, offset + 4);
374
+ return frameHeader;
375
}
376
201
- _handleClose(code, reason) {
377
+ _handleDataClose(data) {
378
if (this._isClosed) return;
379
380
+ const code = data.code || 1000;
381
+ const reason = data.reason || "";
382
+
383
console.log(
205
- "[WebSocket Polyfill] Closing connection, code:",
384
+ "[WebSocket Polyfill] Connection closed, code:",
385
code,
386
"reason:",
387
reason
@@ -211,100 +390,95 @@
390
this._isClosed = true;
391
this.readyState = WebSocket.CLOSED;
392
214
- // Notify server about disconnection
215
- if (this._connId) {
216
- console.log(
217
- "[WebSocket Polyfill] Notifying server about disconnection"
218
- );
219
- fetch(`/sw-cgi/websocket/disconnect/${this._connId}`, {
220
- method: "POST",
221
- }).catch((err) => {
222
- console.error("Failed to notify server about disconnection:", err);
223
- });
224
- }
225
-
226
- // Create CloseEvent
393
const event = new CloseEvent("close", {
394
code: code,
395
reason: reason,
396
wasClean: code === 1000,
397
});
398
233
- console.log(
234
- "[WebSocket Polyfill] Dispatching close event, wasClean:",
235
- code === 1000
236
- );
399
if (this.onclose) {
400
this.onclose(event);
401
}
402
this.dispatchEvent(event);
403
}
404
243
- async send(data) {
244
- if (this.readyState !== WebSocket.OPEN) {
245
- throw new Error("WebSocket is not open");
246
- }
405
+ _handleSendError(data) {
406
+ console.error("[WebSocket Polyfill] Send error:", data.error);
407
+ this._handleError(new Error(data.error));
408
+ }
409
248
- // Add to queue
249
- this._sendQueue.push(data);
410
+ _handleError(error) {
411
+ console.error("[WebSocket Polyfill] Error occurred:", error);
412
251
- // Process queue
252
- this._processSendQueue();
253
- }
413
+ const event = new Event("error");
414
+ event.error = error;
415
255
- async _processSendQueue() {
256
- // Ensure only one send operation at a time
257
- if (this._isSending || this._sendQueue.length === 0) {
258
- return;
416
+ if (this.onerror) {
417
+ this.onerror(event);
418
}
419
+ this.dispatchEvent(event);
420
261
- this._isSending = true;
421
+ // Close connection after error
422
+ if (!this._isClosed) {
423
+ this._handleDataClose({ code: 1006, reason: error.message });
424
+ }
425
+ }
426
263
- while (this._sendQueue.length > 0 && !this._isClosed) {
264
- const data = this._sendQueue.shift();
427
+ send(data) {
428
+ if (this.readyState !== WebSocket.OPEN) {
429
+ throw new Error("WebSocket is not open");
430
+ }
431
266
- try {
267
- let payload;
432
+ if (!this._connId) {
433
+ throw new Error("Connection not established");
434
+ }
435
269
- if (typeof data === "string") {
270
- payload = JSON.stringify({ type: "text", data: data });
271
- } else if (data instanceof ArrayBuffer) {
272
- // Convert ArrayBuffer to base64
273
- const bytes = new Uint8Array(data);
274
- const base64 = btoa(String.fromCharCode(...bytes));
275
- payload = JSON.stringify({ type: "binary", data: base64 });
276
- } else if (data instanceof Blob) {
277
- // Convert Blob to base64
278
- const arrayBuffer = await data.arrayBuffer();
436
+ try {
437
+ // Convert data to Uint8Array
438
+ let bytes;
439
+ let opcode;
440
+
441
+ if (typeof data === "string") {
442
+ const encoder = new TextEncoder();
443
+ bytes = encoder.encode(data);
444
+ opcode = 0x01; // Text frame
445
+ } else if (data instanceof ArrayBuffer) {
446
+ bytes = new Uint8Array(data);
447
+ opcode = 0x02; // Binary frame
448
+ } else if (data instanceof Uint8Array) {
449
+ bytes = data;
450
+ opcode = 0x02; // Binary frame
451
+ } else if (data instanceof Blob) {
452
+ // Handle Blob asynchronously
453
+ data.arrayBuffer().then(arrayBuffer => {
454
const bytes = new Uint8Array(arrayBuffer);
280
- const base64 = btoa(String.fromCharCode(...bytes));
281
- payload = JSON.stringify({ type: "binary", data: base64 });
282
- } else {
283
- throw new Error("Unsupported data type");
284
- }
285
-
286
- const response = await fetch(
287
- `/sw-cgi/websocket/send/${this._connId}`,
288
- {
289
- method: "POST",
290
- headers: {
291
- "Content-Type": "application/json",
292
- },
293
- body: payload,
294
- }
295
- );
296
-
297
- if (!response.ok) {
298
- throw new Error(`Send failed: ${response.status}`);
299
- }
300
- } catch (error) {
301
- console.error("Failed to send message:", error);
302
- this._handleError(error);
303
- break;
455
+ const frame = this._createWebSocketFrame(0x02, bytes);
456
+ navigator.serviceWorker.controller.postMessage({
457
+ type: "SDK_SEND",
458
+ clientId: this._clientId,
459
+ connId: this._connId,
460
+ data: frame,
461
+ });
462
+ });
463
+ return;
464
+ } else {
465
+ throw new Error("Unsupported data type");
466
}
305
- }
467
307
- this._isSending = false;
468
+ // Create WebSocket frame
469
+ const frame = this._createWebSocketFrame(opcode, bytes);
470
+
471
+ // Send to Service Worker
472
+ navigator.serviceWorker.controller.postMessage({
473
+ type: "SDK_SEND",
474
+ clientId: this._clientId,
475
+ connId: this._connId,
476
+ data: frame,
477
+ });
478
+ } catch (error) {
479
+ console.error("[WebSocket Polyfill] Failed to send message:", error);
480
+ this._handleError(error);
481
+ }
482
}
483
484
close(code = 1000, reason = "") {
@@ -321,21 +495,35 @@
495
496
this.readyState = WebSocket.CLOSING;
497
324
- // Send close request
498
+ if (this._connId && this.readyState === WebSocket.OPEN) {
499
+ // Send WebSocket close frame
500
+ const reasonBytes = new TextEncoder().encode(reason);
501
+ const payload = new Uint8Array(2 + reasonBytes.length);
502
+ payload[0] = (code >> 8) & 0xFF;
503
+ payload[1] = code & 0xFF;
504
+ payload.set(reasonBytes, 2);
505
+
506
+ const frame = this._createWebSocketFrame(0x08, payload);
507
+
508
+ navigator.serviceWorker.controller.postMessage({
509
+ type: "SDK_SEND",
510
+ clientId: this._clientId,
511
+ connId: this._connId,
512
+ data: frame,
513
+ });
514
+ }
515
+
516
+ // Close SDK connection
517
if (this._connId) {
326
- fetch(`/sw-cgi/websocket/send/${this._connId}`, {
327
- method: "POST",
328
- headers: {
329
- "Content-Type": "application/json",
330
- },
331
- body: JSON.stringify({ type: "close", code: code, reason: reason }),
332
- }).catch((err) => {
333
- console.error("Failed to send close frame:", err);
518
+ navigator.serviceWorker.controller.postMessage({
519
+ type: "SDK_CLOSE",
520
+ clientId: this._clientId,
521
+ connId: this._connId,
522
});
523
}
524
525
// Handle close locally
338
- this._handleClose(code, reason);
526
+ this._handleDataClose({ code, reason });
527
}
528
529
// EventTarget implementation
@@ -395,7 +583,7 @@
583
// Use polyfill for same-origin, native for cross-origin
584
if (isSameOrigin(url)) {
585
console.log(
398
- "[WebSocket Polyfill] Using HTTP-based polyfill for same-origin connection:",
586
+ "[WebSocket Polyfill] Using E2EE polyfill for same-origin connection:",
587
url
588
);
589
return new WebSocketPolyfill(url, protocols);
@@ -414,7 +602,7 @@
602
window.WebSocket.CLOSING = NativeWebSocket.CLOSING;
603
window.WebSocket.CLOSED = NativeWebSocket.CLOSED;
604
417
- console.log("[WebSocket Polyfill] Initialized");
605
+ console.log("[WebSocket Polyfill] Initialized with E2EE and WebSocket protocol support");
606
607
// Remove the polyfill script tag after initialization
608
if (currentScript && currentScript.parentNode) {
cmd/webclient/service-worker.js
+23
@@ -124,6 +124,17 @@ self.addEventListener("activate", (e) => {
124
);
125
});
126
127
+// Helper function to broadcast message to all clients
128
+async function broadcastToClients(message) {
129
+ const clients = await self.clients.matchAll();
130
+ clients.forEach((client) => {
131
+ client.postMessage(message);
132
+ });
133
+}
134
+
135
+// Expose to WASM
136
+self.__sdk_post_message = broadcastToClients;
137
+
138
self.addEventListener("message", (event) => {
139
if (event.data && event.data.type === "CLAIM_CLIENTS") {
140
self.clients
@@ -138,6 +149,18 @@ self.addEventListener("message", (event) => {
149
.catch((error) => {
150
console.error("[SW] Manual clients.claim() failed:", error);
151
});
152
+ return;
153
+ }
154
+
155
+ // Handle SDK messages (SDK_CONNECT, SDK_SEND, SDK_CLOSE)
156
+ if (event.data && event.data.type && event.data.type.startsWith("SDK_")) {
157
+ if (typeof __sdk_message_handler === "undefined") {
158
+ console.error("[SW] SDK message handler not available");
159
+ return;
160
+ }
161
+
162
+ // Call WASM message handler
163
+ __sdk_message_handler(event.data.type, event.data);
164
}
165
});
166