chrome-ai-bridge 2.3.9 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/extension/README.md +181 -0
- package/build/extension/background.mjs +1318 -0
- package/build/extension/debug-logger.mjs +148 -0
- package/build/extension/icons/icon-128.png +0 -0
- package/build/extension/icons/icon-16.png +0 -0
- package/build/extension/icons/icon-32.png +0 -0
- package/build/extension/icons/icon-48.png +0 -0
- package/build/extension/icons/icon.svg +19 -0
- package/build/extension/manifest.json +28 -0
- package/build/extension/relay-server.ts +539 -0
- package/build/extension/ui/connect.html +429 -0
- package/build/extension/ui/connect.js +491 -0
- package/build/src/extension/relay-server.js +27 -5
- package/build/src/fast-cdp/extension-raw.js +4 -1
- package/build/src/fast-cdp/fast-chat.js +13 -6
- package/build/src/fast-cdp/network-interceptor.js +96 -26
- package/package.json +2 -1
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RelayServer - WebSocket server for Extension communication
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import WebSocket, { WebSocketServer } from 'ws';
|
|
6
|
+
import { EventEmitter } from 'events';
|
|
7
|
+
import crypto from 'crypto';
|
|
8
|
+
import http from 'http';
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
|
|
11
|
+
// デバッグログをファイルに出力
|
|
12
|
+
const DEBUG_LOG_PATH = '/tmp/relay-server-debug.log';
|
|
13
|
+
function debugLog(...args: any[]) {
|
|
14
|
+
const timestamp = new Date().toISOString();
|
|
15
|
+
const message = `[${timestamp}] ${args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ')}\n`;
|
|
16
|
+
fs.appendFileSync(DEBUG_LOG_PATH, message);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface RelayServerOptions {
|
|
20
|
+
port?: number; // 0 for auto-assign
|
|
21
|
+
host?: string;
|
|
22
|
+
token?: string; // Authentication token
|
|
23
|
+
sessionId?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface CDPCommand {
|
|
27
|
+
id: number;
|
|
28
|
+
method: string;
|
|
29
|
+
params?: any;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface CDPEvent {
|
|
33
|
+
method: string;
|
|
34
|
+
params?: any;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class RelayServer extends EventEmitter {
|
|
38
|
+
private wss: WebSocketServer | null = null;
|
|
39
|
+
private ws: WebSocket | null = null; // Single connection (1 tab per server)
|
|
40
|
+
private port: number = 0;
|
|
41
|
+
private host: string;
|
|
42
|
+
private token: string;
|
|
43
|
+
private sessionId: string;
|
|
44
|
+
private instanceId: string;
|
|
45
|
+
private startedAt: number;
|
|
46
|
+
private tabId: number | null = null;
|
|
47
|
+
private ready: boolean = false;
|
|
48
|
+
private nextId = 1;
|
|
49
|
+
private pending = new Map<number, {
|
|
50
|
+
resolve: (value: any) => void;
|
|
51
|
+
reject: (err: Error) => void;
|
|
52
|
+
method: string;
|
|
53
|
+
startedAt: number;
|
|
54
|
+
}>();
|
|
55
|
+
private discoveryServer: http.Server | null = null;
|
|
56
|
+
private discoveryPort: number | null = null;
|
|
57
|
+
private keepAliveTimer: ReturnType<typeof setInterval> | null = null;
|
|
58
|
+
private _lastDiscoveryOptions: {
|
|
59
|
+
tabUrl?: string;
|
|
60
|
+
tabId?: number;
|
|
61
|
+
newTab?: boolean;
|
|
62
|
+
allowTabTakeover?: boolean;
|
|
63
|
+
} = {};
|
|
64
|
+
|
|
65
|
+
constructor(options: RelayServerOptions = {}) {
|
|
66
|
+
super();
|
|
67
|
+
this.host = options.host || '127.0.0.1';
|
|
68
|
+
this.token = options.token || this.generateToken();
|
|
69
|
+
this.sessionId = options.sessionId || this.generateSessionId();
|
|
70
|
+
this.instanceId = crypto.randomUUID();
|
|
71
|
+
this.startedAt = Date.now();
|
|
72
|
+
this.port = options.port || 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Start WebSocket server
|
|
77
|
+
*/
|
|
78
|
+
async start(): Promise<number> {
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
this.wss = new WebSocketServer({
|
|
81
|
+
host: this.host,
|
|
82
|
+
port: this.port
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
this.wss.on('listening', () => {
|
|
86
|
+
const address = this.wss!.address() as WebSocket.AddressInfo;
|
|
87
|
+
this.port = address.port;
|
|
88
|
+
debugLog(`[RelayServer] Listening on ws://${this.host}:${this.port}`);
|
|
89
|
+
resolve(this.port);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
this.wss.on('error', (error) => {
|
|
93
|
+
debugLog('[RelayServer] Server error:', error);
|
|
94
|
+
reject(error);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
this.wss.on('connection', (ws, req) => {
|
|
98
|
+
this.handleConnection(ws, req);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Handle WebSocket connection from Extension
|
|
105
|
+
*/
|
|
106
|
+
private handleConnection(ws: WebSocket, req: any) {
|
|
107
|
+
debugLog('[RelayServer] New connection from Extension');
|
|
108
|
+
|
|
109
|
+
// Validate token
|
|
110
|
+
const url = new URL(req.url || '', `ws://${this.host}`);
|
|
111
|
+
const clientToken = url.searchParams.get('token');
|
|
112
|
+
const clientSessionId = url.searchParams.get('sid');
|
|
113
|
+
|
|
114
|
+
if (clientToken !== this.token) {
|
|
115
|
+
debugLog('[RelayServer] Invalid token');
|
|
116
|
+
ws.close(1008, 'Invalid token');
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (clientSessionId && clientSessionId !== this.sessionId) {
|
|
120
|
+
debugLog('[RelayServer] Invalid session id', {expected: this.sessionId, received: clientSessionId});
|
|
121
|
+
ws.close(1008, 'Invalid session id');
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Only allow one connection
|
|
126
|
+
if (this.ws) {
|
|
127
|
+
debugLog('[RelayServer] Connection already exists');
|
|
128
|
+
ws.close(1008, 'Connection already exists');
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
this.ws = ws;
|
|
133
|
+
this.startKeepAlive();
|
|
134
|
+
|
|
135
|
+
ws.on('message', (data) => {
|
|
136
|
+
this.handleMessage(data.toString());
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Guard: only update state if this socket is still the current one.
|
|
140
|
+
// Prevents a stale socket's close event from corrupting a newer connection.
|
|
141
|
+
ws.on('close', () => {
|
|
142
|
+
if (this.ws !== ws) {
|
|
143
|
+
debugLog('[RelayServer] Stale socket closed (ignored — already replaced)');
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
debugLog('[RelayServer] Extension disconnected');
|
|
147
|
+
this.stopKeepAlive();
|
|
148
|
+
this.rejectPendingRequests(
|
|
149
|
+
new Error('RELAY_DISCONNECTED: Extension socket closed before request completion'),
|
|
150
|
+
);
|
|
151
|
+
this.ws = null;
|
|
152
|
+
this.ready = false;
|
|
153
|
+
this.emit('disconnected');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
ws.on('error', (error) => {
|
|
157
|
+
debugLog('[RelayServer] WebSocket error:', error);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
debugLog('[RelayServer] Extension connected');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private rejectPendingRequests(error: Error): void {
|
|
164
|
+
if (this.pending.size === 0) return;
|
|
165
|
+
const pendingEntries = Array.from(this.pending.entries());
|
|
166
|
+
this.pending.clear();
|
|
167
|
+
for (const [id, pending] of pendingEntries) {
|
|
168
|
+
debugLog('[RelayServer] Rejecting pending request', {
|
|
169
|
+
id,
|
|
170
|
+
method: pending.method,
|
|
171
|
+
startedAt: pending.startedAt,
|
|
172
|
+
reason: error.message,
|
|
173
|
+
});
|
|
174
|
+
pending.reject(error);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Handle message from Extension
|
|
180
|
+
*/
|
|
181
|
+
private handleMessage(data: string) {
|
|
182
|
+
try {
|
|
183
|
+
const message = JSON.parse(data);
|
|
184
|
+
|
|
185
|
+
if (typeof message.id === 'number' && (message.result !== undefined || message.error !== undefined)) {
|
|
186
|
+
const pending = this.pending.get(message.id);
|
|
187
|
+
if (pending) {
|
|
188
|
+
this.pending.delete(message.id);
|
|
189
|
+
if (message.error) {
|
|
190
|
+
const error =
|
|
191
|
+
typeof message.error === 'string'
|
|
192
|
+
? new Error(message.error)
|
|
193
|
+
: new Error(message.error.message || 'Unknown error');
|
|
194
|
+
pending.reject(error);
|
|
195
|
+
} else {
|
|
196
|
+
pending.resolve(message.result);
|
|
197
|
+
}
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (message.error) {
|
|
202
|
+
const error =
|
|
203
|
+
typeof message.error === 'string'
|
|
204
|
+
? message.error
|
|
205
|
+
: message.error.message || 'Unknown error';
|
|
206
|
+
this.emit('cdp-error', { id: message.id, error });
|
|
207
|
+
} else {
|
|
208
|
+
this.emit('cdp-result', { id: message.id, result: message.result });
|
|
209
|
+
}
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (message?.method === 'forwardCDPEvent' && message.params) {
|
|
214
|
+
this.emit('cdp-event', {
|
|
215
|
+
method: message.params.method,
|
|
216
|
+
params: message.params.params,
|
|
217
|
+
sessionId: message.params.sessionId,
|
|
218
|
+
});
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
switch (message.type) {
|
|
223
|
+
case 'ready':
|
|
224
|
+
this.tabId = message.tabId;
|
|
225
|
+
this.ready = true;
|
|
226
|
+
debugLog(`[RelayServer] Connection ready for tab ${this.tabId}`);
|
|
227
|
+
this.emit('ready', this.tabId);
|
|
228
|
+
// Release discovery port after WebSocket is established.
|
|
229
|
+
// 1-second grace period lets Extension finish processing the response.
|
|
230
|
+
setTimeout(() => this.stopDiscoveryServer(), 1000);
|
|
231
|
+
break;
|
|
232
|
+
case 'pong':
|
|
233
|
+
debugLog('[RelayServer] Received keep-alive pong');
|
|
234
|
+
break;
|
|
235
|
+
|
|
236
|
+
case 'forwardCDPResult':
|
|
237
|
+
this.emit('cdp-result', { id: message.id, result: message.result });
|
|
238
|
+
break;
|
|
239
|
+
|
|
240
|
+
case 'forwardCDPError':
|
|
241
|
+
this.emit('cdp-error', { id: message.id, error: message.error });
|
|
242
|
+
break;
|
|
243
|
+
|
|
244
|
+
case 'forwardCDPEvent':
|
|
245
|
+
this.emit('cdp-event', {
|
|
246
|
+
method: message.method,
|
|
247
|
+
params: message.params
|
|
248
|
+
});
|
|
249
|
+
break;
|
|
250
|
+
|
|
251
|
+
case 'detached':
|
|
252
|
+
debugLog(`[RelayServer] Tab ${message.tabId} detached: ${message.reason}`);
|
|
253
|
+
this.emit('detached', message.reason);
|
|
254
|
+
break;
|
|
255
|
+
|
|
256
|
+
default:
|
|
257
|
+
debugLog('[RelayServer] Unknown message type:', message.type);
|
|
258
|
+
}
|
|
259
|
+
} catch (error) {
|
|
260
|
+
debugLog('[RelayServer] Failed to parse message:', error);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Send CDP command to Extension
|
|
266
|
+
*/
|
|
267
|
+
sendCDPCommand(id: number, method: string, params?: any): void {
|
|
268
|
+
if (!this.ws || !this.ready) {
|
|
269
|
+
throw new Error('Extension not connected or not ready');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
this.ws.send(JSON.stringify({
|
|
273
|
+
type: 'forwardCDPCommand',
|
|
274
|
+
id,
|
|
275
|
+
method,
|
|
276
|
+
params
|
|
277
|
+
}));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
sendMessage(message: any): void {
|
|
281
|
+
if (!this.ws || !this.ready) {
|
|
282
|
+
throw new Error(
|
|
283
|
+
`Extension not connected or not ready (connected=${Boolean(this.ws)}, ready=${this.ready})`,
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
if (this.ws.readyState !== WebSocket.OPEN) {
|
|
287
|
+
throw new Error('WebSocket not open');
|
|
288
|
+
}
|
|
289
|
+
this.ws.send(JSON.stringify(message));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async sendRequest(method: string, params?: any, timeoutMs = 30000): Promise<any> {
|
|
293
|
+
if (!this.ws || !this.ready) {
|
|
294
|
+
throw new Error(
|
|
295
|
+
`Extension not connected or not ready (method=${method}, connected=${Boolean(this.ws)}, ready=${this.ready})`,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
if (this.ws.readyState !== WebSocket.OPEN) {
|
|
299
|
+
throw new Error('WebSocket not open');
|
|
300
|
+
}
|
|
301
|
+
const id = this.nextId++;
|
|
302
|
+
const payload = {id, method, params};
|
|
303
|
+
const startedAt = Date.now();
|
|
304
|
+
const response = new Promise<any>((resolve, reject) => {
|
|
305
|
+
const timeoutId = setTimeout(() => {
|
|
306
|
+
this.pending.delete(id);
|
|
307
|
+
reject(new Error(`RELAY_REQUEST_TIMEOUT: method=${method} timeoutMs=${timeoutMs}`));
|
|
308
|
+
}, timeoutMs);
|
|
309
|
+
timeoutId.unref();
|
|
310
|
+
this.pending.set(id, {
|
|
311
|
+
resolve: (value: any) => {
|
|
312
|
+
clearTimeout(timeoutId);
|
|
313
|
+
debugLog(`[RelayServer] Request success: ${method}`, {id, elapsedMs: Date.now() - startedAt});
|
|
314
|
+
resolve(value);
|
|
315
|
+
},
|
|
316
|
+
reject: (err: Error) => {
|
|
317
|
+
clearTimeout(timeoutId);
|
|
318
|
+
debugLog(`[RelayServer] Request failed: ${method}`, {id, elapsedMs: Date.now() - startedAt, error: err.message});
|
|
319
|
+
reject(err);
|
|
320
|
+
},
|
|
321
|
+
method,
|
|
322
|
+
startedAt,
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
try {
|
|
326
|
+
this.ws.send(JSON.stringify(payload));
|
|
327
|
+
debugLog(`[RelayServer] Request sent: ${method}`, {id});
|
|
328
|
+
} catch (error) {
|
|
329
|
+
this.pending.delete(id);
|
|
330
|
+
throw error;
|
|
331
|
+
}
|
|
332
|
+
return response;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Start simple discovery HTTP server for extension to find relay URL.
|
|
337
|
+
* Extension polls this endpoint when user clicks the extension icon.
|
|
338
|
+
*/
|
|
339
|
+
async startDiscoveryServer(options: {
|
|
340
|
+
tabUrl?: string;
|
|
341
|
+
tabId?: number;
|
|
342
|
+
newTab?: boolean;
|
|
343
|
+
allowTabTakeover?: boolean;
|
|
344
|
+
} = {}): Promise<number | null> {
|
|
345
|
+
this._lastDiscoveryOptions = options;
|
|
346
|
+
const ports = [38765, 38766, 38767, 38768, 38769, 38770, 38771, 38772, 38773, 38774, 38775];
|
|
347
|
+
const wsUrl = this.getConnectionURL();
|
|
348
|
+
|
|
349
|
+
for (const port of ports) {
|
|
350
|
+
const started = await new Promise<boolean>((resolve) => {
|
|
351
|
+
const server = http.createServer(async (req, res) => {
|
|
352
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
353
|
+
|
|
354
|
+
if (req.method === 'GET' && req.url === '/relay-info') {
|
|
355
|
+
res.setHeader('Content-Type', 'application/json');
|
|
356
|
+
res.end(JSON.stringify({
|
|
357
|
+
wsUrl,
|
|
358
|
+
tabUrl: options.tabUrl || null,
|
|
359
|
+
tabId: options.tabId ?? null,
|
|
360
|
+
newTab: Boolean(options.newTab),
|
|
361
|
+
allowTabTakeover: Boolean(options.allowTabTakeover),
|
|
362
|
+
sessionId: this.sessionId,
|
|
363
|
+
startedAt: this.startedAt,
|
|
364
|
+
instanceId: this.instanceId,
|
|
365
|
+
expiresAt: Date.now() + 60000,
|
|
366
|
+
}));
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (req.method === 'POST' && req.url === '/reload-extension') {
|
|
371
|
+
res.setHeader('Content-Type', 'application/json');
|
|
372
|
+
if (!this.ws || !this.ready) {
|
|
373
|
+
res.statusCode = 503;
|
|
374
|
+
res.end(JSON.stringify({ error: 'Extension not connected' }));
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
try {
|
|
378
|
+
await this.sendRequest('reloadExtension');
|
|
379
|
+
res.end(JSON.stringify({ success: true }));
|
|
380
|
+
} catch (err: any) {
|
|
381
|
+
// Extension reloads and drops connection - this is expected
|
|
382
|
+
res.end(JSON.stringify({ success: true, note: 'Extension reloading' }));
|
|
383
|
+
}
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
res.statusCode = 404;
|
|
388
|
+
res.end('Not Found');
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
server.on('error', (error: any) => {
|
|
392
|
+
if (error?.code === 'EADDRINUSE') {
|
|
393
|
+
resolve(false);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
debugLog('[RelayServer] Discovery server error:', error);
|
|
397
|
+
resolve(false);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
server.listen(port, this.host, () => {
|
|
401
|
+
this.discoveryServer = server;
|
|
402
|
+
this.discoveryPort = port;
|
|
403
|
+
debugLog(`[RelayServer] Discovery available on http://${this.host}:${port}/relay-info`);
|
|
404
|
+
resolve(true);
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
if (started) {
|
|
409
|
+
return port;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
debugLog('[RelayServer] Could not start discovery server on any port');
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Release the discovery HTTP server (port).
|
|
419
|
+
* Called automatically after the Extension WebSocket connects (ready event).
|
|
420
|
+
* The port becomes available for other sessions.
|
|
421
|
+
*/
|
|
422
|
+
stopDiscoveryServer(): void {
|
|
423
|
+
if (this.discoveryServer) {
|
|
424
|
+
this.discoveryServer.close();
|
|
425
|
+
debugLog(`[RelayServer] Discovery server released (port ${this.discoveryPort})`);
|
|
426
|
+
this.discoveryServer = null;
|
|
427
|
+
this.discoveryPort = null;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Re-acquire a discovery port (e.g. after WebSocket disconnect for reconnection).
|
|
433
|
+
* Uses the same options as the last startDiscoveryServer() call.
|
|
434
|
+
*/
|
|
435
|
+
async restartDiscoveryServer(options?: {
|
|
436
|
+
tabUrl?: string;
|
|
437
|
+
tabId?: number;
|
|
438
|
+
newTab?: boolean;
|
|
439
|
+
allowTabTakeover?: boolean;
|
|
440
|
+
}): Promise<number | null> {
|
|
441
|
+
this.stopDiscoveryServer();
|
|
442
|
+
return this.startDiscoveryServer(options || this._lastDiscoveryOptions);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Stop server
|
|
447
|
+
*/
|
|
448
|
+
async stop(): Promise<void> {
|
|
449
|
+
this.stopKeepAlive();
|
|
450
|
+
|
|
451
|
+
if (this.ws) {
|
|
452
|
+
try {
|
|
453
|
+
this.ws.close();
|
|
454
|
+
} catch {
|
|
455
|
+
// ignore close errors
|
|
456
|
+
}
|
|
457
|
+
this.ws = null;
|
|
458
|
+
}
|
|
459
|
+
this.ready = false;
|
|
460
|
+
this.tabId = null;
|
|
461
|
+
|
|
462
|
+
this.rejectPendingRequests(
|
|
463
|
+
new Error('RELAY_STOPPED: Relay stopped before request completion'),
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
this.stopDiscoveryServer();
|
|
467
|
+
|
|
468
|
+
if (this.wss) {
|
|
469
|
+
return new Promise((resolve) => {
|
|
470
|
+
this.wss!.close(() => {
|
|
471
|
+
this.wss = null;
|
|
472
|
+
debugLog('[RelayServer] Server stopped');
|
|
473
|
+
resolve();
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Start keep-alive ping to prevent Service Worker from sleeping
|
|
481
|
+
*/
|
|
482
|
+
private startKeepAlive(): void {
|
|
483
|
+
this.stopKeepAlive();
|
|
484
|
+
this.keepAliveTimer = setInterval(() => {
|
|
485
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
486
|
+
this.ws.send(JSON.stringify({ type: 'ping' }));
|
|
487
|
+
debugLog('[RelayServer] Sent keep-alive ping');
|
|
488
|
+
}
|
|
489
|
+
}, 30000); // 30 seconds
|
|
490
|
+
debugLog('[RelayServer] Keep-alive started');
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Stop keep-alive ping
|
|
495
|
+
*/
|
|
496
|
+
private stopKeepAlive(): void {
|
|
497
|
+
if (this.keepAliveTimer) {
|
|
498
|
+
clearInterval(this.keepAliveTimer);
|
|
499
|
+
this.keepAliveTimer = null;
|
|
500
|
+
debugLog('[RelayServer] Keep-alive stopped');
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Generate random token
|
|
506
|
+
*/
|
|
507
|
+
private generateToken(): string {
|
|
508
|
+
return crypto.randomBytes(32).toString('hex');
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
getPort(): number {
|
|
512
|
+
return this.port;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
getToken(): string {
|
|
516
|
+
return this.token;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
getTabId(): number | null {
|
|
520
|
+
return this.tabId;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
isReady(): boolean {
|
|
524
|
+
return this.ready;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
getConnectionURL(): string {
|
|
529
|
+
return `ws://${this.host}:${this.port}?token=${this.token}&sid=${encodeURIComponent(this.sessionId)}`;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
getSessionId(): string {
|
|
533
|
+
return this.sessionId;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
private generateSessionId(): string {
|
|
537
|
+
return crypto.randomBytes(16).toString('hex');
|
|
538
|
+
}
|
|
539
|
+
}
|