diff --git a/AGENTS.md b/AGENTS.md index 5a83ed3..55d3a19 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. 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), `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. 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. diff --git a/README.md b/README.md index 1d7476d..3baccd8 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,13 @@ missing): curl -fsSL https://url.moduletrace.com/cobble | sudo bash ``` +HTTPS is on by default, and a fresh install asks before continuing: it +encrypts the connection, and phones need it to install the app and receive +notifications. The installer creates a local CA for the machine, so each +device trusts it once. Answer no, or pass `--no-https`, if a proxy or tunnel +already handles HTTPS; `--yes` accepts the default without asking. Updates +keep the mode already chosen. + Open the address it prints, usually `https://:7227`. The certificate is signed by a local CA the installer made, so the browser warns until you trust it: open `https://:7227/ca.crt` on the device and @@ -56,11 +63,13 @@ thing that should reach it (with `--no-https`). ## HTTPS -The installer serves HTTPS by default. It generates a local CA and a -certificate for this machine into `$COBBLE_HOME/certs` and mounts them into -the container read only. The CA is kept across updates; the certificate is -re-issued when the machine's addresses change or it is within 30 days of -expiring, and a rerun of the updater does both. +The installer serves HTTPS by default, and a fresh install asks before +continuing (`--yes` skips the question, `--no-https` answers it, an update +keeps the saved mode). It generates a local CA and a certificate for this +machine into `$COBBLE_HOME/certs` and mounts them into the container read +only. The CA is kept across updates; the certificate is re-issued when the +machine's addresses change or it is within 30 days of expiring, and a rerun +of the updater does both. Trust the CA once on each device. Until it is trusted the browser warns, and the installed app, its service worker and Web Push stay off: @@ -127,6 +136,7 @@ curl -fsSL https://url.moduletrace.com/cobble | sudo bash -s -- --uninstall --pu | `--no-pull` | Use an image that is already on the machine | | `--skip-backup` | Update without a database backup | | `--no-docker-install` | Fail instead of installing Docker | +| `--yes`, `-y` | Accept the defaults without asking (HTTPS on) | | `--uninstall` | Stop and remove the app container | | `--purge` | With `--uninstall`, also delete the data directory | diff --git a/install.sh b/install.sh index 15bc280..033dca0 100644 --- a/install.sh +++ b/install.sh @@ -77,13 +77,17 @@ Options: --force Recreate the container even when it is current --skip-backup Do not back up the database before an update --no-docker-install Fail instead of installing Docker when it is missing + --yes, -y Accept the defaults without asking (HTTPS on) --uninstall Stop and remove the app container --purge With --uninstall, also delete the data directory --help Show this help HTTPS: - It is on by default. A local CA and a certificate for this machine are - generated into HOME/certs. Trust the CA on each device first: + It is on by default, and a fresh install asks before continuing. It + encrypts the connection, and phones need it to install the app and + receive notifications; a proxy or tunnel that already handles HTTPS is + the reason to answer no. A local CA and a certificate for this machine + are generated into HOME/certs, so each device trusts the CA once: https://:/ca.crt, or HOME/certs/ca.crt. iPhone and iPad install it in one tap from https://:/ca.mobileconfig. @@ -144,7 +148,9 @@ esac ENV_FILE="$HOME_DIR/.env" STATE_FILE="$HOME_DIR/install.conf" +HAD_STATE=0 if [ -f "$STATE_FILE" ]; then + HAD_STATE=1 # shellcheck disable=SC1090 . "$STATE_FILE" fi @@ -170,9 +176,11 @@ SKIP_BACKUP=0 PURGE=0 HTTPS_DISABLED=0 HTTPS_FORCE=0 +HTTPS_EXPLICIT=0 HTTPS_HOSTS_GIVEN=0 HTTPS_CERT_GIVEN=0 HTTPS_REGEN=0 +ASSUME_YES=0 HTTPS_MANAGED=1 CERTS_CHANGED=0 CERT_EXPIRY="" @@ -226,10 +234,12 @@ while [ $# -gt 0 ]; do --https) HTTPS_ENABLED=1 HTTPS_FORCE=1 + HTTPS_EXPLICIT=1 shift ;; --no-https) HTTPS_DISABLED=1 + HTTPS_EXPLICIT=1 shift ;; --https-host) @@ -239,20 +249,28 @@ while [ $# -gt 0 ]; do HTTPS_HOSTS="${2:-}" HTTPS_HOSTS_GIVEN=1 fi + HTTPS_EXPLICIT=1 shift 2 ;; --https-cert) HTTPS_CERT_SOURCE="${2:-}" HTTPS_CERT_GIVEN=1 + HTTPS_EXPLICIT=1 shift 2 ;; --https-key) HTTPS_KEY_SOURCE="${2:-}" HTTPS_CERT_GIVEN=1 + HTTPS_EXPLICIT=1 shift 2 ;; --https-regen) HTTPS_REGEN=1 + HTTPS_EXPLICIT=1 + shift + ;; + --yes | -y) + ASSUME_YES=1 shift ;; --force) @@ -460,6 +478,50 @@ health_url() { printf '%s://%s:%s/api/health' "$(health_scheme)" "$(health_host)" "$PORT" } +# A fresh install asks about HTTPS and states why; updates keep the saved +# mode, and any --https* flag (or --yes) skips the question. The prompt +# reads /dev/tty, never stdin: with `curl | bash` stdin is the script. +ask_https() { + local answer="" + + if [ "$HAD_STATE" = 1 ] || [ "$HTTPS_EXPLICIT" = 1 ] || [ "$ACTION" = "uninstall" ] || [ "$ASSUME_YES" = 1 ]; then + return 0 + fi + + if ! { : /dev/null; then + printf '\nHTTPS is on by default: it encrypts the connection, and phones need it to install the app and receive notifications. Each device trusts the local certificate once. Pass --no-https if a proxy or tunnel already handles HTTPS.\n\n' >&2 + return 0 + fi + + printf '\n' >&2 + printf 'Cobble can serve HTTPS, and that is the default.\n' >&2 + printf 'It encrypts the connection, and phones need it to install the app\n' >&2 + printf 'and receive notifications. The installer creates a local CA for\n' >&2 + printf 'this machine, so each device trusts it once (on an iPhone: install\n' >&2 + printf 'the profile, then turn it on under Certificate Trust Settings).\n' >&2 + printf '\n' >&2 + printf 'Answer no if a proxy or tunnel already handles HTTPS for Cobble.\n' >&2 + printf 'Serve HTTPS? [Y/n] ' >&2 + + exec 3&2 + ;; + *) + printf '\nHTTPS on. Trust the certificate on each device before using the app.\n' >&2 + ;; + esac +} + set_env() { local key="$1" value="$2" escaped escaped="$(printf '%s' "$value" | sed -e 's/[\/&]/\\&/g')" @@ -862,6 +924,8 @@ uninstall() { command -v curl >/dev/null 2>&1 || fail "curl is required" +ask_https + ensure_docker if [ "$ACTION" = "uninstall" ]; then diff --git a/src/server.ts b/src/server.ts index 18fe86d..4824424 100644 --- a/src/server.ts +++ b/src/server.ts @@ -86,9 +86,11 @@ if ( bunext.bunextLog.info(`Using the development certificate in certs/`); } -if (!cert_file && !key_file && process.env.NODE_ENV != "production") { +if (!cert_file && !key_file) { bunext.bunextLog.info( - "Serving plain HTTP (run `bun run dev:phone`, or `bun run gen:certs` then restart, for HTTPS)", + 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)", ); }