import { DEFAULT_SSH_HOST, NGINX_MARKER, NGINX_SITE_LINK, NGINX_SNIPPET, SERVER_PUBLIC_ROOT, SERVER_RELEASES_ROOT, SERVER_STAGING_ROOT, parseOptions, renderNoReleasePage, runSsh, usageError, validateSshHost } from './lib.mjs' const usage = `Usage: node scripts/updates/provision-server.mjs [--ssh-host local001] Creates /srv/ycode, /srv/ycode/releases/versions and /srv/ycode-staging, backs up the nginx site configuration, installs a YCode-only snippet, validates nginx, then reloads. It never serves an index and never creates a fake latest.json. ` const snippet = `${NGINX_MARKER} location = /ycode/releases/latest.json { alias ${SERVER_RELEASES_ROOT}/latest.json; default_type application/json; add_header Cache-Control "no-store" always; add_header X-Content-Type-Options "nosniff" always; limit_except GET { deny all; } } location = /ycode/releases/index.html { alias ${SERVER_RELEASES_ROOT}/index.html; default_type text/html; charset utf-8; add_header Cache-Control "no-store" always; add_header X-Content-Type-Options "nosniff" always; limit_except GET { deny all; } } location /ycode/ { alias ${SERVER_PUBLIC_ROOT}/; autoindex off; index index.html; add_header X-Content-Type-Options "nosniff" always; limit_except GET { deny all; } } ` async function main() { const options = parseOptions(process.argv.slice(2), { values: ['ssh-host'] }) const host = validateSshHost(options['ssh-host'] || DEFAULT_SSH_HOST) const snippetBase64 = Buffer.from(snippet, 'utf8').toString('base64') const noReleasePageBase64 = Buffer.from(renderNoReleasePage(), 'utf8').toString('base64') const script = `set -euo pipefail export LC_ALL=C site_link='${NGINX_SITE_LINK}' snippet='${NGINX_SNIPPET}' site="$(readlink -f "$site_link")" test -f "$site" test -f /etc/nginx/nginx.conf nginx -t >/dev/null 2>&1 install -d -o root -g www-data -m 0755 '${SERVER_PUBLIC_ROOT}' install -d -o root -g www-data -m 0755 '${SERVER_RELEASES_ROOT}' install -d -o root -g www-data -m 0755 '${SERVER_RELEASES_ROOT}/versions' install -d -o root -g root -m 0700 '${SERVER_STAGING_ROOT}' page_tmp="$(mktemp '${SERVER_RELEASES_ROOT}/.index.html.XXXXXX')" printf '%s' '${noReleasePageBase64}' | base64 -d > "$page_tmp" chmod 0644 "$page_tmp" if [ -f '${SERVER_RELEASES_ROOT}/latest.json' ]; then if [ ! -f '${SERVER_RELEASES_ROOT}/index.html' ]; then rm -f "$page_tmp" echo "latest.json exists without a generated download page" >&2 exit 1 fi if grep -q 'data-ycode-state="no-release"' '${SERVER_RELEASES_ROOT}/index.html'; then rm -f "$page_tmp" echo "latest.json exists while the download page still says no release" >&2 exit 1 fi rm -f "$page_tmp" elif [ ! -f '${SERVER_RELEASES_ROOT}/index.html' ]; then mv -f "$page_tmp" '${SERVER_RELEASES_ROOT}/index.html' else if ! grep -q 'data-ycode-state="no-release"' '${SERVER_RELEASES_ROOT}/index.html'; then rm -f "$page_tmp" echo "download page exists without latest.json" >&2 exit 1 fi rm -f "$page_tmp" fi backup_dir=/var/backups/nginx install -d -o root -g root -m 0700 "$backup_dir" stamp="$(date -u +%Y%m%dT%H%M%SZ)" config_backup="$backup_dir/ycode-before-${'$'}{stamp}-$(basename "$site")" snippet_backup="$backup_dir/ycode-before-${'$'}{stamp}-ycode-releases.conf" test ! -e "$config_backup" cp -a "$site" "$config_backup" snippet_existed=0 if [ -f "$snippet" ]; then cp -a "$snippet" "$snippet_backup" snippet_existed=1 fi restore() { changed=0 cp -a "$config_backup" "$site" if [ "$snippet_existed" -eq 1 ]; then cp -a "$snippet_backup" "$snippet" else rm -f "$snippet" fi } changed=0 snippet_tmp= cleanup() { rc=$? if [ -n "$snippet_tmp" ]; then rm -f "$snippet_tmp" fi if [ "$rc" -ne 0 ] && [ "$changed" -eq 1 ]; then restore nginx -t >/dev/null 2>&1 && systemctl reload nginx >/dev/null 2>&1 || true fi exit "$rc" } trap cleanup EXIT install -d -o root -g root -m 0755 /etc/nginx/snippets snippet_tmp="$(mktemp /etc/nginx/snippets/.ycode-releases.XXXXXX)" printf '%s' '${snippetBase64}' | base64 -d > "$snippet_tmp" chmod 0644 "$snippet_tmp" mv -f "$snippet_tmp" "$snippet" changed=1 python3 - "$site" <<'PY' import os import re import shutil import sys import tempfile site = sys.argv[1] marker = "${NGINX_MARKER}" include = "\tinclude ${NGINX_SNIPPET};" with open(site, "r", encoding="utf-8") as stream: original = stream.read() for required in ("location /baocode/ {", "location /baocode/releases/ {"): if original.count(required) != 1: raise SystemExit(f"refusing to change nginx: expected one {required}") if marker in original: if include not in original: raise SystemExit("refusing to change nginx: YCode marker exists without include") updated = original else: match = re.search(r"(?m)^([ \\t]*)location /baocode/ \\{", original) if not match: raise SystemExit("refusing to change nginx: baocode anchor not found") indent = match.group(1) insertion = f"{indent}{marker}\\n{indent}include ${NGINX_SNIPPET};\\n\\n" updated = original[:match.start()] + insertion + original[match.start():] if updated != original: directory = os.path.dirname(site) descriptor, temporary = tempfile.mkstemp(prefix=".ycode-nginx-", dir=directory) try: with os.fdopen(descriptor, "w", encoding="utf-8") as stream: stream.write(updated) stream.flush() os.fsync(stream.fileno()) shutil.copystat(site, temporary) os.replace(temporary, site) finally: if os.path.exists(temporary): os.unlink(temporary) with open(site, "r", encoding="utf-8") as stream: final = stream.read() for required in ("location /baocode/ {", "location /baocode/releases/ {"): if final.count(required) != 1: raise SystemExit("refusing to change nginx: baocode locations changed") if marker not in final or include not in final: raise SystemExit("nginx include was not installed") PY if ! nginx -t; then echo "nginx validation failed; configuration restored" >&2 exit 1 fi if ! systemctl reload nginx; then echo "nginx reload failed; configuration restored" >&2 exit 1 fi status="$(curl -sS -o /dev/null -w '%{http_code}' http://127.0.0.1/ycode/releases/latest.json || true)" if [ -f '${SERVER_RELEASES_ROOT}/latest.json' ]; then expected_status=200 else expected_status=404 fi if [ "$status" != "$expected_status" ]; then echo "unexpected /ycode/releases/latest.json status: $status (expected $expected_status)" >&2 exit 1 fi page_status="$(curl -sS -o /dev/null -w '%{http_code}' http://127.0.0.1/ycode/releases/ || true)" if [ "$page_status" != "200" ]; then echo "unexpected /ycode/releases/ status: $page_status (expected 200)" >&2 exit 1 fi printf 'site=%s\\n' "$site" printf 'backup=%s\\n' "$config_backup" printf 'snippet=%s\\n' "$snippet" printf 'latest_status=%s\\n' "$status" printf 'page_status=%s\\n' "$page_status" ` const output = runSsh(host, script) process.stdout.write(output) console.log(`Provisioned ${host}; no release manifest was created.`) } main().catch((error) => usageError(usage, error))