UNPKG

onibrowser

Version:
330 lines (297 loc) 11.6 kB
import express from 'express'; import http from 'http'; import https from 'https'; import fetch from 'node-fetch'; import multer from 'multer'; import WebSocket from 'ws'; import fs from 'fs'; import path from 'path'; import bodyParser from 'body-parser'; import cookieParser from 'cookie-parser'; import {pluginsDemoPage} from './public/plugins/demo/page.js'; import zl from './zombie-lord/api.js'; import {start_mode} from './args.js'; import {version, BRANCH, COOKIENAME, GO_SECURE, DEBUG, CONNECTION_ID_URL} from './common.js'; import {timedSend, eventSendLoop} from './server.js'; const protocol = GO_SECURE ? https : http; const COOKIE_OPTS = { secure: GO_SECURE, httpOnly: true, maxAge: 345600000, sameSite: 'Strict' }; const storage = multer.diskStorage({ destination: (req, file, cb) => cb(null, path.join(__dirname,'..', 'uploads')), filename: (req, file, cb) => { return cb(null, nextFileName(path.extname(file.originalname))) } }); const upload = multer({storage}); const Queue = { funcs: [] }; let messageQueueRunning = false; let browserTargetId; let requestId = 0; export async function start_ws_server(port, zombie_port, allowed_user_cookie, session_token) { console.log(`Starting websocket server on ${port}`); const app = express(); const server_port = port; app.use(bodyParser.urlencoded({extended:true})); app.use(bodyParser.json({extended:true})); app.use(cookieParser()); if ( start_mode == "signup" ) { app.get("/", (req,res) => res.sendFile(path.join(__dirname, 'public', 'index.html'))); } else { app.get("/", (req,res) => res.sendFile(path.join(__dirname, 'public', 'image.html'))); app.get("/login", (req,res) => { const {token,ran} = req.query; if ( token == session_token ) { res.cookie(COOKIENAME, allowed_user_cookie, COOKIE_OPTS); const url = `/?ran=${ran||Math.random()}#${session_token}`; res.redirect(url); } else { res.type("html"); if ( session_token == 'token2' ) { res.end(`Incorrect token ${token}/token2. <a href=/login?token=token2>Login first.</a>`); } else { res.end(`Incorrect token "${token}". <a href=https://${req.hostname}/signup.html>Login first.</a>`); } } }); } app.use(express.static('public')); app.post('/current/:current/event/:event', wrap(async (req, res) => { const actualUri = 'https://' + req.headers.host + ':8001' + req.url; const resp = await fetch(actualUri, {method: 'POST', body: JSON.stringify(req.body), headers: { 'Content-Type': 'application/json' }}).then(r => r.text()); res.end(pluginsDemoPage({body:resp})); })); const sslBranch = BRANCH == 'master' ? 'master' : 'staging'; const secure_options = {}; try { const sec = { cert: fs.readFileSync(`./sslcert/${sslBranch}/fullchain.pem`), key: fs.readFileSync(`./sslcert/${sslBranch}/privkey.pem`), ca: fs.readFileSync(`./sslcert/${sslBranch}/chain.pem`), }; Object.assign(secure_options, sec); } catch(e) { console.warn(`No certs found so will use insecure no SSL.`); } const secure = secure_options.cert && secure_options.ca && secure_options.key; const server = protocol.createServer.apply(protocol, GO_SECURE && secure ? [secure_options, app] : [app]); const wss = new WebSocket.Server({server}); wss.on('connection', (ws, req) => { const cookie = req.headers.cookie; zl.act.saveIP(req.connection.remoteAddress); console.log({connectionIp:req.connection.remoteAddress}); if ( DEBUG.dev || allowed_user_cookie == 'cookie' || (cookie && cookie.includes(`${COOKIENAME}=${allowed_user_cookie}`)) ) { zl.life.onDeath(zombie_port, () => { //console.log("Closing as zombie crashed."); //ws.close(); }); ws.on('message', message => { const func = async () => { const Data = [], Frames = [], Meta = [], State = {}; message = JSON.parse(message); const {zombie, tabs, messageId} = message; try { if ( zombie ) { const {events} = zombie; let {receivesFrames} = zombie; if ( !!receivesFrames ) { // switch it on in DEBUG and save it on the websocket for all future events DEBUG.noShot = false; ws.receivesFrames = receivesFrames; } else { receivesFrames = ws.receivesFrames; } DEBUG.val && console.log(`Starting ${events.length} events for message ${messageId}`); await eventSendLoop(events, {Data, Frames, Meta, State, receivesFrames}); DEBUG.val && console.log(`Ending ${events.length} events for message ${messageId}\n`); const {totalBandwidth} = State; so(ws,{messageId, data:Data, frameBuffer:Frames, meta:Meta, totalBandwidth}); } else if ( tabs ) { let {data:{targetInfos:targets}} = await timedSend({ name: "Target.getTargets", params: {}, }, zombie_port); browserTargetId = browserTargetId ||zl.act.getBrowserTargetId(zombie_port); targets = targets.filter(({targetId,type}) => type == 'page' && zl.act.hasSession(targetId, zombie_port)); const activeTarget = zl.act.getActiveTarget(zombie_port); zl.act.addTargets(targets, zombie_port); so(ws,{messageId, activeTarget, tabs:targets}); } else { console.warn(JSON.stringify({unknownMessage:message})); } } catch(e) { so(ws,{messageId, data:[ { error: "Failed to communicate with cloud browser", resetRequired: true } ], frameBuffer:[], meta:[], totalBandwidth: 0}); } }; Queue.funcs.push(func); if ( ! messageQueueRunning ) { runMessageQueue(); } }); } else { const hadSession = !! cookie && cookie.includes(COOKIENAME); const msg = hadSession ? `Your session has expired. Please log back in.` : `No session detected, have you signed up yet?`; const error = {msg, willCloseSocket: true, hadSession}; so(ws, {messageId:1,data:[{error}]}); console.log("Closing as not authorized."); ws.close(); } }); server.listen(server_port, async err => { if ( err ) { console.error('err', err); process.exit(1); } else { addHandlers(); console.log({uptime:new Date, message:'websocket server up', server_port}); } }); function so(socket, message) { if ( !message ) return; if ( typeof message == "string" || Array.isArray(message) ){ message = message; } else { message = JSON.stringify(message); } try { socket.send(message); } catch(e) { console.warn(`Websocket error with sending message`, e, message); } } function addHandlers() { app.get(`/api/${version}/tabs`, wrap(async (req, res) => { const cookie = req.cookies[COOKIENAME]; if ( !DEBUG.dev && allowed_user_cookie !== 'cookie' && (cookie !== allowed_user_cookie) ) { return res.status(401).send('{"err":"forbidden"}'); } requestId++; res.type('json'); let targets; try { ({data:{targetInfos:targets}} = await timedSend({ name: "Target.getTargets", params: {}, }, zombie_port)); browserTargetId = browserTargetId ||zl.act.getBrowserTargetId(zombie_port); targets = targets.filter(({targetId,type}) => type == 'page' && zl.act.hasSession(targetId, zombie_port)); const activeTarget = zl.act.getActiveTarget(zombie_port); zl.act.addTargets(targets, zombie_port); res.end(JSON.stringify({tabs:targets,activeTarget,requestId})); } catch(e) { console.warn('th', e); //res.end(JSON.stringify({error:e+'', resetRequired:true})); throw e; } })); app.post("/file", upload.array("files", 6), async (req,res) => { const cookie = req.cookies[COOKIENAME]; if ( !DEBUG.dev && allowed_user_cookie !== 'cookie' && (cookie !== allowed_user_cookie) ) { return res.status(401).send('{"err":"forbidden"}'); } const {files} = req; const {sessionid:sessionId} = req.body; const action = ! files || files.length == 0 ? 'cancel' : 'accept'; const command = { name: "Page.handleFileChooser", params: { files: files && files.map(({path}) => path), action }, sessionId }; DEBUG.val > DEBUG.med && console.log("We need to send the right command to the browser session", files, sessionId, action, command); const result = await zl.act.send(command, zombie_port); if ( result.error ) { res.status(500).send(JSON.stringify({error:'there was an error attaching the files'})); } else { DEBUG.val > DEBUG.med && console.log("Sent files to file input", result, files); const result = { success: true, files: files.map(({originalName,size}) => ({name:originalName,size})) }; res.json(files); } }); // error handling middleware app.use('*', (err, req, res, next) => { try { res.type('json'); } catch(e){} let message = ''; if ( DEBUG.dev && DEBUG.val ) { message = s({error: { msg: err.message, stack: err.stack.split(/\n/g) }}); } else { message = s({error: err.message || err+'', resetRequired:true}); } res.write(message); res.end(); console.warn(err); }); } async function runMessageQueue() { if ( messageQueueRunning ) return; messageQueueRunning = true; while( Queue.funcs.length ) { const func = Queue.funcs.shift(); try { await func(); } catch(e) { console.warn("error while running message queue", e); } // await sleep(TIME_BETWEEN_MESSAGES); } messageQueueRunning = false; } // helpers function wrap(fn) { return async function handler(req, res, next) { try { await fn(req, res, next); } catch(e) { DEBUG.val && console.log(e); console.info(`caught error in ${fn}`); next(e); } } } function s(o) { let r; if ( typeof o == "string" ) r = 0; else try { r = JSON.stringify(o, null, 2); } catch(e) { DEBUG.val > DEBUG.hi && console.warn(e); } try { r = r + ''; } catch(e) { DEBUG.val > DEBUG.hi && console.warn(e); } return r; } } function nextFileName(ext = '') { if ( ! ext.startsWith('.') ) { ext = '.' + ext; } return `file${(Math.random()*1000000).toString(36)}${ext}`; }