Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/elm-watch.json.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ type ElmWatchJson = {
output: string;
};
};
certificate?: {
cert: string;
key: string;
};
};
```

Expand All @@ -47,6 +51,10 @@ Example:
],
"output": "build/other/dist.js"
}
},
"certificate": {
"cert": "./ssl/le-certs/foo.crt",
"key": "./ssl/le-certs/foo.key"
}
}
```
Expand All @@ -56,6 +64,7 @@ Example:
| [targets](#targets) | `Record<string, object>` | _Required_ | The input Elm files to compile and the output JavaScript files to write to. At least one target is required. |
| [postprocess](../postprocess/) | `NonEmptyArray<string>` | 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

Expand Down
2 changes: 1 addition & 1 deletion docs/https.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
32 changes: 32 additions & 0 deletions src/Certificate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof Certificate>;
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
Expand Down
2 changes: 2 additions & 0 deletions src/ElmWatchJson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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" }
);
Expand Down
6 changes: 5 additions & 1 deletion src/Hot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
WebSocketToClientMessage,
WebSocketToServerMessage,
} from "../client/WebSocketMessages";
import { CertificateChoice } from "./Certificate";
import * as Compile from "./Compile";
import { ElmWatchStuffJsonWritable } from "./ElmWatchStuffJson";
import {
Expand Down Expand Up @@ -378,6 +379,7 @@ export async function run(
webSocketState: WebSocketState | undefined,
project: Project,
portChoice: PortChoice,
certificateChoice: CertificateChoice,
hotKillManager: HotKillManager
): Promise<HotRunResult> {
const exitOnError = __ELM_WATCH_EXIT_ON_ERROR in env;
Expand All @@ -391,6 +393,7 @@ export async function run(
webSocketState,
project,
portChoice,
certificateChoice,
hotKillManager
),
init: init(getNow(), restartReasons, project.elmJsonsErrors),
Expand Down Expand Up @@ -464,6 +467,7 @@ const initMutable =
webSocketState: WebSocketState | undefined,
project: Project,
portChoice: PortChoice,
certificateChoice: CertificateChoice,
hotKillManager: HotKillManager
) =>
(
Expand Down Expand Up @@ -514,7 +518,7 @@ const initMutable =
);

const {
webSocketServer = new WebSocketServer(portChoice),
webSocketServer = new WebSocketServer(portChoice, certificateChoice),
webSocketConnections = [],
} = webSocketState ?? {};

Expand Down
6 changes: 6 additions & 0 deletions src/Run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
16 changes: 11 additions & 5 deletions src/WebSocketServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -116,7 +121,7 @@ class PolyHttpServer {
}

export class WebSocketServer {
private polyHttpServer = new PolyHttpServer();
private polyHttpServer: PolyHttpServer;

private webSocketServer = new WsServer({ noServer: true });

Expand All @@ -128,7 +133,8 @@ export class WebSocketServer {

listening: Promise<void>;

constructor(portChoice: PortChoice) {
constructor(portChoice: PortChoice, certificate: CertificateChoice) {
this.polyHttpServer = new PolyHttpServer(certificate);
this.dispatch = this.dispatchToQueue;

this.webSocketServer.on("connection", (webSocket, request) => {
Expand Down