mirror of
https://github.com/hensm/fx_cast.git
synced 2026-06-12 02:29:59 +00:00
Add option to use a secure connection for the daemon
This commit is contained in:
@@ -1,35 +1,31 @@
|
|||||||
import http, { IncomingMessage } from "http";
|
import http from "http";
|
||||||
import WebSocket from "ws";
|
import https from "https";
|
||||||
|
|
||||||
import { spawn } from "child_process";
|
import { spawn } from "child_process";
|
||||||
import { Readable } from "stream";
|
import { Readable } from "stream";
|
||||||
|
|
||||||
|
import WebSocket from "ws";
|
||||||
|
|
||||||
import { DecodeTransform, EncodeTransform } from "./transforms.js";
|
import { DecodeTransform, EncodeTransform } from "./transforms.js";
|
||||||
|
|
||||||
interface DaemonOpts {
|
export interface DaemonOpts {
|
||||||
host: string;
|
host: string;
|
||||||
port: number;
|
port: number;
|
||||||
password?: string;
|
password?: string;
|
||||||
|
secure?: boolean;
|
||||||
|
key?: Buffer;
|
||||||
|
cert?: Buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function init(opts: DaemonOpts) {
|
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 });
|
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 => {
|
wss.on("connection", socket => {
|
||||||
// Stream for incoming WebSocket messages
|
// Stream for incoming WebSocket messages
|
||||||
const messageStream = new Readable({ objectMode: true });
|
const messageStream = new Readable({ objectMode: true });
|
||||||
@@ -72,7 +68,7 @@ export function init(opts: DaemonOpts) {
|
|||||||
* Authenticates requests by checking password URL param against
|
* Authenticates requests by checking password URL param against
|
||||||
* server password specified in launch options.
|
* server password specified in launch options.
|
||||||
*/
|
*/
|
||||||
function authenticate(req: IncomingMessage) {
|
function authenticate(req: http.IncomingMessage) {
|
||||||
if (!opts.password) return true;
|
if (!opts.password) return true;
|
||||||
|
|
||||||
const password = new URL(
|
const password = new URL(
|
||||||
@@ -121,5 +117,17 @@ export function init(opts: DaemonOpts) {
|
|||||||
res.end();
|
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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
|
||||||
import yargs from "yargs";
|
import yargs from "yargs";
|
||||||
|
|
||||||
|
import type { DaemonOpts } from "./daemon";
|
||||||
|
|
||||||
import { applicationName, applicationVersion } from "../config.json";
|
import { applicationName, applicationVersion } from "../config.json";
|
||||||
|
|
||||||
const argv = yargs()
|
const argv = yargs()
|
||||||
@@ -30,14 +35,46 @@ the port set in the extension options.`,
|
|||||||
alias: "P",
|
alias: "P",
|
||||||
describe: `Set an optional password for the daemon WebSocket server. \
|
describe: `Set an optional password for the daemon WebSocket server. \
|
||||||
This must match the password set in the extension options.
|
This must match the password set in the extension options.
|
||||||
WARNING: This password is intended only as a basic access control measure and \
|
Note: If using this option it is highly recommended that you enable secure \
|
||||||
is transmitted in plain text even over remote connections!`,
|
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"
|
type: "string"
|
||||||
})
|
})
|
||||||
.check(argv => {
|
.check(argv => {
|
||||||
|
// Ensure valid port range
|
||||||
if (argv.port < 1025 || argv.port > 65535) {
|
if (argv.port < 1025 || argv.port > 65535) {
|
||||||
throw new Error("Invalid port specified!");
|
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;
|
return true;
|
||||||
})
|
})
|
||||||
@@ -45,11 +82,18 @@ is transmitted in plain text even over remote connections!`,
|
|||||||
|
|
||||||
if (argv.daemon) {
|
if (argv.daemon) {
|
||||||
import("./daemon").then(daemon => {
|
import("./daemon").then(daemon => {
|
||||||
daemon.init({
|
const daemonOpts: DaemonOpts = {
|
||||||
host: argv.host,
|
host: argv.host,
|
||||||
port: argv.port,
|
port: argv.port,
|
||||||
password: argv.password
|
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 {
|
} else {
|
||||||
import("./bridge");
|
import("./bridge");
|
||||||
|
|||||||
@@ -309,8 +309,12 @@
|
|||||||
"message": "If the regular bridge connection fails, attempt to connect to a bridge running in daemon mode.",
|
"message": "If the regular bridge connection fails, attempt to connect to a bridge running in daemon mode.",
|
||||||
"description": "Backup daemon checkbox description."
|
"description": "Backup daemon checkbox description."
|
||||||
},
|
},
|
||||||
|
"optionsBridgeBackupSecure": {
|
||||||
|
"message": "Use a secure daemon connection",
|
||||||
|
"description": "Daemon secure option checkbox label."
|
||||||
|
},
|
||||||
"optionsBridgeBackupPassword": {
|
"optionsBridgeBackupPassword": {
|
||||||
"message": "...with password:",
|
"message": "Password:",
|
||||||
"description": "Daemon password option label."
|
"description": "Daemon password option label."
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export interface Options {
|
|||||||
bridgeBackupEnabled: boolean;
|
bridgeBackupEnabled: boolean;
|
||||||
bridgeBackupHost: string;
|
bridgeBackupHost: string;
|
||||||
bridgeBackupPort: number;
|
bridgeBackupPort: number;
|
||||||
|
bridgeBackupSecure: boolean;
|
||||||
bridgeBackupPassword: string;
|
bridgeBackupPassword: string;
|
||||||
mediaEnabled: boolean;
|
mediaEnabled: boolean;
|
||||||
mediaSyncElement: boolean;
|
mediaSyncElement: boolean;
|
||||||
@@ -28,6 +29,7 @@ export default {
|
|||||||
bridgeBackupEnabled: false,
|
bridgeBackupEnabled: false,
|
||||||
bridgeBackupHost: "localhost",
|
bridgeBackupHost: "localhost",
|
||||||
bridgeBackupPort: 9556,
|
bridgeBackupPort: 9556,
|
||||||
|
bridgeBackupSecure: false,
|
||||||
bridgeBackupPassword: "",
|
bridgeBackupPassword: "",
|
||||||
mediaEnabled: true,
|
mediaEnabled: true,
|
||||||
mediaSyncElement: false,
|
mediaSyncElement: false,
|
||||||
|
|||||||
@@ -10,11 +10,15 @@ type MessageListener = (message: Message) => void;
|
|||||||
* Create backup server URL from configured options.
|
* Create backup server URL from configured options.
|
||||||
*/
|
*/
|
||||||
async function getBackupServerUrl() {
|
async function getBackupServerUrl() {
|
||||||
const { bridgeBackupHost, bridgeBackupPort, bridgeBackupPassword } =
|
const {
|
||||||
await options.getAll();
|
bridgeBackupHost,
|
||||||
|
bridgeBackupPort,
|
||||||
|
bridgeBackupSecure,
|
||||||
|
bridgeBackupPassword
|
||||||
|
} = await options.getAll();
|
||||||
|
|
||||||
const url = new URL(
|
const url = new URL(
|
||||||
`ws://${
|
`${bridgeBackupSecure ? "wss" : "ws"}://${
|
||||||
// Handle IPv6 address formatting
|
// Handle IPv6 address formatting
|
||||||
bridgeBackupHost.includes(":")
|
bridgeBackupHost.includes(":")
|
||||||
? `[${bridgeBackupHost}]`
|
? `[${bridgeBackupHost}]`
|
||||||
@@ -168,7 +172,9 @@ export async function sendNativeMessage(application: string, message: Message) {
|
|||||||
try {
|
try {
|
||||||
return await browser.runtime.sendNativeMessage(application, message);
|
return await browser.runtime.sendNativeMessage(application, message);
|
||||||
} catch {
|
} catch {
|
||||||
const bridgeBackupEnabled = await options.get("bridgeBackupEnabled");
|
const { bridgeBackupEnabled, bridgeBackupSecure } =
|
||||||
|
await options.getAll();
|
||||||
|
|
||||||
if (!bridgeBackupEnabled) {
|
if (!bridgeBackupEnabled) {
|
||||||
throw logger.error(
|
throw logger.error(
|
||||||
"Bridge connection failed and backup not enabled."
|
"Bridge connection failed and backup not enabled."
|
||||||
@@ -178,7 +184,7 @@ export async function sendNativeMessage(application: string, message: Message) {
|
|||||||
const backupServerUrl = await getBackupServerUrl();
|
const backupServerUrl = await getBackupServerUrl();
|
||||||
|
|
||||||
const backupServerHttpUrl = new URL(backupServerUrl);
|
const backupServerHttpUrl = new URL(backupServerUrl);
|
||||||
backupServerHttpUrl.protocol = "http";
|
backupServerHttpUrl.protocol = bridgeBackupSecure ? "https" : "http";
|
||||||
|
|
||||||
// Send HTTP request to check authentication
|
// Send HTTP request to check authentication
|
||||||
if ((await fetch(backupServerHttpUrl)).status === 401) {
|
if ((await fetch(backupServerHttpUrl)).status === 401) {
|
||||||
|
|||||||
@@ -272,24 +272,41 @@
|
|||||||
bind:value={opts.bridgeBackupPort}
|
bind:value={opts.bridgeBackupPort}
|
||||||
/>
|
/>
|
||||||
{backupMessageEnd}
|
{backupMessageEnd}
|
||||||
|
</label>
|
||||||
|
<div class="option__description">
|
||||||
|
{_("optionsBridgeBackupEnabledDescription")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if opts.showAdvancedOptions}
|
{#if opts.showAdvancedOptions}
|
||||||
<label class="bridge__backup-password">
|
<fieldset class="category" disabled={!opts.bridgeBackupEnabled}>
|
||||||
|
<div class="option option--inline">
|
||||||
|
<div class="option__control">
|
||||||
|
<input
|
||||||
|
id="bridgeBackupSecure"
|
||||||
|
type="checkbox"
|
||||||
|
bind:checked={opts.bridgeBackupSecure}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<label class="option__label" for="bridgeBackupSecure">
|
||||||
|
{_("optionsBridgeBackupSecure")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="option">
|
||||||
|
<label class="option__label" for="bridgeBackupPassword">
|
||||||
{_("optionsBridgeBackupPassword")}
|
{_("optionsBridgeBackupPassword")}
|
||||||
|
</label>
|
||||||
|
<div class="option__control">
|
||||||
<input
|
<input
|
||||||
id="bridgeBackupPassword"
|
id="bridgeBackupPassword"
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
type="password"
|
type="password"
|
||||||
bind:value={opts.bridgeBackupPassword}
|
bind:value={opts.bridgeBackupPassword}
|
||||||
/>
|
/>
|
||||||
</label>
|
</div>
|
||||||
{/if}
|
</div>
|
||||||
</label>
|
</fieldset>
|
||||||
<div class="option__description">
|
{/if}
|
||||||
{_("optionsBridgeBackupEnabledDescription")}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if !isLoadingInfo}
|
{#if !isLoadingInfo}
|
||||||
|
|||||||
@@ -208,6 +208,10 @@ input:placeholder-shown {
|
|||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bridge__options > .category {
|
||||||
|
grid-template-columns: 100px minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
.form > .category {
|
.form > .category {
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
}
|
}
|
||||||
@@ -274,6 +278,11 @@ input:placeholder-shown {
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.category:disabled .option__label,
|
||||||
|
.category:disabled .option__description {
|
||||||
|
opacity: 0.65;
|
||||||
|
}
|
||||||
|
|
||||||
.option__recommended {
|
.option__recommended {
|
||||||
background-color: var(--blue-60);
|
background-color: var(--blue-60);
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
|
|||||||
Reference in New Issue
Block a user