#!/bin/sh
# This file is part of Epoptes, https://epoptes.org
# Copyright (C) 2010-2020 the Epoptes team, see AUTHORS
# SPDX-License-Identifier: GPL-3.0-or-later
#
# Implements the client side of the epoptes communications protocol.
# The daemon reads this file when it starts, and sends it to clients when they
# connect. The clients actually source it and then wait for further commands.

# Dash handles signals a bit differently from bash.
# We need to trap both 0 and 15 otherwise the trap function isn't called at all.
# 0 is normal program termination, it's called on `service epoptes restart`.
# 15 is SIGTERM, it's called when socat reaches the -T timeout.
# We don't always want to relaunch though.
# For user sessions, ping() aborts when X isn't running.
# Root connections abort when the system is shutting down.
# Also, epoptes-client kills -QUIT all the other epoptes-client processes.
on_signal() {
    # local code  # we can't do that, it resets $?

    code="$?"
    trap - 0 15
    # Don't relaunch if the system is shutting down
    if [ "$UID" -eq 0 ] && [ -d /run/systemd/system ] && [ -x /bin/systemctl ] \
        && systemctl list-jobs | grep -q 'shutdown.target[[:space:]]*start'
    then
        echo "epoptes-client got signal: $1, but system is shutting down" >&5
    else
        echo "epoptes-client got signal: $1, relaunching..." >&5
        # Restore the stdio descriptors to the previously saved 5
        ( sleep 10; ping; exec epoptes-client $SERVER $PORT >&5 2>&1; ) &
    fi
    exit "$code"
}

# Output a message and exit with an error.
# Parameters:
# $1..$N = The message.
die() {
    echo "epoptes-client ERROR: $*" >&5
    trap - 0 15
    exit 1
}

# Some actions like broadcasting need access to the display even if they
# run as root. Xorg might have been restarted though, so detect DISPLAY.
ensure_display() {
    local vars

    test "$UID" -eq 0 || return 0
    if xwininfo -root -size >/dev/null 2>&1; then
        return 0
    elif vars=$(./get-display); then
        export $vars
    else
        return 1
    fi
}

xprop_set() {
    xprop -root -f "$1" 8u -set "$1" "$2"
}

xprop_get() {
    # xprop -root UNSET_VAR echoes an error message to stdout instead of stderr
    xprop -root "$1" 2>/dev/null | sed 's/^[^=]*//;s/^= "//;s/"$//;s/\\"/"/'
}

xprop_unset() {
    xprop -root -remove "$1"
}

