| #!/usr/bin/env bash |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| set -euo pipefail |
|
|
| cd "$(dirname "$0")/.." |
|
|
| CADDYFILE="${CADDYFILE:-/etc/caddy/Caddyfile}" |
| SITE="${SITE:-bot.elghaly.dev}" |
| UPSTREAM="${UPSTREAM:-127.0.0.1:8081}" |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| PUBLIC="${PUBLIC:-0}" |
| WEB_USER="${BOT_WEB_USER:-admin}" |
|
|
| step() { printf '\n\033[1m── %s\033[0m\n' "$*"; } |
| ok() { printf ' \033[32mok\033[0m — %s\n' "$*"; } |
| bad() { printf ' \033[31mFAILED\033[0m — %s\n' "$*"; } |
|
|
| command -v caddy >/dev/null 2>&1 || { |
| bad "caddy is not installed — this box appears to use a different proxy" |
| exit 1 |
| } |
| [ -f "$CADDYFILE" ] || { bad "no $CADDYFILE"; exit 1; } |
| [ "$(id -u)" -eq 0 ] || { bad "run with sudo"; exit 1; } |
|
|
| step "1/5 is the bot actually listening on ${UPSTREAM#127.0.0.1:}?" |
| if ss -lnt 2>/dev/null | grep -q "${UPSTREAM}"; then |
| ok "something is listening on $UPSTREAM" |
| else |
| bad "nothing on $UPSTREAM — start the bot first, or the site will 502" |
| fi |
|
|
| step "2/5 backing up $CADDYFILE" |
| BACKUP="${CADDYFILE}.bak.$(date +%Y%m%d-%H%M%S)" |
| cp "$CADDYFILE" "$BACKUP" |
| ok "$BACKUP" |
|
|
| step "3/5 building the $SITE block" |
|
|
| AUTHBLOCK="" |
| if [ "$PUBLIC" = "1" ]; then |
| ok "PUBLIC=1 — read-only pages will be reachable without a password" |
| else |
| if [ -z "${BOT_WEB_PASS:-}" ]; then |
| printf ' password for web user "%s" (typing is hidden): ' "$WEB_USER" |
| stty -echo 2>/dev/null || true |
| read -r BOT_WEB_PASS |
| stty echo 2>/dev/null || true |
| echo |
| fi |
| [ -n "${BOT_WEB_PASS:-}" ] || { bad "empty password — refusing to install an open site"; exit 1; } |
| |
| |
| |
| HASH="$(printf '%s' "$BOT_WEB_PASS" | caddy hash-password 2>/dev/null)" || { |
| bad "caddy hash-password failed"; exit 1; } |
| unset BOT_WEB_PASS |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| AUTHDIR=basic_auth |
| PROBE="$(mktemp)" |
| printf 'http://probe.invalid {\n\tbasic_auth {\n\t\tu %s\n\t}\n\trespond 200\n}\n' \ |
| "$HASH" > "$PROBE" |
| if ! caddy validate --config "$PROBE" --adapter caddyfile >/dev/null 2>&1; then |
| AUTHDIR=basicauth |
| fi |
| rm -f "$PROBE" |
|
|
| AUTHBLOCK=$'\n\t'"${AUTHDIR}"$' {\n\t\t'"${WEB_USER} ${HASH}"$'\n\t}\n' |
| ok "site will require a password for user \"$WEB_USER\" (using \`$AUTHDIR\`)" |
| fi |
|
|
| NEWBLOCK="$(cat <<EOF |
| ${SITE} { |
| encode gzip |
| ${AUTHBLOCK} |
| # Read-only surface. Nothing here can act and nothing here returns a |
| # secret. An ALLOWLIST, so a route added to the Python server tomorrow |
| # stays unreachable until someone publishes it on purpose. |
| # |
| # Kept even when the site is password-protected: auth answers "who are |
| # you", the allowlist answers "what may be reached at all", and |
| # /command should stay unroutable even for someone holding the |
| # password. |
| @public { |
| path / |
| path /status |
| path /api/pnl |
| path /api/receipts |
| path /api/routes |
| path /api/nearmiss |
| path /api/engine |
| path /receipts.csv |
| } |
| handle @public { |
| reverse_proxy ${UPSTREAM} |
| } |
| |
| # /command (fund-moving) and /logs (operational detail) land here. |
| # 404, not 403: a 403 confirms the path exists and is worth attacking. |
| handle { |
| respond "not found" 404 |
| } |
| |
| header { |
| Content-Security-Policy "default-src 'none'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'" |
| X-Content-Type-Options "nosniff" |
| Referrer-Policy "no-referrer" |
| X-Frame-Options "DENY" |
| -Server |
| } |
| } |
| EOF |
| )" |
|
|
| |
| |
| |
| SITE="$SITE" NEWBLOCK="$NEWBLOCK" awk ' |
| BEGIN { site = ENVIRON["SITE"]; blk = ENVIRON["NEWBLOCK"]; depth = 0; skip = 0; done = 0 } |
| { |
| line = $0 |
| if (!skip && depth == 0 && index(line, site) == 1) { |
| # Start of the target block. Emit the replacement, then swallow the |
| # original through to its matching close brace. |
| print blk |
| done = 1 |
| skip = 1 |
| depth = 0 |
| } |
| if (skip) { |
| n = gsub(/{/, "{", line); m = gsub(/}/, "}", line) |
| depth += n - m |
| if (depth <= 0) { skip = 0; depth = 0 } |
| next |
| } |
| print line |
| } |
| END { if (!done) { print ""; print blk } } |
| ' "$CADDYFILE" > "${CADDYFILE}.new" |
|
|
| mv "${CADDYFILE}.new" "$CADDYFILE" |
| caddy fmt --overwrite "$CADDYFILE" >/dev/null 2>&1 || true |
| ok "$SITE block replaced; all other blocks copied through unchanged" |
|
|
| step "4/5 validating and reloading" |
| if ! caddy validate --config "$CADDYFILE" >/dev/null 2>&1; then |
| bad "config does not validate — restoring $BACKUP and changing nothing" |
| cp "$BACKUP" "$CADDYFILE" |
| caddy validate --config "$CADDYFILE" >/dev/null 2>&1 && ok "backup restored and valid" |
| exit 1 |
| fi |
| ok "config is valid" |
| systemctl reload caddy || systemctl restart caddy |
| ok "caddy reloaded" |
|
|
| step "5/5 verifying from outside" |
| sleep 2 |
| FAIL=0 |
| check() { |
| code="$(curl -sS -o /dev/null -w '%{http_code}' --max-time 12 "https://${SITE}$1" || echo 000)" |
| if [ "$code" = "$2" ]; then |
| ok "$1 -> $code ($3)" |
| else |
| bad "$1 -> $code, expected $2 ($3)" |
| FAIL=1 |
| fi |
| } |
| |
| |
| |
| if [ "$PUBLIC" = "1" ]; then EXPECT=200; else EXPECT=401; fi |
| check "/" "$EXPECT" "landing page" |
| check "/api/pnl" "$EXPECT" "ledger JSON" |
| check "/api/receipts" "$EXPECT" "on-chain receipts" |
| check "/api/routes" "$EXPECT" "route pruning transparency" |
| check "/api/nearmiss" "$EXPECT" "failure-mode log" |
| check "/api/engine" "$EXPECT" "decay, tips, guards" |
| |
| check "/logs" 404 "MUST be unreachable — leaks wallet/tx detail" |
| check "/command" 404 "MUST be unreachable — can move funds" |
|
|
| echo |
| if [ "$FAIL" -eq 0 ]; then |
| if [ "$PUBLIC" = "1" ]; then |
| printf '\033[32m✅ https://%s is public read-only, and /command and /logs are not routed.\033[0m\n' "$SITE" |
| else |
| printf '\033[32m✅ https://%s is PRIVATE — anonymous requests get 401, and\n /command and /logs are not routed at all.\033[0m\n' "$SITE" |
| printf ' Log in as "%s" with the password you just set.\n' "$WEB_USER" |
| fi |
| else |
| printf '\033[31m⚠️ Something is off above. Restore with:\033[0m\n' |
| printf ' sudo cp %s %s && sudo systemctl reload caddy\n' "$BACKUP" "$CADDYFILE" |
| exit 1 |
| fi |
| printf ' backup kept at %s\n\n' "$BACKUP" |
|
|