#!/bin/sh -efu
#
# Run sshd daemon within a hasher chroot.
#
# Copyright (C) 2026  Gleb Fotengauer-Malinovskiy <glebfm@altlinux.org>
# All rights reserved.
#
# SPDX-License-Identifier: GPL-2.0-or-later
#

# shellcheck enable=all disable=SC2250,SC3043 source-path=/bin

sshd_log=/dev/null
sshd_hostname=localhost-hasher
sshd_authorized_keys=
sshd_timeout_idle_secs=0

. hsh-sh-functions

# This function is a copy of lock_workdir from hsh-sh-functions,
# but with lockf instead of bash-builtin-lockf, it's safe because it uses the
# same lockf(3) mechanism.
# Unlike other hsh- utilities we want chrootuid2.sh process to inherit the lock.
lock_workdir_lockf()
{
	[ -z "$workdir_is_locked" ] || return 0
	workdir_is_locked=1
	[ -z "$no_lock" ] || return 0

	if flock -n 3; then
		echo $$ >"$workdir/pid"
		verbose "Locked working directory \`$workdir'"
		return 0
	fi

	local pid
	if [ -n "$verbose" ] || [ -n "$lock_nowait" ] &&
	   pid="$(head -c32 -- "$workdir/pid")" &&
	   [ "$pid" -gt 0 ] 2>/dev/null; then
		if kill -0 -- "$pid" 2>/dev/null; then
			message "working directory \`$workdir' is already locked by pid=$pid."
			a='' ps hp "$pid" >&2 ||:
		else
			message "working directory \`$workdir' is already locked by stale pid=$pid."
		fi
	fi

	if [ -z "$lock_nowait" ]; then
		verbose "Waiting for working directory lock..."
		if flock 3; then
			echo $$ >"$workdir/pid"
			verbose "Locked working directory \`$workdir'"
			return 0
		fi
	fi

	fatal "Unable to lock working directory \`$workdir'"
}

# This function is a copy of chrootuid2 from hsh-sh-functions,
# but with nohup, logging, &, and pidfile overwrite.
chrootuid2_nohup()
{
	[ $# -gt 0 ] || set -- /.host/entry
	local rc=0
	if [ -x "$aptbox/setarch" ]; then
		nohup "$aptbox/setarch" "$chrootuid2" \
			${number:+-$number} "$chroot" "$@" \
			>>"$sshd_log" 2>&1 &
	else
		nohup "$chrootuid2" \
			${number:+-$number} "$chroot" "$@" \
			>>"$sshd_log" 2>&1 &
	fi || rc=$?
	[ "$rc" != 0 ] ||
		echo $! >"$workdir/pid"
	# shellcheck disable=SC2034
	wlimit_time_elapsed='' wlimit_time_idle='' wlimit_bytes_written=''
	return "$rc"
}

print_version()
{
	local prog
	prog="$1" && shift
	cat <<EOF
$prog version 1.0
Written by Gleb Fotengauer-Malinovskiy <glebfm@altlinux.org>.

Copyright (C) 2026  Gleb Fotengauer-Malinovskiy <glebfm@altlinux.org>
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
EOF
        exit
}

show_help()
{
	cat <<EOF
hsh-sshd - run sshd within a hasher chroot.

Usage: $PROG [options] [--workdir=DIR] [--start]
   or: $PROG [options] [--workdir=DIR] --kill

<DIR> must be valid hasher directory.
EOF
	cat <<EOF

Options:
  --start                   start sshd in a hasher chroot (default);
  --kill                    kill all processes in a hasher chroot;
  --hasher-priv-dir=DIR     hasher-priv directory;
  --mountpoints=LIST        comma-separated list of mount points;
  --number=NUMBER           subconfig identifier;
  --wait-lock               wait for workdir and hasher-priv locks;
  --no-wait-lock            do not wait for workdir and hasher-priv locks;
  --workdir=DIR             path to workdir to use;
  -q, --quiet               try to be more quiet;
  -v, --verbose             print a message for each action;
  -V, --version             print program version and exit;
  -h, --help                show this text and exit.

Report bugs to https://bugzilla.altlinux.org/

EOF
	exit
}

mode=start

# shellcheck disable=SC2006,SC2086,SC2119
TEMP=`getopt -n $PROG -o $getopt_common_opts -l hasher-priv-dir:,mountpoints:,no-lock,number:,wait-lock,no-wait-lock,workdir:,start,kill,$getopt_common_longopts -- "$@"` ||
	show_usage
prepare_cgroup "$TEMP" "$0" "$@"
eval set -- "$TEMP"

while :; do
	case "$1" in
		--hasher-priv-dir)
			hasher_priv_dir="$(opt_check_dir "$1" "$2")"
			shift
			;;
		--mountpoints) shift; requested_mountpoints="$1"
			;;
		--no-lock) no_lock=1
			;;
		--number)
			number="$(opt_check_number_ge_0 "$1" "$2")"
			shift
			;;
		--wait-lock) lock_nowait=
			;;
		--no-wait-lock) lock_nowait=1
			;;
		--workdir) shift; workdir="$1"
			;;
		--start) mode=start ;;
		--kill) mode='kill' ;;
		--) shift; break
			;;
		*) parse_common_option "$1"
			;;
	esac
	shift
