#!/bin/sh
# This file is part of Epoptes, https://epoptes.org
# Copyright 2010-2023 the Epoptes team, see AUTHORS.
# SPDX-License-Identifier: GPL-3.0-or-later

# epoptes-client is called either from systemd as root, to control the client,
# or from /etc/xdg/autostart as a user, to control the user session.
# Users can cancel that from their System > Preferences > Services gnome menu.

usage() {
    cat <<EOF
Usage: $0 [-c|-h|-v] [SERVER] [PORT]
Connect to a remote server and offer it a local shell.
EOF
}

version() {
    export VERSION="1.0" # Automatically updated while packaging
}

die() {
    echo "epoptes-client ERROR: $*" >&2
    exit 1
}

# The "boolean_is_true" name is used as a sentinel that prevents ltsp_config
# from sourcing ltsp_common_functions. So we're using a different name.
my_boolean_is_true() {
    case "$1" in
    # match all cases of true|y|yes
    [Tt][Rr][Uu][Ee] | [Yy] | [Yy][Ee][Ss]) return 0 ;;
    *) return 1 ;;
    esac
}

# Return true if we're in a chroot.
chrooted() {
    # The result is cached in a variable with the same name as the function :P
    test -n "$chrooted" && return "$chrooted"
    test -n "$UID" || UID=$(id -u)
    if [ "$UID" -gt 0 ]; then
        chrooted=1
    elif [ "$(stat -c %d/%i /)" = "$(stat -Lc %d/%i /proc/1/root 2>/dev/null)" ]; then
        # the devicenumber/inode pair of / is the same as that of /sbin/init's
        # root, so we're *not* in a chroot and hence return false.
        chrooted=1
    else
        chrooted=0
    fi
    return "$chrooted"
}

# Get $UID and $TYPE of the client, and the default $SERVER and $PORT.
basic_info() {
    test -n "$UID" || UID=$(id -u)

    # We temporarily need LTSP_CLIENT and LTSP_FATCLIENT to decide TYPE.
    # Unfortunately, when epoptes-client is ran as a system service, they're
    # not in our environment, and we need to source ltsp_config.
    # But we don't want to pollute the environment with any of its other vars.
    if [ "$UID" -eq 0 ] && [ -f /usr/share/ltsp/ltsp_config ] && ! chrooted &&
        grep -Eqs 'ltsp|nfs|nbd' /proc/cmdline; then
        # shellcheck disable=SC2046
        export $(
            . /usr/share/ltsp/ltsp_config >/dev/null
            echo "LTSP_CLIENT=$LTSP_CLIENT"
            echo "LTSP_FATCLIENT=$LTSP_FATCLIENT"
            echo "EPOPTES_CLIENT_VERIFY_CERTIFICATE=$EPOPTES_CLIENT_VERIFY_CERTIFICATE"
        )
        # LTSP_CLIENT may not be available in system sessions, if so fake it
        LTSP_CLIENT=${LTSP_CLIENT:-127.0.0.1}
    fi

    # LTSP_FATCLIENT may not be available in user sessions, autodetect it
    if [ -n "$LTSP_CLIENT" ] && [ -z "$LTSP_FATCLIENT" ] &&
        [ "$UID" -gt 0 ] && [ -x /usr/bin/getltscfg ] &&
        grep -Eqs 'ltsp|nfs|nbd' /proc/cmdline; then
        LTSP_FATCLIENT=True
    fi

    if my_boolean_is_true "$LTSP_FATCLIENT" || [ -d /run/ltsp/client ]; then
        TYPE="fat"
    elif [ -n "$LTSP_CLIENT" ]; then
        TYPE="thin"
    else
        TYPE="standalone"
    fi

    if { [ "$TYPE" = "thin" ] && [ "$UID" -gt 0 ]; } || chrooted; then
        SERVER=localhost
    else
        SERVER=server
    fi
    PORT=789

    export UID TYPE SERVER PORT
}

fetch_certificate() {
    test "$UID" -eq 0 || die "Need to be root to fetch the certificate"
    mkdir -p /etc/epoptes
    openssl s_client -connect $SERVER:$PORT </dev/null |
        sed '/-----BEGIN CERTIFICATE-----/,/-----END CERTIFICATE-----/!d' \
            >/etc/epoptes/server.crt
    if [ -s /etc/epoptes/server.crt ]; then
        echo "Successfully fetched certificate from $SERVER:$PORT"
        exit 0
    else
        die "Failed to fetch certificate from $SERVER:$PORT"
    fi
}

re() {
    "$@" || echo die "Command failed: $*"
}

wait_for_dns() {
    local delay

    # Don't wait for DNS if SERVER is an IP
    if [ -z "$(echo "$SERVER" | tr -d '0-9.')" ]; then
        server_ip=$SERVER
        return
    fi
    delay=5
    while true; do
        server_ip=$(getent hosts "$SERVER" | awk '{ print $1; exit }')
        test -n "$server_ip" && break
        # If DNS is up and SERVER is not an MDNS name, abort
        if [ "${SERVER%.local}" = "${SERVER}" ] &&
            [ -n "$(getent hosts "ntp.org" | awk '{ print $1; exit }')" ]; then
            echo "Cannot resolve $SERVER while DNS appears to work, aborting."
            exit 1
        fi
        if [ "$delay" -eq 5 ]; then
            echo "Cannot resolve $SERVER, will keep retrying."
        fi
        sleep $delay
        # Increase delay up to a minute
        if [ "$delay" -lt 60 ]; then
            delay=$((delay + 1))
        fi
    done
}

