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.
This commit is contained in:
+5
-4
@@ -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.
|
||||
|
||||
@@ -115,7 +115,7 @@ Inspect live data with `bun llm-code-assistant/query-db.ts "<sql>"` - 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).
|
||||
|
||||
|
||||
+1
-1
@@ -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"]
|
||||
|
||||
@@ -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://<the name you passed>:7227` on the phone and install the CA
|
||||
from `https://<the name you passed>: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 |
|
||||
| -------------------- | ------------------------------------ |
|
||||
|
||||
@@ -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
|
||||
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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://<host>:<port>` still fails, but
|
||||
* `http://<host>:<port + 1>` 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://<host>:<port + 1>` 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
|
||||
|
||||
+39
-42
@@ -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<typeof resolveTlsOptions> = 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.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Parameters<typeof pickTlsFiles>[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");
|
||||
});
|
||||
});
|
||||
@@ -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" };
|
||||
}
|
||||
Reference in New Issue
Block a user