From 97f4718ca2cb9db33d8d8d6bd019fc7caf3ba8d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczy=C5=84ski?= Date: Tue, 11 Apr 2023 01:55:29 +0200 Subject: [PATCH] Bridge: Tune the TLS related settings and code (#153) --- cli/assets/views/ws_client.html | 101 +++++++++++++------------ cli/cli.js | 9 +-- cli/macos_pkgbuild_scripts/postinstall | 9 ++- cli/ws_server.js | 26 +++++-- docs/halo-bridge.md | 4 + halo/gateway/executor.js | 10 --- web/web_utils.js | 88 +++++++++++++++++++++ web/weblib.js | 6 +- 8 files changed, 174 insertions(+), 79 deletions(-) create mode 100644 docs/halo-bridge.md create mode 100644 web/web_utils.js diff --git a/cli/assets/views/ws_client.html b/cli/assets/views/ws_client.html index 4ad8a3f..876ee9f 100644 --- a/cli/assets/views/ws_client.html +++ b/cli/assets/views/ws_client.html @@ -12,12 +12,12 @@ This is the example web application of HaLo Bridge Server. Please connect your PC/SC reader and tap the compatible HaLo tag.

-
Connecting to the server...
+
Connecting to the server...
diff --git a/cli/cli.js b/cli/cli.js index 1985c3a..68c3b1d 100644 --- a/cli/cli.js +++ b/cli/cli.js @@ -149,16 +149,11 @@ function runHalo(entryMode, args) { if (entryMode === "server") { console.log('Launching Web Socket Server...'); - let serverInfo = wsCreateServer( - args, () => Object.keys(nfc.readers).map(r => nfc.readers[r].name)); + wsCreateServer(args, () => Object.keys(nfc.readers).map(r => nfc.readers[r].name)); console.log('Web Socket Server is listening...'); if (!args.nonInteractive) { - if (serverInfo.hasTLS) { - open('https://halo-bridge.local:' + args.listenPortTLS); - } else { - open('http://127.0.0.1:' + args.listenPort); - } + open('http://127.0.0.1:' + args.listenPort); } } else { stopPCSCTimeout = setTimeout(stopPCSC, 4000, "timeout", args.output); diff --git a/cli/macos_pkgbuild_scripts/postinstall b/cli/macos_pkgbuild_scripts/postinstall index 58d255c..4c6d1c6 100755 --- a/cli/macos_pkgbuild_scripts/postinstall +++ b/cli/macos_pkgbuild_scripts/postinstall @@ -14,23 +14,24 @@ mkdir -p /usr/local/etc/halo-bridge # remove stale files rm -f /usr/local/etc/halo-bridge/private_key.pem rm -f /usr/local/etc/halo-bridge/server.csr -rm -f /usr/local/etc/halo-bridge/server.csr +rm -f /usr/local/etc/halo-bridge/server.crt # ask user whether he wants to generate a certificate for halo-bridge.local if osascript -e 'display dialog "In order for halo-bridge to work correctly, we will need to generate a self-signed certificate for the '\''halo-bridge.local'\'' domain and mark it as trusted in the system.\n\nYou can skip that step if you don'\''t need to use halo-bridge tool." buttons {"Skip", "Generate certificate"} default button "Generate certificate" cancel button "Skip" with icon caution with title "HaLo Tools Installer"' then # generate new local certificate openssl genrsa -out /usr/local/etc/halo-bridge/private_key.pem 2048 - openssl req -new -sha256 -key /usr/local/etc/halo-bridge/private_key.pem -out /usr/local/etc/halo-bridge/server.csr -subj '/CN=halo-bridge.local/' - openssl req -x509 -sha256 -days 3650 -extensions SAN -config <(cat /etc/ssl/openssl.cnf <(printf "[SAN]\nsubjectAltName='DNS:halo-bridge.local'")) -key /usr/local/etc/halo-bridge/private_key.pem -in /usr/local/etc/halo-bridge/server.csr -out /usr/local/etc/halo-bridge/server.pem + openssl req -new -sha256 -key /usr/local/etc/halo-bridge/private_key.pem -out /usr/local/etc/halo-bridge/server.csr -subj '/CN=halo-tools (Local Certificate)/' + openssl req -x509 -sha256 -days 3650 -extensions HALO -config <(printf "[HALO]\nsubjectAltName='DNS:halo-bridge.local,DNS:localhost,IP:127.0.0.1,IP:::1'\nbasicConstraints=critical,CA:FALSE\nkeyUsage=critical,digitalSignature,keyEncipherment\nextendedKeyUsage=critical,serverAuth") -key /usr/local/etc/halo-bridge/private_key.pem -in /usr/local/etc/halo-bridge/server.csr -out /usr/local/etc/halo-bridge/server.crt # add certificate to the trust list - security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain /usr/local/etc/halo-bridge/server.pem + security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain /usr/local/etc/halo-bridge/server.crt fi # add halo-bridge.local domain to /etc/hosts if it doesn't exist yet if ! grep -q "halo-bridge.local" /etc/hosts then + echo "" >> /etc/hosts echo "127.0.0.1 halo-bridge.local" >> /etc/hosts fi diff --git a/cli/ws_server.js b/cli/ws_server.js index 0900f98..62ddfb0 100644 --- a/cli/ws_server.js +++ b/cli/ws_server.js @@ -9,6 +9,7 @@ const https = require("https"); const fs = require("fs"); const path = require("path"); const os = require("os"); +const util = require("util"); let wss = null; @@ -51,7 +52,7 @@ async function validateCSRFToken(token) { } function sendToCurrentWs(ws, data) { - console.log('send', data); + console.log('send', util.inspect(data, {showHidden: false, depth: null, colors: true})); if (currentWsClient !== null && (ws === null || currentWsClient === ws)) { currentWsClient.send(JSON.stringify(data)); @@ -125,10 +126,10 @@ function readTLSData() { if (process.platform === "win32") { privateKeyPath = path.join(os.homedir(), ".halo-bridge\\private_key.pem"); - certificatePath = path.join(os.homedir(), ".halo-bridge\\server.pem"); + certificatePath = path.join(os.homedir(), ".halo-bridge\\server.crt"); } else { privateKeyPath = '/usr/local/etc/halo-bridge/private_key.pem'; - certificatePath = '/usr/local/etc/halo-bridge/server.pem'; + certificatePath = '/usr/local/etc/halo-bridge/server.crt'; } if (fs.existsSync(privateKeyPath) && fs.existsSync(certificatePath)) { @@ -148,7 +149,6 @@ function wsCreateServer(args, getReaderNames) { const app = express(); const server = app.listen(args.listenPort, args.listenHost); - let forceUseTLS = (process.platform === "darwin"); let displayTLSWarn = (process.platform === "darwin"); if (tlsData) { @@ -172,13 +172,18 @@ function wsCreateServer(args, getReaderNames) { app.get('/', (req, res) => { res.render('ws_client.html', { - forceUseTLS: forceUseTLS, - displayTLSWarn: displayTLSWarn, wsPort: args.listenPort, wssPort: args.listenPortTLS }); }); + app.get('/health', (req, res) => { + res.type('text/plain') + .set('Access-Control-Allow-Origin', req.headers.origin) + .set('Access-Control-Allow-Methods', 'GET') + .send('OK'); + }); + app.get('/consent', async (req, res) => { let url = new URL(req.query.website); let csrfToken = await makeCSRFToken(); @@ -229,6 +234,13 @@ function wsCreateServer(args, getReaderNames) { } wss.on('connection', (ws, req) => { + let parts = req.url.split('?'); + + if (parts.length === 2 && parts[1] === "ping=1") { + ws.close(4090, "Pong."); + return; + } + let permitted = false; let originHostname = new URL(req.headers.origin).hostname; @@ -267,7 +279,7 @@ function wsCreateServer(args, getReaderNames) { } let packet = JSON.parse(data); - console.log('recv', packet); + console.log('recv', util.inspect(packet, {showHidden: false, depth: null, colors: true})); if (packet.type === "exec_halo") { try { diff --git a/docs/halo-bridge.md b/docs/halo-bridge.md new file mode 100644 index 0000000..a890c5c --- /dev/null +++ b/docs/halo-bridge.md @@ -0,0 +1,4 @@ +(TODO description of halo-bridge) + +(TODO how to connect with halo-bridge from external web app - check HTTPS then fall back to HTTP endpoint, +would give nearly 100% coverage of all systems and browsers) diff --git a/halo/gateway/executor.js b/halo/gateway/executor.js index b173847..ae8f94a 100644 --- a/halo/gateway/executor.js +++ b/halo/gateway/executor.js @@ -1,20 +1,10 @@ const {JWEUtil} = require("../jwe_util"); -const WebSocketAsPromised = require("websocket-as-promised"); const queryString = require("query-string"); let currentCmd = null; let jweUtil = new JWEUtil(); let wsp; -function createWs(url) { - return new WebSocketAsPromised(url, { - packMessage: data => JSON.stringify(data), - unpackMessage: data => JSON.parse(data), - attachRequestId: (data, requestId) => Object.assign({uid: requestId}, data), - extractRequestId: data => data && data.uid - }); -} - async function haloGateExecutorCreateWs(logCallback, newCommandCallback) { let protocol; let searchParts = window.location.hash.split('/'); diff --git a/web/web_utils.js b/web/web_utils.js new file mode 100644 index 0000000..2140675 --- /dev/null +++ b/web/web_utils.js @@ -0,0 +1,88 @@ +const WebSocketAsPromised = require('websocket-as-promised'); + +function haloCreateWs(url) { + return new WebSocketAsPromised(url, { + packMessage: data => JSON.stringify(data), + unpackMessage: data => JSON.parse(data), + attachRequestId: (data, requestId) => Object.assign({uid: requestId}, data), + extractRequestId: data => data && data.uid + }); +} + +function runHealthCheck(url) { + return new Promise((resolve, reject) => { + let closeTimeout = null; + + const pingUrl = url.includes('?') ? (url + '&ping=1') : (url + '?ping=1'); + const wsp = new WebSocketAsPromised(pingUrl, {timeout: 5000}); + + wsp.onClose.addListener(event => { + if (event.code === 4090) { + if (closeTimeout) { + clearTimeout(closeTimeout); + } + + resolve(url); + } else { + throw new Error("Unexpected WebSocket close code: " + event.code); + } + }); + + wsp.open() + .then(() => { + closeTimeout = setTimeout(() => { + wsp.close(); + reject(new Error('WebSocket didn\'t close as expected.')); + }, 2000); + }) + .catch((err) => { + reject(err); + }) + }); +} + +function createChecks(wsPort, wssPort) { + return [ + runHealthCheck('wss://halo-bridge.local:' + wssPort + '/ws'), + runHealthCheck('ws://127.0.0.1:' + wsPort + '/ws') + ]; +} + +async function haloFindBridge(options) { + options = Object.assign({}, options) || {}; + + if (!options.wsPort) { + options.wsPort = 32868; + } + + if (!options.wssPort) { + options.wssPort = 32869; + } + + if (options.diagnose) { + let res = await Promise.allSettled(createChecks(wsPort, wssPort)); + let urls = []; + let errors = []; + + for (let o of res) { + if (o.status === "fulfilled") { + urls.push(o.value); + } else { + errors.push(o.reason); + } + } + + return { + urls, + errors + }; + } else { + try { + return await Promise.any(createChecks(wsPort, wssPort)); + } catch (e) { + throw new Error("Unable to locate halo bridge."); + } + } +} + +module.exports = {haloCreateWs, haloFindBridge}; diff --git a/web/weblib.js b/web/weblib.js index de18ab8..0d4343e 100644 --- a/web/weblib.js +++ b/web/weblib.js @@ -19,9 +19,9 @@ const { arr2hex, hex2arr, parsePublicKeys, convertSignature, recoverPublicKey } = require("../halo/utils"); const {__runTestSuite} = require("../halo/tests"); -const WebSocketAsPromised = require("websocket-as-promised"); const {HaloGateway} = require("../halo/gateway/requestor"); const {haloGateExecutorCreateWs, haloGateExecutorUserConfirm} = require("../halo/gateway/executor"); +const {haloFindBridge, haloCreateWs} = require("./web_utils"); module.exports = { // utilities @@ -33,6 +33,10 @@ module.exports = { // for web usage execHaloCmdWeb, + haloFindBridge, + + // for bridge demo + haloCreateWs, // for web usage with gateway HaloGateway,