Last active
March 1, 2022 03:32
-
-
Save o0101/3bee14b0149eae581036f8b5c9054004 to your computer and use it in GitHub Desktop.
Simple proxy for a Websocket endpoint
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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