#!/usr/bin/env perl

#   Mosh: the mobile shell
#   Copyright 2012 Keith Winstein
#
#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
#   In addition, as a special exception, the copyright holders give
#   permission to link the code of portions of this program with the
#   OpenSSL library under certain conditions as described in each
#   individual source file, and distribute linked combinations including
#   the two.
#
#   You must obey the GNU General Public License in all respects for all
#   of the code used other than OpenSSL. If you modify file(s) with this
#   exception, you may extend this exception to your version of the
#   file(s), but you are not obligated to do so. If you do not wish to do
#   so, delete this exception statement from your version. If you delete
#   this exception statement from all source files in the program, then
#   also delete it here.

my $MOSH_VERSION = '1.2.2';

use warnings;
use strict;
use Socket;
use IO::Pty;
use Getopt::Long;

$|=1;

my $client = 'mosh-client';
my $server = 'mosh-server';

my $predict = undef;

my $port_request = undef;

my $ssh = 'ssh';

my $help = undef;
my $version = undef;

my @cmdline = @ARGV;

my $usage =
qq{Usage: $0 [options] [--] [user@]host [command...]
        --client=PATH        mosh client on local machine
                                (default: "mosh-client")
        --server=COMMAND     mosh server on remote machine
                                (default: "mosh-server")

        --predict=adaptive      local echo for slower links [default]
-a      --predict=always        use local echo even on fast links
-n      --predict=never         never use local echo
        --predict=experimental  aggressively echo even when incorrect

-p NUM  --port=NUM           server-side UDP port

        --ssh=COMMAND        ssh command to run when setting up session
                                (example: "ssh -p 2222")
                                (default: "ssh")

        --help               this message
        --version            version and copyright information

Please report bugs to mosh-devel\@mit.edu.
Mosh home page: http://mosh.mit.edu\n};

my $version_message = qq{mosh $MOSH_VERSION
Copyright 2012 Keith Winstein <mosh-devel\@mit.edu>
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.\n};

sub predict_check {
  my ( $predict, $env_set ) = @_;

  if ( not exists { adaptive => 0, always => 0,
		    never => 0, experimental => 0 }->{ $predict } ) {
    my $explanation = $env_set ? " (MOSH_PREDICTION_DISPLAY in environment)" : "";
    print STDERR qq{$0: Unknown mode \"$predict\"$explanation.\n\n};

    die $usage;
  }
}

GetOptions( 'client=s' => \$client,
	    'server=s' => \$server,
	    'predict=s' => \$predict,
	    'port=i' => \$port_request,
	    'a' => sub { $predict = 'always' },
	    'n' => sub { $predict = 'never' },
	    'p=i' => \$port_request,
	    'ssh=s' => \$ssh,
	    'help' => \$help,
	    'version' => \$version,
	    'fake-proxy!' => \my $fake_proxy ) or die $usage;

die $usage if ( defined $help );
die $version_message if ( defined $version );

if ( defined $predict ) {
  predict_check( $predict, 0 );
} elsif ( defined $ENV{ 'MOSH_PREDICTION_DISPLAY' } ) {
  $predict = $ENV{ 'MOSH_PREDICTION_DISPLAY' };
  predict_check( $predict, 1 );
} else {
  $predict = 'adaptive';
  predict_check( $predict, 0 );
}

if ( defined $port_request ) {
  if ( $port_request =~ m{^[0-9]+$}
       and $port_request >= 0
       and $port_request <= 65535 ) {
    # good port
  } else {
    die "$0: Server-side port ($port_request) must be within valid range [0..65535].\n";
  }
}

delete $ENV{ 'MOSH_PREDICTION_DISPLAY' };

if ( defined $fake_proxy ) {
  use Errno qw(EINTR);
  use IO::Socket::INET;
  use POSIX qw(_exit);

  my ( $host, $port ) = @ARGV;

  # Resolve hostname
  my $packed_ip = gethostbyname $host;
  if ( not defined $packed_ip ) {
    die "$0: Could not resolve hostname $host\n";
  }
  my $ip = inet_ntoa $packed_ip;

  print STDERR "MOSH IP $ip\n";

  # Act like netcat
  my $sock = IO::Socket::INET->new( PeerAddr => $ip,
				    PeerPort => $port,
				    Proto => "tcp" )
    or die "$0: connect to host $ip port $port: $!\n";
  binmode($sock);
  binmode(STDIN);
  binmode(STDOUT);

  sub cat {
    my ( $from, $to ) = @_;
    while ( my $n = $from->sysread( my $buf, 4096 ) ) {
      next if ( $n == -1 && $! == EINTR );
      $n >= 0 or last;
      $to->write( $buf ) or last;
    }
  }

  defined( my $pid = fork ) or die "$0: fork: $!\n";
  if ( $pid == 0 ) {
    cat $sock, \*STDOUT; $sock->shutdown( 0 );
    _exit 0;
  }
  $SIG{ 'HUP' } = 'IGNORE';
  cat \*STDIN, $sock; $sock->shutdown( 1 );
  waitpid $pid, 0;
  exit;
}

if ( scalar @ARGV < 1 ) {
  die $usage;
}

my $userhost = shift;
my @command = @ARGV;

# Run SSH and read password
my $pty = new IO::Pty;
my $pty_slave = $pty->slave;

$pty_slave->clone_winsize_from( \*STDIN );

# Count colors
open COLORCOUNT, '-|', $client, ('-c') or die "Can't count colors: $!\n";
my $colors = "";
{
  local $/ = undef;
  $colors = <COLORCOUNT>;
}
close COLORCOUNT or die;

chomp $colors;

if ( (not defined $colors)
    or $colors !~ m{^[0-9]+$}
    or $colors < 0 ) {
  $colors = 0;
}

my $pid = fork;
die "$0: fork: $!\n" unless ( defined $pid );
if ( $pid == 0 ) { # child
  $pty->close_slave();
  $pty->set_raw();
  open STDOUT, ">&", $pty or die;
  open STDERR, ">&", $pty or die;

  my @server = ( 'new', '-s' );

  push @server, ( '-c', $colors );

  if ( defined $port_request ) {
    push @server, ( '-p', $port_request );
  }

  for ( &locale_vars ) {
    push @server, ( '-l', $_ );
  }

  if ( scalar @command > 0 ) {
    push @server, '--', @command;
  }

  my $quoted_self = shell_quote( $0 );
  exec "$ssh " . shell_quote( '-S', 'none', '-o', "ProxyCommand=$quoted_self --fake-proxy -- %h %p", '-t', $userhost, '--', "$server " . shell_quote( @server ) );
  die "Cannot exec ssh: $!\n";
} else { # server
  my ( $ip, $port, $key );
  close $pty;
  LINE: while ( <$pty_slave> ) {
    chomp;
    if ( m{^MOSH IP } ) {
      if ( defined $ip ) {
	die "$0 error: detected attempt to redefine MOSH IP.\n";
      }
      ( $ip ) = m{^MOSH IP (\S+)\s*$} or die "Bad MOSH IP string: $_\n";
    } elsif ( m{^MOSH CONNECT } ) {
      if ( ( $port, $key ) = m{^MOSH CONNECT (\d+?) ([A-Za-z0-9/+]{22})\s*$} ) {
	last LINE;
      } else {
	die "Bad MOSH CONNECT string: $_\n";
      }
    } else {
      print "$_\n";
    }
  }
  waitpid $pid, 0;

  if ( not defined $ip ) {
      die "$0: Did not find remote IP address (is SSH ProxyCommand disabled?).\n";
  }

  if ( not defined $key or not defined $port ) {
    die "$0: Did not find mosh server startup message.\n";
  }

  # Now start real mosh client
  $ENV{ 'MOSH_KEY' } = $key;
  $ENV{ 'MOSH_PREDICTION_DISPLAY' } = $predict;
  exec {$client} ("$client @cmdline |", $ip, $port);
}

sub shell_quote { join ' ', map {(my $a = $_) =~ s/'/'\\''/g; "'$a'"} @_ }

sub locale_vars {
  my @names = qw[LANG LANGUAGE LC_CTYPE LC_NUMERIC LC_TIME LC_COLLATE LC_MONETARY LC_MESSAGES LC_PAPER LC_NAME LC_ADDRESS LC_TELEPHONE LC_MEASUREMENT LC_IDENTIFICATION LC_ALL];

  my @assignments;

  for ( @names ) {
    if ( defined $ENV{ $_ } ) {
      push @assignments, $_ . q{=} . $ENV{ $_ };
    }
  }

  return @assignments;
}