#!/bin/bash
# Launches ttyd with dejima-attach as its command, reading ttyd_port from
# the config's [remote-access] section so the port has exactly one source of
# truth (the systemd unit deliberately does not repeat it). Started by
# systemd/dejima-ttyd.service.
# See docs/remote-access.md.
#
# --interface 127.0.0.1 is a requirement, not a preference. The egress policy
# permits traffic to the container's own host, and attaching
# tmux gives a HOST shell - the prefix key opens a window outside the
# container. A listener on any address the container can route to is a
# compromised session's escape hatch; loopback is not one of them. Put nginx
# in front, bound to one LAN address - docs/nginx-dejima.conf.example.
#
# ttyd carries no credential of its own, so the proxy is the only thing
# authenticating and the loopback bind is what makes that safe. --writable
# makes it a terminal rather than a viewer.
#
# --check-origin is not optional: without it ttyd does no origin validation
# at all, and browsers do not apply same-origin to WebSockets, so any page
# the operator loads could open wss://<front-door>/ws with the browser
# attaching its cached credential and be handed this shell. Proxy auth cannot
# stop that - the victim's own browser satisfies it. ttyd compares Origin
# against Host, which is why the nginx example passes Host through unchanged.
#
# --url-arg carries ?arg=<run-id> for the notification deep link. It reaches
# dejima-attach as argv, never a shell word, and anything but --list or empty
# must match the run-id shape before it is used as a path component.
set -euo pipefail

# --help before anything else runs: no libraries sourced, no config read. The
# moment someone reaches for it is often the moment the config is wrong.
# stdout and exit 0, where usage() stays on stderr and exits nonzero.
case "${1:-}" in
    -h|--help)
        cat <<'DEJIMA_TTYD_HELP'
dejima-ttyd - serve the web terminal that fronts dejima-attach

Usage: dejima-ttyd

Takes no arguments. Started by systemd/dejima-ttyd.service; running it by hand
is only useful for debugging.

Reads ttyd_port from [remote-access] in the config so the port has exactly one
source of truth - the systemd unit deliberately does not repeat it.

Binds 127.0.0.1 only, and that is a requirement rather than a default worth
overriding. Attaching gives a tmux session on the host, so a listener on any
address a container can route to is an escape hatch out of the sandbox;
loopback is not one. Put nginx in front of it, bound to one LAN address - see
docs/nginx-dejima.conf.example.

Serves nothing until [remote-access] enabled is on.

See docs/remote-access.md.
DEJIMA_TTYD_HELP
        exit 0 ;;
esac

SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)"
# shellcheck source=dejima-project-lib.sh
source "$SCRIPT_DIR/dejima-project-lib.sh"

if ! remote_enabled; then
    echo "Remote access is not configured (no [remote-access] enabled = on in $(config_path))." >&2
    echo "See README \"Remote access\"." >&2
    exit 1
fi

# Read BEFORE the ttyd check below, deliberately: an operator with both a
# broken config and no ttyd is otherwise told only about the package,
# installs it, comes back to the identical failure, and never learns that
# their config is the other half of it. remote_ttyd_port validates the whole
# file and names the line that broke it, and a failure here is fatal under
# set -e on the assignment.
port="$(remote_ttyd_port)"

# Optional, and applied to EVERY client - ttyd 1.7.7 takes client options only
# at launch (the URL-query override that would allow a per-client size landed
# after that release), so there is no way to give a phone one size and a
# desktop another from here. Left unset, ttyd keeps its own default.
#
# It reaches the page as an xterm.js option, through the same SET_PREFERENCES
# message ttyd has always used for -t, so the terminal page below neither knows
# nor needs to know that this knob exists.
font_size="$(remote_ttyd_font_size)"

# Consumed by the terminal page's PREFIX, window and ZOOM buttons, which are the
# only tmux navigation a phone has - the status bar's own window names are a few
# pixels wide on a phone-width screen. Unset means the page uses stock tmux's
# C-b; see docs/remote-access.md.
tmux_prefix="$(remote_ttyd_tmux_prefix)"

# Whether a tap on the terminal raises the soft keyboard. Passed on only when
# the operator set it: unset means the page decides from the pointer type,
# which is right for almost everyone and is something a host-wide setting
# cannot see. See docs/remote-access.md.
focus_on_tap="$(remote_ttyd_focus_on_tap)"

# Whether the toolbar is colour-coded. Passed on only when set, for the same
# reason as the line above: the page has a default and it is the right one.
button_colors="$(remote_ttyd_button_colors)"

# Checked rather than left to exec, whose failure is bash's "ttyd: not found"
# on a line number - true, and useless to an operator who needs to know that
# the missing piece is a package.
if ! command -v ttyd >/dev/null 2>&1; then
    echo "ttyd is not installed - the web front door needs it, 1.7.4 or newer" >&2
    echo "(-W/--writable arrived in 1.7.4; older builds are writable by default" >&2
    echo "and reject the flag, so every start would be a usage error)." >&2
    exit 1
fi

