#!/bin/sh
#
# Copyright (c) 2021, 2022  Peter Pentchev <roam@ringlet.net>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
# shellcheck disable=SC2310

set -e

remrun_version='0.2.3'

version()
{
	echo "remrun $remrun_version"
}

usage()
{
	cat <<'EOUSAGE'
Usage:	remrun [-Nsv] [--noop] [--stdin] [--verbose]
		[-C local_cksum_command] [-c remote_cksum_command]
		destination /path/to/file [args]
	remrun -h | --help | -V | --version | --features

	-C	specify the local command to run instead of sha256sum
	-c	specify the remote command to run instead of sha256sum
	-h	display program usage information and exit
	-N	no-operation mode; do not execute the file on the remote host
	-s	let the remote command read from the standard input stream
	-V	display program version information and exit
	-v	verbose mode; display diagnostic output

Examples:
	Copy the whoami.sh file over to the 'server' host, logging in as
	the 'jrl' user account, then run it:

	remrun jrl@server whoami.sh

	Do the same with more verbose output and allowing the executed program
	to read from our standard input stream:

	remrun -vs jrl@server whoami.sh

	Specify the commands to run directly:

	printf 'uname -a\ndate -R\n' | remrun jrl@server -
EOUSAGE
}

features()
{
	echo "Features: remrun=$remrun_version hostspec=1.0"
}

diag()
{
	[ -z "$v" ] || printf -- '%s\n' "$*" 1>&2
}

parse_hostspec_uri()
{
	local hostspec="$1"
	# This is not something one does in a shell script, is it...

	local schema="${hostspec%%://*}"
	local rest="${hostspec#*://}"
	if [ "$schema" != 'ssh' ]; then
		echo "Only the 'ssh' URI schema is supported, not '$schema'" 1>&2
		exit 1
	fi
	if [ "${rest#*&}" != "$rest" ] || [ "${rest#*%}" != "$rest" ]; then
		echo "URI entities or percent-encoded characters not supported" 1>&2
		exit 1
	fi

	local path
	if [ "${rest#*/}" != "$rest" ]; then
		path="${rest#*/}"
		rest="${rest%%/*}"
	else
		path=''
	fi
	if [ "$path" != '' ]; then
		echo "URI path not supported" 1>&2
		exit 1
	fi

	local port
	if [ "${rest##*:}" != "$rest" ]; then
		port="${rest##*:}"
		rest="${rest%:*}"
	else
		port=''
	fi

	local host
	if [ "${rest##*@}" != "$rest" ]; then
		host="${rest##*@}"
		rest="${rest%@*}"
	else
		host="$rest"
		rest=''
	fi

	if [ "${rest#*;}" != "$rest" ] || [ "${rest#*,}" != "$rest" ]; then
		echo "URI parameters not supported" 1>&2
		exit 1
	fi

	ssh_port="$port"
	ssh_userhost="${rest:+$rest@}$host"
}

parse_hostspec()
{
	local hostspec="$1"

	if [ "${hostspec%://*}" != "$hostspec" ]; then
		parse_hostspec_uri "$hostspec"
		return
	fi

	local username host
	if [ "${hostspec#*@}" != "$hostspec" ]; then
		username="${hostspec%%@*}"
		host="${hostspec#*@}"
	else
		username=''
		host="$hostspec"
	fi

	ssh_port=''
	ssh_userhost="${username:+$username@}$host"
}

_run_ssh()
{
	local nopt="$1"
	shift 1

	# shellcheck disable=SC2086  # nopt is under our control
	ssh $nopt ${ssh_port:+-p "$ssh_port"} -- "$ssh_userhost" "$@"
}

run_ssh()
{
	_run_ssh '' "$@"
}

run_ssh_n()
{
	_run_ssh '-n' "$@"
}

create_remote_tempfile()
{
	local tempf

	diag "About to create a temporary file at '$ssh_userhost'"
	if ! tempf="$(run_ssh_n mktemp remrun.XXXXXX)"; then
		echo "Could not run mktemp at '$ssh_userhost' via SSH" 1>&2
		exit 1
	fi
	diag "Got '$tempf' as the remote temporary filename"
	printf -- '%s\n' "$tempf"
}

remove_remote_tempfile()
{
	local remtempf="$1"

	diag "About to remove the remote temporary file '$remtempf'"
	if ! run_ssh_n rm -- "$remtempf"; then
		echo "Could not remove the '$remtempf' temporary file at '$ssh_userhost'" 1>&2
		exit 1
	fi
}

