#!/usr/bin/perl -w
#
$myversion = 'ssh-with-2fa  V8.4  12 May 26';
#
# Current comments and instructions are in
# http://www.maths.usyd.edu.au/u/psz/ssh-howto.html
#
# Helper program for ssh connection through Maths firewall: with 2FA
# (skey or TOTP) authentication when coming in (and was through proxy
# when going out).
#
# There is no NEED for this script, but you may still want to use it
# to do things "right" e.g. choose sensible options, use short names
# (e.g. @enna instead of @maths.usyd.edu.au), and avoid typing skeys
# (or passwords) too often.
#
# Boring perl code (and developer comments) only below.
# You probably should not change anything below
# (but please fix, and/or report, any bugs or problems).

##########

# Version history

# ssh-with-2fa  V8.4  12 May 26  Use -x when no X-Windows
# ssh-with-2fa  V8.3  15 Apr 26  Accept -x option
# ssh-with-2fa  V8.2   2 Jan 26  With xpra --headerbar=no
# ssh-with-2fa  V8.1  27 Dec 25  No more Sydnet4 addresses
# ssh-with-2fa  V8.0  29 Oct 25  Handle Sydnet6 addresses
# ssh-with-2fa  V7.7  17 May 25  With xpra attach
# ssh-with-2fa  V7.6  22 Feb 25  Ports in WAIT state are not in use
# ssh-with-2fa  V7.5  15 Nov 24  Check for localhost
# ssh-with-2fa  V7.4  20 Sep 24  With XPRA_FOCUS_RECHECK_DELAY etc
# ssh-with-2fa  V7.3  13 Mar 24  With VcXsrv (or Xming)
# ssh-with-2fa  V7.2   4 Jan 24  Incoming ssk to enna (not dora), POP to rome unused
# ssh-with-2fa  V7.1  21 Jan 23  Handle ssh from dora to rome (or other internal server)
# ssh-with-2fa  V7.0  11 Nov 22  Better multi-user detection
# ssh-with-2fa  V6.9  20 Sep 22  Handle Intune PCs with weird hostname
# ssh-with-2fa  V6.8  13 May 21  Using nxagent instead of xnest
# ssh-with-2fa  V6.7   6 May 21  Using plink with xpra on Windows
# ssh-with-2fa  V6.6  19 Jan 21  With xpra support
# ssh-with-2fa  V6.5  15 Jan 21  With xsess (LDM Xsession) support
# ssh-with-2fa  V6.4  27 Sep 19  Quirks for Windows10 ssh, better Xming options
# ssh-with-2fa  V6.3  31 Jan 19  Better check for Xming location
# ssh-with-2fa  V6.2  21 Aug 18  Use Windows10 ssh if present
# ssh-with-2fa  V6.1  12 Aug 18  Check if Xming is running
# ssh-with-2fa  V6.0   5 Aug 18  Accept rsync or unison as ssh (remote) command
# ssh-with-2fa  V5.7   4 Aug 18  With quiet option (automatic for rsync)
# ssh-with-2fa  V5.6   6 Nov 17  Renamed. Fix netstat on Mac, tidy up code
# ssh-with-skey V5.5  16 Dec 16  With XauthLocation for Mac Sierra
# ssh-with-skey V5.4  30 Nov 15  No more telnet or cconnect
# ssh-with-skey V5.3  23 Nov 15  Make use of existing 12022 forwarding
# ssh-with-skey V5.2  22 Nov 15  Using dynamic forwarding
# ssh-with-skey V5.1  20 Dec 13  Have transparent proxy (tproxy) on siv
# ssh-with-skey V5.0  20 Jul 13  Warn about XQuartz on Macs
# ssh-with-skey V4.8  26 Nov 12  With ForwardX11Timeout=596h on Macs
# ssh-with-skey V4.7  12 Jun 12  With port forwarding for IMAP
# ssh-with-skey V4.6  19 Oct 10  With port forwarding for POP and SMTP
# ssh-with-skey V4.5   4 Jun 10  Use -C for compression
# ssh-with-skey V4.4  23 Feb 10  Better ifconfig IP address detection
# ssh-with-skey V4.3  11 Nov 08  Better IP address detection
# ssh-with-skey V4.2   3 May 08  Better multi-user detection
# ssh-with-skey V4.1  31 Mar 07  Better argument handling for sshout
# ssh-with-skey V4.0  29 Mar 07  Do sshout (scpout sftpout), cconnect also
# ssh-with-skey V3.1  19 Mar 07  Comment out unused code, drop deprecated
# ssh-with-skey V3.0  14 Mar 07  No more skey stuff, all done within sskd
# ssh-with-skey V2.1  18 Jan 07  Handle Mac (darwin) also
# ssh-with-skey V2.0  25 Sep 06  Do outgoing also; do random local port
# ssh-with-skey V1.6  16 Aug 06  With check for Maths machines
# ssh-with-skey V1.5   6 May 06  Better show errors, option for local port
# ssh-with-skey V1.4   5 May 06  Option -k for "no Tk", better defaults
# ssh-with-skey V1.3   4 May 06  Allow command-line skey chat
# ssh-with-skey V1.2   3 May 06  Cygwin compatibility won't work
# ssh-with-skey V1.1  25 Apr 06  Neater defaults, better comments
# ssh-with-skey V1.0  21 Apr 06  Original coding (from pirated parts)

##########

( $cmd = $0 ) =~ s!.*/!!;

$sshname = 'ssh';
foreach (qw(scp sftp sshfs)) {
  $sshname = $_, last if $cmd =~ m/$_/;
}
$cmdname = $sshname;
if ($sshname eq 'ssh') {
     if ($cmd =~ m/pra.*(sess|ldm)/i) { $cmdname = 'xprasess'; $pra = 1; $ses = 1; }
  elsif ($cmd =~ m/xpr.*att/i)        { $cmdname = 'xprattch'; $pra = 1; $att = 1; }
  elsif ($cmd =~ m/xpra/i)            { $cmdname = 'xpraterm'; $pra = 1; }
  elsif ($cmd =~ m/xsess|ldm/i)       { $cmdname = 'xsess'; $ses = 1; }
}


$usage = "
USAGE: Command line options depend on command name used:
  $0  -->  $cmdname
