UNPKG

bbpro

Version:

BrowserBox - remote browser isolation tool

723 lines (655 loc) 19.6 kB
#!/usr/bin/env node 'use strict'; import fs from 'fs'; import os from 'os'; import path from 'path'; import url from 'url'; import childProcess from 'child_process'; import http from 'http'; import https from 'https'; import helmet from 'helmet'; import compression from 'compression'; import cors from 'cors'; import cookieParser from 'cookie-parser'; import rateLimit from 'express-rate-limit'; import csrf from 'csurf'; import express from 'express'; import exitOnEpipe from 'exit-on-epipe'; import {WebSocket,WebSocketServer} from 'ws'; import {PassThrough} from 'node:stream'; import {Reader,Writer} from '@dosy/wav'; import { DEBUG as APP_DEBUG, APP_ROOT, GO_SECURE, COOKIENAME, ALLOWED_3RD_PARTY_EMBEDDERS, CONFIG } from '../../../common.js'; const DEBUG = { showAllData: false, showDroppedSilents: false, showPrimeFilter: false, showPrimeChecks: false, showAllMessages: false, showAcks: false, showConnections: true, val: 1, showFormat: true, mode: 'dev', goSecure: true } const Clients = new Set(); const sockets = new Set(); const SAMPLE_RATE = 44100; const FORMAT_BIT_DEPTH = 16; const MAX_DATA_SIZE = 1200; //SAMPLE_RATE * (FORMAT_BIT_DEPTH/8) // we actually only get around 300 bytes per chunk on average, ~ 3 msec; const CutOff = {}; const primeCache = [2, 3, 5]; let Encoders = new Set(); let primeSet = new Set(primeCache); const SparsePrimes = createSparsePrimes(MAX_DATA_SIZE); const argv = process.argv; let device = argv[3] || 'rtp.monitor' || 'auto_null.monitor'; const encoders = { mp3: { contentType: 'audio/mpeg', command: 'lame', /*args: ['-S', '--noreplaygain', '-f', '-r', '-B', '20', '-', '-'],*/ args: ['-S', '-r', '-', '-'] }, wav: { contentType: 'audio/wav', ...(process.env.TORBB ? { command: () => new Writer({ endianness: 'LE', sampleRate: 44100, bitDepth: 16, channels: 1 }), } : {}) }, flac: { contentType: 'audio/flac', command: 'flac', args: [ '-0', '--endian=little', '--sign=signed', '--sample-rate=44100', '--bps=16', '--channels=2', '--silent', '-' ] } }; const encoderType = process.env.TORBB ? 'wav' : 'wav'; var hooks = 0; let parec = undefined; let savedEncoder; let encoderExpiration; const ENCODER_EXPIRY_MS = 13*60*1000; // shut down encoder after 13 minutes of nobody listening const SSL_OPTS = {}; const COOKIE_OPTS = { secure: true, httpOnly: true, maxAge: 345600000, sameSite: 'None' }; const RateLimiter = rateLimit({ windowMs: 1000 * 60 * 3, max: DEBUG.mode == 'dev' ? 10000 : 2000 }); function maybeStartEncoderExpiryClock(client) { Clients.delete(client); hooks--; if (hooks > 0) return; DEBUG.val && console.log(`Starting encoder expiration clock`); encoderExpiration = setTimeout(killAudio, ENCODER_EXPIRY_MS); } function killAudio() { DEBUG.val && console.log('killing encoder because nobody is listening'); for( let encoder of Encoders.values()) { try { encoder?.kill?.('SIGINT'); encoder?.parec?.kill?.('SIGINT'); } catch(e) { console.warn('error killing encoder', e, encoder); } } Encoders = new Set(); } function stopEncoderExpiryClock(client) { DEBUG.val && console.log(`Stopping encoder expiration clock`); Clients.add(client); hooks++; clearTimeout(encoderExpiration); encoderExpiration = false; } const port = parseInt(argv[2]); const PORT = port; const COOKIE = process.argv[4]; const TOKEN = process.argv[5]; DEBUG.val && console.log('starting http server on port', port); console.log({ALLOWED_3RD_PARTY_EMBEDDERS}); let certsFound = false; if ( DEBUG.goSecure ) { try { Object.assign(SSL_OPTS, { key: fs.readFileSync(path.resolve(os.homedir(), CONFIG.sslcerts(port), 'privkey.pem')), cert: fs.readFileSync(path.resolve(os.homedir(), CONFIG.sslcerts(port), 'fullchain.pem')), ca: fs.existsSync(path.resolve(os.homedir(), CONFIG.sslcerts(port), 'chain.pem')) ? fs.readFileSync(path.resolve(os.homedir(), CONFIG.sslcerts(port), 'chain.pem')) : undefined }); certsFound = true; } catch(e) { DEBUG.err && console.warn(e); } DEBUG.val && console.log(SSL_OPTS, {GO_SECURE}); console.log(SSL_OPTS, {GO_SECURE}, path.resolve(os.homedir(), CONFIG.sslcerts(port), 'privkey.pem')); } const MODE = certsFound ? https: http; const app = express(); let shuttingDown = false; let audioProcessShuttingDown = false; app.set('etag', false); if ( ! APP_DEBUG.noSecurityHeaders ) { app.use(helmet({ frameguard: false, crossOriginResourcePolicy: { policy: "cross-origin" }, contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], imgSrc: [ "'self'", "data:", "blob:" ], mediaSrc: [ "'self'", "https://localhost:*", "https://*.dosyago.com:*", "https://*.browserbox.pro:*" ], frameSrc: [ "'self'", `https://*.dosyago.com:${PORT+1}`, `https://*.dosyago.com:${PORT+2}`, `https://*.dosyago.com:${PORT+3}`, "https://*.browserbox.pro:*", ...ALLOWED_3RD_PARTY_EMBEDDERS ], frameAncestors: [ "'self'", "https://*.dosyago.com:*", "https://*.browserbox.pro:*", ...ALLOWED_3RD_PARTY_EMBEDDERS ], connectSrc: [ "'self'", "wss://*.dosyago.com:*", `https://*.dosyago.com:${PORT+1}`, "wss://*.browserbox.pro:*" ], styleSrc: [ "'self'", "'unsafe-inline'" ], scriptSrc: [ "'self'", "'unsafe-eval'", "'sha256-ktnwD9kIpbxpOmbTg7NUsKRlpicCv8bryYhIbiRDFaQ='", ], objectSrc: ["'none'"], upgradeInsecureRequests: [], }, reportOnly: false, } })); } else { app.use(cors()); } app.use(compression({ level: 4 })); app.use(RateLimiter); app.use(express.urlencoded({extended:true})); app.use(cookieParser()); app.use(csrf({cookie:{sameSite:'None', secure:true}})); const staticPath = path.resolve(APP_ROOT, 'services', 'instance', 'parec-server', 'public'); console.log({staticPath}); app.use(express.static(staticPath)); app.use((req,res,next) => { res.set('Cache-Control', 'no-store'); next(); }); app.get('/login', (req, res) => { res.type('html'); const {token} = req.query; const cookie = req.cookies[COOKIENAME+PORT]; let loggedIn = false; if ( token == TOKEN ) { res.cookie(COOKIENAME+PORT, COOKIE, COOKIE_OPTS); loggedIn = true; } else if ( cookie == COOKIE ) { loggedIn = true; } if ( loggedIn ) { res.end(` <!DOCTYPE html> <script src=request_audio.js></script> `); } else { res.end(` <!DOCTYPE html> <script src=request_login.js></script> `); } }); if ( process.env.TORBB ) { app.get('/', (request, response) => { const {token} = request.query; const cookie = request.cookies[COOKIENAME+PORT]; if ( token == TOKEN || cookie == COOKIE ) { var contentType = encoders[encoderType].contentType; DEBUG.val && console.log(' setting Content-Type to', contentType); response.writeHead(200, { 'Connection': 'keep-alive', 'Content-Type': contentType }); const enc = getEncoder(); let unpipe; if ( enc?.stdout ) { DEBUG.val && console.log('Setting encoder stdout to pipe'); enc.stdout.pipe(response); unpipe = () => enc.stdout.unpipe(resopnse); } else if ( enc?.pipe ) { DEBUG.val && console.log('Setting encoder to pipe'); enc.pipe(response); unpipe = () => enc.unpipe(response); } else { console.warn(`Encoder has no stdout or pipe properties`); } exitOnEpipe(response); request.on('close', function() { DEBUG.val && console.log('Request closing'); try { unpipe(); if ( process.env.TORBB ) { killEncoder(enc); } } catch(e) { console.warn(`Error on unpipe at end of request / resopnse`); } response.end(); }); } else { response.sendStatus(401); } }); } const server = MODE.createServer(SSL_OPTS, app); const socketWaveStreamer = new WebSocketServer({ server, perMessageDeflate: false, }); socketWaveStreamer.on('connection', (ws, req) => { cookieParser()(req, {}, () => console.log('cookie parsed')); const cookie = req.cookies[COOKIENAME+PORT]; console.log({cookie}); if ( cookie == COOKIE ) { console.log('auth'); } else { console.log('Audio: closing as not authorized'); ws.send("closing as not authorized"); ws.close(); return; } const client = { ACK_VALUE: 3, DATA_SIZE: 44100, BUF_WINDOW: 4, ackReceived: 0, firstAck: true, buffer: [], packet: [], ip: `${ws?._socket?.remoteAddress}:${ws?._socket?.remotePort}`, }; stopEncoderExpiryClock(client); DEBUG.showConnections && console.log(`Now have ${Clients.size} clients`, Clients); try { DEBUG.val && console.log('ws (wave header + pcm stream) connection (server #2)'); const reader = getEncoder().stdout; ws.send("give me ack"); let totalLength = 0; const processData = data => { DEBUG.showAllData && console.log(`Got data with length`, data.length); if ( CONFIG.audioDropPossiblySilentFrames && isSilent(data) ) { DEBUG.showDroppedSilents && console.log('drop', data.length, data); client.packet.length = 0; totalLength = 0; client.buffer.length = 0; return; } totalLength += data.length; client.packet.push(data); if ( totalLength >= client.DATA_SIZE ) { const packet = Buffer.concat(client.packet, totalLength); totalLength = 0; client.packet.length = 0; client.buffer.push(packet); } while ( client.buffer.length > client.BUF_WINDOW ) { client.buffer.shift(); } if ( client.buffer.length && client.ackReceived && ws.readyState < WebSocket.CLOSING ) { DEBUG.showAllData && console.log(`sending`, Array.from(data).join(' '), 'silent?', isSilent(data)); ws.send(client.buffer.shift()); client.ackReceived--; } }; reader.on('data', processData); function isSilent(data) { return data[0] === 0 && data[data.length-1] === 0 && checks(data); } function checks(dat) { const sz = dat.length; if ( sz < 3 ) return true; const pr = SparsePrimes.slice(0, CutOff[sz]); const P = pr.length; DEBUG.showPrimeChecks && console.log( "miss end by", dat.length - 1 - pr[pr.length-1], "last dat at", dat.length - 1, "last check at", pr[pr.length-1] ); for(let i = 0, p; i < P; i++) { p = pr[i]; if ( dat[p] !== 0 ) { DEBUG.showPrimeChecks && console.log({p, datp: dat[p], p, len: dat.length}); return false; } } return true; } ws.on('open', () => { client.packet.length = 0; totalLength = 0; client.buffer.length = 0; }); ws.on('message', async msg => { msg = msg.toString(); DEBUG.showAllMessages && console.log(msg); if ( msg === "ack" ) { DEBUG.showAcks && console.log(`Ack received`); if ( client.isFirstAck ) { client.firstAck = false; client.packet.length = 0; totalLength = 0; client.buffer.length = 0; console.log("first ack", true, client.ackReceived); } client.ackReceived = client.ACK_VALUE; while ( client.ackReceived && client.buffer.length && ws.readyState < WebSocket.CLOSING ) { DEBUG.val && console.log(`sending`, client.buffer[0]); ws.send(client.buffer.shift()); client.ackReceived--; } } else if ( msg === "stop" ) { /* client.packet.length = 0; totalLength = 0; client.buffer.length = 0; */ } }); ws.on('error', err => console.warn('WebSocket error', err)); ws.on('close', info => { console.log(`WebSocket closing`, info); reader.off('data', processData); maybeStartEncoderExpiryClock(client); client.packet.length = 0; totalLength = 0; client.buffer.length = 0; }); } catch(e) { console.warn(e); } }); server.on('connection', function(socket) { sockets.add(socket); socket.on('close', () => sockets.delete(socket)); DEBUG.showConnections && console.log('New http connection: setting no delay true'); socket.setNoDelay(true); }); server.on('upgrade', (req, socket, head) => { sockets.add(socket); socket.on('close', () => sockets.delete(socket)); console.log('http upgrade'); const {pathname} = url.parse(req.url); switch(pathname) { case "/stream": { if ( process.env.TORBB ) { // reject all audio websocket connections in TOR console.log('Destroying socket as tor mode cannot use webaudio for streaming, need a raw (and laggy) audio tag only)', pathname); sockets.delete(socket); socket.destroy(); } }; break; default: { console.log('Destroying socket as wrong path', pathname); sockets.delete(socket); socket.destroy(); }; break; } }); server.listen(port); try { childProcess.execSync(`sudo renice -n ${CONFIG.reniceValue} -p ${process.pid}`); } catch(e) { console.info(e); console.warn(`Could not renice node audio service`); } console.warn('Shut down buries errors'); //process.on('SIGINT', shutDown); //process.on('SIGUSR2', shutDown); //process.on('exit', shutDown); function getEncoder() { audioProcessShuttingDown = false; if (savedEncoder) return savedEncoder; let encoder = undefined; let parec = undefined; DEBUG.val && console.log('starting encoder'); // Notes on args /* note that the following args break it or are unnecessary '--fix-rate', // break '--fix-format', // break '--no-remix', // unknown if break or unnecessary */ const args = [ '-r', /* record: the default if run as parec */ `--rate=${SAMPLE_RATE}`, `--format=s${FORMAT_BIT_DEPTH}le`, '--channels=1', '--process-time-msec=100', '--latency-msec=100', ...(process.platform == 'darwin' ? [] : [ '-d', device ]) ] parec = childProcess.spawn('pacat', args); parec.on('spawn', e => { console.log('parec spawned', {error:e, pid: parec.pid, args: args.join(' ')}); try { childProcess.execSync(`sudo renice -n ${CONFIG.reniceValue} -p ${parec.pid}`); console.log(`reniced parec`); } catch(e) { console.warn(`Error renicing parec`, e); } }); exitOnEpipe(parec.stdout); exitOnEpipe(parec.stderr); parec.stderr.pipe(process.stdout); parec.on('error', e => { console.log('parec error', e); killEncoder(encoder); //shutDown(); }); parec.on('close', e => { console.log('parec close', e); killEncoder(encoder); //shutDown(); }); parec.on('exit', e => { console.log('parec exit', e); killEncoder(encoder); //shutDown(); }); const encoderCommand = encoders[encoderType]; if ( typeof encoderCommand?.command === "string" ) { encoder = childProcess.spawn(encoderCommand.command, encoderCommand.args); encoder.parec = parec; Encoders.add(encoder); exitOnEpipe(encoder.stdout); exitOnEpipe(encoder.stderr); encoder.stderr.pipe(process.stdout); encoder.on('error', e => { console.log('encoder error', e); killEncoder(encoder); }); encoder.on('close', e => { console.log('encoder close', e); killEncoder(encoder); }); encoder.on('exit', e => { console.log('encoder exit', e); killEncoder(encoder); }); childProcess.execSync(`sudo renice -n ${CONFIG.reniceValue} -p ${encoder.pid}`); parec.stdout.pipe(encoder.stdin); if ( ! process.env.TORBB ) { savedEncoder = encoder; } return encoder; } else if ( typeof encoderCommand?.command === "function" ) { encoder = encoderCommand.command(); encoder.parec = parec; Encoders.add(encoder); parec.stdout.pipe(encoder); if ( ! process.env.TORBB ) { savedEncoder = encoder; } return encoder; } else { encoder = parec; return parec; } } function killEncoder(encoder) { console.log('killing encoder'); if ( encoder ) { try { encoder?.kill?.(); encoder?.parec?.kill?.(); Encoders.delete(encoder); encoder = null; } catch(e) { console.log(`Error killing encoder`, e); } } } function audioProcessShutDown() { if ( audioProcessShuttingDown ) return; audioProcessShuttingDown = true; for( let encoder of Encoders.values()) { if ( encoder ) { try { encoder?.kill && encoder.kill(); encoder = null; } catch(e) { console.log(`Error killing encoder`, e); } } } Encoders = new Set(); if ( parec ) { try { parec?.kill && parec.kill(); parec = null; } catch(e) { console.log(`Error killing parec`, e); } } } function shutDown(...args) { if ( shuttingDown ) return; shuttingDown = true; audioProcessShutDown(); server.close(() => console.info(`Server closed on ${[args].join(',')}`)); sockets.forEach(socket => { try { socket.destroy(); sockets.delete(socket); } catch(e) { DEBUG && console.warn(`MAIN SERVER: port ${port}, error closing socket`, e) } }); process.exit(0); }; function createSparsePrimes(len) { // take the primes up to len and then weed out 61.8 % of them const primes = primesUpTo(len); const filteredPrimes = primes.filter(() => Math.random() <= 0.618); for(let j = 1, end = 0, p = filteredPrimes[0], nextP = filteredPrimes[1]; j < filteredPrimes.length && end < MAX_DATA_SIZE; end++) { if ( end > nextP ) { j++; p = nextP; nextP = filteredPrimes[j]; } CutOff[end] = j; } DEBUG.showPrimeFilter && console.log(filteredPrimes.join(' '), CutOff); return filteredPrimes; } /* from my code: https://github.com/crisdosyago/weird-json/blob/9f8868a68e523317b51f9ed1f328b4d52dd6ff58/src/prime-code/index.js */ function primesUpTo(n, beginning = 2) { // return primes up to n, optionally starting at beginning let fillCache = beginning == 2; const primes = []; let i = beginning; while(i <= n) { if ( isPrime(i) ) { primes.push(i); if ( fillCache && primes.length > primeCache.length ) { primeCache.push(i); } } i++; } primeSet = new Set(primeCache); return primes; } function isPrime(n) { if ( n === 2 || n === 3 ) return true; let remainder = n % 2; if ( remainder === 0 ) return false; remainder = n % 3; if ( remainder === 0 ) return false; if ( primeSet.has(n) ) return true; const maxFactor = Math.floor(Math.sqrt(n)); let primeIndex = 2; let factor = primeCache[primeIndex]; // 5 while(factor <= maxFactor) { remainder = n % factor; if ( remainder === 0 ) return false; primeIndex++; if ( primeIndex < primeCache.length ) { factor = primeCache[primeIndex]; } else { const sixRemainder = factor % 6; if ( sixRemainder === 5 ) { factor += 2; } else if ( sixRemainder === 1 ) { factor += 4; } } } return true; }