# Calculate, export and return a collection of useful variables.
info() {
    local server_ip def_iface

    if [ -z "$cached_info" ]; then
        VERSION=${VERSION:-0.4.3} # Just in case the client wasn't updated
        test -n "$USER" || USER=$(whoami)
        NAME=$(getent passwd "$UID" | cut -d':' -f5 | cut -d',' -f1)
        test -n "$HOME" || HOME=$(getent passwd "$UID" | cut -d: -f6)
        if [ -n "$LTSP_CLIENT_HOSTNAME" ]; then
            HOSTNAME="$LTSP_CLIENT_HOSTNAME"
        else
            HOSTNAME=$(hostname)
            test -n "$HOSTNAME" || die "Empty hostname"
        fi
        if [ -n "$LTSP_CLIENT" ] && [ -n "$LTSP_CLIENT_MAC" ]; then
            # LTSP exports those vars, use them if available.
            MAC=$(echo "$LTSP_CLIENT_MAC" | awk '{ print tolower($1) }')
            IP="$LTSP_CLIENT"
        else
            server_ip=$(getent hosts "$SERVER" | awk '{ print $1; exit }')
            server_ip=${server_ip:-$SERVER}
            read -r def_iface IP <<EOF
$(ip route get "$server_ip" | sed -n 's/.*dev *\([^ ]*\).*src *\([^ ]*\).*/\1 \2/p')
EOF
            test "${def_iface:-lo}" != "lo" || read -r def_iface IP <<EOF
$(ip route get 192.168.67.0 | sed -n 's/.*dev *\([^ ]*\).*src *\([^ ]*\).*/\1 \2/p')
EOF
            test "${def_iface:-lo}" != "lo" || die "Empty def_iface"
            test -n "$IP" || die "Empty IP"
            MAC=$(ip link show dev "$def_iface" |
                awk '/ether/ { print tolower($2) }')
            # Some interfaces might not have a MAC (e.g. tun), use a fallback
            test -n "$MAC" || MAC=$(ip link show |
                awk '/ether/ { print tolower($2); exit }')
            test -n "$MAC" || die "Empty MAC"
        fi
        CPU=$(awk -F': ' '/^model name/ { print $2; exit }' /proc/cpuinfo)
        RAM=$(awk '/^MemTotal:/ { print int($2/1024) }' /proc/meminfo)
        VGA=$(lspci -nn -m 2>/dev/null | sed -n -e '/"VGA/s/[^"]* "[^"]*" "[^"]*" "\([^"]*\)" .*/\1/p' | tr '\n' ' ')
        OS=$(uname -o)

        export VERSION USER NAME HOME HOSTNAME IP MAC CPU RAM VGA OS
        cached_info=true
    fi
    cat <<EOF
uid=$UID
type=$TYPE
version=$VERSION
user=$USER
name=$NAME
home=$HOME
hostname=$HOSTNAME
mac=$MAC
ip=$IP
cpu=$CPU
ram=$RAM
vga=$VGA
os=$OS
EOF
}

# Execute a command in the background and optionally print its pid.
# For internal use. Parameters:
# [-p]   = print the pid.
# $1..$N = the command and its parameters.
background() {
    local print_pid

    ensure_display
    if [ "$1" = "-p" ]; then
        print_pid=true
        shift
    fi
    # The command is run on a subshell with stdin and stdout redirected to
    # /dev/null, so that it doesn't interfere with the output of other commands.
    # stderr isn't changed, i.e. ~/.xsession-errors will be used.
    # See issues #58 and #103 for explanation of the following syntax.
    ( unset DESKTOP_AUTOSTART_ID; exec 0</dev/null >/dev/null; "$@" ) &

    test -n "$print_pid" && echo $!
}

# Execute a command in the background.
# Parameters:
# $1 = the command.
execute() {
    local launcher

    # Do some logging, either in ~/.xsession-errors or on the console.
    echo "$(LANG=C date '+%c'), epoptes-client executing: $1" >&5

    case "$1" in
        '')
            echo "Can't execute an empty command." >&5
            ;;
        www.*)
            set "http://$1"
            launcher="xdg-open"
            ;;
        http:*|https:*|ftp:*|file:*|mailto:*)
            launcher="xdg-open"
            ;;
        *)
            if [ -e "$1" ] && { [ ! -x "$1" ] || [ -d "$1" ] ;}; then
                launcher="xdg-open"
            elif which -- "$1" >/dev/null; then
                # Don't waste RAM for sh if it's just an executable.
                launcher=""
            fi
    esac
    # In all unhandled cases, run the command with sh -c.
    launcher=${launcher-sh -c}
    background $launcher "$1"
}


# Log out the connected user.
logout() {
    ./endsession --logout
}

# Reboot the client.
reboot() {
    ./endsession --reboot
}

# Shut down the client.
shutdown() {
    ./endsession --shutdown
}

# Create a thumbshot of the user screen.
# Parameters:
# $1 = thumbshot width.
# $2 = thumbshot height.
thumbshot() {
    # TODO: remove compatibility fallback after a few versions
    local executable

    if [ -f "./thumbshot.py" ]; then
        executable=./thumbshot.py
    else
        executable=./screenshot
    fi
    if ! $executable "$1" "$2"; then
        BAD_THUMBSHOTS=$((BAD_THUMBSHOTS+1))
        echo "$BAD_THUMBSHOTS failed thumbshots so far!" >&5
    fi
}