# The distro's own ttyd package ships an ENABLED system service that runs
# `login` and, having no --interface of its own, binds every interface - the
# bridge address a sandbox can route to included. Installing ttyd to set this
# feature up is therefore enough to open a host login prompt to the sandbox,
# on this very port, before any of this runs.
#
# It also takes the port. Binding the loopback address below while another
# process already holds that port on every interface fails with EADDRINUSE, so
# this unit dies at every start while the reverse proxy in front of it happily
# proxies to whatever DID get the port. The operator sees the password prompt
# they configured and then a host login shell, and nothing in that path looks
# broken.
#
# So refuse, and say which. Left to exec, the whole failure is ttyd's one-line
# bind error in a unit nobody is watching.
if command -v ss >/dev/null 2>&1; then
    listeners="$(ss -ltnH "sport = :$port" 2>/dev/null || true)"
    if [ -n "$listeners" ]; then
        echo "Something is already listening on port $port:" >&2
        echo "$listeners" >&2
        echo >&2
        echo "The web front door cannot start, and until that listener is gone the" >&2
        echo "reverse proxy in front of this port serves IT, not dejima." >&2
        echo >&2
        echo "The usual cause is the distro's own ttyd service, which is enabled on" >&2
        echo "install and runs 'login' on every interface - reachable from inside a" >&2
        echo "sandbox. Check and disable it:" >&2
        echo >&2
        echo "  systemctl status ttyd.service" >&2
        echo "  sudo systemctl disable --now ttyd.service" >&2
        echo "  sudo systemctl mask ttyd.service" >&2
        echo >&2
        echo "See README \"Remote access\"." >&2
        exit 1
    fi
fi

# The terminal page. ttyd serves exactly three paths - a token endpoint, a
# redirect and the index - so there is no route a separate .js or .css could be
# fetched from, which is why web/index.html is one self-contained file with
# everything inlined. It is generated from web/src, and committed, so a plain
# checkout has it and nothing here has to run a build.
#
# Refused rather than skipped when it is missing. Falling back to ttyd's stock
# page would leave a front door that works and a phone that has quietly lost
# every gesture, resize and toolbar button, with nothing anywhere saying why.
index_html="$(cd "$SCRIPT_DIR/.." && pwd)/web/index.html"
if [ ! -f "$index_html" ]; then
    echo "The terminal page is missing: $index_html" >&2
    echo >&2
    echo "It is a generated file that is committed to this repo, so this means an" >&2
    echo "incomplete checkout. Rebuild it with:" >&2
    echo >&2
    echo "  cd $(dirname "$index_html") && npm ci && node build.mjs" >&2
    exit 1
fi

font_opt=()
[ -z "$font_size" ] || font_opt=(-t "fontSize=$font_size")

prefix_opt=()
[ -z "$tmux_prefix" ] || prefix_opt=(-t "tmuxPrefix=$tmux_prefix")
focus_opt=()
[ -z "$focus_on_tap" ] || focus_opt=(-t "focusOnTap=$focus_on_tap")
colors_opt=()
[ -z "$button_colors" ] || colors_opt=(-t "buttonColors=$button_colors")

# The sixteen ANSI slots xterm.js draws with, which without this are its own
# defaults - white on black, a bright red, a magenta. dejima's own output
# reaches a terminal in indexed colour precisely so the terminal's theme fills
# those slots (see dejima-color-lib.sh), and here dejima IS the terminal: the
# browser has no gruvbox to inherit, so the front door showed the stock
# palette while the same session over SSH showed the operator's.
#
# So the exact hexes belong here, by the same rule as web/src/page.css: they
# are the two surfaces where dejima owns the background. A static test keeps
# the values they share in step.
#
# Upstream gruvbox dark, by its own names. `background` and `foreground` are
# bg0/light1, which is what a gruvbox terminal is set to.
theme_opt=(-t 'theme={
  "background":"#282828","foreground":"#ebdbb2",
  "cursor":"#ebdbb2","cursorAccent":"#282828",
  "selectionBackground":"#504945",
  "black":"#282828","red":"#cc241d","green":"#98971a","yellow":"#d79921",
  "blue":"#458588","magenta":"#b16286","cyan":"#689d6a","white":"#a89984",
  "brightBlack":"#928374","brightRed":"#fb4934","brightGreen":"#b8bb26",
  "brightYellow":"#fabd2f","brightBlue":"#83a598","brightMagenta":"#d3869b",
  "brightCyan":"#8ec07c","brightWhite":"#ebdbb2"
}')

exec ttyd --interface 127.0.0.1 --port "$port" --check-origin --url-arg --writable \
    --index "$index_html" \
    ${font_opt[@]+"${font_opt[@]}"} \
    ${prefix_opt[@]+"${prefix_opt[@]}"} \
    ${focus_opt[@]+"${focus_opt[@]}"} \
    ${colors_opt[@]+"${colors_opt[@]}"} \
    "${theme_opt[@]}" \
    "$SCRIPT_DIR/dejima-attach"
