#!/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 "$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 </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"