# Lock the screen.
# Parameters:
# $1 = seconds to keep screen locked, 0 means forever - currently ignored.
# $2 = message to display to the user.
lock_screen() {
    local pid

    unlock_screen
    # TODO: remove compatibility fallback after a few versions
    if [ -f "./lock_screen.py" ]; then
        pid=$(background -p ./lock_screen.py "$2")
    else
        pid=$(background -p ./lock-screen "$2")
    fi
    xprop_set EPOPTES_LOCK_SCREEN_PID "$pid"
}

# Unlock a locked screen.
unlock_screen() {
    local pid

    ensure_display
    pid=$(xprop_get EPOPTES_LOCK_SCREEN_PID)
    if [ -n "$pid" ]; then
        kill "$pid"
        xprop_unset EPOPTES_LOCK_SCREEN_PID
    fi
}

# Mute the system sound.
# Parameters:
# $1 = seconds to keep sound muted, 0 means forever - currently ignored.
mute_sound() {
    if [ -x /usr/bin/pactl ]; then
        background pactl set-sink-mute @DEFAULT_SINK@ 1
    elif [ -x /usr/bin/amixer ]; then
        background amixer -c 0 -q sset Master mute
    fi
}

# Unute the system sound.
unmute_sound() {
    if [ -x /usr/bin/pactl ]; then
        background pactl set-sink-mute @DEFAULT_SINK@ 0
    elif [ -x /usr/bin/amixer ]; then
        background amixer -c 0 -q sset Master unmute
    fi
}

# Start a network benchmark and print its pid.
# Stop it automatically after a while.
# Before starting, also stop any previous benchmarks that are still running.
# Parameters:
# $1 = seconds to run (default=10).
start_benchmark() {
    # benchmark_pid can't be unset in the background, but no harm done
    benchmark_pid=$(background -p stop_start_benchmark "$@")
    { sleep $((2*${1:-10}+5)); stop_benchmark "$benchmark_pid"; } &
    echo "$benchmark_pid"
}

# Helper function to avoid running stop_benchmark in foreground.
# Parameters:
# $1 = seconds to run.
stop_start_benchmark() {
    test -n "$benchmark_pid" && stop_benchmark "$benchmark_pid"
    iperf -c "$SERVER" -r ${1:+-t "$1"}
}

# Manually stop a previously running benchmark.
# Parameters:
# $1 = pid
stop_benchmark() {
    if [ -n "$1" ]; then
        kill "$1" || return 0
        sleep 0.2
        # For some reason, iperf -c <unknown host> needs 2 kills!
        kill "$1" || return 0
        sleep 0.2
        # If it's not killed by now, it's hanged
        kill -9 "$1" && sleep 0.2
    fi 2>/dev/null
}

# Display some text to the user.
# Parameters:
# $1 = text.
# $2 = title.
# $3 = True/False, whether the text contains pango markup.
# $4 = icon name.
message() {
    # TODO: remove compatibility fallback after a few versions
    if [ -f "./message.py" ]; then
        background ./message.py "$@"
    else
        background ./message "$@"
    fi
}

# Connect to the server to be monitored.
# Parameters:
# $1 = port.
get_monitored() {
    background x11vnc -noshm -24to32 -viewonly -connect_or_exit "$SERVER:$1"
}

# Connect to the server to get assistance.
# Parameters:
# $1 = port.
# $2 = grab keyboard and mouse.
get_assisted() {
    background x11vnc -noshm -24to32 ${2:+-grabptr -grabkbd} -connect_or_exit "$SERVER:$1"
}

# Deactivate the screensaver, in order for the users to watch a broadcast.
reset_screensaver() {
    if [ -x /usr/bin/xdg-screensaver ]; then
        xdg-screensaver reset
    else
        xset s reset
    fi
}

