#! /usr/bin/env perl
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
use common::sense;

use MIME::Base64;
use IO::Compress::Gzip qw{ gzip };
use Sys::Hostname      ();

use File::Basename;
use lib dirname(__FILE__) . '/../../../lib/perl';
use OVH::Result;
use OVH::Bastion;
use OVH::Bastion::Plugin qw( :DEFAULT );
use OVH::Bastion::Plugin::otherProtocol;

# stdout is used by sftp, so ensure we output everything through stderr
local $ENV{'FORCE_STDERR'} = 1;

# don't output fancy stuff, this can get digested by sftp and we get garbage output
local $ENV{'PLUGIN_QUIET'} = 1;

my $remainingOptions = OVH::Bastion::Plugin::begin(
    argv    => \@ARGV,
    header  => undef,
    options => {},
    help    => \&help,
);

sub help {
    delete $ENV{'FORCE_STDERR'};
    delete $ENV{'PLUGIN_QUIET'};

    osh_header("sftp");
    my $bastionCommand = OVH::Bastion::config('bastionCommand')->value;
    my $bastionName    = OVH::Bastion::config('bastionName')->value;
    $bastionCommand =~ s/USER|ACCOUNT/$self/g;
    $bastionCommand =~ s/CACHENAME|BASTIONNAME/$bastionName/g;
    my $hostname = Sys::Hostname::hostname();
    $bastionCommand =~ s/HOSTNAME/$hostname/g;

    # for sftp, if the bastionCommand contains -t, we need to get rid of it
    $bastionCommand =~ s/ -t( |$)/$1/;

    # same thing for --
    $bastionCommand =~ s/ --/ /;
    my $script = <<'END_OF_SCRIPT';
#! /usr/bin/env bash
set -u
# <custom-section>
SELF="%SELF%"
BASTION_CMD="%BASTION_CMD%"
VERSION="%VERSION%"
# </custom_section>

: "${BASTION_SFTP_DEBUG:=}"
[ "$BASTION_SFTP_DEBUG" = 1 ] && echo "sftpwrapper: args: $*" >&2

BASTION_SSH_EXTRA_ARGS=""
BASTION_SFTP_EXTRA_ARGS=""

usage() {
    cat >&2 <<EOF
usage: $0 [-i identity_file] [-P port] [-o ssh_option] [-b batchfile] [-F ssh_config] [-l limit] [-afprv] destination
EOF
    exit 1
}

while [ -n "${1:-}" ]; do
    case "$1" in
        '-a'|'-f'|'-p'|'-r')
            BASTION_SFTP_EXTRA_ARGS="$BASTION_SFTP_EXTRA_ARGS $1"
            shift;;
        '-v')
            BASTION_SSH_EXTRA_ARGS="$BASTION_SSH_EXTRA_ARGS $1"
            BASTION_SFTP_EXTRA_ARGS="$BASTION_SFTP_EXTRA_ARGS $1"
            shift;;
        '-i'|'-o'|'-F')
            if [ -z "${2:-}" ]; then
                echo "sftpwrapper: missing argument after '$1'" >&2
                exit 1
            fi
            BASTION_SSH_EXTRA_ARGS="$BASTION_SSH_EXTRA_ARGS $1 $2"
            BASTION_SFTP_EXTRA_ARGS="$BASTION_SFTP_EXTRA_ARGS $1 $2"
            shift 2;;
        '-b'|'-l')
            if [ -z "${2:-}" ]; then
                echo "sftpwrapper: missing argument after '$1'" >&2
                exit 1
            fi
            BASTION_SFTP_EXTRA_ARGS="$BASTION_SFTP_EXTRA_ARGS $1 $2"
            shift 2;;
        "-P")
            if [ -z "${2:-}" ]; then
                echo "sftpwrapper: missing argument after '$1'" >&2
                exit 1
            fi
            REMOTE_PORT="$2"
            shift 2;;
        "-*")
            echo "sftpwrapper: unsupported option '$1'" >&2
            exit 1;;
        *)  break;;
    esac
done

