1#!/usr/bin/env perl 2 3# Mosh: the mobile shell 4# Copyright 2012 Keith Winstein 5# 6# This program is free software: you can redistribute it and/or modify 7# it under the terms of the GNU General Public License as published by 8# the Free Software Foundation, either version 3 of the License, or 9# (at your option) any later version. 10# 11# This program is distributed in the hope that it will be useful, 12# but WITHOUT ANY WARRANTY; without even the implied warranty of 13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14# GNU General Public License for more details. 15# 16# You should have received a copy of the GNU General Public License 17# along with this program. If not, see <http://www.gnu.org/licenses/>. 18# 19# In addition, as a special exception, the copyright holders give 20# permission to link the code of portions of this program with the 21# OpenSSL library under certain conditions as described in each 22# individual source file, and distribute linked combinations including 23# the two. 24# 25# You must obey the GNU General Public License in all respects for all 26# of the code used other than OpenSSL. If you modify file(s) with this 27# exception, you may extend this exception to your version of the 28# file(s), but you are not obligated to do so. If you do not wish to do 29# so, delete this exception statement from your version. If you delete 30# this exception statement from all source files in the program, then 31# also delete it here. 32 33use 5.8.8; 34 35use warnings; 36use strict; 37use Getopt::Long; 38use IO::Socket; 39use Text::ParseWords; 40use Socket qw(IPPROTO_TCP); 41use Errno qw(EINTR); 42use POSIX qw(_exit); 43 44BEGIN { 45 my @gai_reqs = qw( getaddrinfo getnameinfo AI_CANONNAME AI_NUMERICHOST NI_NUMERICHOST ); 46 eval { Socket->import( @gai_reqs ); 1; } 47 || eval { require Socket::GetAddrInfo; Socket::GetAddrInfo->import( ':newapi', @gai_reqs ); 1; } 48 || eval { Socket::GetAddrInfo->import( '0.22', @gai_reqs ); 1; } 49 || die "$0 error: requires Perl 5.14 or Socket::GetAddrInfo.\n"; 50} 51 52my $have_ipv6 = eval { 53 require IO::Socket::IP; 54 IO::Socket::IP->import('-register'); 55 1; 56} || eval { 57 require IO::Socket::INET6; 58 1; 59}; 60 61$|=1; 62 63my $client = 'mosh-client'; 64my $server = 'mosh-server'; 65 66my $predict = undef; 67 68my $bind_ip = undef; 69 70my $use_remote_ip = 'proxy'; 71 72my $family = 'prefer-inet'; 73my $port_request = undef; 74 75my @ssh = ('ssh'); 76 77my $term_init = 1; 78 79my $localhost = undef; 80 81my $ssh_pty = 1; 82 83my $help = undef; 84my $version = undef; 85 86my @cmdline = @ARGV; 87 88my $usage = 89qq{Usage: $0 [options] [--] [user@]host [command...] 90 --client=PATH mosh client on local machine 91 (default: "mosh-client") 92 --server=COMMAND mosh server on remote machine 93 (default: "mosh-server") 94 95 --predict=adaptive local echo for slower links [default] 96-a --predict=always use local echo even on fast links 97-n --predict=never never use local echo 98 --predict=experimental aggressively echo even when incorrect 99 100-4 --family=inet use IPv4 only 101-6 --family=inet6 use IPv6 only 102 --family=auto autodetect network type for single-family hosts only 103 --family=all try all network types 104 --family=prefer-inet use all network types, but try IPv4 first [default] 105 --family=prefer-inet6 use all network types, but try IPv6 first 106-p PORT[:PORT2] 107 --port=PORT[:PORT2] server-side UDP port or range 108 (No effect on server-side SSH port) 109 --bind-server={ssh|any|IP} ask the server to reply from an IP address 110 (default: "ssh") 111 112 --ssh=COMMAND ssh command to run when setting up session 113 (example: "ssh -p 2222") 114 (default: "ssh") 115 116 --no-ssh-pty do not allocate a pseudo tty on ssh connection 117 118 --no-init do not send terminal initialization string 119 120 --local run mosh-server locally without using ssh 121 122 --experimental-remote-ip=(local|remote|proxy) select the method for 123 discovering the remote IP address to use for mosh 124 (default: "proxy") 125 126 --help this message 127 --version version and copyright information 128 129Please report bugs to mosh-devel\@mit.edu. 130Mosh home page: https://mosh.org\n}; 131 132my $version_message = '@PACKAGE_STRING@ [build @VERSION@]' . qq{ 133Copyright 2012 Keith Winstein <mosh-devel\@mit.edu> 134License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>. 135This is free software: you are free to change and redistribute it. 136There is NO WARRANTY, to the extent permitted by law.\n}; 137 138sub predict_check { 139 my ( $predict, $env_set ) = @_; 140 141 if ( not exists { adaptive => 0, always => 0, 142 never => 0, experimental => 0 }->{ $predict } ) { 143 my $explanation = $env_set ? " (MOSH_PREDICTION_DISPLAY in environment)" : ""; 144 print STDERR qq{$0: Unknown mode \"$predict\"$explanation.\n\n}; 145 146 die $usage; 147 } 148} 149 150GetOptions( 'client=s' => \$client, 151 'server=s' => \$server, 152 'predict=s' => \$predict, 153 'port=s' => \$port_request, 154 'a' => sub { $predict = 'always' }, 155 'n' => sub { $predict = 'never' }, 156 'family=s' => \$family, 157 '4' => sub { $family = 'inet' }, 158 '6' => sub { $family = 'inet6' }, 159 'p=s' => \$port_request, 160 'ssh=s' => sub { @ssh = shellwords($_[1]); }, 161 'ssh-pty!' => \$ssh_pty, 162 'init!' => \$term_init, 163 'local' => \$localhost, 164 'help' => \$help, 165 'version' => \$version, 166 'fake-proxy!' => \my $fake_proxy, 167 'bind-server=s' => \$bind_ip, 168 'experimental-remote-ip=s' => \$use_remote_ip) or die $usage; 169 170if ( defined $help ) { 171 print $usage; 172 exit; 173} 174if ( defined $version ) { 175 print $version_message; 176 exit; 177} 178 179if ( defined $predict ) { 180 predict_check( $predict, 0 ); 181} elsif ( defined $ENV{ 'MOSH_PREDICTION_DISPLAY' } ) { 182 $predict = $ENV{ 'MOSH_PREDICTION_DISPLAY' }; 183 predict_check( $predict, 1 ); 184} else { 185 $predict = 'adaptive'; 186 predict_check( $predict, 0 ); 187} 188 189if ( not grep { $_ eq $use_remote_ip } qw { local remote proxy } ) { 190 die "Unknown parameter $use_remote_ip"; 191} 192 193$family = lc( $family ); 194# Handle IPv4-only Perl installs. 195if (!$have_ipv6) { 196 # Report failure if IPv6 needed and not available. 197 if (defined($family) && $family eq "inet6") { 198 die "$0: IPv6 sockets not available in this Perl install\n"; 199 } 200 # Force IPv4. 201 $family = "inet"; 202} 203 204if ( defined $port_request ) { 205 if ( $port_request =~ m{^(\d+)(:(\d+))?$} ) { 206 my ( $low, $clause, $high ) = ( $1, $2, $3 ); 207 # good port or port-range 208 if ( $low < 0 or $low > 65535 ) { 209 die "$0: Server-side (low) port ($low) must be within valid range [0..65535].\n"; 210 } 211 if ( defined $high ) { 212 if ( $high <= 0 or $high > 65535 ) { 213 die "$0: Server-side high port ($high) must be within valid range [1..65535].\n"; 214 } 215 if ( $low == 0 ) { 216 die "$0: Server-side port ranges may not be used with starting port 0 ($port_request).\n"; 217 } 218 if ( $low > $high ) { 219 die "$0: Server-side port range ($port_request): low port greater than high port.\n"; 220 } 221 } 222 } else { 223 die "$0: Server-side port or range ($port_request) is not valid.\n"; 224 } 225} 226 227delete $ENV{ 'MOSH_PREDICTION_DISPLAY' }; 228 229my $userhost; 230my @command; 231my @bind_arguments; 232 233if ( ! defined $fake_proxy ) { 234 if ( scalar @ARGV < 1 ) { 235 die $usage; 236 } 237 $userhost = shift; 238 @command = @ARGV; 239 if ( not defined $bind_ip or $bind_ip =~ m{^ssh$}i ) { 240 if ( not defined $localhost ) { 241 push @bind_arguments, '-s'; 242 } else { 243 push @bind_arguments, ('-i', "$userhost"); 244 } 245 } elsif ( $bind_ip =~ m{^any$}i ) { 246 # do nothing 247 } else { 248 push @bind_arguments, ('-i', "$bind_ip"); 249 } 250} else { 251 my ( $host, $port ) = @ARGV; 252 253 my @res = resolvename( $host, $port, $family ); 254 255 # Now try and connect to something. 256 my $err; 257 my $sock; 258 my $addr_string; 259 my $service; 260 for my $ai ( @res ) { 261 ( $err, $addr_string, $service ) = getnameinfo( $ai->{addr}, NI_NUMERICHOST ); 262 next if $err; 263 if ( $sock = IO::Socket->new( Domain => $ai->{family}, 264 Family => $ai->{family}, 265 PeerHost => $addr_string, 266 PeerPort => $port, 267 Proto => 'tcp' )) { 268 print STDERR 'MOSH IP ', $addr_string, "\n"; 269 last; 270 } else { 271 $err = $@; 272 } 273 } 274 die "$0: Could not connect to ${host}, last tried ${addr_string}: ${err}\n" if !$sock; 275 276 # Act like netcat 277 binmode($sock); 278 binmode(STDIN); 279 binmode(STDOUT); 280 281 sub cat { 282 my ( $from, $to ) = @_; 283 while ( my $n = $from->sysread( my $buf, 4096 ) ) { 284 next if ( $n == -1 && $! == EINTR ); 285 $n >= 0 or last; 286 $to->write( $buf ) or last; 287 } 288 } 289 290 defined( my $pid = fork ) or die "$0: fork: $!\n"; 291 if ( $pid == 0 ) { 292 close STDIN; 293 cat $sock, \*STDOUT; $sock->shutdown( 0 ); 294 _exit 0; 295 } 296 $SIG{ 'HUP' } = 'IGNORE'; 297 close STDOUT; 298 cat \*STDIN, $sock; $sock->shutdown( 1 ); 299 close STDIN; 300 waitpid $pid, 0; 301 exit; 302} 303 304# Count colors 305open COLORCOUNT, '-|', $client, ('-c') or die "Can't count colors: $!\n"; 306my $colors = ""; 307{ 308 local $/ = undef; 309 $colors = <COLORCOUNT>; 310} 311close COLORCOUNT or die; 312 313chomp $colors; 314 315if ( (not defined $colors) 316 or $colors !~ m{^[0-9]+$} 317 or $colors < 0 ) { 318 $colors = 0; 319} 320 321$ENV{ 'MOSH_CLIENT_PID' } = $$; # We don't support this, but it's useful for test and debug. 322 323# If we are using a locally-resolved address, we have to get it before we fork, 324# so both parent and child get it. 325my $ip; 326if ( $use_remote_ip eq 'local' ) { 327 # "parse" the host from what the user gave us 328 my ($user, $host) = $userhost =~ /^((?:.*@)?)(.*)$/; 329 # get list of addresses 330 my @res = resolvename( $host, 22, $family ); 331 # Use only the first address as the Mosh IP 332 my $hostaddr = $res[0]; 333 if ( !defined $hostaddr ) { 334 die( "could not find address for $host" ); 335 } 336 my ( $err, $addr_string, $service ) = getnameinfo( $hostaddr->{addr}, NI_NUMERICHOST ); 337 if ( $err ) { 338 die( "could not use address for $host" ); 339 } 340 $ip = $addr_string; 341 $userhost = "$user$ip"; 342} 343 344my $pid = open(my $pipe, "-|"); 345die "$0: fork: $!\n" unless ( defined $pid ); 346if ( $pid == 0 ) { # child 347 open(STDERR, ">&STDOUT") or die; 348 349 my @sshopts = ( '-n' ); 350 if ($ssh_pty) { 351 push @sshopts, '-tt'; 352 } 353 354 my $ssh_connection = ""; 355 if ( $use_remote_ip eq 'remote' ) { 356 # Ask the server for its IP. The user's shell may not be 357 # Posix-compatible so invoke sh explicitly. 358 $ssh_connection = "sh -c " . 359 shell_quote ( '[ -n "$SSH_CONNECTION" ] && printf "\nMOSH SSH_CONNECTION %s\n" "$SSH_CONNECTION"' ) . 360 " ; "; 361 # Only with 'remote', we may need to tell SSH which protocol to use. 362 if ( $family eq 'inet' ) { 363 push @sshopts, '-4'; 364 } elsif ( $family eq 'inet6' ) { 365 push @sshopts, '-6'; 366 } 367 } 368 my @server = ( 'new' ); 369 370 push @server, ( '-c', $colors ); 371 372 push @server, @bind_arguments; 373 374 if ( defined $port_request ) { 375 push @server, ( '-p', $port_request ); 376 } 377 378 for ( &locale_vars ) { 379 push @server, ( '-l', $_ ); 380 } 381 382 if ( scalar @command > 0 ) { 383 push @server, '--', @command; 384 } 385 386 if ( defined( $localhost )) { 387 delete $ENV{ 'SSH_CONNECTION' }; 388 chdir; # $HOME 389 print "MOSH IP ${userhost}\n"; 390 exec( $server, @server ); 391 die "Cannot exec $server: $!\n"; 392 } 393 if ( $use_remote_ip eq 'proxy' ) { 394 # Non-standard shells and broken shrc files cause the ssh 395 # proxy to break mysteriously. 396 $ENV{ 'SHELL' } = '/bin/sh'; 397 my $quoted_proxy_command = shell_quote( $0, "--family=$family" ); 398 push @sshopts, ( '-S', 'none', '-o', "ProxyCommand=$quoted_proxy_command --fake-proxy -- %h %p" ); 399 } 400 my @exec_argv = ( @ssh, @sshopts, $userhost, '--', $ssh_connection . "$server " . shell_quote( @server ) ); 401 exec @exec_argv; 402 die "Cannot exec ssh: $!\n"; 403} else { # parent 404 my ( $sship, $port, $key ); 405 my $bad_udp_port_warning = 0; 406 LINE: while ( <$pipe> ) { 407 chomp; 408 if ( m{^MOSH IP } ) { 409 if ( defined $ip ) { 410 die "$0 error: detected attempt to redefine MOSH IP.\n"; 411 } 412 ( $ip ) = m{^MOSH IP (\S+)\s*$} or die "Bad MOSH IP string: $_\n"; 413 } elsif ( m{^MOSH SSH_CONNECTION } ) { 414 my @words = split; 415 if ( scalar @words == 6 ) { 416 $sship = $words[4]; 417 } else { 418 die "Bad MOSH SSH_CONNECTION string: $_\n"; 419 } 420 } elsif ( m{^MOSH CONNECT } ) { 421 if ( ( $port, $key ) = m{^MOSH CONNECT (\d+?) ([A-Za-z0-9/+]{22})\s*$} ) { 422 last LINE; 423 } else { 424 die "Bad MOSH CONNECT string: $_\n"; 425 } 426 } else { 427 if ( defined $port_request and $port_request =~ m{:} and m{Bad UDP port} ) { 428 $bad_udp_port_warning = 1; 429 } 430 print "$_\n"; 431 } 432 } 433 waitpid $pid, 0; 434 close $pipe; 435 436 if ( not defined $ip ) { 437 if ( defined $sship ) { 438 warn "$0: Using remote IP address ${sship} from \$SSH_CONNECTION for hostname ${userhost}\n"; 439 $ip = $sship; 440 } else { 441 die "$0: Did not find remote IP address (is SSH ProxyCommand disabled?).\n"; 442 } 443 } 444 445 if ( not defined $key or not defined $port ) { 446 if ( $bad_udp_port_warning ) { 447 die "$0: Server does not support UDP port range option.\n"; 448 } 449 die "$0: Did not find mosh server startup message. (Have you installed mosh on your server?)\n"; 450 } 451 452 # Now start real mosh client 453 $ENV{ 'MOSH_KEY' } = $key; 454 $ENV{ 'MOSH_PREDICTION_DISPLAY' } = $predict; 455 $ENV{ 'MOSH_NO_TERM_INIT' } = '1' if !$term_init; 456 exec {$client} ("$client", "-# @cmdline |", $ip, $port); 457} 458 459sub shell_quote { join ' ', map {(my $a = $_) =~ s/'/'\\''/g; "'$a'"} @_ } 460 461sub locale_vars { 462 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]; 463 464 my @assignments; 465 466 for ( @names ) { 467 if ( defined $ENV{ $_ } ) { 468 push @assignments, $_ . q{=} . $ENV{ $_ }; 469 } 470 } 471 472 return @assignments; 473} 474 475sub resolvename { 476 my ( $host, $port, $family ) = @_; 477 my $err; 478 my @res; 479 my $af; 480 481 # If the user selected a specific family, parse it. 482 if ( defined( $family ) && ( $family eq 'inet' || $family eq 'inet6' )) { 483 # Choose an address family, or cause pain. 484 my $afstr = 'AF_' . uc( $family ); 485 $af = eval { IO::Socket->$afstr } or die "$0: Invalid family $family\n"; 486 } 487 488 # First try the address as a numeric. 489 my %hints = ( flags => AI_NUMERICHOST, 490 socktype => SOCK_STREAM, 491 protocol => IPPROTO_TCP ); 492 if ( defined( $af )) { 493 $hints{family} = $af; 494 } 495 ( $err, @res ) = getaddrinfo( $host, $port, \%hints ); 496 if ( $err ) { 497 # Get canonical name for this host. 498 $hints{flags} = AI_CANONNAME; 499 ( $err, @res ) = getaddrinfo( $host, $port, \%hints ); 500 die "$0: could not get canonical name for $host: ${err}\n" if $err; 501 # Then get final resolution of the canonical name. 502 delete $hints{flags}; 503 my @newres; 504 ( $err, @newres ) = getaddrinfo( $res[0]{canonname}, $port, \%hints ); 505 die "$0: could not resolve canonical name ${res[0]{canonname}} for ${host}: ${err}\n" if $err; 506 @res = @newres; 507 } 508 509 if ( defined( $af )) { 510 # If v4 or v6 was specified, reduce the host list. 511 @res = grep {$_->{family} == $af} @res; 512 } elsif ( $family =~ /^prefer-/ ) { 513 # If prefer-* was specified, reorder the host list to put that family first. 514 my $prefer_afstr = 'AF_' . uc( ($family =~ /prefer-(.*)/)[0] ); 515 my $prefer_af = eval { IO::Socket->$prefer_afstr } or die "$0: Invalid preferred family $family\n"; 516 @res = (grep({$_->{family} == $prefer_af} @res), grep({$_->{family} != $prefer_af} @res)); 517 } elsif ( $family ne 'all' ) { 518 # If v4/v6/all were not specified, verify that this host only has one address family available. 519 for my $ai ( @res ) { 520 if ( !defined( $af )) { 521 $af = $ai->{family}; 522 } else { 523 die "$0: host has both IPv4 and IPv6 addresses, use --family to specify behavior\n" 524 if $af != $ai->{family}; 525 } 526 } 527 } 528 return @res; 529} 530