#!/bin/sh
#
# ssh-reverse
# Copyright (c)2021-2026 John Lawson & Sons
# All Rights Reserved
#

set -eu
umask 077

# ---------------------------------------------------------------------
# Get unique device ID
# ---------------------------------------------------------------------
ID="$(tr -d '\0' < /sys/firmware/devicetree/base/serial-number 2>/dev/null \
      | tr '[:lower:]' '[:upper:]' || true)"

[ -n "${ID:-}" ] || \
ID="$(cat /etc/machine-id 2>/dev/null | tr '[:lower:]' '[:upper:]' || true)"

[ -n "${ID:-}" ] || exit 0

API_URL="https://api.isignage.app/devices/$ID/port"
RUNDIR="/run/ssh-reverse"
PORTFILE="$RUNDIR/port"
ERRFILE="$RUNDIR/ssh.err"

mkdir -p "$RUNDIR"

# ---------------------------------------------------------------------
# Local service to expose on the device
# ---------------------------------------------------------------------
LOCAL_PORT=65535

# ---------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------
fetch_port() {
  PORT="$(
    curl -fsS \
      --user-agent "iSIGNAGE" \
      --connect-timeout 5 \
      --max-time 10 \
      --retry 3 \
      --retry-delay 1 \
      --retry-all-errors \
      "$API_URL" \
    | tr -d '\r\n'
  )"

  # digits only
  case "$PORT" in
    ''|*[!0-9]*)
      return 1
    ;;
  esac

  # Enforce 49152-65535 only
  if [ "$PORT" -lt 49152 ] 2>/dev/null || [ "$PORT" -gt 65535 ] 2>/dev/null; then
    return 1
  fi

  printf '%s\n' "$PORT" > "$PORTFILE"
  echo "$PORT"
  return 0
}

# ---------------------------------------------------------------------
# Main loop: connect forever with sensible backoff
# ---------------------------------------------------------------------
# Backoff for "forwarding failed" (stale server-side tunnel holding port)
fwd_fail_delay=5   # start at 5s
fwd_fail_max=300   # cap at 5 min

# Backoff for "can't reach server / dns / network"
net_fail_delay=5
net_fail_max=60

while :; do
  if ! PORT="$(fetch_port 2>/dev/null)"; then
    # API/DNS/network issue or bad response; don't thrash
    sleep 60
    continue
  fi

  : > "$ERRFILE" || true

  # Start reverse tunnel (blocks while connected)
  ssh \
    -o BatchMode=yes \
    -o ConnectTimeout=10 \
    -o ExitOnForwardFailure=yes \
    -o ServerAliveInterval=15 \
    -o ServerAliveCountMax=2 \
    -o StrictHostKeyChecking=yes \
    -o UserKnownHostsFile=/root/.ssh/known_hosts \
    -N \
    -R "${PORT}:localhost:${LOCAL_PORT}" \
    root@ssh.isignage.uk \
    2>"$ERRFILE" || true

  # If we get here, ssh exited. Decide how long to wait before retrying.
  if grep -q "remote port forwarding failed for listen port" "$ERRFILE" 2>/dev/null; then
    # Likely stale tunnel still holding the port on the server: back off (but keep trying)
    sleep "$fwd_fail_delay"
    fwd_fail_delay=$((fwd_fail_delay * 2))
    [ "$fwd_fail_delay" -gt "$fwd_fail_max" ] && fwd_fail_delay="$fwd_fail_max"
    # (optional) also slow down net backoff reset doesn't matter here
    continue
  fi

  # Other failures: retry fairly quickly, but with a small cap (covers Wi-Fi flaps, DNS)
  sleep "$net_fail_delay"
  net_fail_delay=$((net_fail_delay * 2))
  [ "$net_fail_delay" -gt "$net_fail_max" ] && net_fail_delay="$net_fail_max"

  # On a successful long connection, ssh exits only when it breaks; reset forward-fail delay
  # so we recover quickly next time the port is free again.
  fwd_fail_delay=5
done