# here, we should have only destination in $1
[ $# -ne 1 ] && usage

dst="${1:-}"
if [[ $dst =~ ^(sftp://)?(([^@:/]+)@)?([^@:/]+)(:([0-9]+))?(/(.+))?$ ]]; then
    REMOTE_USER="${BASH_REMATCH[3]:-$SELF}"
    REMOTE_HOST="${BASH_REMATCH[4]}"
    # if already set through -P, halt if we have a different port specified through sftp://
    if [ -n "${REMOTE_PORT:-}" ]; then
        if [ -n "${BASH_REMATCH[6]:-}" ] && [ "${BASH_REMATCH[6]:-}" != "$REMOTE_PORT" ]; then
            echo "sftpwrapper: conflicting ports specified ($REMOTE_PORT and ${BASH_REMATCH[6]})" >&2
            exit 1
        fi
    else
        REMOTE_PORT="${BASH_REMATCH[6]:-22}"
    fi
    REMOTE_PATH="${BASH_REMATCH[7]}"
    [ "$BASTION_SFTP_DEBUG" = 1 ] && echo "sftpwrapper: parsed user=$REMOTE_USER host=$REMOTE_HOST port=$REMOTE_PORT path=$REMOTE_PATH" >&2
else
    echo "sftpwrapper: couldn't parse destination '$dst'" >&2
    exit 1
fi

if [ -z "${REMOTE_HOST:-}" ]; then
    echo "sftpwrapper: no remote host found in your command '$dst'" >&2
    exit 1
fi

t=$(mktemp)
# we don't expand $t here because it might change during the course of the script
trap 'rm -f $t' EXIT
# verify that the tmpfile was created in a spot that's NOT mounted in noexec
chmod +x "$t"
if ! "$t" 2>/dev/null; then
    # oops, noexec => default to user's HOME directory instead
    [ "$BASTION_SFTP_DEBUG" = 1 ] && echo "sftpwrapper: current TMPDIR is in noexec ($t), changing to user's home instead" >&2
    rm -f "$t"
    t=$(TMPDIR="$HOME" mktemp)
else
    chmod -x "$t"
fi

# shellcheck disable=SC2086
[ "$BASTION_SFTP_DEBUG" = 1 ] && set -x
$BASTION_CMD -t $BASTION_SSH_EXTRA_ARGS -- --osh sftp --host "$REMOTE_HOST" --port "$REMOTE_PORT" --user "$REMOTE_USER" --generate-mfa-token | tee "$t"
[ "$BASTION_SFTP_DEBUG" = 1 ] && set +x
token=$(grep -Eo '^MFA_TOKEN=[a-zA-Z0-9,]+' "$t" | tail -n 1 | cut -d= -f2)

if [ -z "$token" ]; then
    echo "sftpwrapper: Couldn't get an MFA token, aborting." >&2
    exit 1
fi

# now craft the wrapper to be used by sftp through -S
cat >"$t" <<'EOF'
#! /usr/bin/env bash
shopt -s nocasematch

REMOTE_USER=${USER:-}
REMOTE_PORT=22
sshcmdline=""

[ "$BASTION_SFTP_DEBUG" = 1 ] && echo "sftphelper: args: $*" >&2
while ! [ "${1:-}" = "--" ] ; do
    # handle several ways of specifying remote user
    if [ "${1:-}" = "-l" ] ; then
        REMOTE_USER="${2:-}"
        shift 2
    elif [[ ${1:-} =~ ^-oUser[=\ ]([^\ ]+)$ ]] ; then
        REMOTE_USER="${BASH_REMATCH[1]}"
        shift
    elif [ "${1:-}" = "-o" ] && [[ ${2:-} =~ ^user=([0-9]+)$ ]] ; then
        REMOTE_USER="${BASH_REMATCH[1]}"
        shift 2

    # handle several ways of specifying remote port
    elif [ "${1:-}" = "-p" ] ; then
        REMOTE_PORT="${2:-22}"
        shift 2
    elif [[ ${1:-} =~ ^-oPort[=\ ]([0-9]+)$ ]] ; then
        REMOTE_PORT="${BASH_REMATCH[1]}"
        shift
    elif [ "${1:-}" = "-o" ] && [[ $2 =~ ^port=([0-9]+)$ ]] ; then
        REMOTE_PORT="${BASH_REMATCH[1]}"
        shift 2

    # other '-oFoo Bar'
    elif [[ ${1:-} =~ ^-o([^\ ]+)\ (.+)$ ]] ; then
        sshcmdline="$sshcmdline -o${BASH_REMATCH[1]}=${BASH_REMATCH[2]}"
        shift

    # don't forward -s
    elif [ "$1" = "-s" ]; then
        shift

    # other stuff passed directly to ssh
    else
        sshcmdline="$sshcmdline $1"
        shift
    fi
done

[ "$BASTION_SFTP_DEBUG" = 1 ] && echo "sftphelper: remaining args: $*" >&2

# sane default
: "${REMOTE_USER:-$USER}"
[ -z "$REMOTE_USER" ] && REMOTE_USER="$(whoami)"

# after '--', remaining args are always host then 'sftp'
REMOTE_HOST="$2"
subsystem="$3"
if [ "$subsystem" != sftp ]; then
    echo "Unknown subsystem requested '$subsystem', expected 'sftp'" >&2
    exit 1
fi

# if host is in the form REMOTE_USER@REMOTE_HOST, split it
if [[ $REMOTE_HOST =~ @ ]]; then
    REMOTE_USER="${REMOTE_HOST%@*}"
    REMOTE_HOST="${REMOTE_HOST#*@}"
fi

# and go
[ "$BASTION_SFTP_DEBUG" = 1 ] && set -x
EOF
echo "exec $BASTION_CMD -T \$sshcmdline $BASTION_SSH_EXTRA_ARGS -- --user \"\$REMOTE_USER\" --port \"\$REMOTE_PORT\" --host \"\$REMOTE_HOST\" --osh sftp --mfa-token $token" >> "$t"
chmod +x "$t"

# don't use exec below, because we need the trap to be executed on exit
export BASTION_SFTP_DEBUG
[ "$BASTION_SFTP_DEBUG" = 1 ] && set -x
sftp $BASTION_SFTP_EXTRA_ARGS -S "$t" sftp://"$REMOTE_USER"@"$REMOTE_HOST":"$REMOTE_PORT""$REMOTE_PATH"
END_OF_SCRIPT

    $script =~ s{%BASTION_CMD%}{$bastionCommand}g;
    $script =~ s{%SELF%}{$self}g;
    $script =~ s{%VERSION%}{$OVH::Bastion::VERSION}g;
    my $compressed = '';
    gzip \$script => \$compressed;
    my $base64 = encode_base64($compressed);
    chomp $base64;
    osh_info <<"EOF";
Description:
    Transfers files to/from a host through the bastion

Usage:
    To use sftp through the bastion, you need a wrapper script to use instead of
calling your sftp client directly. It'll be specific to your account and this bastion,
don't share it with others! To download your customized script, copy/paste this command:
EOF
    print "\necho \"$base64\"|base64 -d|gunzip -c > ~/sftp-via-$bastionName \\\n"
      . "&& chmod +x ~/sftp-via-$bastionName\n\n";

    osh_info <<"EOF";
To use sftp through this bastion, use this script instead of your regular sftp command.
For example:
\$ ~/sftp-via-$bastionName login\@server:port

The following environment variables modify the behavior of the script:
- `BASTION_SFTP_DEBUG`: if set to 1, debug info is printed on the console
- `BASTION_SFTP_EXTRA_ARGS`: if set, the contents of this variable is added
    to the resulting sftp command called by the script
- `BASTION_SSH_EXTRA_ARGS`: if set, the contents of this variable is added
    to the resulting ssh commands used in the script

For example:
\$ BASTION_SFTP_DEBUG=1 BASTION_SSH_EXTRA_ARGS="-v" ~/sftp-via-$bastionName login\@server

Please note that you need to be granted to be allowed to use sftp to the remote host,
in addition to having the right to SSH to it.
For a group, the right should be added with --sftp of the groupAddServer command.
For a personal access, the right should be added with --sftp of the selfAddPersonalAccess command.
EOF
    osh_ok({script => $base64, "content-encoding" => 'base64-gzip'});
    return 0;
}

#
# code
#
my $fnret;

if (not $host) {
    help();
    osh_exit;
}

if (not $ip) {
    # note that the calling-side sftp will not passthrough this exit code, but most probably "1" instead.
    osh_exit 'ERR_HOST_NOT_FOUND', "Sorry, couldn't resolve the host you specified ('$host'), aborting.";
}

$port ||= 22;       # sftp uses 22 if not specified, so we need to test access to that port and not any port (aka undef)
$user ||= $self;    # same for user

$fnret = OVH::Bastion::Plugin::otherProtocol::has_protocol_access(
    account  => $self,
    user     => $user,
    ip       => $ip,
    port     => $port,
    protocol => 'sftp',
);
$fnret or osh_exit($fnret);

my $machine = $fnret->value->{'machine'};
my @keys    = @{$fnret->value->{'keys'} || []};

# now build the command

my @cmd = qw{ ssh -x -oForwardAgent=no -oPermitLocalCommand=no -oClearAllForwardings=yes };
push @cmd, ('-p', $port) if $port;
push @cmd, ('-l', $user) if $user;
push @cmd, '-s';

foreach my $key (@keys) {
    push @cmd, ('-i', $key);
}

push @cmd, "--", $ip, 'sftp';

print STDERR ">>> Hello $self, starting up sftp subsystem to $machine...\n";

$fnret = OVH::Bastion::execute(cmd => \@cmd, expects_stdin => 1, is_binary => 1);
if ($fnret->err ne 'OK') {
    osh_exit 'ERR_TRANSFER_FAILED', "Error launching transfer: $fnret";
}
print STDERR ">>> Done, "
  . $fnret->value->{'bytesnb'}{'stdin'}
  . " bytes uploaded, "
  . $fnret->value->{'bytesnb'}{'stdout'}
  . " bytes downloaded.\n";
if ($fnret->value->{'sysret'} != 0) {
    print STDERR ">>> On bastion side, sftp exited with return code " . $fnret->value->{'sysret'} . ".\n";
}

# don't use osh_exit() to avoid getting a footer
exit OVH::Bastion::EXIT_OK;

