diff --git a/nginx/moshpit-origin.conf b/nginx/moshpit-origin.conf index 3c9c456..7d272c5 100644 --- a/nginx/moshpit-origin.conf +++ b/nginx/moshpit-origin.conf @@ -18,11 +18,44 @@ server { listen [::]:80; server_name NAME; - # Keep this. Upgrading to HTTPS is right, and it was only ever a problem - # when there was no 443 block for the name to land on — which produced a - # certificate for whatever the default vhost happened to hold, and an error - # that looks like Moshpit is broken when it is nginx that is misconfigured. - return 301 https://$host$request_uri; + # This used to redirect to HTTPS, with a comment saying the only problem + # was a missing 443 block. That reasoning was incomplete: it assumes the + # client can verify what it lands on, and the two biggest classes cannot. + # + # No CA will issue for an ending outside the DNS root, so the 443 block + # below presents a self-signed certificate. TronBrowser, moshpit-proxy and + # the SDKs check it against the registry's published pin and are happy. + # `curl` and a stock browser have no pin to check against, so they reject + # it — and a redirect gives them no way back: + # + # curl chovy.hacker -L + # curl: (60) SSL certificate problem: self-signed certificate + # + # Through the gateway it is worse. pit.moshcode.sh forwards the status and + # not the Location, so a visitor gets a 301 pointing nowhere at all. + # + # So port 80 serves the site instead. Both ports now serve it, neither + # redirects to the other, and each client takes the one it can verify — + # pin-checking clients still get TLS, and everyone else gets the page + # rather than an error that reads as "Moshpit is broken". + # + # HTTPS-only is still the right default for a name in the public DNS. It + # is not available for a name outside it, and pretending otherwise costs + # the visitor the site. + + access_log /var/log/nginx/NAME.access.log; + error_log /var/log/nginx/NAME.error.log warn; + + root /var/www/NAME; + index index.html; + + location / { + try_files $uri $uri/ =404; + } + + location ~ /\. { + deny all; + } } server { diff --git a/scripts/moshpit-update.sh b/scripts/moshpit-update.sh new file mode 100755 index 0000000..8139a99 --- /dev/null +++ b/scripts/moshpit-update.sh @@ -0,0 +1,115 @@ +#!/bin/sh +# Keep /opt/moshpit at the tip of this repo, so merging ships. +# +# It did not. /opt/moshpit was a hand-copied nginx/ and scripts/, which meant a +# merged fix reached the box only when somebody remembered to copy it — and the +# symptom of forgetting is not an error, it is the fix appearing not to work. +# A whole afternoon went into that: the origin template still redirected to +# HTTPS on a box whose repo had stopped doing so, and every reconcile pass +# faithfully re-applied the old one. +# +# sh scripts/moshpit-update.sh # pull and apply +# sh scripts/moshpit-update.sh --dry-run # say what would change +# +# Config, read from /etc/moshpit/update.conf (override with MOSHPIT_UPDATE_CONF): +# +# MOSHPIT_REPO=https://github.com/profullstack/moshpit-proxy.git +# MOSHPIT_BRANCH=main +# MOSHPIT_PREFIX=/opt/moshpit +# +# Converts a hand-copied prefix into a checkout on first run rather than +# demanding someone do it by hand: an upgrade path nobody has to read about is +# the only kind that gets taken. The old tree is kept next to the new one, so a +# local edit somebody forgot to upstream is recoverable rather than gone. +set -eu + +CONF="${MOSHPIT_UPDATE_CONF:-/etc/moshpit/update.conf}" +[ -f "$CONF" ] && . "$CONF" + +REPO="${MOSHPIT_REPO:-https://github.com/profullstack/moshpit-proxy.git}" +BRANCH="${MOSHPIT_BRANCH:-main}" +PREFIX="${MOSHPIT_PREFIX:-/opt/moshpit}" +DRY_RUN=0 + +BOLD=''; DIM=''; RED=''; OFF='' +if [ -t 2 ]; then BOLD=$(printf '\033[1m'); DIM=$(printf '\033[2m'); RED=$(printf '\033[31m'); OFF=$(printf '\033[0m'); fi +say() { printf '%s\n' "$*" >&2; } +step() { printf '%s==>%s %s\n' "$BOLD" "$OFF" "$*" >&2; } +die() { printf '%serror:%s %s\n' "$RED" "$OFF" "$*" >&2; exit 1; } + +while [ $# -gt 0 ]; do + case "$1" in + --dry-run) DRY_RUN=1 ;; + -h|--help) sed -n '2,24p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; + *) die "unknown option: $1" ;; + esac + shift +done + +command -v git >/dev/null 2>&1 || die "git is required" + +# ---- first run: adopt a hand-copied prefix --------------------------------- + +if [ ! -d "$PREFIX/.git" ]; then + step "$PREFIX is not a checkout — converting" + if [ "$DRY_RUN" = "1" ]; then + say " ${DIM}would clone $REPO into $PREFIX, keeping the old tree beside it${OFF}" + exit 0 + fi + + TMP="$PREFIX.new.$$" + rm -rf "$TMP" + git clone --quiet --branch "$BRANCH" --depth 50 "$REPO" "$TMP" \ + || die "clone failed — $PREFIX left exactly as it was" + + if [ -e "$PREFIX" ]; then + # Kept, not deleted. If somebody edited a script in place and never + # upstreamed it, this is the only copy that has it. + OLD="$PREFIX.superseded.$(date -u +%Y%m%dT%H%M%SZ)" + mv "$PREFIX" "$OLD" + say " ${DIM}previous tree kept at $OLD${OFF}" + fi + mv "$TMP" "$PREFIX" + say " ${DIM}$PREFIX is now a checkout of $BRANCH${OFF}" + CHANGED=1 +else + # ---- normal run: fast-forward ------------------------------------------ + + before="$(git -C "$PREFIX" rev-parse HEAD)" + git -C "$PREFIX" fetch --quiet origin "$BRANCH" || die "fetch failed — nothing changed" + + # Refuse to clobber local edits. Overwriting them silently is how a fix + # somebody applied on the box at 3am disappears without trace. + if ! git -C "$PREFIX" diff --quiet || ! git -C "$PREFIX" diff --cached --quiet; then + die "$PREFIX has uncommitted changes — refusing to overwrite them. Commit, stash, or revert first." + fi + + after="$(git -C "$PREFIX" rev-parse "origin/$BRANCH")" + if [ "$before" = "$after" ]; then + say "${DIM}already at $(git -C "$PREFIX" rev-parse --short HEAD) — nothing to do${OFF}" + exit 0 + fi + + step "updating $(echo "$before" | cut -c1-7) -> $(echo "$after" | cut -c1-7)" + git -C "$PREFIX" --no-pager log --oneline "$before..$after" | sed 's/^/ /' >&2 || true + [ "$DRY_RUN" = "1" ] && exit 0 + + git -C "$PREFIX" merge --ff-only --quiet "origin/$BRANCH" \ + || die "not a fast-forward — $PREFIX has diverged and needs a look" + CHANGED=1 +fi + +# ---- apply ---------------------------------------------------------------- + +# The origin template is read fresh by every reconcile pass, so a template +# change needs no action here. Only nginx has to be told. +if [ "${CHANGED:-0}" = "1" ] && command -v nginx >/dev/null 2>&1; then + if nginx -t >/dev/null 2>&1; then + nginx -s reload >/dev/null 2>&1 || true + say "${DIM}nginx reloaded${OFF}" + else + # Loud, and not fatal: the update landed, and nginx was already broken or + # is broken by something unrelated. Failing here would strand the checkout. + say "${RED}warning:${OFF} nginx -t fails — not reloading. Run \`nginx -t\` to see why." + fi +fi diff --git a/systemd/moshpit-update.service b/systemd/moshpit-update.service new file mode 100644 index 0000000..a3a8254 --- /dev/null +++ b/systemd/moshpit-update.service @@ -0,0 +1,16 @@ +[Unit] +# Merging should ship. Before this, /opt/moshpit was a hand-copied nginx/ and +# scripts/, so a merged fix reached the box only when somebody remembered to +# copy it — and forgetting does not produce an error, it produces a fix that +# appears not to work. +Description=Update /opt/moshpit to the tip of moshpit-proxy +Documentation=https://github.com/profullstack/moshpit-proxy +After=network-online.target +Wants=network-online.target + +[Service] +Type=oneshot +ExecStart=/opt/moshpit/scripts/moshpit-update.sh +# A failed update must leave the box exactly as it was, which the script +# guarantees; systemd only needs to not retry into a loop. +Restart=no diff --git a/systemd/moshpit-update.timer b/systemd/moshpit-update.timer new file mode 100644 index 0000000..1f40842 --- /dev/null +++ b/systemd/moshpit-update.timer @@ -0,0 +1,16 @@ +[Unit] +Description=Update /opt/moshpit periodically +Documentation=https://github.com/profullstack/moshpit-proxy + +[Timer] +# Every 15 minutes rather than every minute like reconcile: this reaches the +# network and a merge is not urgent, where an unserved name is. +OnBootSec=2min +OnUnitActiveSec=15min +# Spread across boxes so a merge does not have every origin fetch at once. +RandomizedDelaySec=2min +# A box that was off must still catch up rather than wait a full interval. +Persistent=true + +[Install] +WantedBy=timers.target