From 459eebf73d04c8dcbea30931a47eafb331049064 Mon Sep 17 00:00:00 2001 From: Benjamin Toby Date: Tue, 20 Oct 2026 10:29:37 +0100 Subject: [PATCH] Use the certs/ fallback in every environment TLS no longer depends on NODE_ENV: the TLS_* variables win, else certs/ is used whenever it exists (the container's /app/certs mount lands there too), and TLS_DISABLED=1 is the only way to serve plain HTTP with a certificate present. The plain-HTTP redirect listener now starts whenever TLS is on, not only in development, and HTTP_REDIRECT_PORT replaces DEV_HTTP_PORT. The installer writes TLS_DISABLED=1 for --no-https, and the health check follows the same rule. --- .env.example | 9 ++- AGENTS.md | 4 +- Dockerfile | 2 +- README.md | 15 ++-- install.sh | 2 + src/dict/env-dict.ts | 7 +- src/functions/server/start-http-redirect.ts | 13 ++-- src/server.ts | 81 ++++++++++---------- src/utils/pick-tls-files.test.ts | 69 +++++++++++++++++ src/utils/pick-tls-files.ts | 83 +++++++++++++++++++++ 10 files changed, 222 insertions(+), 63 deletions(-) create mode 100644 src/utils/pick-tls-files.test.ts create mode 100644 src/utils/pick-tls-files.ts diff --git a/.env.example b/.env.example index 35aaad9..4e31ee7 100644 --- a/.env.example +++ b/.env.example @@ -17,14 +17,15 @@ DEV_EMAIL_RECIPIENT= ENCRYPTION_KEY= ENCRYPTION_SALT= -# HTTPS (optional in development; see README). Generate files with -# `bun run gen:certs` and point these at them. The installer sets them on a -# real install. Leave all three empty to serve plain HTTP. +# HTTPS (see README). Generate files with `bun run gen:certs`; when these +# are empty the server uses certs/ if it exists, in every environment. Set +# TLS_DISABLED=1 to serve plain HTTP even when a certificate is there. TLS_CERT_FILE= TLS_KEY_FILE= TLS_CA_FILE= +TLS_DISABLED= # Plain-HTTP port that redirects to the HTTPS one (default PORT + 1). -DEV_HTTP_PORT= +HTTP_REDIRECT_PORT= # Web Push: generated into Settings > Environment on first start. Set a # fixed pair here only if you want to keep one across installs. diff --git a/AGENTS.md b/AGENTS.md index 55d3a19..ec1f355 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -115,7 +115,7 @@ Inspect live data with `bun llm-code-assistant/query-db.ts ""` - a read-onl `install.sh` is the only supported install and update path: `curl -fsSL https://url.moduletrace.com/cobble | sudo bash`. It is idempotent, so the same script is the updater, and `--uninstall [--purge]` is the way back out. It installs Docker when missing (Linux), creates the `cobble` network, writes `$COBBLE_HOME/install.conf` and a generated `.env` (mode 600), and starts one `cobble` container. Existing secrets are never regenerated; the database is backed up to `$COBBLE_HOME/backups` before a container is replaced. The default home is `/opt/cobble`, configurable with `--home`, and everything that must survive an update lives in its `data/` directory (`db`, `media`, `computers`). -HTTPS is the default. `src/scripts/generate-local-certs.ts` (run inside the app image, since a container's interfaces are not the host's; the host's IPs and name are passed with `--host`) writes a local CA and a 397-day leaf into `$COBBLE_HOME/certs`; the certificate, key and CA are mounted read only into `/app/certs` (never the CA's key) and pointed at by `TLS_CERT_FILE`, `TLS_KEY_FILE` and `TLS_CA_FILE` (process-only envs). `src/utils/resolve-tls-options.ts` turns them into `Bun.serve`'s `tls`; a half-set or unreadable pair stops the process instead of falling back to HTTP, and `isSecureRequest` then sees an `https:` URL so cookies get `Secure`. `GET /ca.crt` (`serve-local-ca.ts`) serves the local CA for devices to trust, and `GET /ca.mobileconfig` (`build-ca-profile.ts`) serves the same CA as an iOS/macOS configuration profile for one-tap install (the trust switch after it is Apple's, not ours). The CA is kept across updates; the leaf is re-issued when the `sans.txt` names change or it is within 30 days of expiring, which a rerun does. `--no-https` removes the mount and envs (for a proxy or tunnel), `--https` turns it back on, `--https-cert`/`--https-key` copy in a real certificate, `--https-host` adds names or addresses, `--https-regen` replaces the CA. A fresh install asks about HTTPS once, with the reasons, before doing any work (`ask_https`: updates keep the saved mode, `--yes` skips it, any `--https*` flag answers it; it reads `/dev/tty`, never stdin, so `curl | bash` still works, and without a terminal it prints the notice and keeps the default). The installer and the image's `HEALTHCHECK` use `curl -k` when TLS is on. `bun run gen:certs` generates the same files into `certs/` (gitignored), `bun run dev` uses them when `TLS_*` are unset (development only), and `bun run dev:phone` generates and starts in one command. A TLS port cannot answer plain HTTP, so development also starts a redirect listener one port above (`DEV_HTTP_PORT`) that 308s to the HTTPS page (`startHttpRedirect`, tested by `build-https-redirect-url`); production publishes only the HTTPS port. +HTTPS is the default. `src/scripts/generate-local-certs.ts` (run inside the app image, since a container's interfaces are not the host's; the host's IPs and name are passed with `--host`) writes a local CA and a 397-day leaf into `$COBBLE_HOME/certs`; the certificate, key and CA are mounted read only into `/app/certs` (never the CA's key) and pointed at by `TLS_CERT_FILE`, `TLS_KEY_FILE` and `TLS_CA_FILE` (process-only envs). `src/utils/resolve-tls-options.ts` turns them into `Bun.serve`'s `tls`; a half-set or unreadable pair stops the process instead of falling back to HTTP, and `isSecureRequest` then sees an `https:` URL so cookies get `Secure`. `GET /ca.crt` (`serve-local-ca.ts`) serves the local CA for devices to trust, and `GET /ca.mobileconfig` (`build-ca-profile.ts`) serves the same CA as an iOS/macOS configuration profile for one-tap install (the trust switch after it is Apple's, not ours). The CA is kept across updates; the leaf is re-issued when the `sans.txt` names change or it is within 30 days of expiring, which a rerun does. `--no-https` removes the mount and envs (for a proxy or tunnel), `--https` turns it back on, `--https-cert`/`--https-key` copy in a real certificate, `--https-host` adds names or addresses, `--https-regen` replaces the CA. A fresh install asks about HTTPS once, with the reasons, before doing any work (`ask_https`: updates keep the saved mode, `--yes` skips it, any `--https*` flag answers it; it reads `/dev/tty`, never stdin, so `curl | bash` still works, and without a terminal it prints the notice and keeps the default). The installer and the image's `HEALTHCHECK` use `curl -k` when TLS is on. `bun run gen:certs` generates the same files into `certs/` (gitignored); the server uses them whenever `TLS_*` are unset, in every environment (there is no `NODE_ENV` gate; `pick-tls-files` decides, and `TLS_DISABLED=1` is the explicit opt-out), and `bun run dev:phone` generates and starts in one command. A TLS port cannot answer plain HTTP, so every environment starts a redirect listener one port above (`HTTP_REDIRECT_PORT`) that 308s to the HTTPS page (`startHttpRedirect`, tested by `build-https-redirect-url`); the container publishes only the HTTPS port, so that listener is reachable there only if its port is published too. The container runs with `--network cobble`, `-p BIND:PORT`, `.env` at `/app/.env` read only, the host data directory at `/data`, the docker socket at `/var/run/docker.sock`, `MAIN_SERVER=1` and `PORT`, `--init`, a `unless-stopped` restart policy and bounded json-file logs. `/data` must stay a bind mount: `resolveHostPath` turns paths under the app's own mounts into host paths for computer containers, and a named volume would break them. `DATA_DIR=/data` and `DB_DIR=/data/db` come from the Dockerfile; `entrypoint.sh` keeps bun-sqlite's schema record on the same volume. @@ -125,7 +125,7 @@ Images are built by GitHub Actions on a published release (`.github/workflows/re Envs are set in Settings, under Environment (super admin only), not in `.env`. Table `envs` (private): `key`, encrypted `value`, `description`, `paradigm` (`EnvParadigms`: general, workspace, computer) with `workspace_id`/`computer_id`, `secret`, `expose_to_computers`. One per key and scope, enforced in `saveEnv` (`src/functions/backend/db/envs/`), since SQLite unique constraints treat NULL ids as distinct. Decrypted rows are cached per process (`grabAllEnvs`; `saveEnv`/`deleteEnv` clear it). -- **App settings** (`EnvDict` in `src/dict/env-dict.ts`: `MAIN_COMPUTER_URL`, the `VAPID_*` Web Push keys and every provider preset's `key_env`) are what the app reads, through `grabEnv({ key })`: the general value, else `process.env`, so an existing `.env` still works. General only, never on computers. `ProcessOnlyEnvKeys` (`ENCRYPTION_KEY`/`SALT`, `PORT`, `DATA_DIR`, `MAIN_SERVER`, `TLS_CERT_FILE`, `TLS_KEY_FILE`, `TLS_CA_FILE`) can't be saved: they encrypt the table or are the container's own start-up settings. +- **App settings** (`EnvDict` in `src/dict/env-dict.ts`: `MAIN_COMPUTER_URL`, the `VAPID_*` Web Push keys and every provider preset's `key_env`) are what the app reads, through `grabEnv({ key })`: the general value, else `process.env`, so an existing `.env` still works. General only, never on computers. `ProcessOnlyEnvKeys` (`ENCRYPTION_KEY`/`SALT`, `PORT`, `DATA_DIR`, `MAIN_SERVER`, `TLS_CERT_FILE`, `TLS_KEY_FILE`, `TLS_CA_FILE`, `TLS_DISABLED`, `HTTP_REDIRECT_PORT`) can't be saved: they encrypt the table or are the container's own start-up settings. - **Bot and computer envs** are any other key. `grabComputerEnvs` resolves one run's set (`resolveScopedEnvs`): computer beats workspace beats general; workspaces are the non-archived ones the computer shares with the run's bot, oldest winning a tie. `openComputerTools` writes them to `~/.cobble/env` and `env.json` on the computer (owner only, rewritten every run) over the sandbox's REST API (`writeComputerEnv`); the sandbox's shells read no profile, so `withComputerEnv` makes `sandbox_execute_bash` source the file and `sandbox_execute_code` (Python, JavaScript) load the JSON, invisibly to the model. Bots get keys and descriptions in the task prompt, never values, and every computer tool result is passed through `redactSecrets` (each secret value shown as `[KEY]`). - **API:** `api/admin/envs/` (`save-env`, `delete-env`), super admin only, logged by key, each returning `grabEnvsOverview` (secret values left out; a secret is only replaced, never read back). diff --git a/Dockerfile b/Dockerfile index 7a8162a..3f3033a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -33,6 +33,6 @@ COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ - CMD if [ -n "$TLS_CERT_FILE" ]; then curl -fsSk "https://localhost:${PORT:-7227}/api/health" || exit 1; else curl -fsS "http://localhost:${PORT:-7227}/api/health" || exit 1; fi + CMD if [ -z "$TLS_DISABLED" ] && ( [ -n "$TLS_CERT_FILE" ] || [ -f /app/certs/server.crt ] ); then curl -fsSk "https://localhost:${PORT:-7227}/api/health" || exit 1; else curl -fsS "http://localhost:${PORT:-7227}/api/health" || exit 1; fi ENTRYPOINT ["/entrypoint.sh"] diff --git a/README.md b/README.md index 3baccd8..08b5c0a 100644 --- a/README.md +++ b/README.md @@ -210,20 +210,23 @@ bun run dev:phone -- --host cobble-laptop.tailnet-name.ts.net ``` That generates `certs/` and starts the dev server with it, with no `.env` -edits. Afterwards a plain `bun run dev` reuses the certificate whenever -`certs/server.crt` and `certs/server.key` exist (production never does). -`bun run gen:certs` on its own regenerates them if you need to add a name. +edits. Afterwards any server, `bun run dev` or `bun run start`, reuses the +certificate whenever `certs/server.crt` and `certs/server.key` exist; set +`TLS_DISABLED=1` to serve plain HTTP instead. `bun run gen:certs` on its own +regenerates them if you need to add a name. Open `https://:7227` on the phone and install the CA from `https://:7227/ca.mobileconfig` first. On the laptop, trust the same `certs/ca.crt` once, so `https://localhost:7227` stops warning. -With `certs/` present the dev server also listens one port above (`7228`) -for plain HTTP and redirects to the HTTPS page, so a stray +With a certificate the server also listens one port above (`7228`) for +plain HTTP and redirects to the HTTPS page, so a stray `http://localhost:7228/...` link still lands. The HTTPS port itself cannot answer plain HTTP, so `http://localhost:7227` still fails; use -`DEV_HTTP_PORT` to move the redirect. +`HTTP_REDIRECT_PORT` to move the redirect. The container publishes only the +HTTPS port, so the redirect is reachable there only if that second port is +published. | Command | Purpose | | -------------------- | ------------------------------------ | diff --git a/install.sh b/install.sh index 033dca0..50f9dd1 100644 --- a/install.sh +++ b/install.sh @@ -963,6 +963,7 @@ ensure_secret ENCRYPTION_SALT ensure_env PAGE_LASTMOD_FILE "/data/sitemap-lastmod.json" if [ "$HTTPS_ENABLED" = 1 ]; then + unset_env TLS_DISABLED set_env TLS_CERT_FILE /app/certs/server.crt set_env TLS_KEY_FILE /app/certs/server.key if [ "$HTTPS_MANAGED" = 1 ]; then @@ -974,6 +975,7 @@ else unset_env TLS_CERT_FILE unset_env TLS_KEY_FILE unset_env TLS_CA_FILE + set_env TLS_DISABLED 1 fi { diff --git a/src/dict/env-dict.ts b/src/dict/env-dict.ts index 0dd2e90..bc8cb99 100644 --- a/src/dict/env-dict.ts +++ b/src/dict/env-dict.ts @@ -38,8 +38,9 @@ export type KnownEnv = { * * Not here, and never in the `envs` table: `ENCRYPTION_KEY` and * `ENCRYPTION_SALT` (they encrypt the table), and `PORT`, `DATA_DIR`, - * `MAIN_SERVER` and the `TLS_*` certificate paths (the container's own - * start-up settings). Those stay in the process environment. + * `MAIN_SERVER`, `TLS_DISABLED`, `HTTP_REDIRECT_PORT` and the `TLS_*` + * certificate paths (the container's own start-up settings). Those stay in + * the process environment. */ export const EnvDict: readonly KnownEnv[] = [ { @@ -89,6 +90,8 @@ export const ProcessOnlyEnvKeys = [ "TLS_CERT_FILE", "TLS_KEY_FILE", "TLS_CA_FILE", + "TLS_DISABLED", + "HTTP_REDIRECT_PORT", ] as const; /** diff --git a/src/functions/server/start-http-redirect.ts b/src/functions/server/start-http-redirect.ts index e3824d6..c24d3e5 100644 --- a/src/functions/server/start-http-redirect.ts +++ b/src/functions/server/start-http-redirect.ts @@ -8,18 +8,19 @@ type Params = { }; /** - * Development convenience: a plain-HTTP listener on the port above the - * HTTPS one, which sends the browser to the same page over HTTPS. + * A plain-HTTP listener on the port above the HTTPS one, which sends the + * browser to the same page over HTTPS. * * A port that speaks TLS cannot answer plain HTTP, so this cannot sit on * the HTTPS port itself: `http://:` still fails, but - * `http://:` lands on the right page. `DEV_HTTP_PORT` - * picks another port. Production does not start it (only one port is - * published, and it is HTTPS). + * `http://:` lands on the right page. `HTTP_REDIRECT_PORT` + * picks another port. Every environment starts it; the container publishes + * only the HTTPS port, so there it is reachable only if that second port is + * published too. */ export default function startHttpRedirect({ https_port }: Params) { const base = Number(https_port); - const configured = Number(process.env.DEV_HTTP_PORT); + const configured = Number(process.env.HTTP_REDIRECT_PORT); const redirect_port = Number.isInteger(configured) && configured > 0 ? configured diff --git a/src/server.ts b/src/server.ts index 4824424..358a59f 100644 --- a/src/server.ts +++ b/src/server.ts @@ -14,6 +14,7 @@ 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 pickTlsFiles from "./utils/pick-tls-files"; import { ComputerProxyPrefix } from "./dict/computer-dict"; import ensureDockerNetwork from "./functions/backend/docker/ensure-docker-network"; import { ensureVapidKeys } from "./push/vapid"; @@ -53,51 +54,45 @@ try { } /** - * 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. + * Which certificate to serve with, the same way in every environment: the + * `TLS_*` variables when set, else `certs/` (which is also where the + * container's `/app/certs` mount lands). Plain HTTP needs `TLS_DISABLED`. */ -/** - * 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"; +const picked = pickTlsFiles({ + cert_file: process.env.TLS_CERT_FILE, + key_file: process.env.TLS_KEY_FILE, + ca_file: process.env.TLS_CA_FILE, + disabled: process.env.TLS_DISABLED, + fallback_cert_file: "certs/server.crt", + fallback_key_file: "certs/server.key", + fallback_ca_file: "certs/ca.crt", + file_exists: existsSync, +}); -let cert_file = process.env.TLS_CERT_FILE; -let key_file = process.env.TLS_KEY_FILE; +if (picked.ca_file) process.env.TLS_CA_FILE = picked.ca_file; +else delete process.env.TLS_CA_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) { - bunext.bunextLog.info( - process.env.NODE_ENV == "production" - ? "Serving plain HTTP (set TLS_CERT_FILE and TLS_KEY_FILE for HTTPS)" - : "Serving plain HTTP (run `bun run dev:phone`, or `bun run gen:certs` then restart, for HTTPS)", - ); +switch (picked.source) { + case "fallback": + bunext.bunextLog.info(`Using the certificate in certs/`); + break; + case "disabled": + bunext.bunextLog.info(`Serving plain HTTP (TLS_DISABLED is set)`); + break; + case "none": + bunext.bunextLog.info( + "Serving plain HTTP (no certificate; run `bun run gen:certs`, or set TLS_CERT_FILE and TLS_KEY_FILE)", + ); + break; } let tls: ReturnType = undefined; try { - tls = resolveTlsOptions({ cert_file, key_file }); + tls = resolveTlsOptions({ + cert_file: picked.cert_file, + key_file: picked.key_file, + }); } catch (error) { console.error( `Can't start without a working TLS certificate: ${error instanceof Error ? error.message : String(error)}`, @@ -184,21 +179,23 @@ bunext.bunextLog.info( ); /** - * A TLS port cannot answer plain HTTP, so a development `http://` visit - * is sent to the HTTPS page from the port above it (`startHttpRedirect`). + * A TLS port cannot answer plain HTTP, so a plain `http://` visit is sent + * to the HTTPS page from the port above it (`startHttpRedirect`). The + * container publishes only the HTTPS port, so there it is only reachable + * if that second port is published. */ -if (tls && process.env.NODE_ENV != "production") { +if (tls) { try { const redirect = startHttpRedirect({ https_port: port }); if (redirect) { bunext.bunextLog.info( - `HTTP redirect on port ${redirect.port} (DEV_HTTP_PORT to change it)`, + `HTTP redirect on port ${redirect.port} (HTTP_REDIRECT_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.`, + `Could not start the HTTP redirect on port ${Number(port) + 1}: ${error instanceof Error ? error.message : String(error)}. Stop the other server or set HTTP_REDIRECT_PORT.`, ); } } diff --git a/src/utils/pick-tls-files.test.ts b/src/utils/pick-tls-files.test.ts new file mode 100644 index 0000000..32d799e --- /dev/null +++ b/src/utils/pick-tls-files.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, test } from "bun:test"; +import pickTlsFiles from "./pick-tls-files"; + +const fallback = { + fallback_cert_file: "certs/server.crt", + fallback_key_file: "certs/server.key", + fallback_ca_file: "certs/ca.crt", +}; + +const exists = (paths: string[]) => (path: string) => paths.includes(path); + +const pick = ( + options: Partial[0]> = {}, + paths: string[] = ["certs/server.crt", "certs/server.key", "certs/ca.crt"], +) => + pickTlsFiles({ + ...fallback, + file_exists: exists(paths), + ...options, + }); + +describe("pickTlsFiles", () => { + test("prefers the TLS variables", () => { + const picked = pick({ + cert_file: "/etc/cert.pem", + key_file: "/etc/key.pem", + ca_file: "/etc/ca.pem", + }); + + expect(picked).toEqual({ + cert_file: "/etc/cert.pem", + key_file: "/etc/key.pem", + ca_file: "/etc/ca.pem", + source: "explicit", + }); + }); + + test("uses certs/ when the variables are unset, in any environment", () => { + const picked = pick({ cert_file: "", key_file: "" }); + + expect(picked).toEqual({ + cert_file: "certs/server.crt", + key_file: "certs/server.key", + ca_file: "certs/ca.crt", + source: "fallback", + }); + }); + + test("serves HTTP when the certificate is explicitly disabled", () => { + expect( + pick({ + cert_file: "/etc/cert.pem", + key_file: "/etc/key.pem", + disabled: "1", + }), + ).toEqual({ source: "disabled" }); + expect(pick({ disabled: "true" })).toEqual({ source: "disabled" }); + expect(pick({ disabled: "no" }).source).toBe("fallback"); + }); + + test("serves HTTP when there is nothing to serve with", () => { + expect(pick({}, []).source).toBe("none"); + expect(pick({}, ["certs/server.crt"]).source).toBe("none"); + }); + + test("keeps half a configuration for the resolver to reject", () => { + expect(pick({ cert_file: "/etc/cert.pem" }, []).source).toBe("explicit"); + }); +}); diff --git a/src/utils/pick-tls-files.ts b/src/utils/pick-tls-files.ts new file mode 100644 index 0000000..be37a8f --- /dev/null +++ b/src/utils/pick-tls-files.ts @@ -0,0 +1,83 @@ +type Params = { + /** + * `TLS_CERT_FILE`, when the environment names one. + */ + cert_file?: string; + /** + * `TLS_KEY_FILE`, when the environment names one. + */ + key_file?: string; + /** + * `TLS_CA_FILE`, served for devices to trust. + */ + ca_file?: string; + /** + * `TLS_DISABLED`, the explicit opt-out. + */ + disabled?: string; + /** + * Where a certificate is looked for when the environment names none: + * `certs/` at the project root, which is also where the container's + * `/app/certs` mount lands. + */ + fallback_cert_file: string; + fallback_key_file: string; + fallback_ca_file: string; + file_exists: (path: string) => boolean; +}; + +export type TlsFiles = { + cert_file?: string; + key_file?: string; + ca_file?: string; + /** + * Where the choice came from, for the startup log: the environment, + * the `certs/` fallback, an explicit disable, or nothing at all. + */ + source: "explicit" | "fallback" | "disabled" | "none"; +}; + +const DisabledValues = new Set(["1", "true", "yes", "on"]); + +/** + * Which certificate files to serve with, in every environment: the + * `TLS_*` variables when they are set, else `certs/` when it exists, and + * plain HTTP only when `TLS_DISABLED` says so or there is nothing to serve + * with. + * + * Nothing here is tied to `NODE_ENV`: a production process that has a + * certificate serves HTTPS, and the only way to turn it off is to say so. + */ +export default function pickTlsFiles({ + cert_file, + key_file, + ca_file, + disabled, + fallback_cert_file, + fallback_key_file, + fallback_ca_file, + file_exists, +}: Params): TlsFiles { + if (disabled && DisabledValues.has(disabled.trim().toLowerCase())) { + return { source: "disabled" }; + } + + const cert = cert_file?.trim(); + const key = key_file?.trim(); + + if (cert || key) { + return { cert_file: cert, key_file: key, ca_file: ca_file?.trim(), source: "explicit" }; + } + + if (file_exists(fallback_cert_file) && file_exists(fallback_key_file)) { + return { + cert_file: fallback_cert_file, + key_file: fallback_key_file, + ca_file: ca_file?.trim() || + (file_exists(fallback_ca_file) ? fallback_ca_file : undefined), + source: "fallback", + }; + } + + return { source: "none" }; +}