Actions depend also on location of client: internal or external to Maths.\n
ssh [--quiet] [--debug] [ssh_options] [user\@]host[:port] [remcmd]
scp [--quiet] [--debug] [scp_options] file1 file2
sftp [--quiet] [--debug] [sftp_options] [user\@]host[:port]
xsess [--quiet] [--debug] [-full-X-Y-size] [ssh_options] [user\@]host[:port]
xpraterm [--quiet] [--debug] [ssh_options] [user\@]host[:port]
xprasess [--quiet] [--debug] [-full-X-Y-size] [ssh_options] [user\@]host[:port]
xprattch [--quiet] [--debug] [-full-X-Y-size] [ssh_options] [user\@]host[:port]
  --quiet         quiet (no chatty messages)
  --debug         debug (verbose messages)
  cmd_options     any (except user, host or port) options that ssh/putty,
                  scp/pscp, or sftp may take; not checked or sanitized
                  in any way; some options may clash with our usage.
  user            username for authentication.
  host            server host to connect to. For incoming, use any of host
                  names enna, dora, rome, or ssh (or with maths.usyd.edu.au).
  port            server port to connect to, default 22.
  remcmd          remote command for ssh, must be rsync or unison.
  file1 or file2  must be in 'quasi scp syntax' [user\@]host[:port]:path so
                  user, host and port will be taken from last one matched;
                  any number of file arguments are permitted and will be
                  modified, the last two only are checked.
  -full-X-Y-size  nxagent size to be full-screen minus specified pixels.\n
Notes:
  xsess emulates a Y-term terminal login, runs an LDM Xsession (via nxagent).
  xpraterm runs an xterm, xprasess runs an LDM Xsession, via xpra; use
  xprattch to (re-)attach to existing session.
  sftp is supported in interactive mode only, use scp for others.
  sshfs handling is not implemented yet.\n
";


sub debug {
  $debug or return;
  my $t = localtime();
  $t =~ m/(..:..:..)/ and $t = $1;
  print "$t - ", join(' ',@_), "\n";
}

while (@ARGV and defined $ARGV[0] and $ARGV[0]) {
  $debug = 1, shift, next if $ARGV[0] eq '--debug' or $ARGV[0] eq '--verbose';
  $quiet = 1, shift, next if $ARGV[0] eq '--quiet';
  #$noTk = 1,
              shift, next if $ARGV[0] eq '--noTk';	# Ignored, for backwards compatibility only
  last;
}

debug "Date/time: " . localtime();
debug "Version: $myversion ";
debug "PID: $$";
debug "Command and args:", $0, @ARGV;
debug "Doing $cmdname ($sshname)";

print "\nBEWARE: Unrecognized OS $^O\n\n" unless $^O eq 'linux' or $^O eq 'darwin' or $^O eq 'MSWin32';


sub hostbyname {
  return join(".", unpack("C4", scalar gethostbyname($_[0]) || ''));
}
sub hostbyaddr {
  use Socket;
  return (gethostbyaddr( inet_aton( $_[0] ), AF_INET ) || '');
}


@ORIGARGV = @ARGV;

# Half-baked argument parsing: accept rsync or unison as remcmd
@CMDARGV = ();
if ($cmdname eq 'ssh') {
  @TMPARGV = ();
  while (@ARGV) {
    if (defined $ARGV[0] and $ARGV[0]) {
      if ($ARGV[0] =~ m/^(rsync|unison)($| )/) {
        @CMDARGV = @ARGV;
        $quiet = 1;	# Should be quiet if called from rsync (or unison?)
        last;
      }
    }
    push @TMPARGV, shift;
  }
  @ARGV = @TMPARGV;
}

sub go_out
{
  my ( $msg ) = @_;
  debug "Doing $cmdname out (reason=$msg)";
  $msg = "\n$msg\n$usage\n" if $msg;
  $cmdname =~ m/^(ssh|scp|sftp)$/ or die( $msg || "Cannot do out for $cmdname\n" );
  -e "/usr/bin/$sshname" or die( $msg || "There is no /usr/bin/$sshname\n" );
  # With transparent proxy (tproxy), we can just do the "real thing",
  # no need for any ProxyCommand.
  debug "Running $sshname", @ORIGARGV;
  # Windows10 (or its ssh.exe?) is weird: if we use exec then ssh will run
  # "in the background", dissociated from keyboard input. But this is never
  # for Windows, so can use exec
  exec "/usr/bin/$sshname", @ORIGARGV;
  die "Exec $sshname failed ($!)\n";
}

# Doing sshout is trivial, what is difficult is to decide when we need that:
# do if asked, or if behind tsocks or proxychains
$cmdname =~ m/^(ssh|scp|sftp)$/ and
  $cmd =~ m/out$/ and
  go_out('asked to do');
$cmdname =~ m/^(ssh|scp|sftp)$/ and
  ( $ENV{'LD_PRELOAD'} && $ENV{'LD_PRELOAD'} =~ m/lib(tsocks|proxychains)\.so/ ) and
  go_out('behind tsocks or proxychains');


die "$usage\n" unless @ARGV;


##### Find my own IP address, see whether we need skeys
# (in fact needed to determine what command-line options to take)
# Three ways of finding own IP address. Do easiest, fastest first...

# In code below we use constructs like `cmd 2>&1`, so perl would not say
#   Can't exec "cmd": No such file or directory
#   'cmd' is not recognized as an internal or external command, ...
# and do not use `cmd 2>&-` because Windows does not recognize that.
# In fact use (`cmd 2>&-` || '') to avoid "uninitialized value".

undef $cli_addr;
#
$cli_addr or eval {
  if ($^O eq 'MSWin32') {
    @ifconfig = ('ipconfig /all');
  } else {
    @ifconfig = ('/sbin/ifconfig -a', 'ifconfig -a', 'ip a', 'hostname -I');
  }
  foreach $try (@ifconfig) {
    debug "Running $try";
    $x = `$try 2>&1`;
    next unless $x;
    while ($x =~ m/\b(\d+\.\d+\.\d+\.\d+)\b/g) {
      $cli_addr = $1;
      debug "$try shows $cli_addr";
      # Skip things that look like localhost or netmask...
      if (not $cli_addr or $cli_addr =~ m/^(127\.0|255)/ ) {
        undef $cli_addr;
      }
      last if $cli_addr;
    }
    last if $cli_addr;
  }
};
#
not $cli_addr and
  ($^O eq 'linux' or $^O eq 'darwin') and
  die "\n
