#!/bin/bash
# peer-shell — the forced-command dispatcher invoked by an incoming SSH peer.
#
# Deployed by peerInstallShell() to ~<manager>/.local/bin/peer-shell, then
# every incoming peer key in authorized_keys has:
#
#   command="~/.local/bin/peer-shell <peer-name>",no-pty,no-port-forwarding,...
#
# sshd sets $SSH_ORIGINAL_COMMAND to whatever the caller asked for. We parse
# it, whitelist the verb, and refuse anything else. The peer name passed as
# $1 (by the forced-command itself) lets us scope and audit calls per peer.
#
# Trust boundary: this script runs as the manager user, the same identity the
# rest of LibrePortal runs as on a rootless install. It deliberately has no
# write paths into root-owned territory — everything it can touch is what
# the manager could already touch through the WebUI.

set -u

LP_PEER_NAME="${1:-unknown}"

# Audit log — appended; rotated by logrotate if present, otherwise harmless.
LP_PEER_LOG="${HOME}/.local/state/libreportal/peer-shell.log"
mkdir -p "$(dirname "$LP_PEER_LOG")" 2>/dev/null || true
_log() {
    printf '%s peer=%s verb=%s detail=%s\n' \
        "$(date -Iseconds)" "$LP_PEER_NAME" "$1" "${2:-}" >> "$LP_PEER_LOG" 2>/dev/null || true
}

_die() {
    _log error "$1"
    printf '{"error":"%s"}\n' "$1" >&2
    exit 1
}

# Bootstrap shared LibrePortal config (containers_dir, manager helpers).
# Source the standard env if we can find it; otherwise infer paths.
if [[ -r "${HOME}/.libreportal-env" ]]; then
    # shellcheck disable=SC1090
    source "${HOME}/.libreportal-env" >/dev/null 2>&1 || true
fi
: "${containers_dir:=/libreportal-containers/}"

CMD="${SSH_ORIGINAL_COMMAND:-}"
if [[ -z "$CMD" ]]; then
    _die "no-command"
fi

# Parse: first token = verb, rest = args. No shell expansion of CMD's content.
read -r VERB ARGS <<< "$CMD"

# App-slug validator. Slugs are lowercase alnum + dash; anything else means
# someone's trying path traversal or shell injection through the args.
_valid_slug() {
    [[ "$1" =~ ^[a-z0-9][a-z0-9-]{0,62}$ ]]
}

verb_ping() {
    _log ping ""
    printf '{"ok":true,"peer":"%s","time":"%s"}\n' \
        "$LP_PEER_NAME" "$(date -Iseconds)"
}

verb_list_apps() {
    _log list-apps ""
    local first=1
    printf '{"peer":"%s","apps":[' "$LP_PEER_NAME"
    local d slug size_kb
    for d in "$containers_dir"*/; do
        [[ -d "$d" ]] || continue
        slug=$(basename "$d")
        [[ -f "${d}docker-compose.yml" || -f "${d}compose.yml" ]] || continue
        _valid_slug "$slug" || continue
        size_kb=$(du -sk "$d" 2>/dev/null | awk '{print $1}')
        [[ -z "$size_kb" ]] && size_kb=0
        (( first )) || printf ','
        first=0
        printf '{"slug":"%s","size_kb":%s}' "$slug" "$size_kb"
    done
    printf ']}\n'
}

verb_stream_app() {
    local slug
    read -r slug <<< "$ARGS"
    if [[ -z "$slug" ]] || ! _valid_slug "$slug"; then
        _die "invalid-slug"
    fi
    if [[ ! -d "${containers_dir}${slug}" ]]; then
        _die "no-such-app"
    fi
    _log stream-app "$slug"
    # Stream a tar of the app dir to stdout. --warning=no-file-changed because
    # live data dirs change during read; we accept eventual consistency. The
    # caller is the receiver's peer_pull.sh, which untars into a staging dir
    # and then runs the migrate-flow.
    tar --warning=no-file-changed --warning=no-file-removed \
        -C "$containers_dir" -cf - "$slug"
}

case "$VERB" in
    ping)        verb_ping ;;
    list-apps)   verb_list_apps ;;
    stream-app)  verb_stream_app ;;
    *)           _die "unknown-verb" ;;
esac
