From cde15cfd91f98185a1dd50ae6c4ab9bd86a4c36d Mon Sep 17 00:00:00 2001 From: hensm Date: Fri, 2 Sep 2022 09:19:23 +0100 Subject: [PATCH] Add option to use a secure connection for the daemon --- app/src/daemon.ts | 50 +++++++++++++++------------ app/src/main.ts | 52 ++++++++++++++++++++++++++--- ext/src/_locales/en/messages.json | 6 +++- ext/src/defaultOptions.ts | 2 ++ ext/src/lib/nativeMessaging.ts | 16 ++++++--- ext/src/ui/options/Bridge.svelte | 37 ++++++++++++++------ ext/src/ui/options/styles/index.css | 9 +++++ 7 files changed, 131 insertions(+), 41 deletions(-) diff --git a/app/src/daemon.ts b/app/src/daemon.ts index 16d31ae..09c79ee 100644 --- a/app/src/daemon.ts +++ b/app/src/daemon.ts @@ -1,35 +1,31 @@ -import http, { IncomingMessage } from "http"; -import WebSocket from "ws"; - +import http from "http"; +import https from "https"; import { spawn } from "child_process"; import { Readable } from "stream"; +import WebSocket from "ws"; + import { DecodeTransform, EncodeTransform } from "./transforms.js"; -interface DaemonOpts { +export interface DaemonOpts { host: string; port: number; password?: string; + secure?: boolean; + key?: Buffer; + cert?: Buffer; } export function init(opts: DaemonOpts) { - const server = http.createServer(); + const server = !opts.secure + ? http.createServer() + : https.createServer({ + key: opts.key, + cert: opts.cert + }); + const wss = new WebSocket.Server({ noServer: true }); - process.stdout.write( - `Starting WebSocket server at ws://${ - opts.host.includes(":") ? `[${opts.host}]` : opts.host - }:${opts.port}... ` - ); - - server.on("listening", () => { - process.stdout.write("Done!\n"); - }); - server.on("error", err => { - console.error("Failed!"); - console.error(err.message); - }); - wss.on("connection", socket => { // Stream for incoming WebSocket messages const messageStream = new Readable({ objectMode: true }); @@ -72,7 +68,7 @@ export function init(opts: DaemonOpts) { * Authenticates requests by checking password URL param against * server password specified in launch options. */ - function authenticate(req: IncomingMessage) { + function authenticate(req: http.IncomingMessage) { if (!opts.password) return true; const password = new URL( @@ -121,5 +117,17 @@ export function init(opts: DaemonOpts) { res.end(); }); - server.listen({ port: opts.port, host: opts.host }); + process.stdout.write( + `Starting WebSocket server at ${opts.secure ? "wss" : "ws"}://${ + opts.host.includes(":") ? `[${opts.host}]` : opts.host + }:${opts.port}... ` + ); + server.listen({ port: opts.port, host: opts.host }, () => { + process.stdout.write("Done!\n"); + }); + + server.on("error", err => { + console.error("Failed!"); + console.error(err.message); + }); } diff --git a/app/src/main.ts b/app/src/main.ts index 8c247bc..fcc9e9d 100644 --- a/app/src/main.ts +++ b/app/src/main.ts @@ -1,4 +1,9 @@ +import fs from "fs"; + import yargs from "yargs"; + +import type { DaemonOpts } from "./daemon"; + import { applicationName, applicationVersion } from "../config.json"; const argv = yargs() @@ -30,14 +35,46 @@ the port set in the extension options.`, alias: "P", describe: `Set an optional password for the daemon WebSocket server. \ This must match the password set in the extension options. -WARNING: This password is intended only as a basic access control measure and \ -is transmitted in plain text even over remote connections!`, +Note: If using this option it is highly recommended that you enable secure \ +connections to avoid leaking plaintext passwords!`, + type: "string" + }) + .option("secure", { + alias: "s", + describe: `Use a secure HTTPS server for WebSocket connections. \ +Requires key/cert file options to be specified.`, + type: "boolean", + default: false + }) + .option("key-file", { + alias: "k", + describe: `Path to the private key PEM file to use for the \ +HTTPS server.`, + type: "string" + }) + .option("cert-file", { + alias: "c", + describe: `Path to the certificate PEM file to use for the \ +HTTPS server.`, type: "string" }) .check(argv => { + // Ensure valid port range if (argv.port < 1025 || argv.port > 65535) { throw new Error("Invalid port specified!"); } + // Ensure secure options are valid + if (argv.secure) { + if (!argv["key-file"] || !argv["cert-file"]) { + throw new Error("Missing required key/cert files."); + } + if ( + !fs.existsSync(argv["key-file"]) || + !fs.existsSync(argv["cert-file"]) + ) { + throw new Error("Specified key/cert files do not exist."); + } + } return true; }) @@ -45,11 +82,18 @@ is transmitted in plain text even over remote connections!`, if (argv.daemon) { import("./daemon").then(daemon => { - daemon.init({ + const daemonOpts: DaemonOpts = { host: argv.host, port: argv.port, password: argv.password - }); + }; + if (argv.secure) { + daemonOpts.secure = true; + daemonOpts.key = fs.readFileSync(argv.keyFile!); + daemonOpts.cert = fs.readFileSync(argv.certFile!); + } + + daemon.init(daemonOpts); }); } else { import("./bridge"); diff --git a/ext/src/_locales/en/messages.json b/ext/src/_locales/en/messages.json index e6c0b21..ff6e965 100755 --- a/ext/src/_locales/en/messages.json +++ b/ext/src/_locales/en/messages.json @@ -309,8 +309,12 @@ "message": "If the regular bridge connection fails, attempt to connect to a bridge running in daemon mode.", "description": "Backup daemon checkbox description." }, + "optionsBridgeBackupSecure": { + "message": "Use a secure daemon connection", + "description": "Daemon secure option checkbox label." + }, "optionsBridgeBackupPassword": { - "message": "...with password:", + "message": "Password:", "description": "Daemon password option label." }, diff --git a/ext/src/defaultOptions.ts b/ext/src/defaultOptions.ts index 58deb5f..08a7d9b 100644 --- a/ext/src/defaultOptions.ts +++ b/ext/src/defaultOptions.ts @@ -5,6 +5,7 @@ export interface Options { bridgeBackupEnabled: boolean; bridgeBackupHost: string; bridgeBackupPort: number; + bridgeBackupSecure: boolean; bridgeBackupPassword: string; mediaEnabled: boolean; mediaSyncElement: boolean; @@ -28,6 +29,7 @@ export default { bridgeBackupEnabled: false, bridgeBackupHost: "localhost", bridgeBackupPort: 9556, + bridgeBackupSecure: false, bridgeBackupPassword: "", mediaEnabled: true, mediaSyncElement: false, diff --git a/ext/src/lib/nativeMessaging.ts b/ext/src/lib/nativeMessaging.ts index d37a460..c2e2b66 100644 --- a/ext/src/lib/nativeMessaging.ts +++ b/ext/src/lib/nativeMessaging.ts @@ -10,11 +10,15 @@ type MessageListener = (message: Message) => void; * Create backup server URL from configured options. */ async function getBackupServerUrl() { - const { bridgeBackupHost, bridgeBackupPort, bridgeBackupPassword } = - await options.getAll(); + const { + bridgeBackupHost, + bridgeBackupPort, + bridgeBackupSecure, + bridgeBackupPassword + } = await options.getAll(); const url = new URL( - `ws://${ + `${bridgeBackupSecure ? "wss" : "ws"}://${ // Handle IPv6 address formatting bridgeBackupHost.includes(":") ? `[${bridgeBackupHost}]` @@ -168,7 +172,9 @@ export async function sendNativeMessage(application: string, message: Message) { try { return await browser.runtime.sendNativeMessage(application, message); } catch { - const bridgeBackupEnabled = await options.get("bridgeBackupEnabled"); + const { bridgeBackupEnabled, bridgeBackupSecure } = + await options.getAll(); + if (!bridgeBackupEnabled) { throw logger.error( "Bridge connection failed and backup not enabled." @@ -178,7 +184,7 @@ export async function sendNativeMessage(application: string, message: Message) { const backupServerUrl = await getBackupServerUrl(); const backupServerHttpUrl = new URL(backupServerUrl); - backupServerHttpUrl.protocol = "http"; + backupServerHttpUrl.protocol = bridgeBackupSecure ? "https" : "http"; // Send HTTP request to check authentication if ((await fetch(backupServerHttpUrl)).status === 401) { diff --git a/ext/src/ui/options/Bridge.svelte b/ext/src/ui/options/Bridge.svelte index 5a45531..04a79a2 100644 --- a/ext/src/ui/options/Bridge.svelte +++ b/ext/src/ui/options/Bridge.svelte @@ -272,24 +272,41 @@ bind:value={opts.bridgeBackupPort} /> {backupMessageEnd} + +
+ {_("optionsBridgeBackupEnabledDescription")} +
+ - {#if opts.showAdvancedOptions} -