Cannot find my own IP address: are you connected to a network??!!
Or maybe ... you do not have ifconfig installed on your machine
(and things will not work well without it...).\n
For Ubuntu Linux, please install the net-tools package:
use command
  sudo apt-get install net-tools\n
(I do not know how you get ifconfig on other Linux distros.)
\n\n";
#
$cli_addr or eval {
  # Avoid "Can't locate ... compilation aborted" with eval within eval
  debug "Using Sys::Hostname";
  eval "use Sys::Hostname;";
  $@ and die "No Sys::Hostname on this machine\n";
  debug "Looking up hostname";
  $chost = hostname();
  debug "hostname() returned $chost";
  # Bug with some Macs (e.g. jmg visitor of wm), slow to look up "own" name
  # (but fast for any others), maybe:
  # http://www.macosxhints.com/article.php?story=20021212074234953
  # http://the.taoofmac.com/space/com/Apple/OSX/DNS%20and%20.local
  $cli_addr = hostbyname($chost);
  debug "Hostname $chost shows IP address $cli_addr";
  # Sometimes (often?) hostname shows nothing, or shows localhost
  # Skip things that look like localhost or netmask...
  if (not $cli_addr or $cli_addr =~ m/^(127\.0|255)/ ) {
    undef $cli_addr;
  }
}, ($@ and chomp($@), debug $@);
#
$cli_addr or eval {
  debug "Using Sys::HostIP";
  eval "use Sys::HostIP;";
  $@ and die "No Sys::HostIP on this machine\n";
  debug "Looking up HostIP";
  $cli_addr = Sys::HostIP->ip;
  debug "HostIP shows IP address $cli_addr";
  # Skip things that look like localhost or netmask...
  if (not $cli_addr or $cli_addr =~ m/^(127\.0|255)/ ) {
    undef $cli_addr;
  }
}, ($@ and chomp($@), debug $@);

$cli_addr or $cli_addr = '';
$cli_loc = 'external';
# At the cost of hardcoded IP addresses, do DNS lookups only when necessary,
# as those can be slow, freeze, or fail. This way we also hope to only do
# "internal" DNS, that we expect to succeed and be quick.
if ($cli_addr =~ m/^10\.(254\.1|136\.(126|127)|129\.(245|246|247))\.\d+$/) {	# Our Sydnet6 ranges, includes Magma
  # Other institutions may use same 10.* IP ranges, find the name and check.
  $cli_name = hostbyaddr($cli_addr);
  debug "gethostbyaddr shows name $cli_name";
  # Weird names we may get:
  # MCS machines have non-Maths DNS and default domain, show *.mcs.usyd.edu.au
  # Intune PCs have Maths DNS and default domain, but wrong hostname Wxxx (with Dell service tag), show Wxxx.maths.usyd.edu.au (?!)
  # Would want to check for a maths.usyd name, but because of the weirdness,
  # be satistfied with either usyd or sydney, .
  if ($cli_name =~ m/\.(usyd|sydney)\.edu\.au$/) {
    # Would want to do forward lookup and compare for sanity check,
    # but cannot because of the weirdness of names we got.
    #$x = hostbyname($cli_name);
    #debug "gethostbyname shows name $x";
    #$x eq $cli_addr and $cli_loc = 'internal';
    $cli_loc = 'internal';
  }
#} elsif ($cli_addr =~ m/^129\.78\.(69|94|95|223)\.\d+$/) {	# Old Sydnet4 addresses
#  $cli_loc = 'internal';
#  # Needed later below, set it now
#  $cli_name = hostbyaddr($cli_addr);
#  debug "gethostbyaddr shows name $cli_name";
#} elsif ($cli_addr =~ m/^129\.78\.68\.\d+$/) {			# Old Sydnet4 addresses
#  $cli_loc = 'externalMaths';
}
debug "Client location $cli_loc for IP address $cli_addr";

##### Find options, settings

@TMPARGV = @ARGV;
if ($cmdname eq 'scp') {
  foreach (1..2) {
    $try = pop @TMPARGV || '';
    ( $user, $shost, $sport ) = $try =~ m/^(?:([\w-]+)\@)?(\w[\w\.-]+)(?::(\d+))?:/;
    last if $shost;
  }
  $shost or go_out("No host specification found in last two arguments");
} else {
  $try = pop @TMPARGV || '';
  ( $user, $shost, $sport ) = $try =~ m/^(?:([\w-]+)\@)?(\w[\w\.-]+)(?::(\d+))?$/;
  $shost and pop;	# Only pop on success: do not remove a -X option
  $ses and defined $ARGV[0] and $ARGV[0] and $ARGV[0] =~ m/^-full-(\d+)-(\d+)-size$/ and $nxsize = shift;
  #($ses or $pra) and ( @ARGV or $CMDARGV ) and die "Un-acceptable $cmdname option(s)\n$usage\n";
}

$shost or go_out("No hostname");

if ($shost =~ m/^(pisa|asti|bari|como|dora|enna|foro|rome|ssh|maths|p639.pc)?\.?(maths\.usyd\.edu\.au)?$/) {
  $x = $shost;
  $shost = 'enna' if not ($cli_name and $cli_name =~ m/^(dora|enna|foro)(\.maths\.usyd\.edu\.au)?$/);
  $shost = 'enna' if $ses and not ($shost =~ m/^(dora|enna|foro|p639.pc)(\.maths\.usyd\.edu\.au)?$/);
  $shost = 'ssh.maths.usyd.edu.au' if $cli_loc =~ m/external/;
  $shost eq $x or debug "Server host was $x, going to $shost instead"; 
  $sport = 22;
  $svr_loc = 'internal';
} else {
  $ses and die "$cmdname must go to enna\n";
  $shost or go_out("No hostname");
  $shost =~ m/^\d*$/ and go_out("Bad hostname $shost");

  # No harm in this DNS lookup: will need to succeed anyway.
  $svr_addr = hostbyname($shost);
  $svr_addr or go_out("No IP for host $shost");
  debug "gethostbyname($shost) shows IP $svr_addr";
  $svr_loc = 'external';
  if ($svr_addr =~ m/^10\.(254\.1|136\.(126|127)|129\.(245|246|247))\.\d+$/) {	# Our Sydnet6 ranges, include Magma
    # Other institutions may use same 10.* IP ranges, do similar to $cli_addr
    $svr_name = hostbyaddr($svr_addr);
    debug "gethostbyaddr($svr_addr) shows name $svr_name";
    # Maybe here we can insist on a maths.usyd name?
    if ($svr_name =~ m/\.maths\.usyd\.edu\.au$/) {
      $svr_loc = 'internal';
    }
#  } elsif ($svr_addr =~ m/^129\.78\.(69|94|95|223)\.\d+$/) {	# Old Sydnet4 internal ranges
#    $svr_loc = 'internal';
  } elsif ($svr_addr =~ m/^127\.0/) {
    $svr_loc = $cli_loc;
  } elsif ($svr_addr) {
    $svr_loc = 'external';
  }
}
debug "Server location $svr_loc for host $shost";

