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