Serve HTTPS by default with a local CA
The installer generates a CA and a 397-day certificate into $COBBLE_HOME/certs (in the app image, with the host's addresses), mounts the certificate, key and CA into the container and serves TLS from Bun on the published port. --no-https opts out; --https-cert/--https-key use a real certificate, --https-host adds names and --https-regen replaces the CA. The installer and the image health check use curl -k when TLS is on. The CA is served at /ca.crt and as an iOS/macOS profile at /ca.mobileconfig for one-tap install. Development picks up certs/ automatically, `bun run dev:phone` generates and starts, and a plain-HTTP listener on PORT + 1 redirects to HTTPS. Port sharing is off, so a stale dev server fails instead of silently answering on the same port.
This commit is contained in:
+87
-1
@@ -1,4 +1,5 @@
|
||||
import bunext from "@moduletrace/bunext";
|
||||
import { existsSync } from "fs";
|
||||
import handleMediaServer from "./functions/server/media/handle-media-server";
|
||||
import cron from "./cron";
|
||||
import { websocket } from "./websocket";
|
||||
@@ -10,6 +11,9 @@ import liveRelay from "./live-relay";
|
||||
import enableForeignKeys from "./functions/backend/db/enable-foreign-keys";
|
||||
import handleComputerProxy from "./functions/server/computers/handle-computer-proxy";
|
||||
import handlePwaFile from "./functions/server/pwa/handle-pwa-file";
|
||||
import serveLocalCa from "./functions/server/serve-local-ca";
|
||||
import startHttpRedirect from "./functions/server/start-http-redirect";
|
||||
import resolveTlsOptions from "./utils/resolve-tls-options";
|
||||
import { ComputerProxyPrefix } from "./dict/computer-dict";
|
||||
import ensureDockerNetwork from "./functions/backend/docker/ensure-docker-network";
|
||||
import { ensureVapidKeys } from "./push/vapid";
|
||||
@@ -48,7 +52,59 @@ try {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTPS when the installer (or a development `.env`) points the server at
|
||||
* a certificate. A broken or half-set configuration stops the process
|
||||
* rather than quietly serving plain HTTP.
|
||||
*/
|
||||
/**
|
||||
* In development, a certificate in `certs/` (made by `bun run gen:certs`,
|
||||
* or by `bun run dev:phone`) is used without any `.env` entries, so
|
||||
* trying the app on a phone over a VPN is one command.
|
||||
*/
|
||||
const DevCertFile = "certs/server.crt";
|
||||
const DevKeyFile = "certs/server.key";
|
||||
const DevCaFile = "certs/ca.crt";
|
||||
|
||||
let cert_file = process.env.TLS_CERT_FILE;
|
||||
let key_file = process.env.TLS_KEY_FILE;
|
||||
|
||||
if (
|
||||
process.env.NODE_ENV != "production" &&
|
||||
!cert_file &&
|
||||
!key_file &&
|
||||
existsSync(DevCertFile) &&
|
||||
existsSync(DevKeyFile)
|
||||
) {
|
||||
cert_file = DevCertFile;
|
||||
key_file = DevKeyFile;
|
||||
|
||||
if (!process.env.TLS_CA_FILE && existsSync(DevCaFile)) {
|
||||
process.env.TLS_CA_FILE = DevCaFile;
|
||||
}
|
||||
|
||||
bunext.bunextLog.info(`Using the development certificate in certs/`);
|
||||
}
|
||||
|
||||
if (!cert_file && !key_file && process.env.NODE_ENV != "production") {
|
||||
bunext.bunextLog.info(
|
||||
"Serving plain HTTP (run `bun run dev:phone`, or `bun run gen:certs` then restart, for HTTPS)",
|
||||
);
|
||||
}
|
||||
|
||||
let tls: ReturnType<typeof resolveTlsOptions> = undefined;
|
||||
|
||||
try {
|
||||
tls = resolveTlsOptions({ cert_file, key_file });
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Can't start without a working TLS certificate: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const server = Bun.serve({
|
||||
...(tls ? { tls } : {}),
|
||||
async fetch(req, server) {
|
||||
try {
|
||||
const url = new URL(req.url);
|
||||
@@ -57,6 +113,10 @@ const server = Bun.serve({
|
||||
|
||||
if (pwa_file) return pwa_file;
|
||||
|
||||
const local_ca = await serveLocalCa({ pathname: url.pathname });
|
||||
|
||||
if (local_ca) return local_ca;
|
||||
|
||||
if (url.pathname.startsWith("/media")) {
|
||||
try {
|
||||
return await handleMediaServer({ req });
|
||||
@@ -108,13 +168,39 @@ const server = Bun.serve({
|
||||
websocket,
|
||||
development,
|
||||
port,
|
||||
/**
|
||||
* Fail loudly on a second dev server instead of letting Bun's default
|
||||
* port sharing split requests between an old and a new process (one
|
||||
* might be plain HTTP while the other is HTTPS).
|
||||
*/
|
||||
reusePort: false,
|
||||
idleTimeout: development ? 80 : undefined,
|
||||
});
|
||||
|
||||
bunext.bunextLog.info(
|
||||
`Server running on http://${server.hostname}:${server.port} ...`,
|
||||
`Server running on ${tls ? "https" : "http"}://${server.hostname}:${server.port} ...`,
|
||||
);
|
||||
|
||||
/**
|
||||
* A TLS port cannot answer plain HTTP, so a development `http://` visit
|
||||
* is sent to the HTTPS page from the port above it (`startHttpRedirect`).
|
||||
*/
|
||||
if (tls && process.env.NODE_ENV != "production") {
|
||||
try {
|
||||
const redirect = startHttpRedirect({ https_port: port });
|
||||
|
||||
if (redirect) {
|
||||
bunext.bunextLog.info(
|
||||
`HTTP redirect on port ${redirect.port} (DEV_HTTP_PORT to change it)`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
bunext.bunextLog.info(
|
||||
`Could not start the development HTTP redirect on port ${Number(port) + 1}: ${error instanceof Error ? error.message : String(error)}. Stop the other server or set DEV_HTTP_PORT.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
cron();
|
||||
botsQueue();
|
||||
liveRelay();
|
||||
Reference in New Issue
Block a user