Files

122 lines
4.2 KiB
JavaScript

import net from 'node:net';
import { randomUUID } from 'node:crypto';
const CONNECTION_READY_TIMEOUT_MS = 15_000;
const listen = (server) => new Promise((resolve, reject) => {
server.once('error', reject);
server.listen(0, '127.0.0.1', () => {
server.off('error', reject);
const address = server.address();
const port = Number(address?.port);
if (!Number.isInteger(port) || port <= 0) {
reject(new Error('Failed to bind a local relay tunnel port'));
return;
}
resolve(port);
});
});
const messageData = (event) => {
if (event?.type === 'ready' || event?.type === 'data' || event?.type === 'close') return event;
return event?.data ?? null;
};
export const createRelayDevTunnelBridge = ({ createMessageChannel, logger = console } = {}) => {
const tunnels = new Map();
const closeTunnel = (key) => {
const tunnel = tunnels.get(key);
if (!tunnel) return false;
tunnels.delete(key);
for (const connection of tunnel.connections.values()) connection.close();
try { tunnel.server.close(); } catch { /* already closing */ }
return true;
};
return {
async open({ targetKey, remotePort, webContents }) {
const port = Number.parseInt(String(remotePort), 10);
if (!Number.isInteger(port) || port <= 0 || port > 65535) throw new Error('A valid remote port is required');
if (!targetKey) throw new Error('A relay target key is required');
if (!webContents || webContents.isDestroyed?.()) throw new Error('The desktop window is unavailable');
const key = `${webContents.id}|${targetKey}|${port}`;
const existing = tunnels.get(key);
if (existing) return { localPort: existing.localPort, reused: true };
const connections = new Map();
const server = net.createServer((socket) => {
socket.setNoDelay(true);
socket.pause();
const connectionId = randomUUID();
const { port1, port2 } = createMessageChannel();
let closed = false;
const readyTimer = setTimeout(() => close(), CONNECTION_READY_TIMEOUT_MS);
const close = () => {
if (closed) return;
closed = true;
clearTimeout(readyTimer);
connections.delete(connectionId);
try { port1.postMessage({ type: 'close' }); } catch { /* already closed */ }
try { socket.destroy(); } catch { /* already closed */ }
try { port1.close(); } catch { /* already closed */ }
};
connections.set(connectionId, { close });
port1.on('message', (event) => {
const message = messageData(event);
if (!message) return;
if (message.type === 'ready') {
clearTimeout(readyTimer);
socket.resume();
return;
}
if (message.type === 'data' && message.data) {
socket.write(Buffer.from(message.data));
return;
}
if (message.type === 'close') close();
});
port1.on('close', close);
port1.start?.();
socket.on('data', (chunk) => {
if (closed) return;
port1.postMessage({ type: 'data', data: Uint8Array.from(chunk) });
});
socket.on('error', close);
socket.on('close', close);
try {
webContents.postMessage('openchamber:relay-dev-tunnel-connect', { connectionId, remotePort: port }, [port2]);
} catch (error) {
logger.warn?.(`[dev-tunnel] failed to hand relay connection to renderer: ${error?.message || error}`);
close();
}
});
const localPort = await listen(server);
server.on('error', (error) => logger.warn?.(`[dev-tunnel] relay listener failed: ${error?.message || error}`));
tunnels.set(key, { server, connections, localPort });
webContents.once?.('destroyed', () => closeTunnel(key));
return { localPort, reused: false };
},
closeAll() {
for (const key of [...tunnels.keys()]) closeTunnel(key);
},
closeForWebContents(webContentsId) {
let closed = 0;
const prefix = `${webContentsId}|`;
for (const key of [...tunnels.keys()]) {
if (!key.startsWith(prefix)) continue;
if (closeTunnel(key)) closed += 1;
}
return closed;
},
};
};