apply_wol() {
    local def_iface _ip

    if [ "$UID" -eq 0 ] && [ -n "$WOL" ] && [ -x /sbin/ethtool ] &&
        [ "$EPOPTES_CLIENT_APPLIED_WOL" != True ]; then
        export EPOPTES_CLIENT_APPLIED_WOL=True
        # Copied from client-functions, these map from server_ip to def_iface
        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
        if [ "${def_iface:-lo}" != "lo" ]; then
            # Only set WOL if def_iface is both != '' and != 'lo'.
            echo "Setting WOL=$WOL for $def_iface"
            ethtool -s "$def_iface" wol "$WOL"
        fi
    fi
}

# Main.
version

# Check the first parameter as it may turn out we don't need to run at all
case "$1" in
-v | --version)
    echo "$VERSION"
    exit
    ;;
-h | --help)
    if [ -x /usr/bin/man ]; then
        exec man epoptes-client
    else
        usage
        exit
    fi
    ;;
-c | --certificate)
    need_certificate=true
    shift
    ;;
esac

# Set a reasonable PATH to execute commands or to relaunch epoptes-client.
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games"

# When launched as a service, LANG might not be set.
if [ -z "$LANG" ] && [ -r /etc/default/locale ]; then
    . /etc/default/locale
    export LANG
fi

basic_info
# The configuration file overrides the default values
if [ -f /etc/default/epoptes-client ]; then
    . /etc/default/epoptes-client
fi
# And the command line parameters override the configuration file
export "SERVER=${1:-$SERVER}"
export "PORT=${2:-$PORT}"

# Provide an easy way to fetch the server certificate
test -n "$need_certificate" && fetch_certificate

servercrt=/etc/epoptes/server.crt
# Don't launch as root on epoptes servers, unless there's a main server
if [ "$UID" -eq 0 ] && [ $TYPE = "standalone" ] && [ -x /usr/bin/epoptes ]; then
    if [ -s /etc/epoptes/main-server.crt ]; then
        servercrt=/etc/epoptes/main-server.crt
        SERVER=${MAIN_SERVER:-main-server}
    else
        exit 0
    fi
fi
# Don't launch inside chroots
if chrooted; then
    exit 0
fi

# Go to the scripts directory, so that we can run them with ./xxx
re cd "$(dirname "$0")"
if [ -d ../epoptes-client ]; then
    re cd ../epoptes-client
else
    re cd /usr/share/epoptes-client
fi

wait_for_dns
apply_wol
printf "Epoptes-client connecting to %s:%s..." "$SERVER" "$PORT"

# Call chain:
#  * systemd executes /usr/sbin/epoptes-client
#  * then socat is called
#  * after a successful connection, socat exec's /bin/sh
#  * and the daemon sends /usr/share/epoptes/client-functions to that shell

# Kill all ghost instances of epoptes-client of the same user.
# The current epoptes-client is excluded because it starts with /bin/sh.
pkill -QUIT -U "$UID" -f '^epoptes-client$'

# Remember the stdout descriptor to use it in the second phase.
# stdio will be redirected to the server, but stderr will be kept in the
# local console, to avoid possible noise from applications started in the
# background.
# If the callee needs to grab stderr, it can use `cmd 2>&1`.
exec 5>&1

# Bash supports launching a program with a different zeroth argument,
# this makes pgrep'ing for epoptes-client easier.
cmdline='bash -c \"exec -a epoptes-client sh\"'

# Offer an lts.conf (or environment) variable to disable cert verification.
if my_boolean_is_true "${EPOPTES_CLIENT_VERIFY_CERTIFICATE:-True}"; then
    cert_param="cafile=$servercrt"
    # Support certificates without or with changed hostname
    if socat -hhh | grep -qw openssl-no-sni; then
        cert_param="$cert_param,openssl-no-sni"
    fi
    if socat -hhh | grep -qw openssl-commonname; then
        cert_param="$cert_param,openssl-commonname=\"\""
    fi
else
    cert_param="verify=0"
fi

# Connect to the server, or keep retrying until the server gets online
# (for standalone workstations booted before the server).
if [ -s "$servercrt" ] || [ "$cert_param" = "verify=0" ]; then
    exec socat -T 60 "openssl-connect:$SERVER:$PORT,$cert_param,interval=60,forever" "EXEC:$cmdline"
elif [ -f "servercrt" ]; then
    exec socat "tcp:$SERVER:$PORT,interval=60,forever" "EXEC:$cmdline,nofork"
else
    die "
The epoptes certificate file, $servercrt, doesn't exist.
You can fetch the server certificate by running:
$0 -c"
fi
