Skip to content

Instantly share code, notes, and snippets.

@o0101
Last active March 1, 2022 03:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save o0101/3bee14b0149eae581036f8b5c9054004 to your computer and use it in GitHub Desktop.
Save o0101/3bee14b0149eae581036f8b5c9054004 to your computer and use it in GitHub Desktop.
Simple proxy for a Websocket endpoint
// requires
// npm i --save express cookie-parser ws
// node built-in imports
import fs from 'fs';
import os from 'os';
import path from 'path';
import https from 'https';
import crypto from 'crypto';
// 3rd-party NPM imports
import express from 'express';
import cookieParser from 'cookie-parser';
import WebSocket from 'ws';
// helpers
const sleep = ms => new Promise(res => setTimeout(res, ms));
// config and commandline args
const COOKIE_OPTS = {
secure: true,
httpOnly: true,
maxAge: 345600000,
sameSite: 'None'
};
const PORT = parseInt(process.argv[2]);
const COOKIE = process.argv[4] || randomHex();
const TOKEN = process.argv[5] || randomHex();
let fail = false;
if ( ! PORT || ! COOKIE || ! TOKEN || ! process.argv[3]) {
fail = true;
throw new TypeError(`Must supply all required arguments.
Received only: ${JSON.stringify({PORT,COOKIE,TOKEN,WS_ENDPOINT:process.argv[3]})}
Usage: <port> <ws_endpoint> (? <cookie> <token>)
Ie: Supply a cookie and token instead of generating random ones.
Example 1: node proxy-wss.js 8000 wss://chrome.browserless.io/?token=abcde-123-00ff
Example 2: node proxy-wss.js 8000 wss://myserver.com:3000/?token=b1b1b1 mycookievalue mytokenvalue
Server will generate both:
a HTTP login link to set the cookie so you can make further requests.
and a wss URL with a token
`);
}
let WS_ENDPOINT;
try {
WS_ENDPOINT = new URL(process.argv[3]);
} catch(e) {
fail = true;
throw new TypeError(`WS_ENDPOINT argument is not a proper URL: ${process.argv[3]}`);
}
if ( fail ) {
process.exit(1);
}
const SSL_OPTS = {};
let GO_SECURE = true;
try {
Object.assign(SSL_OPTS, {
key: fs.readFileSync(path.resolve(os.homedir(), 'sslcerts', 'privkey.pem')),
cert: fs.readFileSync(path.resolve(os.homedir(), 'sslcerts', 'fullchain.pem')),
ca: fs.readFileSync(path.resolve(os.homedir(), 'sslcerts', 'chain.pem')),
});
} catch(e) {
console.warn(`Did not find any SSL certificates in ${path.resolve(os.homedir(), 'sslcerts')}`);
console.info(`No using TLS/HTTPS/WSS for the external-proxy`);
GO_SECURE = false;
}
const SOCKETS = new Map();
const app = express();
app.use(express.urlencoded({extended:true}));
app.use(cookieParser());
app.use(function (req, res, next) {
res.setHeader('Cross-Origin-Resource-Policy', 'same-site');
res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
next();
});
app.get('/login', (req, res) => {
const {token} = req.query;
let authorized;
// if we are bearing a valid token set the cookie
// so future requests will be authorized
if ( token == TOKEN ) {
res.cookie(COOKIENAME+PORT, COOKIE, COOKIE_OPTS);
authorized = true;
} else {
const cookie = req.cookies[COOKIENAME+PORT];
authorized = cookie === COOKIE || NO_AUTH;
}
if ( authorized ) {
res.redirect('/');
} else {
res.end(`
<!DOCTYPE html>
<style>:root { font-family: sans-serif; }</style>
<h1>Logging you into devtools...</h1>
<script src=devtools_login.js></script>
`);
}
});
app.post('/', (req, res) => {
const {token} = req.body;
let authorized;
// if we are bearing a valid token set the cookie
// so future requests will be authorized
if ( token == TOKEN ) {
res.cookie(COOKIENAME+PORT, COOKIE, COOKIE_OPTS);
authorized = true;
} else {
const cookie = req.cookies[COOKIENAME+PORT];
authorized = cookie === COOKIE || NO_AUTH;
}
if ( authorized ) {
res.redirect('/');
} else {
res.sendStatus(401);
}
});
const server = (GO_SECURE ? https : http).createServer(SSL_OPTS, app);
const wss = new WebSocket.Server({server});
wss.on('connection', (ws, req) => {
const cookie = req.headers.cookie;
const authorized = (cookie && cookie.includes(`${COOKIENAME+PORT}=${COOKIE}`)) || (
new URL(`${GO_SECURE?'wss':'ws'}://${req.headers.host}${req.url}`).searchParams.get('token') === TOKEN
);
console.log('External side connection', {cookie, authorized}, req.path, req.url);
if ( authorized ) {
const url = WS_ENDPOINT;
try {
const crdpSocket = new WebSocket(url);
SOCKETS.set(ws, crdpSocket);
crdpSocket.on('open', () => {
console.log('Internal-side Socket open');
});
crdpSocket.on('message', msg => {
//console.log('Browser sends us message', msg);
ws.send(msg);
});
ws.on('message', msg => {
//console.log('We send browser message');
crdpSocket.send(msg);
});
ws.on('close', (code, reason) => {
SOCKETS.delete(ws);
crdpSocket.close(1001, 'client disconnected');
});
crdpSocket.on('close', (code, reason) => {
SOCKETS.delete(ws);
crdpSocket.close(1011, 'browser disconnected');
});
} catch(e) {
console.warn('Error on websocket creation', e);
}
} else {
ws.send(JSON.stringify({error:`Not authorized`}));
ws.close();
}
});
server.listen(PORT, err => {
if ( err ) {
throw err;
}
console.log({crdpSecureProxyServer: { up: new Date, port: PORT, TOKEN, COOKIE }});
});
function randomHex() {
return crypto.randomBytes(16).toString('hex');
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment