fix: wasm for websocket communication.
Hee Sung Son committed
Oct 29, 2025 at 14:30 UTC
237a75bf44ad199c0ac3c2329d5a03f2ac7b0882
15 files changed
+2773
-185
Makefile
+3
@@ -16,6 +16,9 @@ build-wasm:
16
@echo "[wasm] copying service workers and E2EE proxy files..."
17
cp relaydns/wasm/sw-proxy.js cmd/relay-server/wasm/
18
cp relaydns/wasm/sw.js cmd/relay-server/wasm/
19
+ @echo "[wasm] copying SecureWebSocket E2EE files..."
20
+ cp relaydns/wasm/secure-websocket.js cmd/relay-server/wasm/
21
+ cp relaydns/wasm/secure-websocket-sw.js cmd/relay-server/wasm/
22
23
# Build Go relay server (embeds WASM from cmd/relay-server/wasm)
24
build-server:
cmd/relay-server/view.go
+26
-1
@@ -193,7 +193,32 @@ func serveHTTP(ctx context.Context, addr string, serv *relaydns.RelayServer, nod
193
}
194
})
195
196
- // Note: removed /api/leases; admin renders server-side for simplicity
196
+ // API: Get relay server info for E2EE WebSocket
197
+ mux.HandleFunc("/api/relay-info", func(w http.ResponseWriter, r *http.Request) {
198
+ // Build relay WebSocket URL from current request
199
+ scheme := "ws"
200
+ if r.TLS != nil {
201
+ scheme = "wss"
202
+ }
203
+ // Check X-Forwarded-Proto header (for reverse proxy)
204
+ if proto := r.Header.Get("X-Forwarded-Proto"); proto == "https" {
205
+ scheme = "wss"
206
+ }
207
+
208
+ relayUrl := fmt.Sprintf("%s://%s/relay", scheme, r.Host)
209
+
210
+ type relayInfo struct {
211
+ RelayUrl string `json:"relayUrl"`
212
+ NodeID string `json:"nodeId"`
213
+ }
214
+ resp := relayInfo{
215
+ RelayUrl: relayUrl,
216
+ NodeID: nodeID,
217
+ }
218
+ w.Header().Set("Content-Type", "application/json")
219
+ w.Header().Set("Access-Control-Allow-Origin", "*")
220
+ _ = json.NewEncoder(w).Encode(resp)
221
+ })
222
223
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
224
type info struct {
cmd/relay-server/wasm/relaydns_wasm.d.ts
+2
-2
@@ -183,10 +183,10 @@ export interface InitOutput {
183
readonly relayclient_getCredentialId: (a: number) => [number, number];
184
readonly relayclient_requestConnection: (a: number, b: number, c: number, d: number, e: number) => any;
185
readonly init: () => void;
186
- readonly wasm_bindgen__convert__closures_____invoke__hed2088bf6dd2c621: (a: number, b: number, c: any) => void;
187
- readonly wasm_bindgen__closure__destroy__h8eb17c158a55b496: (a: number, b: number) => void;
186
readonly wasm_bindgen__convert__closures_____invoke__h75a085a52d492ff4: (a: number, b: number, c: any) => void;
187
readonly wasm_bindgen__closure__destroy__h2dcad6e62f01cec1: (a: number, b: number) => void;
188
+ readonly wasm_bindgen__convert__closures_____invoke__hed2088bf6dd2c621: (a: number, b: number, c: any) => void;
189
+ readonly wasm_bindgen__closure__destroy__h8eb17c158a55b496: (a: number, b: number) => void;
190
readonly wasm_bindgen__convert__closures_____invoke__h404cda7fa36c69a6: (a: number, b: number, c: any, d: any) => void;
191
readonly __wbindgen_malloc: (a: number, b: number) => number;
192
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
cmd/relay-server/wasm/relaydns_wasm.js
+4
-4
@@ -243,14 +243,14 @@ export function init() {
243
wasm.init();
244
}
245
246
-function wasm_bindgen__convert__closures_____invoke__hed2088bf6dd2c621(arg0, arg1, arg2) {
247
- wasm.wasm_bindgen__convert__closures_____invoke__hed2088bf6dd2c621(arg0, arg1, arg2);
248
-}
249
-
246
function wasm_bindgen__convert__closures_____invoke__h75a085a52d492ff4(arg0, arg1, arg2) {
247
wasm.wasm_bindgen__convert__closures_____invoke__h75a085a52d492ff4(arg0, arg1, arg2);
248
}
249
250
+function wasm_bindgen__convert__closures_____invoke__hed2088bf6dd2c621(arg0, arg1, arg2) {
251
+ wasm.wasm_bindgen__convert__closures_____invoke__hed2088bf6dd2c621(arg0, arg1, arg2);
252
+}
253
+
254
function wasm_bindgen__convert__closures_____invoke__h404cda7fa36c69a6(arg0, arg1, arg2, arg3) {
255
wasm.wasm_bindgen__convert__closures_____invoke__h404cda7fa36c69a6(arg0, arg1, arg2, arg3);
256
}
cmd/relay-server/wasm/relaydns_wasm_bg.wasm
Binary files a/cmd/relay-server/wasm/relaydns_wasm_bg.wasm and b/cmd/relay-server/wasm/relaydns_wasm_bg.wasm differ
cmd/relay-server/wasm/relaydns_wasm_bg.wasm.d.ts
new
+50
@@ -0,0 +1,50 @@
1
+/* tslint:disable */
2
+/* eslint-disable */
3
+export const memory: WebAssembly.Memory;
4
+export const __wbg_httpadapter_free: (a: number, b: number) => void;
5
+export const httpadapter_new: (a: number, b: number) => number;
6
+export const httpadapter_get: (a: number, b: number, c: number) => any;
7
+export const httpadapter_postJson: (a: number, b: number, c: number, d: any) => any;
8
+export const httpadapter_uploadFile: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => any;
9
+export const httpadapter_downloadFile: (a: number, b: number, c: number) => any;
10
+export const __wbg_websocketadapter_free: (a: number, b: number) => void;
11
+export const websocketadapter_new: (a: number, b: number) => number;
12
+export const websocketadapter_connect: (a: number) => any;
13
+export const websocketadapter_onMessage: (a: number, b: any) => void;
14
+export const websocketadapter_onError: (a: number, b: any) => void;
15
+export const websocketadapter_sendText: (a: number, b: number, c: number) => [number, number];
16
+export const websocketadapter_sendBinary: (a: number, b: number, c: number) => [number, number];
17
+export const websocketadapter_close: (a: number) => [number, number];
18
+export const __wbg_datainterpreter_free: (a: number, b: number) => void;
19
+export const datainterpreter_parsePacket: (a: number, b: number) => [number, number, number];
20
+export const datainterpreter_createPacket: (a: any) => [number, number, number, number];
21
+export const __wbg_proxyengine_free: (a: number, b: number) => void;
22
+export const proxyengine_new: (a: number, b: number) => number;
23
+export const proxyengine_shouldIntercept: (a: number, b: number, c: number) => number;
24
+export const proxyengine_handleHttpRequest: (a: number, b: number, c: number, d: number, e: number, f: any, g: number, h: number) => any;
25
+export const proxyengine_openWebSocket: (a: number, b: number, c: number, d: number, e: number) => any;
26
+export const proxyengine_sendWebSocketMessage: (a: number, b: number, c: number, d: any, e: number) => any;
27
+export const proxyengine_receiveWebSocketMessage: (a: number, b: number, c: number) => any;
28
+export const proxyengine_closeWebSocket: (a: number, b: number, c: number, d: number, e: number, f: number) => any;
29
+export const proxyengine_connectTcp: (a: number, b: number, c: number, d: number) => any;
30
+export const proxyengine_getStatus: (a: number) => any;
31
+export const __wbg_relayclient_free: (a: number, b: number) => void;
32
+export const relayclient_connect: (a: number, b: number) => any;
33
+export const relayclient_getRelayInfo: (a: number) => any;
34
+export const relayclient_registerLease: (a: number, b: number, c: number, d: number, e: number) => any;
35
+export const relayclient_getCredentialId: (a: number) => [number, number];
36
+export const relayclient_requestConnection: (a: number, b: number, c: number, d: number, e: number) => any;
37
+export const init: () => void;
38
+export const wasm_bindgen__convert__closures_____invoke__h75a085a52d492ff4: (a: number, b: number, c: any) => void;
39
+export const wasm_bindgen__closure__destroy__h2dcad6e62f01cec1: (a: number, b: number) => void;
40
+export const wasm_bindgen__convert__closures_____invoke__hed2088bf6dd2c621: (a: number, b: number, c: any) => void;
41
+export const wasm_bindgen__closure__destroy__h8eb17c158a55b496: (a: number, b: number) => void;
42
+export const wasm_bindgen__convert__closures_____invoke__h404cda7fa36c69a6: (a: number, b: number, c: any, d: any) => void;
43
+export const __wbindgen_malloc: (a: number, b: number) => number;
44
+export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
45
+export const __wbindgen_exn_store: (a: number) => void;
46
+export const __externref_table_alloc: () => number;
47
+export const __wbindgen_externrefs: WebAssembly.Table;
48
+export const __wbindgen_free: (a: number, b: number, c: number) => void;
49
+export const __externref_table_dealloc: (a: number) => void;
50
+export const __wbindgen_start: () => void;
cmd/relay-server/wasm/secure-websocket-sw.js
new
+283
@@ -0,0 +1,283 @@
1
+/**
2
+ * SecureWebSocket for Service Worker
3
+ *
4
+ * SecureWebSocket implementation for use in Service Worker
5
+ * Communicates with main thread via MessageChannel
6
+ */
7
+
8
+// Service Worker global ProxyEngine (initialized in sw-proxy.js)
9
+// proxyEngine and wasmReady are provided by sw-proxy.js
10
+
11
+/**
12
+ * WebSocket tunnel manager in Service Worker
13
+ */
14
+class ServiceWorkerWebSocketTunnel {
15
+ constructor() {
16
+ this.tunnels = new Map(); // tunnelId -> tunnel info
17
+ this.messageQueues = new Map(); // tunnelId -> message queue
18
+ this.clients = new Map(); // tunnelId -> clientId
19
+ }
20
+
21
+ /**
22
+ * Create WebSocket tunnel
23
+ */
24
+ async createTunnel(url, protocols, clientId) {
25
+ if (!wasmReady || !proxyEngine) {
26
+ throw new Error('WASM ProxyEngine not ready');
27
+ }
28
+
29
+ console.log('[SW-WebSocket] Creating tunnel:', url);
30
+
31
+ try {
32
+ // Open WebSocket tunnel via WASM ProxyEngine
33
+ const result = await proxyEngine.open_websocket(url, protocols || []);
34
+ const tunnelId = result.tunnelId;
35
+ const protocol = result.protocol || '';
36
+
37
+ console.log('[SW-WebSocket] Tunnel created:', tunnelId);
38
+
39
+ // Store tunnel information
40
+ this.tunnels.set(tunnelId, {
41
+ tunnelId,
42
+ url,
43
+ protocol,
44
+ state: 'open',
45
+ created: Date.now()
46
+ });
47
+
48
+ this.messageQueues.set(tunnelId, []);
49
+ this.clients.set(tunnelId, clientId);
50
+
51
+ // Start receiving messages in background
52
+ this._startReceiving(tunnelId);
53
+
54
+ return {
55
+ tunnelId,
56
+ protocol
57
+ };
58
+
59
+ } catch (error) {
60
+ console.error('[SW-WebSocket] Failed to create tunnel:', error);
61
+ throw error;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Background message receiving loop
67
+ */
68
+ async _startReceiving(tunnelId) {
69
+ console.log('[SW-WebSocket] Starting receive loop:', tunnelId);
70
+
71
+ try {
72
+ while (this.tunnels.has(tunnelId)) {
73
+ const tunnel = this.tunnels.get(tunnelId);
74
+ if (!tunnel || tunnel.state !== 'open') {
75
+ break;
76
+ }
77
+
78
+ // Receive message from WASM
79
+ const msg = await proxyEngine.receive_websocket_message(tunnelId);
80
+
81
+ console.log('[SW-WebSocket] Received message:', msg.type);
82
+
83
+ // Add to message queue
84
+ const queue = this.messageQueues.get(tunnelId);
85
+ if (queue) {
86
+ queue.push(msg);
87
+ }
88
+
89
+ // Notify client
90
+ this._notifyClient(tunnelId, msg);
91
+
92
+ // Handle close message
93
+ if (msg.type === 'close') {
94
+ console.log('[SW-WebSocket] Tunnel closed:', tunnelId);
95
+ this._closeTunnel(tunnelId);
96
+ break;
97
+ }
98
+ }
99
+
100
+ } catch (error) {
101
+ console.error('[SW-WebSocket] Receive loop error:', error);
102
+ this._closeTunnel(tunnelId, 1006, error.toString());
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Notify client of message
108
+ */
109
+ async _notifyClient(tunnelId, message) {
110
+ const clientId = this.clients.get(tunnelId);
111
+ if (!clientId) return;
112
+
113
+ try {
114
+ const client = await self.clients.get(clientId);
115
+ if (client) {
116
+ client.postMessage({
117
+ type: 'WEBSOCKET_MESSAGE',
118
+ tunnelId,
119
+ message
120
+ });
121
+ }
122
+ } catch (error) {
123
+ console.error('[SW-WebSocket] Failed to notify client:', error);
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Send message
129
+ */
130
+ async sendMessage(tunnelId, data, isBinary) {
131
+ if (!this.tunnels.has(tunnelId)) {
132
+ throw new Error('Tunnel not found: ' + tunnelId);
133
+ }
134
+
135
+ console.log('[SW-WebSocket] Sending message:', tunnelId, isBinary ? 'binary' : 'text');
136
+
137
+ try {
138
+ await proxyEngine.send_websocket_message(tunnelId, data, isBinary);
139
+ } catch (error) {
140
+ console.error('[SW-WebSocket] Send failed:', error);
141
+ throw error;
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Close tunnel
147
+ */
148
+ async closeTunnel(tunnelId, code = 1000, reason = '') {
149
+ if (!this.tunnels.has(tunnelId)) {
150
+ return;
151
+ }
152
+
153
+ console.log('[SW-WebSocket] Closing tunnel:', tunnelId, code, reason);
154
+
155
+ try {
156
+ await proxyEngine.close_websocket(tunnelId, code, reason);
157
+ } catch (error) {
158
+ console.error('[SW-WebSocket] Close failed:', error);
159
+ }
160
+
161
+ this._closeTunnel(tunnelId);
162
+ }
163
+
164
+ /**
165
+ * Internal tunnel cleanup
166
+ */
167
+ _closeTunnel(tunnelId, code = 1000, reason = '') {
168
+ const tunnel = this.tunnels.get(tunnelId);
169
+ if (tunnel) {
170
+ tunnel.state = 'closed';
171
+ }
172
+
173
+ // Cleanup
174
+ this.tunnels.delete(tunnelId);
175
+ this.messageQueues.delete(tunnelId);
176
+ this.clients.delete(tunnelId);
177
+
178
+ console.log('[SW-WebSocket] Tunnel cleaned up:', tunnelId);
179
+ }
180
+
181
+ /**
182
+ * Get tunnel state
183
+ */
184
+ getTunnelState(tunnelId) {
185
+ const tunnel = this.tunnels.get(tunnelId);
186
+ return tunnel ? tunnel.state : 'closed';
187
+ }
188
+
189
+ /**
190
+ * Get all tunnel information
191
+ */
192
+ getAllTunnels() {
193
+ return Array.from(this.tunnels.values());
194
+ }
195
+}
196
+
197
+// Global tunnel manager instance
198
+let tunnelManager = null;
199
+
200
+/**
201
+ * Initialize tunnel manager
202
+ */
203
+function initTunnelManager() {
204
+ if (!tunnelManager) {
205
+ tunnelManager = new ServiceWorkerWebSocketTunnel();
206
+ console.log('[SW-WebSocket] Tunnel manager initialized');
207
+ }
208
+ return tunnelManager;
209
+}
210
+
211
+/**
212
+ * WebSocket message handler to add to Service Worker
213
+ */
214
+async function handleWebSocketMessage(event) {
215
+ const { type, tunnelId, url, protocols, data, isBinary, code, reason } = event.data || {};
216
+ const manager = initTunnelManager();
217
+
218
+ switch (type) {
219
+ case 'WEBSOCKET_OPEN':
220
+ try {
221
+ const clientId = event.source?.id || event.clientId;
222
+ const result = await manager.createTunnel(url, protocols, clientId);
223
+
224
+ event.ports[0]?.postMessage({
225
+ success: true,
226
+ result
227
+ });
228
+ } catch (error) {
229
+ event.ports[0]?.postMessage({
230
+ success: false,
231
+ error: error.toString()
232
+ });
233
+ }
234
+ break;
235
+
236
+ case 'WEBSOCKET_SEND':
237
+ try {
238
+ await manager.sendMessage(tunnelId, data, isBinary);
239
+ event.ports[0]?.postMessage({ success: true });
240
+ } catch (error) {
241
+ event.ports[0]?.postMessage({
242
+ success: false,
243
+ error: error.toString()
244
+ });
245
+ }
246
+ break;
247
+
248
+ case 'WEBSOCKET_CLOSE':
249
+ try {
250
+ await manager.closeTunnel(tunnelId, code, reason);
251
+ event.ports[0]?.postMessage({ success: true });
252
+ } catch (error) {
253
+ event.ports[0]?.postMessage({
254
+ success: false,
255
+ error: error.toString()
256
+ });
257
+ }
258
+ break;
259
+
260
+ case 'WEBSOCKET_STATE':
261
+ const state = manager.getTunnelState(tunnelId);
262
+ event.ports[0]?.postMessage({
263
+ success: true,
264
+ state
265
+ });
266
+ break;
267
+
268
+ case 'WEBSOCKET_LIST':
269
+ const tunnels = manager.getAllTunnels();
270
+ event.ports[0]?.postMessage({
271
+ success: true,
272
+ tunnels
273
+ });
274
+ break;
275
+
276
+ default:
277
+ return false; // Not handled
278
+ }
279
+
280
+ return true; // Handled
281
+}
282
+
283
+console.log('[SW-WebSocket] Service Worker WebSocket module loaded');
cmd/relay-server/wasm/secure-websocket.js
new
+710
@@ -0,0 +1,710 @@
1
+/**
2
+ * SecureWebSocket - E2EE WebSocket Polyfill using WASM ProxyEngine
3
+ *
4
+ * Provides transparent end-to-end encryption for WebSocket connections
5
+ * through the RelayDNS WASM ProxyEngine.
6
+ */
7
+
8
+// Global WASM instance cache
9
+let wasmInstance = null;
10
+let wasmInitPromise = null;
11
+
12
+/**
13
+ * Get Relay server URL from server or config
14
+ * @returns {Promise<string>} Relay server WebSocket URL
15
+ */
16
+async function getRelayUrl() {
17
+ // 1. Check if manually configured
18
+ if (window.RELAYDNS_RELAY_URL) {
19
+ console.log('[SecureWebSocket] Using configured relay URL:', window.RELAYDNS_RELAY_URL);
20
+ return window.RELAYDNS_RELAY_URL;
21
+ }
22
+
23
+ // 2. Try to get from server API
24
+ try {
25
+ const response = await fetch('/api/relay-info');
26
+ if (response.ok) {
27
+ const data = await response.json();
28
+ if (data.relayUrl) {
29
+ console.log('[SecureWebSocket] Got relay URL from server:', data.relayUrl);
30
+ return data.relayUrl;
31
+ }
32
+ }
33
+ } catch (error) {
34
+ console.warn('[SecureWebSocket] Failed to fetch relay info from server:', error.message);
35
+ }
36
+
37
+ // 3. Auto-detect from current location
38
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
39
+ const host = window.location.host;
40
+ const autoUrl = `${protocol}//${host}/relay`;
41
+
42
+ console.log('[SecureWebSocket] Auto-detected relay URL:', autoUrl);
43
+ return autoUrl;
44
+}
45
+
46
+/**
47
+ * Check if Service Worker is available and has WebSocket support
48
+ * @returns {Promise<boolean>}
49
+ */
50
+async function hasServiceWorkerWebSocket() {
51
+ if (!navigator.serviceWorker || !navigator.serviceWorker.controller) {
52
+ return false;
53
+ }
54
+
55
+ try {
56
+ const channel = new MessageChannel();
57
+ const response = await new Promise((resolve) => {
58
+ channel.port1.onmessage = (event) => resolve(event.data);
59
+ navigator.serviceWorker.controller.postMessage(
60
+ { type: 'GET_STATUS' },
61
+ [channel.port2]
62
+ );
63
+ setTimeout(() => resolve({ success: false }), 1000);
64
+ });
65
+
66
+ return response.success && response.status?.hasWebSocket;
67
+ } catch {
68
+ return false;
69
+ }
70
+}
71
+
72
+/**
73
+ * Initialize and get the WASM ProxyEngine instance
74
+ * @returns {Promise<Object>} WASM module with ProxyEngine
75
+ */
76
+async function getProxyEngine() {
77
+ if (wasmInstance) {
78
+ return wasmInstance;
79
+ }
80
+
81
+ if (wasmInitPromise) {
82
+ return wasmInitPromise;
83
+ }
84
+
85
+ wasmInitPromise = (async () => {
86
+ try {
87
+ // Check if we should use Service Worker
88
+ const useServiceWorker = await hasServiceWorkerWebSocket();
89
+
90
+ if (useServiceWorker) {
91
+ console.log('[SecureWebSocket] Using Service Worker for WebSocket');
92
+ wasmInstance = {
93
+ engine: null,
94
+ wasm: null,
95
+ useServiceWorker: true
96
+ };
97
+ return wasmInstance;
98
+ }
99
+
100
+ // Fallback to direct WASM
101
+ console.log('[SecureWebSocket] Using direct WASM');
102
+
103
+ // Load WASM module
104
+ if (typeof wasm_bindgen === 'undefined') {
105
+ throw new Error('WASM module not loaded. Include relaydns_wasm.js first.');
106
+ }
107
+
108
+ // Initialize WASM
109
+ await wasm_bindgen('/pkg/relaydns_wasm_bg.wasm');
110
+
111
+ // Get relay server URL
112
+ const relayUrl = await getRelayUrl();
113
+
114
+ // Create ProxyEngine instance
115
+ const engine = new wasm_bindgen.ProxyEngine(relayUrl);
116
+
117
+ console.log('[SecureWebSocket] WASM ProxyEngine initialized:', relayUrl);
118
+
119
+ wasmInstance = {
120
+ engine,
121
+ wasm: wasm_bindgen,
122
+ useServiceWorker: false
123
+ };
124
+
125
+ return wasmInstance;
126
+
127
+ } catch (error) {
128
+ console.error('[SecureWebSocket] Failed to initialize WASM:', error);
129
+ wasmInitPromise = null;
130
+ throw error;
131
+ }
132
+ })();
133
+
134
+ return wasmInitPromise;
135
+}
136
+
137
+/**
138
+ * SecureWebSocket - Drop-in replacement for native WebSocket with E2EE
139
+ */
140
+class SecureWebSocket extends EventTarget {
141
+ /**
142
+ * @param {string} url - WebSocket URL
143
+ * @param {string|string[]} protocols - Optional subprotocols
144
+ */
145
+ constructor(url, protocols = []) {
146
+ super();
147
+
148
+ // Normalize protocols
149
+ if (typeof protocols === 'string') {
150
+ protocols = [protocols];
151
+ }
152
+
153
+ // Public properties (read-only)
154
+ Object.defineProperties(this, {
155
+ url: { value: url, writable: false, enumerable: true },
156
+ protocols: { value: protocols, writable: false, enumerable: true },
157
+ });
158
+
159
+ // Internal state
160
+ this._readyState = WebSocket.CONNECTING;
161
+ this._protocol = '';
162
+ this._tunnelId = null;
163
+ this._bufferedAmount = 0;
164
+ this._extensions = '';
165
+ this._binaryType = 'blob';
166
+
167
+ // Event handlers (nullable)
168
+ this.onopen = null;
169
+ this.onmessage = null;
170
+ this.onerror = null;
171
+ this.onclose = null;
172
+
173
+ // Start connection
174
+ this._connect();
175
+ }
176
+
177
+ // Public properties with getters
178
+ get readyState() { return this._readyState; }
179
+ get protocol() { return this._protocol; }
180
+ get bufferedAmount() { return this._bufferedAmount; }
181
+ get extensions() { return this._extensions; }
182
+ get binaryType() { return this._binaryType; }
183
+ set binaryType(value) {
184
+ if (value === 'blob' || value === 'arraybuffer') {
185
+ this._binaryType = value;
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Initialize connection through WASM ProxyEngine
191
+ * @private
192
+ */
193
+ async _connect() {
194
+ try {
195
+ console.log('[SecureWebSocket] Connecting to:', this.url);
196
+
197
+ // Get WASM ProxyEngine or Service Worker
198
+ const instance = await getProxyEngine();
199
+
200
+ if (instance.useServiceWorker) {
201
+ // Use Service Worker
202
+ await this._connectViaServiceWorker();
203
+ } else {
204
+ // Use direct WASM
205
+ await this._connectViaDirect(instance.engine);
206
+ }
207
+
208
+ } catch (error) {
209
+ console.error('[SecureWebSocket] Connection failed:', error);
210
+
211
+ this._readyState = WebSocket.CLOSED;
212
+
213
+ // Dispatch error event
214
+ this._dispatchEvent('error', {
215
+ message: error.toString(),
216
+ error: error
217
+ });
218
+
219
+ // Dispatch close event
220
+ this._dispatchEvent('close', {
221
+ code: 1006,
222
+ reason: error.toString(),
223
+ wasClean: false
224
+ });
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Connect via Service Worker
230
+ * @private
231
+ */
232
+ async _connectViaServiceWorker() {
233
+ console.log('[SecureWebSocket] Connecting via Service Worker');
234
+
235
+ const channel = new MessageChannel();
236
+ const response = await new Promise((resolve, reject) => {
237
+ channel.port1.onmessage = (event) => {
238
+ if (event.data.success) {
239
+ resolve(event.data.result);
240
+ } else {
241
+ reject(new Error(event.data.error));
242
+ }
243
+ };
244
+
245
+ navigator.serviceWorker.controller.postMessage(
246
+ {
247
+ type: 'WEBSOCKET_OPEN',
248
+ url: this.url,
249
+ protocols: this.protocols
250
+ },
251
+ [channel.port2]
252
+ );
253
+
254
+ setTimeout(() => reject(new Error('Service Worker timeout')), 10000);
255
+ });
256
+
257
+ this._tunnelId = response.tunnelId;
258
+ this._protocol = response.protocol || '';
259
+ this._readyState = WebSocket.OPEN;
260
+ this._useServiceWorker = true;
261
+
262
+ console.log('[SecureWebSocket] Connected via SW! Tunnel ID:', this._tunnelId);
263
+
264
+ // Dispatch open event
265
+ this._dispatchEvent('open', {});
266
+
267
+ // Listen for messages from Service Worker
268
+ this._listenToServiceWorker();
269
+ }
270
+
271
+ /**
272
+ * Connect via direct WASM
273
+ * @private
274
+ */
275
+ async _connectViaDirect(engine) {
276
+ console.log('[SecureWebSocket] Connecting via direct WASM');
277
+
278
+ // Open WebSocket tunnel through E2EE proxy
279
+ const result = await engine.open_websocket(this.url, this.protocols);
280
+
281
+ this._tunnelId = result.tunnelId;
282
+ this._protocol = result.protocol || '';
283
+ this._readyState = WebSocket.OPEN;
284
+ this._useServiceWorker = false;
285
+
286
+ console.log('[SecureWebSocket] Connected! Tunnel ID:', this._tunnelId);
287
+
288
+ // Dispatch open event
289
+ this._dispatchEvent('open', {});
290
+
291
+ // Start receiving messages in background
292
+ this._receiveLoop(engine);
293
+ }
294
+
295
+ /**
296
+ * Listen to Service Worker messages
297
+ * @private
298
+ */
299
+ _listenToServiceWorker() {
300
+ const handler = (event) => {
301
+ if (event.data.type === 'WEBSOCKET_MESSAGE' &&
302
+ event.data.tunnelId === this._tunnelId) {
303
+
304
+ const msg = event.data.message;
305
+ this._handleMessage(msg);
306
+ }
307
+ };
308
+
309
+ navigator.serviceWorker.addEventListener('message', handler);
310
+ this._swMessageHandler = handler;
311
+ }
312
+
313
+ /**
314
+ * Handle incoming message
315
+ * @private
316
+ */
317
+ _handleMessage(msg) {
318
+ if (msg.type === 'text') {
319
+ // Text message
320
+ this._dispatchEvent('message', {
321
+ data: msg.data,
322
+ type: 'message',
323
+ origin: this.url
324
+ });
325
+
326
+ } else if (msg.type === 'binary') {
327
+ // Binary message
328
+ let data;
329
+ if (this._binaryType === 'arraybuffer') {
330
+ data = new Uint8Array(msg.data).buffer;
331
+ } else {
332
+ data = new Blob([new Uint8Array(msg.data)]);
333
+ }
334
+
335
+ this._dispatchEvent('message', {
336
+ data: data,
337
+ type: 'message',
338
+ origin: this.url
339
+ });
340
+
341
+ } else if (msg.type === 'close') {
342
+ // Close message
343
+ console.log('[SecureWebSocket] Received close:', msg.code, msg.reason);
344
+
345
+ this._readyState = WebSocket.CLOSED;
346
+
347
+ this._dispatchEvent('close', {
348
+ code: msg.code || 1000,
349
+ reason: msg.reason || '',
350
+ wasClean: true
351
+ });
352
+
353
+ // Cleanup Service Worker listener
354
+ if (this._swMessageHandler) {
355
+ navigator.serviceWorker.removeEventListener('message', this._swMessageHandler);
356
+ }
357
+ }
358
+ }
359
+
360
+ /**
361
+ * Background loop to receive messages from tunnel
362
+ * @private
363
+ * @param {Object} engine - WASM ProxyEngine instance
364
+ */
365
+ async _receiveLoop(engine) {
366
+ try {
367
+ while (this._readyState !== WebSocket.CLOSED && this._readyState !== WebSocket.CLOSING) {
368
+ // Receive message from tunnel
369
+ const msg = await engine.receive_websocket_message(this._tunnelId);
370
+
371
+ if (msg.type === 'text') {
372
+ // Text message
373
+ this._dispatchEvent('message', {
374
+ data: msg.data,
375
+ type: 'message',
376
+ origin: this.url
377
+ });
378
+
379
+ } else if (msg.type === 'binary') {
380
+ // Binary message
381
+ let data;
382
+ if (this._binaryType === 'arraybuffer') {
383
+ data = new Uint8Array(msg.data).buffer;
384
+ } else {
385
+ // Convert to Blob
386
+ data = new Blob([new Uint8Array(msg.data)]);
387
+ }
388
+
389
+ this._dispatchEvent('message', {
390
+ data: data,
391
+ type: 'message',
392
+ origin: this.url
393
+ });
394
+
395
+ } else if (msg.type === 'close') {
396
+ // Close message
397
+ console.log('[SecureWebSocket] Received close:', msg.code, msg.reason);
398
+
399
+ this._readyState = WebSocket.CLOSED;
400
+
401
+ this._dispatchEvent('close', {
402
+ code: msg.code || 1000,
403
+ reason: msg.reason || '',
404
+ wasClean: true
405
+ });
406
+
407
+ break;
408
+ }
409
+ }
410
+
411
+ } catch (error) {
412
+ console.error('[SecureWebSocket] Receive loop error:', error);
413
+
414
+ if (this._readyState !== WebSocket.CLOSED) {
415
+ this._dispatchEvent('error', {
416
+ message: error.toString(),
417
+ error: error
418
+ });
419
+
420
+ this.close(1006, error.toString());
421
+ }
422
+ }
423
+ }
424
+
425
+ /**
426
+ * Send data through the secure tunnel
427
+ * @param {string|ArrayBuffer|Uint8Array|Blob} data - Data to send
428
+ */
429
+ send(data) {
430
+ if (this._readyState !== WebSocket.OPEN) {
431
+ throw new DOMException(
432
+ 'Failed to execute \'send\' on \'WebSocket\': Still in CONNECTING state.',
433
+ 'InvalidStateError'
434
+ );
435
+ }
436
+
437
+ // Handle different data types
438
+ if (typeof data === 'string') {
439
+ // Text message
440
+ this._sendMessage(data, false);
441
+
442
+ } else if (data instanceof ArrayBuffer) {
443
+ // Binary ArrayBuffer
444
+ this._sendMessage(new Uint8Array(data), true);
445
+
446
+ } else if (data instanceof Uint8Array) {
447
+ // Binary Uint8Array
448
+ this._sendMessage(data, true);
449
+
450
+ } else if (data instanceof Blob) {
451
+ // Blob - convert to ArrayBuffer
452
+ this._bufferedAmount += data.size;
453
+
454
+ data.arrayBuffer().then(buffer => {
455
+ this._sendMessage(new Uint8Array(buffer), true);
456
+ this._bufferedAmount = Math.max(0, this._bufferedAmount - data.size);
457
+ });
458
+
459
+ } else {
460
+ throw new TypeError('Data must be string, ArrayBuffer, Uint8Array, or Blob');
461
+ }
462
+ }
463
+
464
+ /**
465
+ * Send message through WASM ProxyEngine
466
+ * @private
467
+ * @param {string|Uint8Array} data - Data to send
468
+ * @param {boolean} isBinary - Whether data is binary
469
+ */
470
+ async _sendMessage(data, isBinary) {
471
+ try {
472
+ // Estimate buffer size
473
+ const size = typeof data === 'string' ? data.length : data.length;
474
+ this._bufferedAmount += size;
475
+
476
+ if (this._useServiceWorker) {
477
+ // Send via Service Worker
478
+ const channel = new MessageChannel();
479
+ await new Promise((resolve, reject) => {
480
+ channel.port1.onmessage = (event) => {
481
+ if (event.data.success) {
482
+ resolve();
483
+ } else {
484
+ reject(new Error(event.data.error));
485
+ }
486
+ };
487
+
488
+ navigator.serviceWorker.controller.postMessage(
489
+ {
490
+ type: 'WEBSOCKET_SEND',
491
+ tunnelId: this._tunnelId,
492
+ data: data,
493
+ isBinary: isBinary
494
+ },
495
+ [channel.port2]
496
+ );
497
+
498
+ setTimeout(() => reject(new Error('Send timeout')), 5000);
499
+ });
500
+ } else {
501
+ // Send via direct WASM
502
+ const { engine } = await getProxyEngine();
503
+ await engine.send_websocket_message(this._tunnelId, data, isBinary);
504
+ }
505
+
506
+ // Decrement buffered amount
507
+ this._bufferedAmount = Math.max(0, this._bufferedAmount - size);
508
+
509
+ } catch (error) {
510
+ console.error('[SecureWebSocket] Send failed:', error);
511
+
512
+ this._dispatchEvent('error', {
513
+ message: error.toString(),
514
+ error: error
515
+ });
516
+ }
517
+ }
518
+
519
+ /**
520
+ * Close the WebSocket connection
521
+ * @param {number} code - Close code (default 1000)
522
+ * @param {string} reason - Close reason (default empty)
523
+ */
524
+ close(code = 1000, reason = '') {
525
+ if (this._readyState === WebSocket.CLOSED || this._readyState === WebSocket.CLOSING) {
526
+ return;
527
+ }
528
+
529
+ console.log('[SecureWebSocket] Closing:', code, reason);
530
+
531
+ this._readyState = WebSocket.CLOSING;
532
+
533
+ // Close tunnel
534
+ (async () => {
535
+ try {
536
+ if (this._useServiceWorker) {
537
+ // Close via Service Worker
538
+ const channel = new MessageChannel();
539
+ await new Promise((resolve, reject) => {
540
+ channel.port1.onmessage = (event) => {
541
+ if (event.data.success) {
542
+ resolve();
543
+ } else {
544
+ reject(new Error(event.data.error));
545
+ }
546
+ };
547
+
548
+ navigator.serviceWorker.controller.postMessage(
549
+ {
550
+ type: 'WEBSOCKET_CLOSE',
551
+ tunnelId: this._tunnelId,
552
+ code: code,
553
+ reason: reason
554
+ },
555
+ [channel.port2]
556
+ );
557
+
558
+ setTimeout(() => resolve(), 2000); // Don't wait forever
559
+ });
560
+ } else {
561
+ // Close via direct WASM
562
+ const { engine } = await getProxyEngine();
563
+ await engine.close_websocket(this._tunnelId, code, reason);
564
+ }
565
+ } catch (error) {
566
+ console.error('[SecureWebSocket] Close failed:', error);
567
+
568
+ // Force close
569
+ this._readyState = WebSocket.CLOSED;
570
+ this._dispatchEvent('close', {
571
+ code: 1006,
572
+ reason: error.toString(),
573
+ wasClean: false
574
+ });
575
+ }
576
+ })();
577
+ }
578
+
579
+ /**
580
+ * Dispatch event to both EventTarget and legacy handler
581
+ * @private
582
+ * @param {string} type - Event type
583
+ * @param {Object} detail - Event details
584
+ */
585
+ _dispatchEvent(type, detail) {
586
+ // Create event
587
+ const event = new Event(type);
588
+ Object.assign(event, detail);
589
+
590
+ // Dispatch to EventTarget listeners
591
+ this.dispatchEvent(event);
592
+
593
+ // Call legacy handler if exists
594
+ const handler = this[`on${type}`];
595
+ if (typeof handler === 'function') {
596
+ try {
597
+ handler.call(this, event);
598
+ } catch (error) {
599
+ console.error(`[SecureWebSocket] Error in on${type} handler:`, error);
600
+ }
601
+ }
602
+ }
603
+}
604
+
605
+// Static constants (same as native WebSocket)
606
+SecureWebSocket.CONNECTING = 0;
607
+SecureWebSocket.OPEN = 1;
608
+SecureWebSocket.CLOSING = 2;
609
+SecureWebSocket.CLOSED = 3;
610
+
611
+// ==============================================================================
612
+// POLYFILL: Replace native WebSocket with SecureWebSocket
613
+// ==============================================================================
614
+
615
+(function() {
616
+ // Save reference to native WebSocket
617
+ const NativeWebSocket = window.WebSocket;
618
+
619
+ // Configuration
620
+ const config = {
621
+ // Enable E2EE for all WebSockets by default
622
+ enabled: window.RELAYDNS_E2EE_ENABLED !== false,
623
+
624
+ // Patterns to intercept (regex strings)
625
+ interceptPatterns: window.RELAYDNS_INTERCEPT_PATTERNS || [
626
+ '.*' // Intercept all by default
627
+ ],
628
+
629
+ // Patterns to bypass (regex strings) - takes precedence
630
+ bypassPatterns: window.RELAYDNS_BYPASS_PATTERNS || [
631
+ '^wss?://localhost:4017/', // Don't intercept relay server itself
632
+ '^wss?://localhost:8000/', // Don't intercept local dev server
633
+ '^wss?://127\\.0\\.0\\.1', // Don't intercept loopback
634
+ ],
635
+
636
+ // Debug mode
637
+ debug: window.RELAYDNS_DEBUG || false
638
+ };
639
+
640
+ /**
641
+ * Check if URL should be intercepted for E2EE
642
+ * @param {string} url - WebSocket URL
643
+ * @returns {boolean} True if should intercept
644
+ */
645
+ function shouldIntercept(url) {
646
+ if (!config.enabled) {
647
+ return false;
648
+ }
649
+
650
+ // Check bypass patterns first (higher priority)
651
+ for (const pattern of config.bypassPatterns) {
652
+ const regex = new RegExp(pattern);
653
+ if (regex.test(url)) {
654
+ if (config.debug) {
655
+ console.log('[SecureWebSocket] Bypassing (matched bypass pattern):', url);
656
+ }
657
+ return false;
658
+ }
659
+ }
660
+
661
+ // Check intercept patterns
662
+ for (const pattern of config.interceptPatterns) {
663
+ const regex = new RegExp(pattern);
664
+ if (regex.test(url)) {
665
+ if (config.debug) {
666
+ console.log('[SecureWebSocket] Intercepting (matched intercept pattern):', url);
667
+ }
668
+ return true;
669
+ }
670
+ }
671
+
672
+ if (config.debug) {
673
+ console.log('[SecureWebSocket] Not intercepting (no match):', url);
674
+ }
675
+ return false;
676
+ }
677
+
678
+ /**
679
+ * Polyfilled WebSocket constructor
680
+ * @param {string} url - WebSocket URL
681
+ * @param {string|string[]} protocols - Optional subprotocols
682
+ * @returns {WebSocket|SecureWebSocket}
683
+ */
684
+ window.WebSocket = function(url, protocols) {
685
+ if (shouldIntercept(url)) {
686
+ // Use E2EE SecureWebSocket
687
+ console.log('[SecureWebSocket] 🔒 Creating encrypted WebSocket:', url);
688
+ return new SecureWebSocket(url, protocols);
689
+ } else {
690
+ // Use native WebSocket
691
+ if (config.debug) {
692
+ console.log('[SecureWebSocket] Creating native WebSocket:', url);
693
+ }
694
+ return new NativeWebSocket(url, protocols);
695
+ }
696
+ };
697
+
698
+ // Copy static properties from native WebSocket
699
+ window.WebSocket.CONNECTING = NativeWebSocket.CONNECTING;
700
+ window.WebSocket.OPEN = NativeWebSocket.OPEN;
701
+ window.WebSocket.CLOSING = NativeWebSocket.CLOSING;
702
+ window.WebSocket.CLOSED = NativeWebSocket.CLOSED;
703
+
704
+ // Expose SecureWebSocket class for direct access if needed
705
+ window.SecureWebSocket = SecureWebSocket;
706
+ window.NativeWebSocket = NativeWebSocket;
707
+
708
+ console.log('[SecureWebSocket] ✅ Polyfill installed. E2EE enabled:', config.enabled);
709
+
710
+})();
cmd/relay-server/wasm/sw-proxy.js
+149
-24
@@ -4,34 +4,110 @@
4
const CACHE_NAME = 'relaydns-proxy-v1';
5
let proxyEngine = null;
6
let wasmReady = false;
7
+let initializationPromise = null;
8
8
-// Install event - Load WASM here (only time importScripts is allowed)
9
+// Dynamic WASM initialization (can be called anytime, not just install)
10
+async function initializeProxyEngine() {
11
+ // If already initializing, wait for it
12
+ if (initializationPromise) {
13
+ return initializationPromise;
14
+ }
15
+
16
+ // If already initialized, return immediately
17
+ if (proxyEngine && wasmReady) {
18
+ return proxyEngine;
19
+ }
20
+
21
+ initializationPromise = (async () => {
22
+ try {
23
+ console.log('[SW-Proxy] Dynamically initializing ProxyEngine...');
24
+
25
+ // Check if wasm_bindgen is available (loaded during install)
26
+ if (typeof wasm_bindgen === 'undefined') {
27
+ console.log('[SW-Proxy] wasm_bindgen not available, loading via dynamic import...');
28
+
29
+ // Use dynamic import for ES6 modules
30
+ const wasmModule = await import('/pkg/relaydns_wasm.js');
31
+
32
+ // Make wasm_bindgen available globally
33
+ self.wasm_bindgen = wasmModule;
34
+
35
+ console.log('[SW-Proxy] ✓ WASM JS module loaded');
36
+ }
37
+
38
+ // Check if SecureWebSocket SW module is loaded
39
+ if (typeof ServiceWorkerWebSocketTunnel === 'undefined') {
40
+ console.log('[SW-Proxy] SecureWebSocket SW module not available, loading via fetch...');
41
+
42
+ const swWsResponse = await fetch('/secure-websocket-sw.js');
43
+ const swWsCode = await swWsResponse.text();
44
+
45
+ // Use indirect eval to execute in global scope
46
+ (1, eval)(swWsCode);
47
+ console.log('[SW-Proxy] ✓ SecureWebSocket SW module loaded');
48
+ }
49
+
50
+ // Initialize WASM module if not already done
51
+ if (!wasmReady) {
52
+ console.log('[SW-Proxy] Initializing WASM module...');
53
+
54
+ // wasm_bindgen is now the module object from dynamic import
55
+ const initWasm = self.wasm_bindgen.default || self.wasm_bindgen;
56
+ await initWasm('/pkg/relaydns_wasm_bg.wasm');
57
+
58
+ console.log('[SW-Proxy] ✓ WASM module initialized');
59
+ }
60
+
61
+ // Get relay URL from API
62
+ let relayUrl = 'ws://localhost:4017/relay'; // Default fallback
63
+ try {
64
+ const relayInfoResponse = await fetch('/api/relay-info');
65
+ if (relayInfoResponse.ok) {
66
+ const relayInfo = await relayInfoResponse.json();
67
+ if (relayInfo.relayUrl) {
68
+ relayUrl = relayInfo.relayUrl;
69
+ console.log('[SW-Proxy] Got relay URL from server:', relayUrl);
70
+ }
71
+ }
72
+ } catch (e) {
73
+ console.warn('[SW-Proxy] Failed to fetch relay URL, using default:', e.message);
74
+ }
75
+
76
+ // Create ProxyEngine
77
+ console.log('[SW-Proxy] Creating ProxyEngine with URL:', relayUrl);
78
+ const ProxyEngine = self.wasm_bindgen.ProxyEngine;
79
+ proxyEngine = new ProxyEngine(relayUrl);
80
+ wasmReady = true;
81
+
82
+ console.log('[SW-Proxy] ✓ ProxyEngine ready');
83
+ return proxyEngine;
84
+
85
+ } catch (error) {
86
+ console.error('[SW-Proxy] Failed to initialize ProxyEngine:', error);
87
+ initializationPromise = null; // Reset so we can retry
88
+ wasmReady = false;
89
+ throw error;
90
+ }
91
+ })();
92
+
93
+ return initializationPromise;
94
+}
95
+
96
+// Install event - Skip importScripts since WASM is ES6 module
97
self.addEventListener('install', (event) => {
98
console.log('[SW-Proxy] Installing...');
99
100
event.waitUntil(
101
(async () => {
102
try {
15
- console.log('[SW-Proxy] Loading WASM module during install...');
16
-
17
- // importScripts can ONLY be called during install
18
- self.importScripts('/pkg/relaydns_wasm_sw.js');
19
- console.log('[SW-Proxy] ✓ WASM script loaded');
103
+ console.log('[SW-Proxy] Service Worker installing...');
104
21
- // Initialize WASM
22
- console.log('[SW-Proxy] Initializing WASM...');
23
- await wasm_bindgen('/pkg/relaydns_wasm_sw_bg.wasm');
24
- console.log('[SW-Proxy] ✓ WASM initialized');
25
-
26
- // Create proxy engine
27
- console.log('[SW-Proxy] Creating ProxyEngine...');
28
- proxyEngine = new wasm_bindgen.ProxyEngine('ws://localhost:4017/relay');
29
- wasmReady = true;
30
- console.log('[SW-Proxy] ✓ Proxy engine ready');
105
+ // Note: We cannot use importScripts with ES6 modules
106
+ // WASM will be loaded dynamically on first request via fetch + dynamic import
107
+ console.log('[SW-Proxy] WASM will be loaded dynamically on first request');
108
109
} catch (error) {
33
- console.error('[SW-Proxy] Failed to initialize WASM:', error);
34
- throw error;
110
+ console.error('[SW-Proxy] Install error:', error);
111
}
112
113
// Skip waiting to activate immediately
@@ -64,9 +140,31 @@ function shouldProxy(url) {
140
// Handle HTTP request through WASM proxy
141
async function proxyHttpRequest(request) {
142
try {
143
+ // Ensure ProxyEngine is initialized (lazy init if needed)
144
if (!wasmReady || !proxyEngine) {
68
- console.warn('[SW-Proxy] WASM not ready, falling back to direct fetch');
69
- return fetch(request);
145
+ console.log('[SW-Proxy] ProxyEngine not ready, attempting lazy initialization...');
146
+ try {
147
+ await initializeProxyEngine();
148
+ } catch (initError) {
149
+ // Security: Never fallback to direct fetch - this would bypass E2EE!
150
+ console.error('[SW-Proxy] Failed to initialize ProxyEngine:', initError);
151
+ return new Response(
152
+ JSON.stringify({
153
+ error: 'E2EE ProxyEngine Initialization Failed',
154
+ message: 'Could not initialize secure proxy. Please refresh the page.',
155
+ code: 'PROXY_ENGINE_INIT_FAILED',
156
+ details: initError.message
157
+ }),
158
+ {
159
+ status: 503,
160
+ statusText: 'Service Unavailable',
161
+ headers: {
162
+ 'Content-Type': 'application/json',
163
+ 'X-E2EE-Status': 'init-failed'
164
+ }
165
+ }
166
+ );
167
+ }
168
}
169
170
console.log('[SW-Proxy] Proxying:', request.method, request.url);
@@ -113,9 +211,22 @@ async function proxyHttpRequest(request) {
211
212
} catch (error) {
213
console.error('[SW-Proxy] Proxy error:', error);
116
- // Fallback to direct fetch on error
117
- console.log('[SW-Proxy] Falling back to direct fetch');
118
- return fetch(request);
214
+ // Security: Never fallback to direct fetch - return error instead
215
+ return new Response(
216
+ JSON.stringify({
217
+ error: 'E2EE Proxy Error',
218
+ message: error.message || 'Failed to proxy request through E2EE tunnel',
219
+ code: 'PROXY_ERROR'
220
+ }),
221
+ {
222
+ status: 502,
223
+ statusText: 'Bad Gateway',
224
+ headers: {
225
+ 'Content-Type': 'application/json',
226
+ 'X-E2EE-Status': 'error'
227
+ }
228
+ }
229
+ );
230
}
231
}
232
@@ -137,13 +248,27 @@ self.addEventListener('fetch', (event) => {
248
self.addEventListener('message', (event) => {
249
const { type } = event.data || {};
250
251
+ // Try WebSocket handler first
252
+ if (typeof handleWebSocketMessage === 'function') {
253
+ const handled = handleWebSocketMessage(event);
254
+ if (handled instanceof Promise) {
255
+ // Async handler
256
+ return;
257
+ } else if (handled) {
258
+ // Synchronously handled
259
+ return;
260
+ }
261
+ }
262
+
263
+ // Standard message handling
264
switch (type) {
265
case 'GET_STATUS':
266
event.ports[0]?.postMessage({
267
success: true,
268
status: {
269
wasmReady,
146
- hasEngine: !!proxyEngine
270
+ hasEngine: !!proxyEngine,
271
+ hasWebSocket: typeof handleWebSocketMessage === 'function'
272
}
273
});
274
break;
relaydns/e2ee_websocket.go
new
+402
@@ -0,0 +1,402 @@
1
+package relaydns
2
+
3
+import (
4
+ "errors"
5
+ "fmt"
6
+ "io"
7
+ "net/url"
8
+
9
+ "github.com/gorilla/websocket"
10
+ "github.com/gosuda/relaydns/relaydns/core/cryptoops"
11
+ "github.com/gosuda/relaydns/relaydns/utils/wsstream"
12
+ "github.com/rs/zerolog/log"
13
+)
14
+
15
+var (
16
+ ErrInvalidURL = errors.New("invalid WebSocket URL")
17
+ ErrUnsupportedALPN = errors.New("unsupported ALPN protocol")
18
+)
19
+
20
+// SecureWebSocketClient provides E2EE WebSocket connections through RelayDNS
21
+type SecureWebSocketClient struct {
22
+ relayClient *RelayClient
23
+ credential *cryptoops.Credential
24
+}
25
+
26
+// NewSecureWebSocketClient creates a new E2EE WebSocket client
27
+//
28
+// Parameters:
29
+// - relayConn: Connection to relay server (usually ws://relay-server/relay)
30
+// - credential: Client credential for E2EE authentication
31
+//
32
+// Example:
33
+//
34
+// // Connect to relay server
35
+// relayConn, _, err := websocket.DefaultDialer.Dial("ws://localhost:4017/relay", nil)
36
+// if err != nil {
37
+// log.Fatal(err)
38
+// }
39
+//
40
+// // Create credential
41
+// cred, err := cryptoops.NewRandomCredential()
42
+// if err != nil {
43
+// log.Fatal(err)
44
+// }
45
+//
46
+// // Create E2EE WebSocket client
47
+// client := NewSecureWebSocketClient(relayConn, cred)
48
+func NewSecureWebSocketClient(relayConn io.ReadWriteCloser, credential *cryptoops.Credential) *SecureWebSocketClient {
49
+ relayClient := NewRelayClient(relayConn)
50
+
51
+ return &SecureWebSocketClient{
52
+ relayClient: relayClient,
53
+ credential: credential,
54
+ }
55
+}
56
+
57
+// Dial establishes an E2EE WebSocket connection to a target peer
58
+//
59
+// The target URL should use the "peer://" scheme:
60
+// - peer://peer-id/path - Connect to peer via E2EE tunnel
61
+//
62
+// Parameters:
63
+// - targetURL: Target WebSocket URL (peer://peer-id/path)
64
+// - alpn: Application-Layer Protocol Negotiation (e.g., "websocket", "http")
65
+//
66
+// Returns:
67
+// - *SecureWebSocketConn: Established E2EE connection
68
+// - error: Error if connection fails
69
+//
70
+// Example:
71
+//
72
+// conn, err := client.Dial("peer://abc123/chat", "websocket")
73
+// if err != nil {
74
+// log.Fatal(err)
75
+// }
76
+// defer conn.Close()
77
+//
78
+// // Send message
79
+// err = conn.WriteMessage(websocket.TextMessage, []byte("Hello!"))
80
+//
81
+// // Receive message
82
+// messageType, data, err := conn.ReadMessage()
83
+func (c *SecureWebSocketClient) Dial(targetURL, alpn string) (*SecureWebSocketConn, error) {
84
+ log.Debug().
85
+ Str("target_url", targetURL).
86
+ Str("alpn", alpn).
87
+ Msg("[SecureWebSocket] Dialing E2EE WebSocket")
88
+
89
+ // Parse target URL
90
+ parsedURL, err := url.Parse(targetURL)
91
+ if err != nil {
92
+ return nil, fmt.Errorf("%w: %v", ErrInvalidURL, err)
93
+ }
94
+
95
+ // Extract peer ID from URL
96
+ // peer://peer-id/path -> peer-id is the lease ID
97
+ peerID := parsedURL.Host
98
+ if peerID == "" {
99
+ return nil, fmt.Errorf("%w: missing peer ID", ErrInvalidURL)
100
+ }
101
+
102
+ log.Debug().
103
+ Str("peer_id", peerID).
104
+ Str("alpn", alpn).
105
+ Msg("[SecureWebSocket] Requesting connection to peer")
106
+
107
+ // Request connection through relay
108
+ code, secConn, err := c.relayClient.RequestConnection(peerID, alpn, c.credential)
109
+ if err != nil {
110
+ return nil, fmt.Errorf("connection request failed: %w", err)
111
+ }
112
+
113
+ if secConn == nil {
114
+ return nil, fmt.Errorf("connection rejected with code: %v", code)
115
+ }
116
+
117
+ log.Debug().
118
+ Str("peer_id", peerID).
119
+ Str("local_id", secConn.LocalID()).
120
+ Str("remote_id", secConn.RemoteID()).
121
+ Msg("[SecureWebSocket] E2EE connection established")
122
+
123
+ return &SecureWebSocketConn{
124
+ conn: secConn,
125
+ peerID: peerID,
126
+ alpn: alpn,
127
+ isClosed: false,
128
+ }, nil
129
+}
130
+
131
+// RegisterService registers this client as a WebSocket service provider
132
+//
133
+// After registration, other clients can connect to this service using:
134
+// - peer://your-peer-id/path
135
+//
136
+// Parameters:
137
+// - name: Service name (for human readability)
138
+// - alpns: Supported ALPN protocols (e.g., []string{"websocket", "http"})
139
+//
140
+// Returns incoming connections via IncomingConnections() channel
141
+//
142
+// Example:
143
+//
144
+// err := client.RegisterService("chat-server", []string{"websocket"})
145
+// if err != nil {
146
+// log.Fatal(err)
147
+// }
148
+//
149
+// log.Printf("Service registered with ID: %s", client.PeerID())
150
+//
151
+// for conn := range client.IncomingConnections() {
152
+// go handleConnection(conn)
153
+// }
154
+func (c *SecureWebSocketClient) RegisterService(name string, alpns []string) error {
155
+ log.Debug().
156
+ Str("name", name).
157
+ Strs("alpns", alpns).
158
+ Msg("[SecureWebSocket] Registering service")
159
+
160
+ err := c.relayClient.RegisterLease(c.credential, name, alpns)
161
+ if err != nil {
162
+ return fmt.Errorf("failed to register service: %w", err)
163
+ }
164
+
165
+ log.Info().
166
+ Str("peer_id", c.credential.ID()).
167
+ Str("name", name).
168
+ Strs("alpns", alpns).
169
+ Msg("[SecureWebSocket] Service registered successfully")
170
+
171
+ return nil
172
+}
173
+
174
+// DeregisterService removes this client from the relay server
175
+func (c *SecureWebSocketClient) DeregisterService() error {
176
+ log.Debug().Msg("[SecureWebSocket] Deregistering service")
177
+
178
+ err := c.relayClient.DeregisterLease(c.credential)
179
+ if err != nil {
180
+ return fmt.Errorf("failed to deregister service: %w", err)
181
+ }
182
+
183
+ log.Info().Str("peer_id", c.credential.ID()).Msg("[SecureWebSocket] Service deregistered")
184
+ return nil
185
+}
186
+
187
+// IncomingConnections returns a channel that receives incoming E2EE connections
188
+//
189
+// This channel delivers connections from other peers connecting to this service.
190
+// Each connection is already authenticated and encrypted.
191
+//
192
+// Example:
193
+//
194
+// for conn := range client.IncomingConnections() {
195
+// log.Printf("New connection from: %s", conn.RemoteID())
196
+// go handleConnection(conn)
197
+// }
198
+func (c *SecureWebSocketClient) IncomingConnections() <-chan *SecureWebSocketConn {
199
+ ch := make(chan *SecureWebSocketConn)
200
+
201
+ go func() {
202
+ for incomingConn := range c.relayClient.IncommingConnection() {
203
+ log.Debug().
204
+ Str("lease_id", incomingConn.LeaseID()).
205
+ Str("remote_id", incomingConn.RemoteID()).
206
+ Msg("[SecureWebSocket] Incoming E2EE connection")
207
+
208
+ ch <- &SecureWebSocketConn{
209
+ conn: incomingConn.SecureConnection,
210
+ peerID: incomingConn.RemoteID(),
211
+ alpn: "", // ALPN is already negotiated
212
+ isClosed: false,
213
+ }
214
+ }
215
+ close(ch)
216
+ }()
217
+
218
+ return ch
219
+}
220
+
221
+// PeerID returns this client's peer ID
222
+//
223
+// This is the identifier that other clients use to connect to this service.
224
+// Example: peer://your-peer-id/path
225
+func (c *SecureWebSocketClient) PeerID() string {
226
+ return c.credential.ID()
227
+}
228
+
229
+// Close closes the relay connection and stops the client
230
+func (c *SecureWebSocketClient) Close() error {
231
+ log.Debug().Msg("[SecureWebSocket] Closing client")
232
+ return c.relayClient.Close()
233
+}
234
+
235
+// SecureWebSocketConn represents an E2EE WebSocket connection
236
+type SecureWebSocketConn struct {
237
+ conn *cryptoops.SecureConnection
238
+ peerID string
239
+ alpn string
240
+ isClosed bool
241
+}
242
+
243
+// ReadMessage reads the next message from the connection
244
+//
245
+// Compatible with gorilla/websocket ReadMessage interface.
246
+// Always returns BinaryMessage type (E2EE encrypts everything as binary).
247
+//
248
+// Returns:
249
+// - messageType: Always websocket.BinaryMessage for E2EE
250
+// - data: Decrypted message data
251
+// - error: Error if read fails
252
+func (c *SecureWebSocketConn) ReadMessage() (messageType int, data []byte, err error) {
253
+ if c.isClosed {
254
+ return 0, nil, io.EOF
255
+ }
256
+
257
+ // Read encrypted data
258
+ buf := make([]byte, 65536) // 64KB buffer
259
+ n, err := c.conn.Read(buf)
260
+ if err != nil {
261
+ if err == io.EOF {
262
+ c.isClosed = true
263
+ }
264
+ return 0, nil, err
265
+ }
266
+
267
+ // Return decrypted data as binary message
268
+ return websocket.BinaryMessage, buf[:n], nil
269
+}
270
+
271
+// WriteMessage writes a message to the connection
272
+//
273
+// Compatible with gorilla/websocket WriteMessage interface.
274
+// Message type is ignored as E2EE encrypts everything as binary.
275
+//
276
+// Parameters:
277
+// - messageType: Ignored (can be TextMessage or BinaryMessage)
278
+// - data: Message data to send (will be encrypted)
279
+//
280
+// Returns:
281
+// - error: Error if write fails
282
+func (c *SecureWebSocketConn) WriteMessage(messageType int, data []byte) error {
283
+ if c.isClosed {
284
+ return io.ErrClosedPipe
285
+ }
286
+
287
+ // Write encrypted data
288
+ _, err := c.conn.Write(data)
289
+ return err
290
+}
291
+
292
+// Read implements io.Reader
293
+func (c *SecureWebSocketConn) Read(p []byte) (n int, err error) {
294
+ if c.isClosed {
295
+ return 0, io.EOF
296
+ }
297
+ return c.conn.Read(p)
298
+}
299
+
300
+// Write implements io.Writer
301
+func (c *SecureWebSocketConn) Write(p []byte) (n int, err error) {
302
+ if c.isClosed {
303
+ return 0, io.ErrClosedPipe
304
+ }
305
+ return c.conn.Write(p)
306
+}
307
+
308
+// Close closes the connection
309
+func (c *SecureWebSocketConn) Close() error {
310
+ if c.isClosed {
311
+ return nil
312
+ }
313
+
314
+ c.isClosed = true
315
+ return c.conn.Close()
316
+}
317
+
318
+// RemoteID returns the peer's ID (authenticated identity)
319
+func (c *SecureWebSocketConn) RemoteID() string {
320
+ return c.conn.RemoteID()
321
+}
322
+
323
+// LocalID returns this client's ID
324
+func (c *SecureWebSocketConn) LocalID() string {
325
+ return c.conn.LocalID()
326
+}
327
+
328
+// PeerID returns the target peer ID
329
+func (c *SecureWebSocketConn) PeerID() string {
330
+ return c.peerID
331
+}
332
+
333
+// ALPN returns the negotiated application-layer protocol
334
+func (c *SecureWebSocketConn) ALPN() string {
335
+ return c.alpn
336
+}
337
+
338
+// DialWebSocketSecure is a convenience function to establish E2EE WebSocket through relay
339
+//
340
+// This is a high-level helper that:
341
+// 1. Connects to relay server
342
+// 2. Creates E2EE client
343
+// 3. Dials target peer
344
+//
345
+// Parameters:
346
+// - relayURL: Relay server WebSocket URL (e.g., "ws://localhost:4017/relay")
347
+// - targetPeerID: Target peer ID to connect to
348
+// - alpn: Application protocol (e.g., "websocket")
349
+// - credential: Client credential (or nil to generate random)
350
+//
351
+// Returns:
352
+// - *SecureWebSocketConn: Established E2EE connection
353
+// - *SecureWebSocketClient: Client instance (keep alive for connection)
354
+// - error: Error if connection fails
355
+//
356
+// Example:
357
+//
358
+// conn, client, err := DialWebSocketSecure(
359
+// "ws://localhost:4017/relay",
360
+// "target-peer-id",
361
+// "websocket",
362
+// nil, // auto-generate credential
363
+// )
364
+// if err != nil {
365
+// log.Fatal(err)
366
+// }
367
+// defer conn.Close()
368
+// defer client.Close()
369
+//
370
+// conn.WriteMessage(websocket.TextMessage, []byte("Hello!"))
371
+func DialWebSocketSecure(relayURL, targetPeerID, alpn string, credential *cryptoops.Credential) (*SecureWebSocketConn, *SecureWebSocketClient, error) {
372
+ // Generate credential if not provided
373
+ if credential == nil {
374
+ var err error
375
+ credential, err = cryptoops.NewCredential()
376
+ if err != nil {
377
+ return nil, nil, fmt.Errorf("failed to generate credential: %w", err)
378
+ }
379
+ }
380
+
381
+ // Connect to relay server
382
+ relayConn, _, err := websocket.DefaultDialer.Dial(relayURL, nil)
383
+ if err != nil {
384
+ return nil, nil, fmt.Errorf("failed to connect to relay: %w", err)
385
+ }
386
+
387
+ // Wrap WebSocket connection
388
+ relayStream := &wsstream.WsStream{Conn: relayConn}
389
+
390
+ // Create E2EE client
391
+ client := NewSecureWebSocketClient(relayStream, credential)
392
+
393
+ // Dial target peer
394
+ targetURL := fmt.Sprintf("peer://%s/", targetPeerID)
395
+ conn, err := client.Dial(targetURL, alpn)
396
+ if err != nil {
397
+ client.Close()
398
+ return nil, nil, fmt.Errorf("failed to dial peer: %w", err)
399
+ }
400
+
401
+ return conn, client, nil
402
+}
relaydns/wasm/.gitignore
+2
@@ -11,6 +11,8 @@ Cargo.lock
11
!examples/*.js
12
!sw.js
13
!sw-proxy.js
14
+!secure-websocket.js
15
+!secure-websocket-sw.js
16
17
# Keep HTML files
18
!*.html
relaydns/wasm/index.html
deleted
-130
@@ -1,130 +0,0 @@
1
-<!DOCTYPE html>
2
-<html>
3
-<head>
4
- <meta charset="UTF-8">
5
- <title>RelayDNS E2EE Proxy - Ready!</title>
6
- <style>
7
- body { font-family: Arial; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; margin: 0; }
8
- .container { max-width: 800px; margin: 50px auto; background: white; padding: 40px; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.3); }
9
- h1 { color: #333; margin: 0 0 10px 0; }
10
- .subtitle { color: #666; margin-bottom: 30px; font-size: 18px; }
11
- button { padding: 15px 30px; margin: 10px 5px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 16px; font-weight: 600; }
12
- button:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4); transition: all 0.3s; }
13
- .status { padding: 20px; margin: 20px 0; border-radius: 8px; font-size: 16px; }
14
- .status.waiting { background: #fff3cd; color: #856404; }
15
- .status.success { background: #d4edda; color: #155724; }
16
- .status.error { background: #f8d7da; color: #721c24; }
17
- input { padding: 12px; width: 100%; margin: 10px 0; border: 2px solid #ddd; border-radius: 6px; font-size: 16px; box-sizing: border-box; }
18
- #log { background: #1e1e1e; color: #d4d4d4; padding: 20px; border-radius: 6px; margin-top: 20px; max-height: 400px; overflow-y: auto; font-family: 'Courier New', monospace; font-size: 14px; }
19
- .log-success { color: #4CAF50; }
20
- .log-error { color: #f44336; }
21
- .badge { display: inline-block; padding: 4px 12px; border-radius: 12px; font-size: 14px; font-weight: 600; margin-left: 10px; }
22
- .badge-green { background: #4CAF50; color: white; }
23
- .badge-red { background: #f44336; color: white; }
24
- </style>
25
-</head>
26
-<body>
27
- <div class="container">
28
- <h1>🚀 RelayDNS E2EE Network Proxy</h1>
29
- <div class="subtitle">All network requests are encrypted end-to-end</div>
30
-
31
- <div id="status" class="status waiting">⏳ Waiting for Service Worker...</div>
32
-
33
- <h2>Test Request</h2>
34
- <input id="testUrl" value="https://api.github.com/zen" placeholder="Enter URL">
35
- <button onclick="testFetch()" id="testBtn">🌐 Make Request (через E2EE прокси)</button>
36
-
37
- <div id="log"></div>
38
- </div>
39
-
40
- <script>
41
- function log(msg, type = 'info') {
42
- const logDiv = document.getElementById('log');
43
- const time = new Date().toLocaleTimeString();
44
- const className = type === 'error' ? 'log-error' : (type === 'success' ? 'log-success' : '');
45
- logDiv.innerHTML += `<div class="${className}">[${time}] ${msg}</div>`;
46
- logDiv.scrollTop = logDiv.scrollHeight;
47
- }
48
-
49
- function updateStatus(msg, type = 'waiting') {
50
- const statusDiv = document.getElementById('status');
51
- statusDiv.textContent = msg;
52
- statusDiv.className = `status ${type}`;
53
- }
54
-
55
- async function testFetch() {
56
- const url = document.getElementById('testUrl').value;
57
- try {
58
- log(`→ Fetching: ${url}`, 'info');
59
- updateStatus('Making request through E2EE tunnel...', 'waiting');
60
-
61
- const start = Date.now();
62
- const response = await fetch(url);
63
- const duration = Date.now() - start;
64
-
65
- log(`← ${response.status} ${response.statusText} (${duration}ms)`, 'success');
66
-
67
- const text = await response.text();
68
- log(`Response: ${text.substring(0, 200)}${text.length > 200 ? '...' : ''}`, 'info');
69
-
70
- updateStatus(`✓ Request completed: ${response.status}`, 'success');
71
-
72
- } catch (error) {
73
- log(`✗ Error: ${error.message}`, 'error');
74
- updateStatus(`✗ Error: ${error.message}`, 'error');
75
- }
76
- }
77
-
78
- // Initialize Service Worker automatically
79
- (async () => {
80
- try {
81
- log('Registering Service Worker...', 'info');
82
- updateStatus('🔄 Registering Service Worker...', 'waiting');
83
-
84
- const registration = await navigator.serviceWorker.register('/sw-proxy.js');
85
- log('✓ Service Worker registered', 'success');
86
-
87
- // Wait for it to be ready
88
- await navigator.serviceWorker.ready;
89
- log('✓ Service Worker active', 'success');
90
-
91
- // Check if WASM is ready
92
- const channel = new MessageChannel();
93
- const status = await new Promise((resolve) => {
94
- channel.port1.onmessage = (event) => resolve(event.data);
95
- registration.active.postMessage({ type: 'GET_STATUS' }, [channel.port2]);
96
- setTimeout(() => resolve({ success: false }), 2000);
97
- });
98
-
99
- if (status.success && status.status.wasmReady) {
100
- log('✓ WASM ProxyEngine ready!', 'success');
101
- log('✓ All external requests will be proxied through E2EE tunnel', 'success');
102
- updateStatus('✅ Ready! All requests will be encrypted end-to-end', 'success');
103
- } else {
104
- log('⚠ WASM not ready yet, requests will be direct', 'info');
105
- updateStatus('⚠ Service Worker active, but WASM still loading...', 'waiting');
106
-
107
- // Retry after a moment
108
- setTimeout(async () => {
109
- const channel2 = new MessageChannel();
110
- const status2 = await new Promise((resolve) => {
111
- channel2.port1.onmessage = (event) => resolve(event.data);
112
- registration.active.postMessage({ type: 'GET_STATUS' }, [channel2.port2]);
113
- setTimeout(() => resolve({ success: false }), 2000);
114
- });
115
-
116
- if (status2.success && status2.status.wasmReady) {
117
- log('✓ WASM ProxyEngine now ready!', 'success');
118
- updateStatus('✅ Ready! E2EE proxy is active', 'success');
119
- }
120
- }, 3000);
121
- }
122
-
123
- } catch (error) {
124
- log(`✗ Error: ${error.message}`, 'error');
125
- updateStatus(`✗ Error: ${error.message}`, 'error');
126
- }
127
- })();
128
- </script>
129
-</body>
130
-</html>
relaydns/wasm/secure-websocket-sw.js
new
+283
@@ -0,0 +1,283 @@
1
+/**
2
+ * SecureWebSocket for Service Worker
3
+ *
4
+ * SecureWebSocket implementation for use in Service Worker
5
+ * Communicates with main thread via MessageChannel
6
+ */
7
+
8
+// Service Worker global ProxyEngine (initialized in sw-proxy.js)
9
+// proxyEngine and wasmReady are provided by sw-proxy.js
10
+
11
+/**
12
+ * WebSocket tunnel manager in Service Worker
13
+ */
14
+class ServiceWorkerWebSocketTunnel {
15
+ constructor() {
16
+ this.tunnels = new Map(); // tunnelId -> tunnel info
17
+ this.messageQueues = new Map(); // tunnelId -> message queue
18
+ this.clients = new Map(); // tunnelId -> clientId
19
+ }
20
+
21
+ /**
22
+ * Create WebSocket tunnel
23
+ */
24
+ async createTunnel(url, protocols, clientId) {
25
+ if (!wasmReady || !proxyEngine) {
26
+ throw new Error('WASM ProxyEngine not ready');
27
+ }
28
+
29
+ console.log('[SW-WebSocket] Creating tunnel:', url);
30
+
31
+ try {
32
+ // Open WebSocket tunnel via WASM ProxyEngine
33
+ const result = await proxyEngine.open_websocket(url, protocols || []);
34
+ const tunnelId = result.tunnelId;
35
+ const protocol = result.protocol || '';
36
+
37
+ console.log('[SW-WebSocket] Tunnel created:', tunnelId);
38
+
39
+ // Store tunnel information
40
+ this.tunnels.set(tunnelId, {
41
+ tunnelId,
42
+ url,
43
+ protocol,
44
+ state: 'open',
45
+ created: Date.now()
46
+ });
47
+
48
+ this.messageQueues.set(tunnelId, []);
49
+ this.clients.set(tunnelId, clientId);
50
+
51
+ // Start receiving messages in background
52
+ this._startReceiving(tunnelId);
53
+
54
+ return {
55
+ tunnelId,
56
+ protocol
57
+ };
58
+
59
+ } catch (error) {
60
+ console.error('[SW-WebSocket] Failed to create tunnel:', error);
61
+ throw error;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Background message receiving loop
67
+ */
68
+ async _startReceiving(tunnelId) {
69
+ console.log('[SW-WebSocket] Starting receive loop:', tunnelId);
70
+
71
+ try {
72
+ while (this.tunnels.has(tunnelId)) {
73
+ const tunnel = this.tunnels.get(tunnelId);
74
+ if (!tunnel || tunnel.state !== 'open') {
75
+ break;
76
+ }
77
+
78
+ // Receive message from WASM
79
+ const msg = await proxyEngine.receive_websocket_message(tunnelId);
80
+
81
+ console.log('[SW-WebSocket] Received message:', msg.type);
82
+
83
+ // Add to message queue
84
+ const queue = this.messageQueues.get(tunnelId);
85
+ if (queue) {
86
+ queue.push(msg);
87
+ }
88
+
89
+ // Notify client
90
+ this._notifyClient(tunnelId, msg);
91
+
92
+ // Handle close message
93
+ if (msg.type === 'close') {
94
+ console.log('[SW-WebSocket] Tunnel closed:', tunnelId);
95
+ this._closeTunnel(tunnelId);
96
+ break;
97
+ }
98
+ }
99
+
100
+ } catch (error) {
101
+ console.error('[SW-WebSocket] Receive loop error:', error);
102
+ this._closeTunnel(tunnelId, 1006, error.toString());
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Notify client of message
108
+ */
109
+ async _notifyClient(tunnelId, message) {
110
+ const clientId = this.clients.get(tunnelId);
111
+ if (!clientId) return;
112
+
113
+ try {
114
+ const client = await self.clients.get(clientId);
115
+ if (client) {
116
+ client.postMessage({
117
+ type: 'WEBSOCKET_MESSAGE',
118
+ tunnelId,
119
+ message
120
+ });
121
+ }
122
+ } catch (error) {
123
+ console.error('[SW-WebSocket] Failed to notify client:', error);
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Send message
129
+ */
130
+ async sendMessage(tunnelId, data, isBinary) {
131
+ if (!this.tunnels.has(tunnelId)) {
132
+ throw new Error('Tunnel not found: ' + tunnelId);
133
+ }
134
+
135
+ console.log('[SW-WebSocket] Sending message:', tunnelId, isBinary ? 'binary' : 'text');
136
+
137
+ try {
138
+ await proxyEngine.send_websocket_message(tunnelId, data, isBinary);
139
+ } catch (error) {
140
+ console.error('[SW-WebSocket] Send failed:', error);
141
+ throw error;
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Close tunnel
147
+ */
148
+ async closeTunnel(tunnelId, code = 1000, reason = '') {
149
+ if (!this.tunnels.has(tunnelId)) {
150
+ return;
151
+ }
152
+
153
+ console.log('[SW-WebSocket] Closing tunnel:', tunnelId, code, reason);
154
+
155
+ try {
156
+ await proxyEngine.close_websocket(tunnelId, code, reason);
157
+ } catch (error) {
158
+ console.error('[SW-WebSocket] Close failed:', error);
159
+ }
160
+
161
+ this._closeTunnel(tunnelId);
162
+ }
163
+
164
+ /**
165
+ * Internal tunnel cleanup
166
+ */
167
+ _closeTunnel(tunnelId, code = 1000, reason = '') {
168
+ const tunnel = this.tunnels.get(tunnelId);
169
+ if (tunnel) {
170
+ tunnel.state = 'closed';
171
+ }
172
+
173
+ // Cleanup
174
+ this.tunnels.delete(tunnelId);
175
+ this.messageQueues.delete(tunnelId);
176
+ this.clients.delete(tunnelId);
177
+
178
+ console.log('[SW-WebSocket] Tunnel cleaned up:', tunnelId);
179
+ }
180
+
181
+ /**
182
+ * Get tunnel state
183
+ */
184
+ getTunnelState(tunnelId) {
185
+ const tunnel = this.tunnels.get(tunnelId);
186
+ return tunnel ? tunnel.state : 'closed';
187
+ }
188
+
189
+ /**
190
+ * Get all tunnel information
191
+ */
192
+ getAllTunnels() {
193
+ return Array.from(this.tunnels.values());
194
+ }
195
+}
196
+
197
+// Global tunnel manager instance
198
+let tunnelManager = null;
199
+
200
+/**
201
+ * Initialize tunnel manager
202
+ */
203
+function initTunnelManager() {
204
+ if (!tunnelManager) {
205
+ tunnelManager = new ServiceWorkerWebSocketTunnel();
206
+ console.log('[SW-WebSocket] Tunnel manager initialized');
207
+ }
208
+ return tunnelManager;
209
+}
210
+
211
+/**
212
+ * WebSocket message handler to add to Service Worker
213
+ */
214
+async function handleWebSocketMessage(event) {
215
+ const { type, tunnelId, url, protocols, data, isBinary, code, reason } = event.data || {};
216
+ const manager = initTunnelManager();
217
+
218
+ switch (type) {
219
+ case 'WEBSOCKET_OPEN':
220
+ try {
221
+ const clientId = event.source?.id || event.clientId;
222
+ const result = await manager.createTunnel(url, protocols, clientId);
223
+
224
+ event.ports[0]?.postMessage({
225
+ success: true,
226
+ result
227
+ });
228
+ } catch (error) {
229
+ event.ports[0]?.postMessage({
230
+ success: false,
231
+ error: error.toString()
232
+ });
233
+ }
234
+ break;
235
+
236
+ case 'WEBSOCKET_SEND':
237
+ try {
238
+ await manager.sendMessage(tunnelId, data, isBinary);
239
+ event.ports[0]?.postMessage({ success: true });
240
+ } catch (error) {
241
+ event.ports[0]?.postMessage({
242
+ success: false,
243
+ error: error.toString()
244
+ });
245
+ }
246
+ break;
247
+
248
+ case 'WEBSOCKET_CLOSE':
249
+ try {
250
+ await manager.closeTunnel(tunnelId, code, reason);
251
+ event.ports[0]?.postMessage({ success: true });
252
+ } catch (error) {
253
+ event.ports[0]?.postMessage({
254
+ success: false,
255
+ error: error.toString()
256
+ });
257
+ }
258
+ break;
259
+
260
+ case 'WEBSOCKET_STATE':
261
+ const state = manager.getTunnelState(tunnelId);
262
+ event.ports[0]?.postMessage({
263
+ success: true,
264
+ state
265
+ });
266
+ break;
267
+
268
+ case 'WEBSOCKET_LIST':
269
+ const tunnels = manager.getAllTunnels();
270
+ event.ports[0]?.postMessage({
271
+ success: true,
272
+ tunnels
273
+ });
274
+ break;
275
+
276
+ default:
277
+ return false; // Not handled
278
+ }
279
+
280
+ return true; // Handled
281
+}
282
+
283
+console.log('[SW-WebSocket] Service Worker WebSocket module loaded');
relaydns/wasm/secure-websocket.js
new
+710
@@ -0,0 +1,710 @@
1
+/**
2
+ * SecureWebSocket - E2EE WebSocket Polyfill using WASM ProxyEngine
3
+ *
4
+ * Provides transparent end-to-end encryption for WebSocket connections
5
+ * through the RelayDNS WASM ProxyEngine.
6
+ */
7
+
8
+// Global WASM instance cache
9
+let wasmInstance = null;
10
+let wasmInitPromise = null;
11
+
12
+/**
13
+ * Get Relay server URL from server or config
14
+ * @returns {Promise<string>} Relay server WebSocket URL
15
+ */
16
+async function getRelayUrl() {
17
+ // 1. Check if manually configured
18
+ if (window.RELAYDNS_RELAY_URL) {
19
+ console.log('[SecureWebSocket] Using configured relay URL:', window.RELAYDNS_RELAY_URL);
20
+ return window.RELAYDNS_RELAY_URL;
21
+ }
22
+
23
+ // 2. Try to get from server API
24
+ try {
25
+ const response = await fetch('/api/relay-info');
26
+ if (response.ok) {
27
+ const data = await response.json();
28
+ if (data.relayUrl) {
29
+ console.log('[SecureWebSocket] Got relay URL from server:', data.relayUrl);
30
+ return data.relayUrl;
31
+ }
32
+ }
33
+ } catch (error) {
34
+ console.warn('[SecureWebSocket] Failed to fetch relay info from server:', error.message);
35
+ }
36
+
37
+ // 3. Auto-detect from current location
38
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
39
+ const host = window.location.host;
40
+ const autoUrl = `${protocol}//${host}/relay`;
41
+
42
+ console.log('[SecureWebSocket] Auto-detected relay URL:', autoUrl);
43
+ return autoUrl;
44
+}
45
+
46
+/**
47
+ * Check if Service Worker is available and has WebSocket support
48
+ * @returns {Promise<boolean>}
49
+ */
50
+async function hasServiceWorkerWebSocket() {
51
+ if (!navigator.serviceWorker || !navigator.serviceWorker.controller) {
52
+ return false;
53
+ }
54
+
55
+ try {
56
+ const channel = new MessageChannel();
57
+ const response = await new Promise((resolve) => {
58
+ channel.port1.onmessage = (event) => resolve(event.data);
59
+ navigator.serviceWorker.controller.postMessage(
60
+ { type: 'GET_STATUS' },
61
+ [channel.port2]
62
+ );
63
+ setTimeout(() => resolve({ success: false }), 1000);
64
+ });
65
+
66
+ return response.success && response.status?.hasWebSocket;
67
+ } catch {
68
+ return false;
69
+ }
70
+}
71
+
72
+/**
73
+ * Initialize and get the WASM ProxyEngine instance
74
+ * @returns {Promise<Object>} WASM module with ProxyEngine
75
+ */
76
+async function getProxyEngine() {
77
+ if (wasmInstance) {
78
+ return wasmInstance;
79
+ }
80
+
81
+ if (wasmInitPromise) {
82
+ return wasmInitPromise;
83
+ }
84
+
85
+ wasmInitPromise = (async () => {
86
+ try {
87
+ // Check if we should use Service Worker
88
+ const useServiceWorker = await hasServiceWorkerWebSocket();
89
+
90
+ if (useServiceWorker) {
91
+ console.log('[SecureWebSocket] Using Service Worker for WebSocket');
92
+ wasmInstance = {
93
+ engine: null,
94
+ wasm: null,
95
+ useServiceWorker: true
96
+ };
97
+ return wasmInstance;
98
+ }
99
+
100
+ // Fallback to direct WASM
101
+ console.log('[SecureWebSocket] Using direct WASM');
102
+
103
+ // Load WASM module
104
+ if (typeof wasm_bindgen === 'undefined') {
105
+ throw new Error('WASM module not loaded. Include relaydns_wasm.js first.');
106
+ }
107
+
108
+ // Initialize WASM
109
+ await wasm_bindgen('/pkg/relaydns_wasm_bg.wasm');
110
+
111
+ // Get relay server URL
112
+ const relayUrl = await getRelayUrl();
113
+
114
+ // Create ProxyEngine instance
115
+ const engine = new wasm_bindgen.ProxyEngine(relayUrl);
116
+
117
+ console.log('[SecureWebSocket] WASM ProxyEngine initialized:', relayUrl);
118
+
119
+ wasmInstance = {
120
+ engine,
121
+ wasm: wasm_bindgen,
122
+ useServiceWorker: false
123
+ };
124
+
125
+ return wasmInstance;
126
+
127
+ } catch (error) {
128
+ console.error('[SecureWebSocket] Failed to initialize WASM:', error);
129
+ wasmInitPromise = null;
130
+ throw error;
131
+ }
132
+ })();
133
+
134
+ return wasmInitPromise;
135
+}
136
+
137
+/**
138
+ * SecureWebSocket - Drop-in replacement for native WebSocket with E2EE
139
+ */
140
+class SecureWebSocket extends EventTarget {
141
+ /**
142
+ * @param {string} url - WebSocket URL
143
+ * @param {string|string[]} protocols - Optional subprotocols
144
+ */
145
+ constructor(url, protocols = []) {
146
+ super();
147
+
148
+ // Normalize protocols
149
+ if (typeof protocols === 'string') {
150
+ protocols = [protocols];
151
+ }
152
+
153
+ // Public properties (read-only)
154
+ Object.defineProperties(this, {
155
+ url: { value: url, writable: false, enumerable: true },
156
+ protocols: { value: protocols, writable: false, enumerable: true },
157
+ });
158
+
159
+ // Internal state
160
+ this._readyState = WebSocket.CONNECTING;
161
+ this._protocol = '';
162
+ this._tunnelId = null;
163
+ this._bufferedAmount = 0;
164
+ this._extensions = '';
165
+ this._binaryType = 'blob';
166
+
167
+ // Event handlers (nullable)
168
+ this.onopen = null;
169
+ this.onmessage = null;
170
+ this.onerror = null;
171
+ this.onclose = null;
172
+
173
+ // Start connection
174
+ this._connect();
175
+ }
176
+
177
+ // Public properties with getters
178
+ get readyState() { return this._readyState; }
179
+ get protocol() { return this._protocol; }
180
+ get bufferedAmount() { return this._bufferedAmount; }
181
+ get extensions() { return this._extensions; }
182
+ get binaryType() { return this._binaryType; }
183
+ set binaryType(value) {
184
+ if (value === 'blob' || value === 'arraybuffer') {
185
+ this._binaryType = value;
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Initialize connection through WASM ProxyEngine
191
+ * @private
192
+ */
193
+ async _connect() {
194
+ try {
195
+ console.log('[SecureWebSocket] Connecting to:', this.url);
196
+
197
+ // Get WASM ProxyEngine or Service Worker
198
+ const instance = await getProxyEngine();
199
+
200
+ if (instance.useServiceWorker) {
201
+ // Use Service Worker
202
+ await this._connectViaServiceWorker();
203
+ } else {
204
+ // Use direct WASM
205
+ await this._connectViaDirect(instance.engine);
206
+ }
207
+
208
+ } catch (error) {
209
+ console.error('[SecureWebSocket] Connection failed:', error);
210
+
211
+ this._readyState = WebSocket.CLOSED;
212
+
213
+ // Dispatch error event
214
+ this._dispatchEvent('error', {
215
+ message: error.toString(),
216
+ error: error
217
+ });
218
+
219
+ // Dispatch close event
220
+ this._dispatchEvent('close', {
221
+ code: 1006,
222
+ reason: error.toString(),
223
+ wasClean: false
224
+ });
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Connect via Service Worker
230
+ * @private
231
+ */
232
+ async _connectViaServiceWorker() {
233
+ console.log('[SecureWebSocket] Connecting via Service Worker');
234
+
235
+ const channel = new MessageChannel();
236
+ const response = await new Promise((resolve, reject) => {
237
+ channel.port1.onmessage = (event) => {
238
+ if (event.data.success) {
239
+ resolve(event.data.result);
240
+ } else {
241
+ reject(new Error(event.data.error));
242
+ }
243
+ };
244
+
245
+ navigator.serviceWorker.controller.postMessage(
246
+ {
247
+ type: 'WEBSOCKET_OPEN',
248
+ url: this.url,
249
+ protocols: this.protocols
250
+ },
251
+ [channel.port2]
252
+ );
253
+
254
+ setTimeout(() => reject(new Error('Service Worker timeout')), 10000);
255
+ });
256
+
257
+ this._tunnelId = response.tunnelId;
258
+ this._protocol = response.protocol || '';
259
+ this._readyState = WebSocket.OPEN;
260
+ this._useServiceWorker = true;
261
+
262
+ console.log('[SecureWebSocket] Connected via SW! Tunnel ID:', this._tunnelId);
263
+
264
+ // Dispatch open event
265
+ this._dispatchEvent('open', {});
266
+
267
+ // Listen for messages from Service Worker
268
+ this._listenToServiceWorker();
269
+ }
270
+
271
+ /**
272
+ * Connect via direct WASM
273
+ * @private
274
+ */
275
+ async _connectViaDirect(engine) {
276
+ console.log('[SecureWebSocket] Connecting via direct WASM');
277
+
278
+ // Open WebSocket tunnel through E2EE proxy
279
+ const result = await engine.open_websocket(this.url, this.protocols);
280
+
281
+ this._tunnelId = result.tunnelId;
282
+ this._protocol = result.protocol || '';
283
+ this._readyState = WebSocket.OPEN;
284
+ this._useServiceWorker = false;
285
+
286
+ console.log('[SecureWebSocket] Connected! Tunnel ID:', this._tunnelId);
287
+
288
+ // Dispatch open event
289
+ this._dispatchEvent('open', {});
290
+
291
+ // Start receiving messages in background
292
+ this._receiveLoop(engine);
293
+ }
294
+
295
+ /**
296
+ * Listen to Service Worker messages
297
+ * @private
298
+ */
299
+ _listenToServiceWorker() {
300
+ const handler = (event) => {
301
+ if (event.data.type === 'WEBSOCKET_MESSAGE' &&
302
+ event.data.tunnelId === this._tunnelId) {
303
+
304
+ const msg = event.data.message;
305
+ this._handleMessage(msg);
306
+ }
307
+ };
308
+
309
+ navigator.serviceWorker.addEventListener('message', handler);
310
+ this._swMessageHandler = handler;
311
+ }
312
+
313
+ /**
314
+ * Handle incoming message
315
+ * @private
316
+ */
317
+ _handleMessage(msg) {
318
+ if (msg.type === 'text') {
319
+ // Text message
320
+ this._dispatchEvent('message', {
321
+ data: msg.data,
322
+ type: 'message',
323
+ origin: this.url
324
+ });
325
+
326
+ } else if (msg.type === 'binary') {
327
+ // Binary message
328
+ let data;
329
+ if (this._binaryType === 'arraybuffer') {
330
+ data = new Uint8Array(msg.data).buffer;
331
+ } else {
332
+ data = new Blob([new Uint8Array(msg.data)]);
333
+ }
334
+
335
+ this._dispatchEvent('message', {
336
+ data: data,
337
+ type: 'message',
338
+ origin: this.url
339
+ });
340
+
341
+ } else if (msg.type === 'close') {
342
+ // Close message
343
+ console.log('[SecureWebSocket] Received close:', msg.code, msg.reason);
344
+
345
+ this._readyState = WebSocket.CLOSED;
346
+
347
+ this._dispatchEvent('close', {
348
+ code: msg.code || 1000,
349
+ reason: msg.reason || '',
350
+ wasClean: true
351
+ });
352
+
353
+ // Cleanup Service Worker listener
354
+ if (this._swMessageHandler) {
355
+ navigator.serviceWorker.removeEventListener('message', this._swMessageHandler);
356
+ }
357
+ }
358
+ }
359
+
360
+ /**
361
+ * Background loop to receive messages from tunnel
362
+ * @private
363
+ * @param {Object} engine - WASM ProxyEngine instance
364
+ */
365
+ async _receiveLoop(engine) {
366
+ try {
367
+ while (this._readyState !== WebSocket.CLOSED && this._readyState !== WebSocket.CLOSING) {
368
+ // Receive message from tunnel
369
+ const msg = await engine.receive_websocket_message(this._tunnelId);
370
+
371
+ if (msg.type === 'text') {
372
+ // Text message
373
+ this._dispatchEvent('message', {
374
+ data: msg.data,
375
+ type: 'message',
376
+ origin: this.url
377
+ });
378
+
379
+ } else if (msg.type === 'binary') {
380
+ // Binary message
381
+ let data;
382
+ if (this._binaryType === 'arraybuffer') {
383
+ data = new Uint8Array(msg.data).buffer;
384
+ } else {
385
+ // Convert to Blob
386
+ data = new Blob([new Uint8Array(msg.data)]);
387
+ }
388
+
389
+ this._dispatchEvent('message', {
390
+ data: data,
391
+ type: 'message',
392
+ origin: this.url
393
+ });
394
+
395
+ } else if (msg.type === 'close') {
396
+ // Close message
397
+ console.log('[SecureWebSocket] Received close:', msg.code, msg.reason);
398
+
399
+ this._readyState = WebSocket.CLOSED;
400
+
401
+ this._dispatchEvent('close', {
402
+ code: msg.code || 1000,
403
+ reason: msg.reason || '',
404
+ wasClean: true
405
+ });
406
+
407
+ break;
408
+ }
409
+ }
410
+
411
+ } catch (error) {
412
+ console.error('[SecureWebSocket] Receive loop error:', error);
413
+
414
+ if (this._readyState !== WebSocket.CLOSED) {
415
+ this._dispatchEvent('error', {
416
+ message: error.toString(),
417
+ error: error
418
+ });
419
+
420
+ this.close(1006, error.toString());
421
+ }
422
+ }
423
+ }
424
+
425
+ /**
426
+ * Send data through the secure tunnel
427
+ * @param {string|ArrayBuffer|Uint8Array|Blob} data - Data to send
428
+ */
429
+ send(data) {
430
+ if (this._readyState !== WebSocket.OPEN) {
431
+ throw new DOMException(
432
+ 'Failed to execute \'send\' on \'WebSocket\': Still in CONNECTING state.',
433
+ 'InvalidStateError'
434
+ );
435
+ }
436
+
437
+ // Handle different data types
438
+ if (typeof data === 'string') {
439
+ // Text message
440
+ this._sendMessage(data, false);
441
+
442
+ } else if (data instanceof ArrayBuffer) {
443
+ // Binary ArrayBuffer
444
+ this._sendMessage(new Uint8Array(data), true);
445
+
446
+ } else if (data instanceof Uint8Array) {
447
+ // Binary Uint8Array
448
+ this._sendMessage(data, true);
449
+
450
+ } else if (data instanceof Blob) {
451
+ // Blob - convert to ArrayBuffer
452
+ this._bufferedAmount += data.size;
453
+
454
+ data.arrayBuffer().then(buffer => {
455
+ this._sendMessage(new Uint8Array(buffer), true);
456
+ this._bufferedAmount = Math.max(0, this._bufferedAmount - data.size);
457
+ });
458
+
459
+ } else {
460
+ throw new TypeError('Data must be string, ArrayBuffer, Uint8Array, or Blob');
461
+ }
462
+ }
463
+
464
+ /**
465
+ * Send message through WASM ProxyEngine
466
+ * @private
467
+ * @param {string|Uint8Array} data - Data to send
468
+ * @param {boolean} isBinary - Whether data is binary
469
+ */
470
+ async _sendMessage(data, isBinary) {
471
+ try {
472
+ // Estimate buffer size
473
+ const size = typeof data === 'string' ? data.length : data.length;
474
+ this._bufferedAmount += size;
475
+
476
+ if (this._useServiceWorker) {
477
+ // Send via Service Worker
478
+ const channel = new MessageChannel();
479
+ await new Promise((resolve, reject) => {
480
+ channel.port1.onmessage = (event) => {
481
+ if (event.data.success) {
482
+ resolve();
483
+ } else {
484
+ reject(new Error(event.data.error));
485
+ }
486
+ };
487
+
488
+ navigator.serviceWorker.controller.postMessage(
489
+ {
490
+ type: 'WEBSOCKET_SEND',
491
+ tunnelId: this._tunnelId,
492
+ data: data,
493
+ isBinary: isBinary
494
+ },
495
+ [channel.port2]
496
+ );
497
+
498
+ setTimeout(() => reject(new Error('Send timeout')), 5000);
499
+ });
500
+ } else {
501
+ // Send via direct WASM
502
+ const { engine } = await getProxyEngine();
503
+ await engine.send_websocket_message(this._tunnelId, data, isBinary);
504
+ }
505
+
506
+ // Decrement buffered amount
507
+ this._bufferedAmount = Math.max(0, this._bufferedAmount - size);
508
+
509
+ } catch (error) {
510
+ console.error('[SecureWebSocket] Send failed:', error);
511
+
512
+ this._dispatchEvent('error', {
513
+ message: error.toString(),
514
+ error: error
515
+ });
516
+ }
517
+ }
518
+
519
+ /**
520
+ * Close the WebSocket connection
521
+ * @param {number} code - Close code (default 1000)
522
+ * @param {string} reason - Close reason (default empty)
523
+ */
524
+ close(code = 1000, reason = '') {
525
+ if (this._readyState === WebSocket.CLOSED || this._readyState === WebSocket.CLOSING) {
526
+ return;
527
+ }
528
+
529
+ console.log('[SecureWebSocket] Closing:', code, reason);
530
+
531
+ this._readyState = WebSocket.CLOSING;
532
+
533
+ // Close tunnel
534
+ (async () => {
535
+ try {
536
+ if (this._useServiceWorker) {
537
+ // Close via Service Worker
538
+ const channel = new MessageChannel();
539
+ await new Promise((resolve, reject) => {
540
+ channel.port1.onmessage = (event) => {
541
+ if (event.data.success) {
542
+ resolve();
543
+ } else {
544
+ reject(new Error(event.data.error));
545
+ }
546
+ };
547
+
548
+ navigator.serviceWorker.controller.postMessage(
549
+ {
550
+ type: 'WEBSOCKET_CLOSE',
551
+ tunnelId: this._tunnelId,
552
+ code: code,
553
+ reason: reason
554
+ },
555
+ [channel.port2]
556
+ );
557
+
558
+ setTimeout(() => resolve(), 2000); // Don't wait forever
559
+ });
560
+ } else {
561
+ // Close via direct WASM
562
+ const { engine } = await getProxyEngine();
563
+ await engine.close_websocket(this._tunnelId, code, reason);
564
+ }
565
+ } catch (error) {
566
+ console.error('[SecureWebSocket] Close failed:', error);
567
+
568
+ // Force close
569
+ this._readyState = WebSocket.CLOSED;
570
+ this._dispatchEvent('close', {
571
+ code: 1006,
572
+ reason: error.toString(),
573
+ wasClean: false
574
+ });
575
+ }
576
+ })();
577
+ }
578
+
579
+ /**
580
+ * Dispatch event to both EventTarget and legacy handler
581
+ * @private
582
+ * @param {string} type - Event type
583
+ * @param {Object} detail - Event details
584
+ */
585
+ _dispatchEvent(type, detail) {
586
+ // Create event
587
+ const event = new Event(type);
588
+ Object.assign(event, detail);
589
+
590
+ // Dispatch to EventTarget listeners
591
+ this.dispatchEvent(event);
592
+
593
+ // Call legacy handler if exists
594
+ const handler = this[`on${type}`];
595
+ if (typeof handler === 'function') {
596
+ try {
597
+ handler.call(this, event);
598
+ } catch (error) {
599
+ console.error(`[SecureWebSocket] Error in on${type} handler:`, error);
600
+ }
601
+ }
602
+ }
603
+}
604
+
605
+// Static constants (same as native WebSocket)
606
+SecureWebSocket.CONNECTING = 0;
607
+SecureWebSocket.OPEN = 1;
608
+SecureWebSocket.CLOSING = 2;
609
+SecureWebSocket.CLOSED = 3;
610
+
611
+// ==============================================================================
612
+// POLYFILL: Replace native WebSocket with SecureWebSocket
613
+// ==============================================================================
614
+
615
+(function() {
616
+ // Save reference to native WebSocket
617
+ const NativeWebSocket = window.WebSocket;
618
+
619
+ // Configuration
620
+ const config = {
621
+ // Enable E2EE for all WebSockets by default
622
+ enabled: window.RELAYDNS_E2EE_ENABLED !== false,
623
+
624
+ // Patterns to intercept (regex strings)
625
+ interceptPatterns: window.RELAYDNS_INTERCEPT_PATTERNS || [
626
+ '.*' // Intercept all by default
627
+ ],
628
+
629
+ // Patterns to bypass (regex strings) - takes precedence
630
+ bypassPatterns: window.RELAYDNS_BYPASS_PATTERNS || [
631
+ '^wss?://localhost:4017/', // Don't intercept relay server itself
632
+ '^wss?://localhost:8000/', // Don't intercept local dev server
633
+ '^wss?://127\\.0\\.0\\.1', // Don't intercept loopback
634
+ ],
635
+
636
+ // Debug mode
637
+ debug: window.RELAYDNS_DEBUG || false
638
+ };
639
+
640
+ /**
641
+ * Check if URL should be intercepted for E2EE
642
+ * @param {string} url - WebSocket URL
643
+ * @returns {boolean} True if should intercept
644
+ */
645
+ function shouldIntercept(url) {
646
+ if (!config.enabled) {
647
+ return false;
648
+ }
649
+
650
+ // Check bypass patterns first (higher priority)
651
+ for (const pattern of config.bypassPatterns) {
652
+ const regex = new RegExp(pattern);
653
+ if (regex.test(url)) {
654
+ if (config.debug) {
655
+ console.log('[SecureWebSocket] Bypassing (matched bypass pattern):', url);
656
+ }
657
+ return false;
658
+ }
659
+ }
660
+
661
+ // Check intercept patterns
662
+ for (const pattern of config.interceptPatterns) {
663
+ const regex = new RegExp(pattern);
664
+ if (regex.test(url)) {
665
+ if (config.debug) {
666
+ console.log('[SecureWebSocket] Intercepting (matched intercept pattern):', url);
667
+ }
668
+ return true;
669
+ }
670
+ }
671
+
672
+ if (config.debug) {
673
+ console.log('[SecureWebSocket] Not intercepting (no match):', url);
674
+ }
675
+ return false;
676
+ }
677
+
678
+ /**
679
+ * Polyfilled WebSocket constructor
680
+ * @param {string} url - WebSocket URL
681
+ * @param {string|string[]} protocols - Optional subprotocols
682
+ * @returns {WebSocket|SecureWebSocket}
683
+ */
684
+ window.WebSocket = function(url, protocols) {
685
+ if (shouldIntercept(url)) {
686
+ // Use E2EE SecureWebSocket
687
+ console.log('[SecureWebSocket] 🔒 Creating encrypted WebSocket:', url);
688
+ return new SecureWebSocket(url, protocols);
689
+ } else {
690
+ // Use native WebSocket
691
+ if (config.debug) {
692
+ console.log('[SecureWebSocket] Creating native WebSocket:', url);
693
+ }
694
+ return new NativeWebSocket(url, protocols);
695
+ }
696
+ };
697
+
698
+ // Copy static properties from native WebSocket
699
+ window.WebSocket.CONNECTING = NativeWebSocket.CONNECTING;
700
+ window.WebSocket.OPEN = NativeWebSocket.OPEN;
701
+ window.WebSocket.CLOSING = NativeWebSocket.CLOSING;
702
+ window.WebSocket.CLOSED = NativeWebSocket.CLOSED;
703
+
704
+ // Expose SecureWebSocket class for direct access if needed
705
+ window.SecureWebSocket = SecureWebSocket;
706
+ window.NativeWebSocket = NativeWebSocket;
707
+
708
+ console.log('[SecureWebSocket] ✅ Polyfill installed. E2EE enabled:', config.enabled);
709
+
710
+})();
relaydns/wasm/sw-proxy.js
+149
-24
@@ -4,34 +4,110 @@
4
const CACHE_NAME = 'relaydns-proxy-v1';
5
let proxyEngine = null;
6
let wasmReady = false;
7
+let initializationPromise = null;
8
8
-// Install event - Load WASM here (only time importScripts is allowed)
9
+// Dynamic WASM initialization (can be called anytime, not just install)
10
+async function initializeProxyEngine() {
11
+ // If already initializing, wait for it
12
+ if (initializationPromise) {
13
+ return initializationPromise;
14
+ }
15
+
16
+ // If already initialized, return immediately
17
+ if (proxyEngine && wasmReady) {
18
+ return proxyEngine;
19
+ }
20
+
21
+ initializationPromise = (async () => {
22
+ try {
23
+ console.log('[SW-Proxy] Dynamically initializing ProxyEngine...');
24
+
25
+ // Check if wasm_bindgen is available (loaded during install)
26
+ if (typeof wasm_bindgen === 'undefined') {
27
+ console.log('[SW-Proxy] wasm_bindgen not available, loading via dynamic import...');
28
+
29
+ // Use dynamic import for ES6 modules
30
+ const wasmModule = await import('/pkg/relaydns_wasm.js');
31
+
32
+ // Make wasm_bindgen available globally
33
+ self.wasm_bindgen = wasmModule;
34
+
35
+ console.log('[SW-Proxy] ✓ WASM JS module loaded');
36
+ }
37
+
38
+ // Check if SecureWebSocket SW module is loaded
39
+ if (typeof ServiceWorkerWebSocketTunnel === 'undefined') {
40
+ console.log('[SW-Proxy] SecureWebSocket SW module not available, loading via fetch...');
41
+
42
+ const swWsResponse = await fetch('/secure-websocket-sw.js');
43
+ const swWsCode = await swWsResponse.text();
44
+
45
+ // Use indirect eval to execute in global scope
46
+ (1, eval)(swWsCode);
47
+ console.log('[SW-Proxy] ✓ SecureWebSocket SW module loaded');
48
+ }
49
+
50
+ // Initialize WASM module if not already done
51
+ if (!wasmReady) {
52
+ console.log('[SW-Proxy] Initializing WASM module...');
53
+
54
+ // wasm_bindgen is now the module object from dynamic import
55
+ const initWasm = self.wasm_bindgen.default || self.wasm_bindgen;
56
+ await initWasm('/pkg/relaydns_wasm_bg.wasm');
57
+
58
+ console.log('[SW-Proxy] ✓ WASM module initialized');
59
+ }
60
+
61
+ // Get relay URL from API
62
+ let relayUrl = 'ws://localhost:4017/relay'; // Default fallback
63
+ try {
64
+ const relayInfoResponse = await fetch('/api/relay-info');
65
+ if (relayInfoResponse.ok) {
66
+ const relayInfo = await relayInfoResponse.json();
67
+ if (relayInfo.relayUrl) {
68
+ relayUrl = relayInfo.relayUrl;
69
+ console.log('[SW-Proxy] Got relay URL from server:', relayUrl);
70
+ }
71
+ }
72
+ } catch (e) {
73
+ console.warn('[SW-Proxy] Failed to fetch relay URL, using default:', e.message);
74
+ }
75
+
76
+ // Create ProxyEngine
77
+ console.log('[SW-Proxy] Creating ProxyEngine with URL:', relayUrl);
78
+ const ProxyEngine = self.wasm_bindgen.ProxyEngine;
79
+ proxyEngine = new ProxyEngine(relayUrl);
80
+ wasmReady = true;
81
+
82
+ console.log('[SW-Proxy] ✓ ProxyEngine ready');
83
+ return proxyEngine;
84
+
85
+ } catch (error) {
86
+ console.error('[SW-Proxy] Failed to initialize ProxyEngine:', error);
87
+ initializationPromise = null; // Reset so we can retry
88
+ wasmReady = false;
89
+ throw error;
90
+ }
91
+ })();
92
+
93
+ return initializationPromise;
94
+}
95
+
96
+// Install event - Skip importScripts since WASM is ES6 module
97
self.addEventListener('install', (event) => {
98
console.log('[SW-Proxy] Installing...');
99
100
event.waitUntil(
101
(async () => {
102
try {
15
- console.log('[SW-Proxy] Loading WASM module during install...');
16
-
17
- // importScripts can ONLY be called during install
18
- self.importScripts('/pkg/relaydns_wasm_sw.js');
19
- console.log('[SW-Proxy] ✓ WASM script loaded');
103
+ console.log('[SW-Proxy] Service Worker installing...');
104
21
- // Initialize WASM
22
- console.log('[SW-Proxy] Initializing WASM...');
23
- await wasm_bindgen('/pkg/relaydns_wasm_sw_bg.wasm');
24
- console.log('[SW-Proxy] ✓ WASM initialized');
25
-
26
- // Create proxy engine
27
- console.log('[SW-Proxy] Creating ProxyEngine...');
28
- proxyEngine = new wasm_bindgen.ProxyEngine('ws://localhost:4017/relay');
29
- wasmReady = true;
30
- console.log('[SW-Proxy] ✓ Proxy engine ready');
105
+ // Note: We cannot use importScripts with ES6 modules
106
+ // WASM will be loaded dynamically on first request via fetch + dynamic import
107
+ console.log('[SW-Proxy] WASM will be loaded dynamically on first request');
108
109
} catch (error) {
33
- console.error('[SW-Proxy] Failed to initialize WASM:', error);
34
- throw error;
110
+ console.error('[SW-Proxy] Install error:', error);
111
}
112
113
// Skip waiting to activate immediately
@@ -64,9 +140,31 @@ function shouldProxy(url) {
140
// Handle HTTP request through WASM proxy
141
async function proxyHttpRequest(request) {
142
try {
143
+ // Ensure ProxyEngine is initialized (lazy init if needed)
144
if (!wasmReady || !proxyEngine) {
68
- console.warn('[SW-Proxy] WASM not ready, falling back to direct fetch');
69
- return fetch(request);
145
+ console.log('[SW-Proxy] ProxyEngine not ready, attempting lazy initialization...');
146
+ try {
147
+ await initializeProxyEngine();
148
+ } catch (initError) {
149
+ // Security: Never fallback to direct fetch - this would bypass E2EE!
150
+ console.error('[SW-Proxy] Failed to initialize ProxyEngine:', initError);
151
+ return new Response(
152
+ JSON.stringify({
153
+ error: 'E2EE ProxyEngine Initialization Failed',
154
+ message: 'Could not initialize secure proxy. Please refresh the page.',
155
+ code: 'PROXY_ENGINE_INIT_FAILED',
156
+ details: initError.message
157
+ }),
158
+ {
159
+ status: 503,
160
+ statusText: 'Service Unavailable',
161
+ headers: {
162
+ 'Content-Type': 'application/json',
163
+ 'X-E2EE-Status': 'init-failed'
164
+ }
165
+ }
166
+ );
167
+ }
168
}
169
170
console.log('[SW-Proxy] Proxying:', request.method, request.url);
@@ -113,9 +211,22 @@ async function proxyHttpRequest(request) {
211
212
} catch (error) {
213
console.error('[SW-Proxy] Proxy error:', error);
116
- // Fallback to direct fetch on error
117
- console.log('[SW-Proxy] Falling back to direct fetch');
118
- return fetch(request);
214
+ // Security: Never fallback to direct fetch - return error instead
215
+ return new Response(
216
+ JSON.stringify({
217
+ error: 'E2EE Proxy Error',
218
+ message: error.message || 'Failed to proxy request through E2EE tunnel',
219
+ code: 'PROXY_ERROR'
220
+ }),
221
+ {
222
+ status: 502,
223
+ statusText: 'Bad Gateway',
224
+ headers: {
225
+ 'Content-Type': 'application/json',
226
+ 'X-E2EE-Status': 'error'
227
+ }
228
+ }
229
+ );
230
}
231
}
232
@@ -137,13 +248,27 @@ self.addEventListener('fetch', (event) => {
248
self.addEventListener('message', (event) => {
249
const { type } = event.data || {};
250
251
+ // Try WebSocket handler first
252
+ if (typeof handleWebSocketMessage === 'function') {
253
+ const handled = handleWebSocketMessage(event);
254
+ if (handled instanceof Promise) {
255
+ // Async handler
256
+ return;
257
+ } else if (handled) {
258
+ // Synchronously handled
259
+ return;
260
+ }
261
+ }
262
+
263
+ // Standard message handling
264
switch (type) {
265
case 'GET_STATUS':
266
event.ports[0]?.postMessage({
267
success: true,
268
status: {
269
wasmReady,
146
- hasEngine: !!proxyEngine
270
+ hasEngine: !!proxyEngine,
271
+ hasWebSocket: typeof handleWebSocketMessage === 'function'
272
}
273
});
274
break;