cadence / tunnel.sh
Vincisteel's picture
Upload 45 files
568e7b7 verified
Raw History Blame Contribute Delete
13.3 kB
#!/usr/bin/env bash
# Cadence β€” put your local backend on a free public https URL.
# No account, no credit card, no domain. All tooling is open source (Apache-2.0 / MIT).
#
# ./tunnel.sh normal run: checks everything, prints your https URL
# bash tunnel.sh same thing (works even if the file isn't executable)
# ./tunnel.sh --check diagnose only, print status, exit
# ./tunnel.sh --install download cloudflared into ./bin (no sudo)
# ./tunnel.sh --http2 force HTTP/2 (use when QUIC/UDP 7844 is blocked)
# ./tunnel.sh --ssh fallback tunnel via ssh (localhost.run) β€” no cloudflared
# ./tunnel.sh --with-backend also start the backend if it isn't running (installs deps if needed)
# PORT=8010 ./tunnel.sh different backend port
set -uo pipefail
cd "$(dirname "$0")"
PORT="${PORT:-8000}"
BASE="http://127.0.0.1:${PORT}"
BIN_DIR="./bin"
MODE="run"
PROTOCOL=""
WITH_BACKEND=0
for arg in "$@"; do
case "$arg" in
--check) MODE="check" ;;
--install) MODE="install" ;;
--http2) PROTOCOL="--protocol http2" ;;
--ssh) MODE="ssh" ;;
--with-backend|--serve) WITH_BACKEND=1 ;;
-h|--help) sed -n '2,12p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) echo "unknown option: $arg (try --help)"; exit 2 ;;
esac
done
bold() { printf '\033[1m%s\033[0m\n' "$*"; }
ok() { printf ' \033[32mβœ“\033[0m %s\n' "$*"; }
bad() { printf ' \033[31mβœ—\033[0m %s\n' "$*"; }
warn() { printf ' \033[33m!\033[0m %s\n' "$*"; }
# ── find cloudflared: PATH, then ./bin ────────────────────────────────────────
find_cloudflared() {
if command -v cloudflared >/dev/null 2>&1; then command -v cloudflared; return 0; fi
if [ -x "${BIN_DIR}/cloudflared" ]; then echo "${BIN_DIR}/cloudflared"; return 0; fi
return 1
}
install_cloudflared() {
os="$(uname -s)"; arch="$(uname -m)"
case "$arch" in x86_64|amd64) a=amd64 ;; arm64|aarch64) a=arm64 ;; *) bad "unsupported CPU: $arch"; return 1 ;; esac
mkdir -p "$BIN_DIR"
if [ "$os" = "Darwin" ]; then
if command -v brew >/dev/null 2>&1; then brew install cloudflared && return 0; fi
url="https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-${a}.tgz"
curl -fsSL "$url" | tar -xz -C "$BIN_DIR" cloudflared || return 1
elif [ "$os" = "Linux" ]; then
url="https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${a}"
curl -fsSL -o "${BIN_DIR}/cloudflared" "$url" || return 1
else
bad "unsupported OS: $os β€” install cloudflared manually, or use --ssh"
return 1
fi
chmod +x "${BIN_DIR}/cloudflared"
return 0
}
backend_up() { curl -fsS -m 3 "${BASE}/healthz" >/dev/null 2>&1; }
py_bin() { if [ -x ./.venv/bin/python ]; then echo "./.venv/bin/python"; else echo "python3"; fi; }
deps_ok() { "$(py_bin)" -c 'import flask, yt_dlp, ytmusicapi, requests' >/dev/null 2>&1; }
install_deps() {
if deps_ok; then return 0; fi
bold "Installing python dependencies (first run only)"
if python3 -m pip install -q -r requirements.txt >/dev/null 2>&1 && \
python3 -c 'import flask, yt_dlp, ytmusicapi, requests' >/dev/null 2>&1; then
ok "installed into system python"; return 0
fi
warn "system pip refused (PEP 668) β€” using ./.venv"
[ -d .venv ] || python3 -m venv .venv || { bad "could not create .venv (install python3-venv)"; return 1; }
./.venv/bin/pip install -q -r requirements.txt || { bad "pip install failed β€” are you offline?"; return 1; }
ok "installed into ./.venv"; return 0
}
start_backend() {
bold "Starting the backend"
install_deps || return 1
: > .cadence-backend.log
PORT="$PORT" "$(py_bin)" app.py >>.cadence-backend.log 2>&1 &
BACKEND_PID=$!
printf ' waiting for %s' "$BASE"
for _ in $(seq 1 40); do
if backend_up; then printf '\n'; ok "backend up (pid ${BACKEND_PID})"; return 0; fi
if ! kill -0 "$BACKEND_PID" 2>/dev/null; then
printf '\n'; bad "the backend exited immediately β€” log:"; sed 's/^/ /' .cadence-backend.log | tail -12; return 1
fi
printf '.'; sleep 1
done
printf '\n'; bad "backend did not answer after 40 s β€” log:"; sed 's/^/ /' .cadence-backend.log | tail -12; return 1
}
# why is nothing on the port? Say so specifically instead of hand-waving.
diagnose_backend() {
if command -v ss >/dev/null 2>&1 && ss -ltn 2>/dev/null | grep -q ":${PORT} "; then
warn "something else is already listening on port ${PORT} β€” try: PORT=$((PORT+1)) bash tunnel.sh --with-backend"
return
fi
if command -v lsof >/dev/null 2>&1 && lsof -iTCP:"${PORT}" -sTCP:LISTEN >/dev/null 2>&1; then
warn "port ${PORT} is held by another program β€” try: PORT=$((PORT+1)) bash tunnel.sh --with-backend"
return
fi
if ! deps_ok; then
warn "python dependencies are not installed yet (./run.sh does that on first start)"
fi
}
# ── --install ─────────────────────────────────────────────────────────────────
if [ "$MODE" = "install" ]; then
bold "Installing cloudflared (Apache-2.0) into ${BIN_DIR}/"
if install_cloudflared; then ok "installed: $($BIN_DIR/cloudflared --version 2>/dev/null | head -1)"; exit 0; fi
bad "download failed β€” check your connection, or use: ./tunnel.sh --ssh"
exit 1
fi
# ── --check ───────────────────────────────────────────────────────────────────
if [ "$MODE" = "check" ]; then
bold "Cadence tunnel diagnostics"
python3 -c 'import flask, yt_dlp, ytmusicapi, requests' 2>/dev/null \
&& ok "python dependencies present" \
|| bad "python dependencies missing β€” run ./run.sh once"
if backend_up; then ok "backend answering on ${BASE}"
else bad "backend NOT answering on ${BASE} β€” start it with bash run.sh, or run: bash tunnel.sh --with-backend"; diagnose_backend; fi
if cf="$(find_cloudflared)"; then ok "cloudflared found: $cf ($("$cf" --version 2>/dev/null | head -1))"
else warn "cloudflared not installed β€” ./tunnel.sh will download it, or use --ssh"; fi
if curl -fsS -m 6 https://api.cloudflare.com/client/v4/ips >/dev/null 2>&1; then ok "cloudflare reachable (https 443)"
else bad "can't reach Cloudflare β€” a firewall/proxy may be blocking the tunnel"; fi
if command -v ssh >/dev/null 2>&1; then ok "ssh available (fallback tunnel)"; else warn "no ssh (fallback unavailable)"; fi
exit 0
fi
# ── --ssh fallback ────────────────────────────────────────────────────────────
if [ "$MODE" = "ssh" ]; then
backend_up || { bad "Backend isn't running on ${BASE} β€” start ./run.sh first"; exit 1; }
command -v ssh >/dev/null 2>&1 || { bad "ssh is not installed"; exit 1; }
bold "Opening an ssh tunnel via localhost.run (no account)"
echo " The URL appears below as https://….lhr.life β€” press Ctrl+C to stop."
echo
exec ssh -o StrictHostKeyChecking=accept-new -o ServerAliveInterval=30 \
-R 80:localhost:"${PORT}" nokey@localhost.run
fi
# ── normal run ────────────────────────────────────────────────────────────────
bold "1/3 Checking the backend"
if backend_up; then
ok "answering on ${BASE}"
elif [ "$WITH_BACKEND" = "1" ]; then
start_backend || { echo; bad "could not start the backend"; exit 1; }
elif [ -t 0 ]; then
# interactive: just offer to do it, instead of making the user open a 2nd terminal
bad "nothing is running on ${BASE}"
diagnose_backend
printf '\n Start it now? [Y/n] '
read -r answer </dev/tty || answer="n"
case "$answer" in
""|y|Y|yes|YES)
start_backend || { echo; bad "could not start the backend"; exit 1; } ;;
*)
cat <<EOF
Start it first, in another terminal:
bash run.sh # macOS / Linux (creates .venv and installs deps)
python app.py # if dependencies are already installed
…or let this script handle it: bash tunnel.sh --with-backend
Different port? PORT=8010 bash tunnel.sh --with-backend
EOF
exit 1 ;;
esac
else
# non-interactive (piped/CI): never block on a prompt
bad "nothing is running on ${BASE}"
diagnose_backend
cat <<EOF
Start it first, in another terminal:
bash run.sh # macOS / Linux (creates .venv and installs deps)
python app.py # if dependencies are already installed
…or let this script handle it: bash tunnel.sh --with-backend
Different port? PORT=8010 bash tunnel.sh --with-backend
EOF
exit 1
fi
bold "2/3 Checking cloudflared"
if cf="$(find_cloudflared)"; then
ok "found: $cf"
else
warn "not installed β€” downloading it now (Apache-2.0, no sudo, goes in ./bin)"
if install_cloudflared; then
cf="${BIN_DIR}/cloudflared"; ok "installed"
else
bad "download failed. Either:"
echo " brew install cloudflared # macOS"
echo " ./tunnel.sh --ssh # fallback, no cloudflared needed"
exit 1
fi
fi
bold "3/3 Opening the tunnel"
echo " (first run takes 5–20 s; press Ctrl+C to stop)"
echo
TUNNEL_PID=""
run_tunnel() { # $1 = logfile β€” sets TUNNEL_PID (no subshell, so signals reach it)
# shellcheck disable=SC2086
"$cf" tunnel --url "${BASE}" --no-autoupdate $PROTOCOL >"$1" 2>&1 &
TUNNEL_PID=$!
}
cleanup() {
[ -n "${TUNNEL_PID:-}" ] && kill "$TUNNEL_PID" 2>/dev/null
if [ -n "${BACKEND_PID:-}" ]; then kill "$BACKEND_PID" 2>/dev/null; echo " (backend stopped β€” it was started by this script)"; fi
}
trap cleanup EXIT INT TERM
LOG="${TMPDIR:-/tmp}/cadence-tunnel.$$.log"
run_tunnel "$LOG"
PID="$TUNNEL_PID"
URL=""
for _ in $(seq 1 60); do
sleep 1
URL="$(grep -oE 'https://[a-z0-9-]+\.trycloudflare\.com' "$LOG" 2>/dev/null | head -1)"
[ -n "$URL" ] && break
if ! kill -0 "$PID" 2>/dev/null; then break; fi
done
if [ -z "$URL" ]; then
# QUIC (UDP 7844) is blocked on many networks β€” retry over HTTP/2 before giving up
warn "no URL yet β€” retrying over HTTP/2 (QUIC is often blocked by firewalls/routers)"
kill "$PID" 2>/dev/null; wait "$PID" 2>/dev/null
PROTOCOL="--protocol http2"
run_tunnel "$LOG"
PID="$TUNNEL_PID"
for _ in $(seq 1 60); do
sleep 1
URL="$(grep -oE 'https://[a-z0-9-]+\.trycloudflare\.com' "$LOG" 2>/dev/null | head -1)"
[ -n "$URL" ] && break
kill -0 "$PID" 2>/dev/null || break
done
fi
if [ -z "$URL" ]; then
bad "couldn't get a tunnel URL. Here's what cloudflared said:"
echo "────────────────────────────────────────────────────────────"
tail -20 "$LOG" 2>/dev/null | sed 's/^/ /'
echo "────────────────────────────────────────────────────────────"
cat <<EOF
Common causes:
β€’ corporate / school network blocking outbound 7844/udp and 443 β†’ ./tunnel.sh --ssh
β€’ VPN or firewall intercepting TLS β†’ turn it off, retry
β€’ cloudflared too old β†’ rm -rf bin && ./tunnel.sh
Alternative that needs no cloudflared: ./tunnel.sh --ssh
EOF
kill "$PID" 2>/dev/null
exit 1
fi
printf '\n'
bold " β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”"
printf ' β”‚ \033[36mCadence is live at:\033[0m\n'
printf ' β”‚ \033[1;4m%s\033[0m\n' "$URL"
bold " β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜"
echo
echo " Open that URL on your phone β†’ Install Cadence."
echo " Audio streams from this machine, so playback avoids the datacenter block."
echo
# prove it end-to-end before handing it over (DNS for a new subdomain takes a moment)
verified=""
for i in $(seq 1 10); do
# IPv6-only DNS answers are common on fresh subdomains; try both stacks
if curl -fsS -m 6 --ipv4 -o /dev/null "${URL}/healthz" 2>/dev/null \
|| curl -fsS -m 6 -o /dev/null "${URL}/healthz" 2>/dev/null; then verified="yes"; break; fi
if [ "$i" = 1 ]; then printf ' waiting for the tunnel to settle…'; fi
printf '.'
sleep 2
done
[ -n "$verified" ] && printf '\n'
if [ -n "$verified" ]; then
ok "verified from the public internet (healthz answered)"
else
printf '\n'
echo " (the tunnel is running; the first request can take ~10 s β€” just open the URL)"
fi
echo
if [ -n "${BACKEND_PID:-}" ]; then
echo " Keep this terminal open. Ctrl+C closes the tunnel and the backend."
else
echo " Keep this terminal open. Ctrl+C closes the tunnel."
fi
echo
wait "$PID"