store_and_run()
{
	local remtempf="$1" cksum_local="$2"
	local cksum_remote remtempfull nopt

	diag "Storing the contents of '$filename' into '$ssh_userhost:$remtempf'"
	if ! run_ssh sh -c "cat > '$remtempf'" < "$filename"; then
		echo "Could not write '$filename' to '$remtempf' at '$ssh_userhost'" 1>&2
		exit 1
	fi

	diag "Obtaining the checksum"
	if ! cksum_remote="$(run_ssh_n "$remote_cksum_cmd -- $remtempf")"; then
		echo "Could not obtain the checksum of '$remtempf' at '$ssh_userhost'" 1>&2
		exit 1
	fi
	cksum_remote="${cksum_remote%% *}"
	diag "Obtained remote checksum $cksum_remote"
	if [ "$cksum_local" != "$cksum_remote" ]; then
		echo "The remote checksum '$cksum_remote' does not match the local one '$cksum_local'" 1>&2
		return 1
	fi

	diag "Setting the access permissions on the remote file"
	if ! run_ssh_n chmod -- 700 "$remtempf"; then
		echo "Failed to set the access permissions of '$remtempf' at '$ssh_userhost'" 1>&2
		exit 1
	fi

	remtempfull="$remtempf"
	if [ "${remtempfull%%/*}" = "$remtempfull" ]; then
		diag "Prepending ./ to the name of the remote file"
		remtempfull="./$remtempfull"
	fi
	diag "Executing the remote file $remtempfull"
	if [ -n "$stdin" ]; then
		diag "Not passing '-n' to the SSH client"
		nopt=''
	else
		nopt='-n'
	fi
	if ! _run_ssh "$nopt" "$noop $remtempfull $opts"; then
		echo "Running '$remtempfull' at '$ssh_userhost' failed" 1>&2
		exit 1
	fi
}

doit()
{
	local cksum_local remtempf tempf_stdin='/nonexistent'

	if [ "$filename" = '-' ]; then
		tempf_stdin="$(mktemp remrun-stdin.XXXXXX)"
		# shellcheck disable=SC2064
		trap "rm -f -- '$tempf_stdin'" HUP INT EXIT QUIT TERM

		diag "Reading the standard input stream into $tempf_stdin"
		cat > "$tempf_stdin"

		diag "Processing $tempf_stdin now"
		filename="$tempf_stdin"
	fi

	if [ ! -f "$filename" ] || [ ! -r "$filename" ]; then
		echo "Not a readable regular file: $filename" 1>&2
		exit 1
	fi
	diag "Obtaining the local checksum of $filename"
	cksum_local="$($local_cksum_cmd -- "$filename")"
	cksum_local="${cksum_local%% *}"
	if [ -z "$cksum_local" ]; then
		echo "The checksum of $filename cannot be an empty string" 1>&2
		exit 1
	fi
	diag "Obtained local checksum $cksum_local"

	remtempf="$(create_remote_tempfile)"
	# shellcheck disable=SC2064
	trap "remove_remote_tempfile '$remtempf'; rm -f -- '$tempf_stdin'" HUP INT EXIT QUIT TERM
	store_and_run "$remtempf" "$cksum_local"
}

unset hflag noop show_features stdin Vflag v
local_cksum_cmd='sha256sum'
remote_cksum_cmd='sha256sum'

while getopts 'C:c:hNsVv-:' o; do
	case "$o" in
		C)
			local_cksum_cmd="$OPTARG"
			;;

		c)
			remote_cksum_cmd="$OPTARG"
			;;

		h)
			hflag=1
			;;

		N)
			noop='echo'
			;;

		s)
			stdin=1
			;;

		V)
			Vflag=1
			;;

		v)
			v='-v'
			;;

		-)
			case "$OPTARG" in
				features)
					show_features=1
					;;

				help)
					hflag=1
					;;

				noop)
					noop='echo'
					;;

				stdin)
					stdin=1
					;;

				verbose)
					v='-v'
					;;

				version)
					Vflag=1
					;;

				*)
					echo "Invalid long option '$OPTARG' specified" 1>&2
					usage 1>&2
					exit 1
					;;
			esac
			;;

		*)
			usage 1>&2
			exit 1
			;;
	esac
done
[ -z "$Vflag" ] || version
[ -z "$hflag" ] || usage
[ -z "$show_features" ] || features
[ -z "$Vflag$hflag$show_features" ] || exit 0

shift "$((OPTIND - 1))"
if [ "$#" -lt 2 ]; then
	usage 1>&2
	exit 1
fi

unset ssh_port ssh_userhost
parse_hostspec "$1"
filename="$2"
shift 2
opts="$*"

if [ -n "$stdin" ] && [ "$filename" = '-' ]; then
	echo 'The -s option may not be used with "-" as a filename' 1>&2
	exit 1
fi

diag "Parsed SSH port '$ssh_port' user@host '$ssh_userhost'"
doit