$sport ||= 22;


if ($^O eq 'MSWin32') {
  # Windows: guess this is ActivePerl
  unless ($sshcmd) {
    if ($pra) {
      # xpra on Windows10 does NOT work with OpenSSH,
      # see https://github.com/Xpra-org/xpra/issues/3113
      if (-f "C:/Program Files/Xpra/plink.exe") {
        # Have xpra's own plink, xpra can find it without need for PATH
        # Would like options "plink -ssh -agent ..." but those two seem default
        $sshcmd = 'plink';
        debug "Using xpra's own C:/Program Files/Xpra/plink.exe";
      }
    }
    else {
      # Check for ssh things present in Windows10,
      # but beware that xpra does not work with OpenSSH
##### Oddity: Sanjana's Win11 laptop does not find ssh.exe here (though the file is in fact present)??!!
##### Maybe the ssh client needs to be "enabled" on Win11, see:
##### https://geekrewind.com/how-to-install-openssh-client-in-windows-11/
      $x = "C:/WINDOWS/System32/OpenSSH/$sshname.exe";
      if (-f $x) {
        $sshcmd = $x;
        # Work-around for Windows10 oddity, see
        # https://github.com/PowerShell/Win32-OpenSSH/issues/1088
        mkdir '/dev'; open F,'>/dev/tty' and print F "Windows10 ssh oddity, see:\r\nhttps://github.com/PowerShell/Win32-OpenSSH/issues/1088\r\n" and close F;
      }
      elsif ( $sshname =~ m/^(ssh|scp|sftp)$/ and (`ver 2>&1` || '') =~ m/Windows.*Version\s*1\d\b/ ) {
        $quiet or print "\nNo OpenSSH on this PC, Win10 or later?\nYou may want to enable:\nSettings > Apps > Optional Features > OpenSSH Client > Install\nNo matter, will look for putty, instead...\n\n";
      }
    }
  }
  unless ($sshcmd) {
    # Look for putty and friends: they have funny names
    $exe = $sshname;
    if ($sshname eq 'ssh') {
      $exe = 'putty';
      if ($pra or $ses) {
        # For xpra we probably had its own plink, already
        debug "Need plink instead of putty";
        $exe = 'plink';
      }
    } elsif ($sshname eq 'scp') {
      $exe = 'pscp';
    } elsif ($sshname eq 'sftp') {
      $exe = 'psftp';
    } elsif ($sshname eq 'sshfs') {
      die "Sorry no sshfs for Windows - see https://code.google.com/p/win-sshfs/ issues\n";
    }
    # Look for exe: may have it installed in bizarre places...
    # Take care not to find ourselves, though unlikely we would be anything.exe
    foreach $d ( split(/;/,$ENV{PATH}), '.', 'Downloads', glob('*Doc*/Downloads'), 'C:', glob('C:/*'), glob('C:/Program*Files*/*'), glob('C:/WINDOWS*/system*') ) {
      $x = "$d/$exe.exe"; $sshcmd = $x, last if $x and $x ne $0 and -f $x;
    }
    $sshcmd or die "Cannot find $exe anywhere, tried in:\n  PATH=$ENV{PATH}\n  '.' Downloads *Doc*/Downloads\n  C: C:/* C:/Program*Files*/* C:/WINDOWS*/system*\nPlease install $exe somewhere it can be found.\n";
    $sshcmd =~ s:\\:/:g; $sshcmd =~ s://+:/:g;
    if ($pra and ( $d, $x ) = $sshcmd =~ m:^(.*)/([^/]*)$: and $d and $x) {
      # (Unlikely this would be needed, surely have xpra's own plink ...)
      # Avoid fancy quoting: add directory to the PATH, for when directory has
      # funny characters e.g. "Program Files" or "Program Files (x86)"
      $ENV{PATH} =~ m/(^|;)\Q$d\E(;|$)/ or $ENV{PATH} = "$d;$ENV{PATH}";
      $sshcmd = $x;
      debug "Added $d to PATH, for the sake of $x";
    }
  }
} elsif ($^O eq 'cygwin') {
  #print "Perl/Tk does not work with Cygwin, using command-line interface ...\n";
  #$noTk = 1;
  # Cygwin on Windows: we assume ssh in cygwin
  unless ($sshcmd) {
    # Look for ssh.exe (make sure we have it)
    # Take care not to find ourselves, though unlikely we would be anything.exe
    foreach $d ( "/bin", "/usr/bin", split(/:/,$ENV{PATH}) ) {
      $x = "$d/$sshname.exe"; $sshcmd = $x, last if $x and $x ne $0 and -f $x and -x _;
    }
    $sshcmd or die "Cannot find $sshname.exe in /bin /usr/bin or PATH=$ENV{PATH}\nPlease install OpenSSH package.\n";
  }
} elsif ($^O eq 'linux' or $^O eq 'darwin') {
  # Linux (or MacOSX: is that similar enough?): we assume ssh
  unless ($sshcmd) {
    # Look for ssh (make sure we have it)
    # Take care not to find ourselves ...
    foreach $d ( "/usr/bin", split(/:/,$ENV{PATH}) ) {
      next if $d =~ m!^/usr/sms/bin/?$!;
      $x = "$d/$sshname"; $sshcmd = $x, last if $x and $x ne $0 and -f $x and -x _;
    }
    $sshcmd or die "Cannot find $sshname in /usr/bin or PATH=$ENV{PATH}\nPlease install $sshname somewhere it can be found.\n";
  }
} else {
  die "Un-recognized operating system.\nCould handle Windows, Linux, MacOSX or Cygwin,\nbut \$^O shows $^O.\n";
}


##### Type of proxy

