@browserbox/browserbox
Version:
BrowserBox CLI - Secure, cross-platform RBI. See dosaygo.com
217 lines (198 loc) • 7.34 kB
JavaScript
// connection-manager.js
import { WebSocket } from 'ws';
import { Agent } from 'https';
import TK from 'terminal-kit';
const { terminal } = TK;
import { DEBUG, focusLog, logMessage } from './log.js';
export class ConnectionManager {
constructor(loginUrl, proxyBaseUrl, apiUrl, handler) {
this.loginUrl = loginUrl;
this.proxyBaseUrl = proxyBaseUrl;
this.apiUrl = apiUrl;
this.cookieHeader = null;
this.cookieValue = null;
this.socket = null;
this.browserbox = null;
this.sendFn = null;
this.Resolvers = {};
this.messageId = 0;
this.handler = handler;
}
async authenticate() {
terminal.cyan('Authenticating to set session cookie...\n');
try {
const response = await fetch(this.loginUrl, {
method: 'GET',
headers: { Accept: 'text/html' },
redirect: 'manual',
});
if (response.status !== 302) {
throw new Error(`Expected 302 redirect, got HTTP ${response.status}: ${await response.text()}`);
}
const setCookie = response.headers.get('set-cookie');
if (!setCookie) throw new Error('No Set-Cookie header in /login response');
const cookieMatch = setCookie.match(/browserbox-[^=]+=(.+?)(?:;|$)/);
if (!cookieMatch) throw new Error('Could not parse browserbox cookie');
this.cookieValue = cookieMatch[1];
const cookieName = setCookie.split('=')[0];
this.cookieHeader = `${cookieName}=${this.cookieValue}`;
DEBUG && console.log(`Captured cookie: ${this.cookieHeader}`);
return { cookieHeader: this.cookieHeader, cookieValue: this.cookieValue };
} catch (error) {
if (DEBUG) console.warn(error);
terminal.red(`Error during login: ${error.message}\n`);
throw error;
}
}
async fetchTargets() {
if (!this.cookieHeader) {
throw new Error('Must authenticate before fetching targets');
}
DEBUG && terminal.cyan('Fetching available tabs...\n');
try {
const response = await fetch(this.apiUrl, {
method: 'GET',
headers: { Accept: 'application/json', Cookie: this.cookieHeader },
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
const data = await response.json();
const targets = (data.tabs || []).filter(t => t.type === 'page' || t.type === 'tab');
if (!targets.length) {
DEBUG && terminal.yellow('No page or tab targets available.\n');
}
return targets;
} catch (error) {
if (DEBUG) console.warn(error);
terminal.red(`Error fetching tabs: ${error.message}\n`);
throw error;
}
}
async setupWebSockets(hostname, token) {
if (!this.cookieValue) {
throw new Error('Must authenticate before setting up WebSockets');
}
// Fetch WebSocket debugger URL
DEBUG && terminal.cyan(`Fetching WebSocket debugger URL from ${this.proxyBaseUrl}/json/version...\n`);
let wsDebuggerUrl;
try {
const response = await fetch(`${this.proxyBaseUrl}/json/version`, {
method: 'GET',
headers: { Accept: 'application/json', 'x-browserbox-local-auth': this.cookieValue },
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
const data = await response.json();
wsDebuggerUrl = data.webSocketDebuggerUrl;
if (!wsDebuggerUrl) throw new Error('No webSocketDebuggerUrl in response');
} catch (error) {
if (DEBUG) console.warn(error);
terminal.red(`Error fetching WebSocket debugger URL: ${error.message}\n`);
throw error;
}
// Setup main WebSocket
wsDebuggerUrl = wsDebuggerUrl.replace('ws://localhost', `wss://${hostname}`);
wsDebuggerUrl = `${wsDebuggerUrl}/${token}`;
DEBUG && terminal.cyan(`Connecting to WebSocket at ${wsDebuggerUrl}...\n`);
this.socket = new WebSocket(wsDebuggerUrl, {
headers: { 'x-browserbox-local-auth': this.cookieValue },
agent: new Agent({ rejectUnauthorized: true }),
});
// Setup browserbox WebSocket
let wsBBUrl = new URL(this.loginUrl);
wsBBUrl.protocol = 'wss:';
wsBBUrl.searchParams.set('session_token', wsBBUrl.searchParams.get('token'));
wsBBUrl.pathname = '/';
this.browserbox = new WebSocket(wsBBUrl, {
headers: { 'x-browserbox-local-auth': token },
agent: new Agent({ rejectUnauthorized: true }),
});
// Wait for connections
await Promise.all([
new Promise((resolve, reject) => {
this.socket.on('open', resolve);
this.socket.on('error', reject);
}),
new Promise(resolve => this.browserbox.on('open', resolve)),
]);
DEBUG && terminal.green('Connected to WebSocket and browserbox\n');
// Setup CDP send function
this.sendFn = this.createSend();
this.socket.on('message', this.createMessageHandler());
this.socket.on('close', () => {
terminal.yellow('WebSocket disconnected\n');
});
this.socket.on('error', err => {
if (DEBUG) console.warn(err);
terminal.red(`WebSocket error: ${err.message}\n`);
});
return { socket: this.socket, browserbox: this.browserbox, send: this.sendFn };
}
createSend() {
const WAIT_FOR_COMMAND_RESPONSE = 10 * 1000;
return async (method, params = {}, sessionId) => {
const message = { method, params, sessionId, id: ++this.messageId };
const key = `${sessionId || 'root'}:${message.id}`;
let resolve;
const promise = new Promise(res => (resolve = res));
this.Resolvers[key] = resolve;
const timeout = setTimeout(() => {
delete this.Resolvers[key];
resolve({});
}, WAIT_FOR_COMMAND_RESPONSE);
try {
logMessage('SEND', message, terminal);
this.socket.send(JSON.stringify(message));
} catch (error) {
clearTimeout(timeout);
delete this.Resolvers[key];
if (DEBUG) console.warn(error);
terminal.red(`Send error: ${error.message}\n`);
throw error;
}
return promise.finally(() => clearTimeout(timeout));
};
}
createMessageHandler() {
return async data => {
let message;
try {
const dataStr = Buffer.isBuffer(data) ? data.toString('utf8') : data;
message = JSON.parse(dataStr);
logMessage('RECEIVE', message, terminal);
} catch (error) {
if (DEBUG) console.warn(error);
terminal.red(`Invalid message: ${('' + data).slice(0, 50)}...\n`);
return;
}
const key = `${message.sessionId || 'root'}:${message.id}`;
if (message.id && this.Resolvers[key]) {
this.Resolvers[key](message.result || message.error);
delete this.Resolvers[key];
}
// Let caller handle specific CDP events
if ( typeof this.handler == "function" ) {
try {
return this.handler(message);
} catch(e) {
focusLog(`Issue in CDP message handler`, e.message, e.stack, e+'');
}
}
};
}
getSocket() {
return this.socket;
}
getBrowserbox() {
return this.browserbox;
}
getSend() {
return this.sendFn;
}
cleanup() {
if (this.socket) this.socket.close();
if (this.browserbox) this.browserbox.close();
this.socket = null;
this.browserbox = null;
this.sendFn = null;
this.Resolvers = {};
}
}