# Receive a broadcasted screen from the server.
# Parameters:
# $1 = port.
# $2 = password (encrypted as in ~/.vnc/passwd and octal-escaped).
# $3 = fullscreen.
receive_broadcast() {
    receiving_broadcast=true
    stop_receptions
    reset_screensaver
    if [ -z "$VNCVIEWER" ]; then
        # If the user installed ssvnc, prefer it over xvnc4viewer
        if [ -x /usr/bin/ssvncviewer ]; then
            VNCVIEWER=ssvncviewer
        elif [ -x /usr/bin/xtigervncviewer ]; then
            VNCVIEWER=xtigervncviewer
        elif [ -x /usr/bin/xvnc4viewer ]; then
            VNCVIEWER=xvnc4viewer
        fi
    fi
    printf "$2" | {
        sleep 0.$(($(od -An -N2 -tu2 /dev/urandom) % 50 + 50))
        if [ "$VNCVIEWER" = "ssvncviewer" ]; then
            exec ssvncviewer -shared -viewonly -passwd /dev/stdin \
                ${3:+-fullscreen} "$SERVER:$1"
        elif [ "$VNCVIEWER" = "xvnc4viewer" ]; then
            exec ${VNCVIEWER} -Shared -ViewOnly -passwd /dev/stdin \
                ${3:+-FullScreen -UseLocalCursor=0 -MenuKey F13} "$SERVER:$1"
        elif [ "$VNCVIEWER" = "xtigervncviewer" ]; then
            exec ${VNCVIEWER} -Shared -ViewOnly -passwd /dev/stdin \
                ${3:+-FullScreen -MenuKey F13} "$SERVER:$1"
        else
            exec vncviewer -shared -viewonly -passwd /dev/stdin \
                ${3:+-fullscreen} "$SERVER:$1"
        fi
    } >/dev/null 2>&1 &
    xprop_set EPOPTES_VNCVIEWER_PID "$!"
}

# The vnc clients should automatically exit when the server is killed.
# Unfortunately, that isn't always true, so try to kill them anyway.
stop_receptions() {
    local pid

    unset receiving_broadcast
    ensure_display
    pid=$(xprop_get EPOPTES_VNCVIEWER_PID)
    if [ -n "$pid" ]; then
        kill "$pid"
        xprop_unset EPOPTES_VNCVIEWER_PID
    fi
}

# Open a root terminal inside the X session.
root_term() {
    background xterm -e bash -l
}

# Send a screen session to the server using socat.
# Parameters:
# $1 = port.
remote_term() {
    local screen_params

    if [ "$UID" -eq 0 ]; then
        screen_params="bash -l"
    else
        screen_params="-l"
    fi
    background sh -c "
cd
sleep 1
TERM=xterm exec socat EXEC:'screen $screen_params',pty,stderr tcp:$SERVER:$1"
}

# Ping is called every few seconds just to make sure the connection is alive.
# But currently we use it as a workaround to kill stale clients too:
# Epoptes-client isn't registered as an X session client, and it doesn't
# exit automatically, so tell it to exit as soon as X is unavailable.
ping() {
    if [ "$UID" -gt 0 ]; then
        xprop -root -f EPOPTES_CLIENT 32c -set EPOPTES_CLIENT $$ || die "No X"
    fi
    if [ "$receiving_broadcast" = true ]; then
        reset_screensaver
    fi
}

# Display a message.
# Parameters:
# $1..$N = The message.
# echo()
# No need to implement it in the shell, it's embedded.

# Main

if [ -z "$UID" ] || [ -z "$TYPE" ] || [ -z "$SERVER" ]; then
    die "Required environment variables are missing."
fi

echo "   ...done" >&5

info

# Prevent dbus from getting autolaunched (issue #66)
export "DBUS_SESSION_BUS_ADDRESS=${DBUS_SESSION_BUS_ADDRESS:-unix:path=/dev/null}"

for i in 0 15; do
    trap "on_signal $i" $i
done