$proxy = '';
if ($cli_loc eq 'internal' and $svr_loc eq 'internal') {
  # Fully internal, can go direct
  # Is this a laptop, or a Windows PC in MCS domain, connecting to enna?
  if ($shost eq 'enna' and $sport == 22) {
    # Should not we do this simply by $cli_addr IP address?!
    # But then, how would we know when to have FQDN enna.maths?
    # We looked up $cli_name already. Beware of weird $cli_name we may get, see comments elsewhere.
    if ($cli_name =~ m/^((([a-z][a-z]\w+|p\d+\w+)\.pc|W\w+)\.maths\.usyd\.edu\.au|\w+\.mcs\.usyd\.edu\.au)$/) {
      $shost = 'enna.maths.usyd.edu.au';	# Not plain enna, for the sake of MCS PCs (with non-Maths DNS and default domain)
      # On laptops, we may want IMAP SMTP and CUPS port forwarding
      $proxy = 'laptop-to-enna' if $sshname eq 'ssh';
    }
  }
} elsif ($cli_loc =~ m/external/ and $svr_loc eq 'external') {
  # Fully external, can go direct
} elsif ($cli_loc eq 'externalMaths' and $svr_loc eq 'internal') {
  # Do not require skeys
} elsif ($cli_loc eq 'external' and $svr_loc eq 'internal') {
  # PSz 14 Mar 07 sskd used PAM module, no need for proxy at all,
  # but leave it marked so we can add port forwarding if useful.
  $proxy = 'skey';
} elsif ($cli_loc eq 'internal' and $svr_loc eq 'external') {
  $proxy = 'tproxy';	# Used to be cproxy
} else {
  die "Logic error: cannot figure out proxy type\nwhen going from $cli_loc to $svr_loc.\n";
}
debug "Proxy type is " . ($proxy || "(empty)");


##### Build command line: original options, and ours

sub sw_vers_since	# Helper for Mac: we want '10.10' ge '10.7'
{
  my ($o) = @_;
  my $n = `sw_vers -productVersion 2>&1` || '';
  $n =~ s/(\d+)/sprintf "%04d",$1/eg;
  $o =~ s/(\d+)/sprintf "%04d",$1/eg;
  return $n ge $o;
}


$localhost = 'localhost';
# Some machines may not know about localhost (?!):
# I think I have seen some MCS Win10 and some Mac like that...
hostbyname('localhost') eq '127.0.0.1' or
  $localhost = '127.0.0.1', debug "Using $localhost instead of localhost";


