Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 38 additions & 5 deletions nginx/moshpit-origin.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
115 changes: 115 additions & 0 deletions scripts/moshpit-update.sh
Original file line number Diff line number Diff line change
@@ -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
16 changes: 16 additions & 0 deletions systemd/moshpit-update.service
Original file line number Diff line number Diff line change
@@ -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
16 changes: 16 additions & 0 deletions systemd/moshpit-update.timer
Original file line number Diff line number Diff line change
@@ -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
Loading