diff --git a/README.md b/README.md index 497355b..a994227 100644 --- a/README.md +++ b/README.md @@ -170,6 +170,34 @@ broken, when what is broken is one missing `server_name`. The script connects ba and checks, rather than trusting that a clean `nginx -t` means the name resolves to the right block. +### One command, including the registry + +`setup-origin.sh` stops at printing the pin because publishing it needs an +account. With an API key it does not stop: + +```sh +MOSHPIT_API_KEY=... sh scripts/setup-origin.sh chovy.hacker \ + --target dev.profullstack.com +``` + +Key, certificate, nginx block, reload, verify, **publish the pin, set the +target, and read it back from the registry** — the whole of "serve this name". +Get a key at [app.moshcode.sh/settings](https://app.moshcode.sh/settings). + +This works because `/api/moshpit` accepts a bearer token as well as a cookie +session, so nothing here needs a browser. Without a key the script behaves +exactly as before and prints the pin for you to paste. + +The read-back at the end is the point. A `201` means the write was accepted; +only a read proves the endpoint clients actually query returns it, and a pin +that was accepted but is not served leaves the name refused with nothing to +show for it. + +**`--target` will not take an IPv4 literal**, because the registry refuses one +by design — an A record on a small host is leased, NATed or shared, and a name +pointed at one goes stale silently. Use a hostname: the address behind it is +resolved normally, which is also how a name reaches IPv4-only clients. + Nothing is proxied on the server side. `moshpit-proxy` runs on the *visitor's* machine — it is the thing that checks the pin on behalf of a browser that cannot. diff --git a/scripts/setup-origin.sh b/scripts/setup-origin.sh index 0c903a3..56288ed 100755 --- a/scripts/setup-origin.sh +++ b/scripts/setup-origin.sh @@ -13,6 +13,15 @@ # for the name and clients checking the key they were handed against it. Which # means the last step is not optional: until the pin is published, every client # refuses the name rather than trusting it on sight. +# +# With MOSHPIT_API_KEY set, that last step stops being manual: +# +# MOSHPIT_API_KEY=... sh setup-origin.sh chovy.hacker --target dev.profullstack.com +# +# publishes the pin and sets the target over the registry API, so the whole of +# "serve this name" is one command. Get a key at app.moshcode.sh/settings. +# Without the key nothing changes -- it prints the pin and tells you where to +# paste it, exactly as before. set -eu NAME="${1:-}" @@ -22,6 +31,9 @@ ENABLEDIR="${MOSHPIT_ENABLEDIR:-/etc/nginx/sites-enabled}" WEBROOT="${MOSHPIT_WEBROOT:-/var/www/$NAME}" DAYS="${MOSHPIT_DAYS:-825}" TEMPLATE="${MOSHPIT_TEMPLATE:-$(dirname "$0")/../nginx/moshpit-origin.conf}" +API_KEY="${MOSHPIT_API_KEY:-}" +REGISTRY="${MOSHPIT_REGISTRY:-https://app.moshcode.sh}" +TARGET="" DRY_RUN=0 RED=''; BOLD=''; DIM=''; OFF='' @@ -32,23 +44,40 @@ warn() { printf '%swarning:%s %s\n' "$RED" "$OFF" "$*" >&2; } die() { printf '%serror:%s %s\n' "$RED" "$OFF" "$*" >&2; exit 1; } have() { command -v "$1" >/dev/null 2>&1; } -shift 2>/dev/null || true -while [ $# -gt 0 ]; do - case "$1" in - --dry-run) DRY_RUN=1 ;; - --days) DAYS="${2:?--days needs a number}"; shift ;; - --webroot) WEBROOT="${2:?--webroot needs a path}"; shift ;; - -h|--help) - cat >&2 <&2 < [options] --dry-run write nothing, print what would happen --days certificate lifetime (default: $DAYS) --webroot site files (default: /var/www/) + --api-key publish the pin instead of printing it for a human + --target also set the name's "points at" (needs --api-key) + --registry registry base (default: $REGISTRY) + +An IPv4 literal is refused by the registry by design -- point a name at an +IPv6 address or a hostname. A hostname is how a name reaches IPv4 clients, +since the address behind it is resolved normally. -environment: MOSHPIT_CERTDIR, MOSHPIT_SITEDIR, MOSHPIT_ENABLEDIR, MOSHPIT_WEBROOT +environment: MOSHPIT_CERTDIR, MOSHPIT_SITEDIR, MOSHPIT_ENABLEDIR, MOSHPIT_WEBROOT, + MOSHPIT_API_KEY, MOSHPIT_REGISTRY EOF - exit 0 ;; +} + +# Before the name is consumed, or `--help` is read as the name and the script +# dies telling you that `--help` is not a Moshpit name. +case "$NAME" in -h|--help) usage; exit 0 ;; esac + +shift 2>/dev/null || true +while [ $# -gt 0 ]; do + case "$1" in + --dry-run) DRY_RUN=1 ;; + --days) DAYS="${2:?--days needs a number}"; shift ;; + --webroot) WEBROOT="${2:?--webroot needs a path}"; shift ;; + --api-key) API_KEY="${2:?--api-key needs a token}"; shift ;; + --target) TARGET="${2:?--target needs an address or hostname}"; shift ;; + --registry) REGISTRY="${2:?--registry needs a base URL}"; shift ;; + -h|--help) usage; exit 0 ;; *) die "unknown option: $1 (try --help)" ;; esac shift @@ -60,6 +89,19 @@ case "$NAME" in *) die "'$NAME' does not look like a Moshpit name" ;; esac have openssl || die "openssl is required" + +# Caught here rather than after the certificate exists, so a typo does not leave +# a half-configured name behind. +if [ -n "$TARGET" ] && [ -z "$API_KEY" ]; then + die "--target needs --api-key (or MOSHPIT_API_KEY) — the target is set through the registry API" +fi +case "$TARGET" in + *://*) die "--target takes a bare address or hostname, not a URL" ;; + # An IPv4 literal is refused by the registry by design; saying so here saves a + # round trip and explains the fix, which the API's own error does not. + [0-9]*.[0-9]*.[0-9]*.[0-9]*) + die "--target will not accept an IPv4 literal — use a hostname that resolves to it (the registry stores IPv6 or hostnames)" ;; +esac [ -f "$TEMPLATE" ] || die "template not found: $TEMPLATE" if [ "$DRY_RUN" = "0" ] && [ "$(id -u)" != "0" ]; then @@ -194,13 +236,70 @@ else PIN="(dry run — no key was generated)" fi -cat >&2 <&1 + } + ok_status() { case "$1" in 2*) return 0 ;; *) return 1 ;; esac; } + report() { + _what="$1"; _out="$2" + _code=$(printf '%s' "$_out" | tail -n1) + _body=$(printf '%s' "$_out" | sed '$d') + if ok_status "$_code"; then + say " ${DIM}$_what — ok ($_code)${OFF}" + return 0 + fi + warn "$_what failed ($_code): $_body" + return 1 + } + + if [ -n "$TARGET" ]; then + step "pointing $NAME at $TARGET" + report "target" "$(api PUT "/api/moshpit/tlds/$TLD/names" \ + "{\"label\":\"$LABEL\",\"target\":\"$TARGET\"}")" || true + fi + + step "publishing the pin" + # 409 means this exact pin is already published under a different kind, which + # is a real mistake worth surfacing rather than a retry. + report "pin" "$(api POST "/api/moshpit/tlds/$TLD/pins" \ + "{\"label\":\"$LABEL\",\"pin\":\"$PIN\",\"kind\":\"tls\",\"note\":\"setup-origin.sh\"}")" || true + + # Read it back. A 201 means the write was accepted; only a read proves the + # thing clients actually query now returns it. + step "confirming the registry serves it" + published=$(curl -sS "$REGISTRY/api/moshpit/tlds/$TLD/pins?label=$LABEL" 2>/dev/null || true) + case "$published" in + *"$PIN"*) say " ${DIM}$NAME is published and verifiable${OFF}" ;; + *) warn "the registry does not list this pin yet: $published" + warn "clients will keep refusing $NAME until it does" ;; + esac +else + cat >&2 <&2 <