if ($sshname eq 'ssh') {
  unless ($quiet) {
    if ($^O eq 'linux' or $^O eq 'darwin') {
      $x = getppid();
      $x = `ps -p $x 2>&1` || '';
      ## Should be quiet if called from rsync (or unison?)
      ## Unclear whether expect can handle long "Using command ..." lines.
      #$x =~ m/rsync|unison|expect/ and $quiet = 1;
      $x =~ m/rsync|unison/ and $quiet = 1;
      ## Should be quiet if called from anything but a shell
      #$x =~ m/\d -?\w{0,4}sh\r?\n?$/ or $quiet = 1;
    }
  }
  unless ( $pra or $ENV{DISPLAY} ) {
    # Seems no X-windows available
    if ( $^O eq 'MSWin32' ) {
      # Do not seem to get a DISPLAY, not even with VcXsrv or Xming running...
      unless ( (`tasklist 2>&1` || '') =~ m/Xming|vcxsrv/ ) {
        foreach $d ( split(/;/,$ENV{PATH}), '.', 'Downloads', glob('*Doc*/Downloads'), 'C:', glob('C:/*'), glob('C:/Program*Files*/*'), glob('C:/WINDOWS*/system*') ) {
          $x = "$d/vcxsrv.exe"; $xming = $x, last if $x and -f $x;
          $x = "$d/VcXsrv/vcxsrv.exe"; $xming = $x, last if $x and -f $x;
          $x = "$d/Xming.exe"; $xming = $x, last if $x and -f $x;
          $x = "$d/Xming/Xming.exe"; $xming = $x, last if $x and -f $x;
        }
        if ( $xming ) {
          $xming =~ s:\\:/:g; $xming =~ s://+:/:g;
          $quiet or print "Starting $xming (iconized) ...\n";
          system("start \"\" /MIN \"$xming\" -clipboard -multiwindow");
          sleep 2;	# Give it time to start (maybe no need, login might take longer)
        } else {
          $quiet or print "\nNo X-windows, should install VcXsrv from\nhttps://github.com/marchaesen/vcxsrv/releases\n\n";
          #$quiet or print "\nNo X-windows, should install Xming and fonts from\nhttp://sourceforge.net/projects/xming/files/\n\n";
          $ses and not $pra and die "$cmdname needs X-windows\n";
          unshift @ARGV,'-x' unless grep m/^-[xXY]$/, @ARGV;
        }
      }
      if ($sshcmd =~ m/WINDOWS.*OpenSSH/ ) {
        # Work-around for Windows10 oddities, see
        # https://unix.stackexchange.com/questions/207365/x-flag-x11-forwarding-does-not-appear-to-work-in-windows/
        # Setting things in the hope we have VcXsrv or Xming, harmless(?) if not
        print "Doing set DISPLAY=$localhost:0\n";
        $ENV{DISPLAY} = "$localhost:0";
        unshift @ARGV,'-Y' unless grep m/^-[xXY]$/, @ARGV;
      }
    } elsif ( $^O eq 'darwin' and sw_vers_since('10.8') ) {
      $quiet or print "\nNo X-windows, should install XQuartz from http://www.xquartz.org\n\n";
      $ses and not $pra and die "$cmdname needs X-windows\n";
      unshift @ARGV,'-x' unless grep m/^-[xXY]$/, @ARGV;
    } else {
      $quiet or print "\nNo X-windows??\n\n";
      $ses and not $pra and die "$cmdname needs X-windows\n";
      unshift @ARGV,'-x' unless grep m/^-[xXY]$/, @ARGV;
    }
  }
  $sshopt = '';
  $sshopt .= " " . join(' ', @ARGV) if @ARGV;
  $sshopt .= " -X" unless $pra or $sshopt =~ m/ -[xXY]( |$)/;	### Do we need -Y for cygwin?
  $sshopt .= " -C" unless $pra or $sshopt =~ m/ -C( |$)/;
  $sshopt .= " -oForwardX11Timeout=596h" if $^O eq 'darwin' and sw_vers_since('10.7');	# Mac bug/feature, see RT#5533 RT#5655
	# The ForwardX11Timeout option is only available from MacOSX 10.7,
	# causes ssh to fail on 10.6.* or earlier, compare:
	#   https://developer.apple.com/library/mac/#documentation/Darwin/Reference/ManPages/10.6/man5/ssh_config.5.html
	#   https://developer.apple.com/library/mac/#documentation/Darwin/Reference/ManPages/10.7/man5/ssh_config.5.html
	#   https://developer.apple.com/legacy/library/documentation/Darwin/Reference/ManPages/man5/ssh_config.5.html
	# The ForwardX11Timeout option is needed still at MacOSX 10.13.4 .
  $sshopt .= " -oXauthLocation=/opt/X11/bin/xauth" if $^O eq 'darwin' and -f '/opt/X11/bin/xauth';	# Mac bug, see e.g. http://stackoverflow.com/questions/39622173/cant-run-ssh-x-on-macos-sierra
	# The XauthLocation option needed since MacOSX 10.12,
	# may not be needed at MacOSX 10.13.4,
	# but is needed again at MacOSX 10.13.6 and later.
  # Does not seem necessary, not even on hotel (wireless?) networks
  #$sshopt .= " -oServerAliveInterval=10" if $proxy eq 'skey' and $sshcmd !~ m/putty|plink/;
  if ( $proxy eq 'skey' or $proxy eq 'laptop-to-enna' ) {
    if ( not multiuser() ) {
      # For skey: do port forwarding for scp so as not to need skeys each time,
      # and for IMAP SMTP CUPS.
      # For laptop: do port forwarding for IMAP SMTP CUPS,
      # others for symmetry.
      # The localhost in next line is looked up on enna, so can use literally (no need for $localhost)
      # No POP to rome anymore (was unused anyway)
      #$sshopt .= " -D 13080 -L 12022:localhost:22 -L 12143:enna:143 -L 12110:rome:110 -L 12025:rome:25 -L 12631:siv:631";
      $sshopt .= " -D 13080 -L 12022:localhost:22 -L 12143:enna:143 -L 12025:rome:25 -L 12631:siv:631";
    } else {
      # Hmm... see comment in have_12022():
      #   https://bugzilla.mindrot.org/show_bug.cgi?id=3802
      #   or maybe use UNIX sockets (not IP ports) on such machines?
      debug "No port forwarding on multi-user machine";
    }
  }

  unless ($pra) {
    $sshopt .= " -l $user" if $user;
    if ($sshcmd =~ m/putty|plink/) {
      $sshopt .= " -P PORT-GOES-HERE";
    } else {
      $sshopt .= " -p PORT-GOES-HERE";
    }
    $sshopt .= " HOST-GOES-HERE";
  }

  if ($ses) {
    # Use "nxagent -run Xsession"
    not $nxsize and $^O eq 'MSWin32' and $nxsize = '-full-0-72-size';
    not $nxsize and $^O eq 'darwin' and $nxsize = '-full-0-106-size';
    not $nxsize and $^O eq 'linux' and $nxsize = '-full-72-80-size';	# For most Ubuntu layouts
    not $pra and $^O eq 'darwin' and print "\n\n    BEWARE\n    XQuartz (and your session) may quit and die ...\n    (bug in XQuartz ??!)\n\n";
    @CMDARGV = split(' ',"/usr/sms/bin/nxagent $nxsize -run /usr/sms/share/ldm/Xsession");
    # Might like to use "ssh -M ..." etc as LDM does. But would not work:
    #  - Windows ssh does NOT support the -M (ControlMaster) option, fails
    #    with message "getsockname failed: Not a socket", see
    #      https://github.com/PowerShell/Win32-OpenSSH/issues/405
    #      https://github.com/PowerShell/Win32-OpenSSH/issues/1328
    #    noting possible workaround with "ssh -O proxy" in
    #      https://github.com/PowerShell/Win32-OpenSSH/issues/405#issuecomment-1481385347
    #  - On Mac have too many window managers:
    #     - gnome - quits instantly (another window manager? no acceleration?)
    #     - fluxbox - complains about another window manager, and quits
    #     - xfce - works almost (but cannot type in terminal window?)
    #     - SMSsession - session manager window cannot be made visible
    #  - On Linux also have too many window managers, anyway probably
    #    un-wanted as "panel"s would be on top of (overwriting) each other.
    # This would be for "ssh -M ...":
    ## We really would like to have:
    ##$ldmsock = ( $ENV{HOME} || $ENV{USERPROFILE} ) . '/.ldmsock';
    ##$ldmsock =~ s:\\:/:g;
    ## but HOME or USERPROFILE may have blanks in them...
    #if ($^O eq 'MSWin32') {
    #  chdir $ENV{USERPROFILE};
    #  $debug and $x = `cd`, chomp $x;
    #  debug "Changed directory to USERPROFILE = $x";
    #} else {
    #  chdir;	# to HOME
    #  $debug and $x = `pwd`, chomp $x;
    #  debug "Changed directory to HOME = $x";
    #}
    #$ldmsock = '.ldmsock';
    ##unlink $ldmsock;
    #-e $ldmsock and die "Object $ldmsock exists, please remove\n";
    #$sshopt .= " -T -M -S $ldmsock -o ControlMaster=yes -o ControlPersist=20 -o ConnectTimeout=5";
    #@CMDARGV = qw(echo LDM-is-OK);
  }

} elsif ($sshname eq 'scp') {
  $sshopt = " -P PORT-GOES-HERE";
  $sshopt .= " " . join(' ', @ARGV);
  $sshopt =~ s/ (?:([\w-]+)\@)?(\w[\w\.-]+)(?::(\d+))?:/ HOST-GOES-HERE:/g;
  $sshopt =~ s/HOST-GOES-HERE/$user\@HOST-GOES-HERE/g if $user;
} elsif ($sshname eq 'sftp') {
  $sshopt = " -P PORT-GOES-HERE";
  $sshopt .= " " . join(' ', @ARGV) if @ARGV;
  $sshopt .= " HOST-GOES-HERE";
  $sshopt =~ s/HOST-GOES-HERE/$user\@HOST-GOES-HERE/g if $user;
} elsif ($sshname eq 'sshfs') {
  die "Sorry, sshfs handling not implemented yet\n";
} else {
  die "Logic error: un-recognized command type $sshname.\n";
}