done

killuid() {
	hasher_privc="${hasher_priv_dir:-$def_hasher_priv_dir}"/hasher-priv
	"$hasher_privc" ${number:+-$number} killuid
}

if [ "$mode" = kill ]; then
	killuid
	exit 0
fi

if [ -z "$sshd_authorized_keys" ]; then
	for f in "$HOME"/.ssh/id_ed25519.pub "$HOME"/.ssh/id_rsa.pub; do
		if [ -s "$f" ]; then
			sshd_authorized_keys="$f"
			verbose "using $f as a source for authorized_keys"
			break
		fi
	done
	if [ -z "$sshd_authorized_keys" ]; then
		message "warning: authorized_keys source not found"
		message "consider adding sshd_authorized_keys= path to .hasher/config"
		sshd_authorized_keys=/dev/null
	fi
fi

if [ ! -s "$sshd_authorized_keys" ]; then
	fatal "$sshd_authorized_keys is not valid"
fi

set_workdir

exec 3<"$workdir"
lock_workdir_lockf

[ -d "$chroot" ] || fatal "$chroot: cannot find chroot."
deduce_lock_hasher_priv

# shellcheck disable=SC2248
if [ ! -x "$chroot"/usr/bin/hsh-dropbear-wrapper ]; then
	hsh-install --no-lock \
		${quiet:+$quiet} \
		${verbose:+$verbose} \
		${number:+--number=$number} \
		${hasher_priv_dir:+--hasher-priv-dir="$hasher_priv_dir"} \
		-- \
		"$workdir" hsh-sshd-hasher
fi

create_entry_header
# shellcheck disable=SC2129
cat >>"$entry" <<__EOF__
rm -f /usr/src/.sshd-sock

cat > /usr/src/.ssh/authorized_keys <<'__E_O_F__'
__EOF__

cat "$sshd_authorized_keys" >>"$entry"

cat >>"$entry" <<__EOF__
__E_O_F__

if [ ! -f /usr/src/.ssh-host-ed25519 ]; then
	dropbearkey -t ed25519 -f /usr/src/.ssh-host-ed25519 >&2
fi
read -r type key _rest < /usr/src/.ssh-host-ed25519.pub
printf "%s %s %s\n" "$sshd_hostname" "\$type" "\$key"
__EOF__

# shellcheck disable=SC2310
chrootuid2 >"$workdir"/.ssh-known-hosts ||
	fatal "$chroot: failed to setup sshd"

cat > "$workdir"/.ssh-config <<EOF
Host hsh
	User builder
	HostName $sshd_hostname
	ProxyCommand netcat -U $chroot/usr/src/.sshd-sock
	UserKnownHostsFile $workdir/.ssh-known-hosts
EOF

# Change entry path to see it in ps(1) output.
entry="$chroot"/.host/sshd
# create_entry_fakeroot_header checks if $USER is "root"
create_entry_header

cat >>"$entry" <<__EOF__
if [ -z "\$FAKEROOTKEY" ]; then
	exec /usr/bin/fakeroot "\$0" "\$@"
fi
chown builder:builder /usr/src
chmod 700 /usr/src
exec hsh-dropbear-wrapper '$sshd_timeout_idle_secs' /usr/src/.sshd-sock \
	/usr/sbin/dropbear -s -i -r /usr/src/.ssh-host-ed25519
__EOF__

# Add /dev/pts unconditionally.
requested_mountpoints="${requested_mountpoints:+$requested_mountpoints,}/dev/pts"

export requested_mountpoints

chrootuid2_nohup /.host/sshd

verbose 'waiting for sshd to start'
for i in $(seq 150); do
	if [ -S "$chroot"/usr/src/.sshd-sock ]; then
		message "sshd is up, run ssh -F $workdir/.ssh-config hsh"
		message "or connect to $chroot/usr/src/.sshd-sock directly"
		exit 0
	fi
	sleep .1
done

killuid
fatal 'giving up on waiting for sshd to start'
