diff --git a/docs/elm-watch.json.md b/docs/elm-watch.json.md index 170f9ba7..9098995d 100644 --- a/docs/elm-watch.json.md +++ b/docs/elm-watch.json.md @@ -23,6 +23,10 @@ type ElmWatchJson = { output: string; }; }; + certificate?: { + cert: string; + key: string; + }; }; ``` @@ -47,6 +51,10 @@ Example: ], "output": "build/other/dist.js" } + }, + "certificate": { + "cert": "./ssl/le-certs/foo.crt", + "key": "./ssl/le-certs/foo.key" } } ``` @@ -56,6 +64,7 @@ Example: | [targets](#targets) | `Record` | _Required_ | The input Elm files to compile and the output JavaScript files to write to. At least one target is required. | | [postprocess](../postprocess/) | `NonEmptyArray` | No postprocessing. | A command to run after each `elm make` to transform Elm’s JavaScript output. | | port | `number` | An arbitrary available port. Tries to re-use the same port as last time you ran elm-watch. | WebSocket port for hot reloading. In case you _have_ to have the exact same port every time. Note that [some ports cannot be used][port-blocking]. | +| certificate | `Certificate` | A self-signed certificate for `localhost`. | WebSocket certificate for hot reloading. In case you can generate a trusted certificate. | ## targets diff --git a/docs/https.md b/docs/https.md index f433909b..13994ed0 100644 --- a/docs/https.md +++ b/docs/https.md @@ -21,7 +21,7 @@ If you use `https://`, then the first time you visit your page you’ll see how Click elm-watch’s [browser UI](../browser-ui/) to expand it. There’s a link there that goes to the WebSocket server. When you click it, your browser will show a scary-looking security screen. That’s because elm-watch uses a self-signed certificate, which isn’t secure. However, there’s no security to worry about here – elm-watch just needs a certificate to be able to use `wss://` (which is basically required on `https://` pages – more on that below). Click a few buttons to proceed to the page anyway. Once you’ve done that once, the browser remembers your choice. Go back to your page (and possibly refresh the page) and now the WebSocket should connect! If you’ve ever created a self-signed certificate yourself for development – that’s exactly what’s happening here. elm-watch ships with a generic self-signed certificate created with `openssl`. -If you’d like to be able to configure the certificate used by elm-watch, let me know! +If you’d like to be able to configure the certificate used by elm-watch, you can pass the PEM formatted `cert` and `key` in [`elm-watch.json`](../elm-watch.json/). Here are my findings from testing different combinations of http/s, ws/s, localhost vs not-localhost, and self-signed vs valid certificates: diff --git a/src/Certificate.ts b/src/Certificate.ts index a6790178..24d7a9ba 100644 --- a/src/Certificate.ts +++ b/src/Certificate.ts @@ -21,6 +21,38 @@ openssl req \ Source: https://stackoverflow.com/a/64309893 */ + +import * as fs from "fs"; +import * as Decode from "tiny-decoders"; + +const fileDecoder = Decode.chain(Decode.string, (filePath) => { + try { + return fs.readFileSync(filePath); + } catch (err) { + if (err instanceof Error) { + throw new Decode.DecoderError({ + message: `File not found: ${err.message}`, + value: filePath, + }); + } else { + throw new Decode.DecoderError({ + message: `File not found`, + value: filePath, + }); + } + } +}); + +export type Certificate = ReturnType; +export const Certificate = Decode.fieldsAuto({ + key: fileDecoder, + cert: fileDecoder, +}); + +export type CertificateChoice = + | { tag: "CertificateFromConfig"; certificate: Certificate } + | { tag: "NoCertificate" }; + export const CERTIFICATE = { key: `-----BEGIN PRIVATE KEY----- MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQC012uZX87KEVJA diff --git a/src/ElmWatchJson.ts b/src/ElmWatchJson.ts index 82bded17..8097d9cd 100644 --- a/src/ElmWatchJson.ts +++ b/src/ElmWatchJson.ts @@ -2,6 +2,7 @@ import * as fs from "fs"; import * as path from "path"; import * as Decode from "tiny-decoders"; +import { Certificate } from "./Certificate"; import { JsonError, toError, toJsonError } from "./Helpers"; import { IS_WINDOWS } from "./IsWindows"; import { @@ -92,6 +93,7 @@ const Config = Decode.fieldsAuto( targets: Decode.chain(Decode.record(Target), targetRecordHelper), postprocess: Decode.optional(NonEmptyArray(Decode.string)), port: Decode.optional(Port), + certificate: Decode.optional(Certificate), }, { exact: "throw" } ); diff --git a/src/Hot.ts b/src/Hot.ts index f63e5bd5..6d0b9492 100644 --- a/src/Hot.ts +++ b/src/Hot.ts @@ -11,6 +11,7 @@ import { WebSocketToClientMessage, WebSocketToServerMessage, } from "../client/WebSocketMessages"; +import { CertificateChoice } from "./Certificate"; import * as Compile from "./Compile"; import { ElmWatchStuffJsonWritable } from "./ElmWatchStuffJson"; import { @@ -378,6 +379,7 @@ export async function run( webSocketState: WebSocketState | undefined, project: Project, portChoice: PortChoice, + certificateChoice: CertificateChoice, hotKillManager: HotKillManager ): Promise { const exitOnError = __ELM_WATCH_EXIT_ON_ERROR in env; @@ -391,6 +393,7 @@ export async function run( webSocketState, project, portChoice, + certificateChoice, hotKillManager ), init: init(getNow(), restartReasons, project.elmJsonsErrors), @@ -464,6 +467,7 @@ const initMutable = webSocketState: WebSocketState | undefined, project: Project, portChoice: PortChoice, + certificateChoice: CertificateChoice, hotKillManager: HotKillManager ) => ( @@ -514,7 +518,7 @@ const initMutable = ); const { - webSocketServer = new WebSocketServer(portChoice), + webSocketServer = new WebSocketServer(portChoice, certificateChoice), webSocketConnections = [], } = webSocketState ?? {}; diff --git a/src/Run.ts b/src/Run.ts index b4991cc5..bfe50da8 100644 --- a/src/Run.ts +++ b/src/Run.ts @@ -276,6 +276,12 @@ export async function run( port: elmWatchStuffJson.port, } : { tag: "NoPort" }, + config.certificate !== undefined + ? { + tag: "CertificateFromConfig", + certificate: config.certificate, + } + : { tag: "NoCertificate" }, hotKillManager ); switch (result.tag) { diff --git a/src/WebSocketServer.ts b/src/WebSocketServer.ts index c2542efe..96651fac 100644 --- a/src/WebSocketServer.ts +++ b/src/WebSocketServer.ts @@ -5,7 +5,7 @@ import type { Duplex } from "stream"; import * as util from "util"; import WebSocket, { Server as WsServer } from "ws"; -import { CERTIFICATE } from "./Certificate"; +import { CERTIFICATE, CertificateChoice } from "./Certificate"; import { Port, PortChoice } from "./Port"; export type WebSocketServerMsg = @@ -45,9 +45,14 @@ class PolyHttpServer { private http = http.createServer(); - private https = https.createServer(CERTIFICATE); + private https: https.Server; - constructor() { + constructor(certificate: CertificateChoice) { + this.https = https.createServer( + certificate.tag === "CertificateFromConfig" + ? certificate.certificate + : CERTIFICATE + ); this.net.on("connection", (socket) => { socket.once("data", (buffer) => { socket.pause(); @@ -116,7 +121,7 @@ class PolyHttpServer { } export class WebSocketServer { - private polyHttpServer = new PolyHttpServer(); + private polyHttpServer: PolyHttpServer; private webSocketServer = new WsServer({ noServer: true }); @@ -128,7 +133,8 @@ export class WebSocketServer { listening: Promise; - constructor(portChoice: PortChoice) { + constructor(portChoice: PortChoice, certificate: CertificateChoice) { + this.polyHttpServer = new PolyHttpServer(certificate); this.dispatch = this.dispatchToQueue; this.webSocketServer.on("connection", (webSocket, request) => {