sub multiuser
{
  # Check whether this machine is multi-user

  # Efficiency: check cached result
  defined $multiuser and return $multiuser;
  $multiuser = 0;
  if ($^O eq 'linux' or $^O eq 'darwin') {
    # Use FIRST_UID or LAST_SYSTEM_UID?
    ( $y ) = ( (`grep FIRST_UID= /etc/adduser.conf 2>&1` || '') =~ m/^#?\w+=(\d+)/m );
    not $y and $^O eq 'darwin' and $y = 500;
    $y ||= 1000;
    ( $z ) = ( (`grep LAST_UID= /etc/adduser.conf 2>&1` || '') =~ m/^#?\w+=(\d+)/m );
    $z ||= 65533;
    # Primitive count of passwd file lines
    $x = 0; while ( @x = getpwent() ) { $x++ if $x[2] >= $y and $x[2] <= $z; }
    if ( $x > 2 ) {
      debug "Seems multi-user machine, $x user accounts";
      $multiuser = 1;
    } else {
      debug "Seems single-user machine, just $x user accounts";
    }
  }
  return $multiuser;
}


sub have_12022
{
  # Check whether we have port forwarding of 12022 already
  # Check for local port 12022 listening

  # Efficiency: check cached result
  defined $have_12022 and return $have_12022;
  $have_12022 = 0;

  # Not on multi-user machines:
  # port 12022 could belong to someone else;
  # we would not set up such forwardings anyway.
  # Hmm... see also:
  # https://bugzilla.mindrot.org/show_bug.cgi?id=3802
  # or maybe use UNIX sockets (not IP ports) on such machines?
  debug "No attempt to use port 12022 on multi-user machine" if multiuser();
  return $have_12022 if multiuser();

  # On linux, netstat is deprecated... use "ss -ae" instead?
  debug "checking netstat for port 12022 ...";
  $netstat = `netstat -an 2>&1` || '';
  unless ( $netstat and $netstat =~ m/(tcp|udp)/i ) {
    # Some Linux machines may not have netstat installed...
    debug "netstat shows nothing" . ( $netstat ? ": $netstat" : '' );
    undef $netstat;
  }
  if ( $netstat ) {
    $netstat =~ m/^\s*(tcp|TCP)4?\s[\s\d\.]+[:\.]12022\s+(0\.0\.0\.0|\*)[:\.](0|\*)\s+LISTEN/m or return $have_12022;
    debug "netstat shows 12022 LISTEN";
  }
  # Should check "owner" of that listening 12022 socket, maybe like identd.
  # On Linux, maybe use "netstat -ane" to show UID also. What about Mac or Windows?
  # Maybe enough to check for a running skey-ed ssh
  if ($^O eq 'linux' or $^O eq 'darwin') {
    $U = $ENV{USER} || getpwuid($<);
    length($U) > 8 and substr($U,7)='\S+';	# Many ps show abcdefg+ instead of abcdefghijk
    debug "checking ps for $U ssh ...";
    (`ps -Af 2>&1` || '') =~ m/^( *$<|$U) (.*[\/ ])?ssh (.* )?-L 12022:localhost:22 /m or return $have_12022;
    debug "ps shows 12022 is ours";
  }
  elsif ( $^O eq 'MSWin32' ) {
    # Somewhat hopeless: -L would not show if we used putty with GUI (but that is not "ours" anyway).
    # tasklist does not show command arguments
    # NOTE: "WMIC is deprecated as of Windows 10 ... superseded by Windows PowerShell for WMI".
    # No simple way of showing username, maybe
    #   wmic process where "name='ssh.exe'" call GetOwner
    # could be used? (To compare with "use Win32; my $username = Win32::LoginName;".)
    # But then, wmic shows commandline for "our" processes only (unless we are privileged).
    (`wmic process get commandline 2>&1` || '') =~ m/(^| |\/|\\)(ssh|putty)(\.exe)? (.* )?-L 12022:localhost:22 /m or return $have_12022;
    debug "wmic shows 12022 is ours";
  }
  $have_12022 = 1;
  return $have_12022;
}


$ses and have_12022() and die "Cannot $cmdname with another session in progress\n";

if ($proxy eq 'skey' and
    $sshname =~ m/^(ssh|scp|sftp)$/ and
    have_12022()) {
  $quiet or print "Saving skeys, using existing port forwarding\n";
  $shost = $localhost;
  $sport = 12022;
  if ($sshcmd !~ m/putty|plink|pscp|psftp/) {
    $sshopt =~ s/^/ -oNoHostAuthenticationForLocalhost=yes/;
  } else {
    # There is no equivalent option for putty and friends
    $quiet or print "\nMay get complaint about mismatched RSA key, we connect to 'localhost'.\n\n";
  }
  # Remove port forwardings that we may have added
  foreach $x (13080, 12022, 12143, 12025, 12631) {
    $sshopt =~ s/\s-[DL]\s+$x(:\S*)?\s/ /;
  }
} else {
  # Check that all forwardings can be done, no port in use,
  # ignoring states of TIME_WAIT and similar. (What about CLOSED?)
  # Surely not us doing previously: would have used localhost:12022 above...
  if ($sshname eq 'ssh' and $sshopt =~ m/\s-[DL]\s/) {
    $netstat or $netstat = `netstat -an 2>&1` || '';
    if ($netstat) {
      foreach ($sshopt =~ m/\s-[DL]\s+(\d+)[:\s]/g) {
        if ($netstat =~ m/^\s*(tcp|TCP)4?\s[\s\d\.]+[:\.]$_\s+[\d\.\*]+[:\.][\d\*]+\s(?!\s*\w*WAIT)/m) {
          $quiet or print "Port $_ in use, removing forward\n";
          $sshopt =~ s/\s-[DL]\s+$_(:\S*)?\s/ /;
        }
      }
    }
  }
  # No proxies anymore: skeys do not need, have tproxy not cproxy.
  # Do the "real thing" directly.
}
$sshopt =~ s/^ *//;
$sshopt =~ s/HOST-GOES-HERE/$shost/g;
$sshopt =~ s/PORT-GOES-HERE/$sport/g;


if ($pra) {
  # Find xpra binary
  if ($^O eq 'MSWin32') {
    foreach $d ( 'C:/Program Files/Xpra', split(/;/,$ENV{PATH}), '.', 'Downloads', glob('*Doc*/Downloads'), 'C:', glob('C:/*'), glob('C:/Program*Files*/*'), glob('C:/WINDOWS*/system*') ) {
      $x = "$d/Xpra.exe"; $xprcmd = $x, last if $x and $x ne $0 and -f $x;
    }
  } elsif ($^O eq 'linux' or $^O eq 'darwin') {
    foreach $d ( "/bin", "/usr/bin", "/usr/local/bin", split(/:/,$ENV{PATH}) ) {
      $x = "$d/xpra"; $xprcmd = $x, last if $x and $x ne $0 and -f $x and -x _;
    }
  }
  $xprcmd or die "Cannot find xpra commmand\nYou may need to install from https://www.xpra.org/\n";
  $xprcmd =~ s:\\:/:g; $xprcmd =~ s://+:/:g;

  # Set configs similar to /usr/sms/bin/xpra-with-conf
  # (wonder whether configs are needed on client or server end).

  # Environment
  # See https://xpra.org/trac/wiki/Usage/EnvironmentOptions (gone?!)
  $ENV{XPRA_PING_TIMEOUT} = 1200;	# Was 300
  # Seems default anyway
  #$ENV{XPRA_OPENGL_DOUBLE_BUFFERED} = 1;
  # See https://github.com/Xpra-org/xpra/issues/2090
  # No longer present, or changed into BATCH_ALWAYS?
  #$ENV{XPRA_FORCE_BATCH} = 1;
  $ENV{XPRA_BATCH_ALWAYS} = 1;
  #$ENV{XPRA_BATCH_MIN_DELAY} = 10;
  #$ENV{XPRA_BATCH_MAX_DELAY} = 50;
  # No longer present?
  ## See https://github.com/Xpra-org/xpra/issues/877 https://github.com/Xpra-org/xpra/issues/891
  #$ENV{XPRA_CLIPBOARD_LIMIT} = 100;
  # See https://github.com/Xpra-org/xpra/issues/4354
  $ENV{XPRA_FOCUS_RECHECK_DELAY} = 15;
  # I wonder what these are for, and whether they might help...
  #$ENV{XPRA_PAINT_DELAY} = 95;
  #$ENV{XPRA_SHOW_DELAY} = 95;
  #$ENV{XPRA_REPAINT_DELAY} = 95;

  # See
  #   https://xpra.org/trac/wiki/Configuration
  # for info on file location. Location seems too variable on Windows,
  # so not doing there (hopefully does not matter?).

  $ENV{HOME} and -d $ENV{HOME} and mkdir "$ENV{HOME}/.xpra", 0700;
  if ($ENV{HOME} and -d "$ENV{HOME}/.xpra") {
    if (open F, "< $ENV{HOME}/.xpra/xpra.conf") {
      $now = join('', <F>);
      close F;
      $add = '';
    } else {
      $add = "# xpra user configuration file, see\n# https://xpra.org/manual.html\n";
    }
    $now ||= '';
    foreach ( qw(mdns webcam speaker pulseaudio printing) ) {
      $now =~ m/^\s*$_\s*=/m and next;
      $exp or $exp = 1, $add .= "\n# Added by $0 on " . localtime() . "\n";
      $add .= "$_ = no\n";
    }
    $add and open F, ">> $ENV{HOME}/.xpra/xpra.conf" and print(F $add), close F;
  }

  # Construct command line

  @XPRARGV = ('start'); $att and @XPRARGV = ('attach');
  $x = 'ssh://'; $user and $x .= "$user\@"; $x .= $shost; $sport and $sport != 22 and $x .= ":$sport";
  push @XPRARGV, $x;
  push @XPRARGV, "--ssh=$sshcmd $sshopt" if $sshopt;
  push @XPRARGV, "--remote-xpra=/usr/sms/bin/xpra-with-conf";
  # xpra options
  #   ... --no-speaker --no-printing ...
  # seem to work well, but the options
  #   ... --mdns=no --webcam=no ...
  # seem ineffective (harmless?).
  push @XPRARGV, qw( --no-speaker --no-printing --mdns=no --webcam=no );
  push @XPRARGV, '--exit-with-children=yes';
  push @XPRARGV, '--headerbar=no';
  unless ( $att ) {
    # We may already have @CMDARGV set for $ses
    @CMDARGV and unshift @CMDARGV, '/usr/sms/bin/xpra-is-up';
    @CMDARGV or @CMDARGV = qw( xterm -ls -fn 9x15 );
    push @XPRARGV, '--start-child='.join(' ',@CMDARGV);
  }

  $showcmd = join(' ', $xprcmd, @XPRARGV);
  $proxy eq 'tproxy' or $quiet or print "Using command:\n\n$showcmd\n\n";
  debug "Running $showcmd";
  # Windows10 (or its ssh.exe?) is weird: if we use exec then ssh will run
  # "in the background", dissociated from keyboard input.
  system $xprcmd, @XPRARGV;
  exit;
}

$showcmd = join(' ', $sshcmd, $sshopt, @CMDARGV);
$proxy eq 'tproxy' or $quiet or print "Using command:\n\n$showcmd\n\n";
debug "Running $showcmd";
# Windows10 (or its ssh.exe?) is weird: if we use exec then ssh will run
# "in the background", dissociated from keyboard input.
# (Also for xsess, if it used "ssh -M", we would want system, not exec.)
#exec $sshcmd, split(' ',$sshopt), @CMDARGV;
#die "Exec of sshcmd failed ($!)\n";
system $sshcmd, split(' ',$sshopt), @CMDARGV;
exit;

# This would be for "ssh -M ...":
#if ($ses) {
#  -e $ldmsock or die "Failed login? No $ldmsock object\n";
#  debug "Running $sshcmd -X -t -S $ldmsock $shost /usr/sms/share/ldm/Xsession";
#  system "$sshcmd -X -t -S $ldmsock $shost /usr/sms/share/ldm/Xsession";
#  # Do not bother with
#  #$resp = `$sshcmd -X -S $ldmsock $shost /usr/sms/share/ldm/ldm-checks 2>&1`;
#  debug "Running $sshcmd -S $ldmsock -O exit $shost";
#  system "$sshcmd -S $ldmsock -O exit $shost";
#  unlink $ldmsock;
#}

#!#
