Skip to content
Snippets Groups Projects
awit-ssh 27.2 KiB
Newer Older
Nigel Kukard's avatar
Nigel Kukard committed
#!/usr/bin/perl
# awit-ssh - SSH initiator which searches LDAP for host details
# Copyright (c) 2016-2019, AllWorldIT
Nigel Kukard's avatar
Nigel Kukard committed
#
# 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/>.

Nigel Kukard's avatar
Nigel Kukard committed


=encoding utf8

=head1 NAME

awit-ssh - LDAP lookup utility for SSH hosts.

=head1 SYNOPSIS

	awit-ssh [--libvirt-vnc HOST:PORT] [--knock HOST:PORT] HOST[:PORT]
	awit-ssh --rsync -- [USER@]HOST:path DEST
Nigel Kukard's avatar
Nigel Kukard committed

=cut

=head1 DESCRIPTION

awit-ssh perl script that automates connecting to a server via ssh by looking up the user and port information from a LDAP
database.

=cut



Nigel Kukard's avatar
Nigel Kukard committed
use strict;
use warnings;

Nigel Kukard's avatar
Nigel Kukard committed

use Term::ANSIColor;
Nigel Kukard's avatar
Nigel Kukard committed
use Getopt::Long;
Robert Spencer's avatar
Robert Spencer committed
use Net::DBus qw(:typing);
# Check Config::IniFiles
if (!eval {require Config::IniFiles; 1;}) {
	print STDERR "You're missing Config::IniFiles, try 'apt-get install libconfig-inifiles-perl'\n";
	exit 1;
}
Nigel Kukard's avatar
Nigel Kukard committed
# Check IO::Socket::INET6
if (!eval {require IO::Socket::INET6; 1;}) {
	print STDERR "You're missing IO::Socket::INET6, try 'apt-get install libio-socket-inet6-perl'\n";
	exit 1;
}
# Check Net::LDAP
if (!eval {require Net::LDAP; 1;}) {
	print STDERR "You're missing Net::LDAP, try 'apt-get install libnet-ldap-perl'\n";
	exit 1;
}
# Check IO::Prompt
if (!eval {require IO::Prompt; 1;}) {
	print STDERR "You're missing IO::Prompt, try 'apt-get install libio-prompt-perl'\n";
Nigel Kukard's avatar
Nigel Kukard committed
	exit 1;
}
## no critic (BuiltinFunctions::ProhibitStringyEval)
eval qq(
	use IO::Prompt qw(prompt);
);
## use critic
use Digest::SHA qw( sha1_hex );
use User::pwent;
Nigel Kukard's avatar
Nigel Kukard committed


Robert Spencer's avatar
Robert Spencer committed
my $NAME = "AWIT-SSH-Client";
our $VERSION = "0.8.11";
Nigel Kukard's avatar
Nigel Kukard committed

print(STDERR "$NAME v$VERSION - Copyright (c) 2016-2019, AllWorldIT\n\n");
Nigel Kukard's avatar
Nigel Kukard committed


Nigel Kukard's avatar
Nigel Kukard committed
=head1 OPTIONS

C<awit-ssh> provides the below commandline options...

=head2 --help|?

	Display this help information.

=head2 --version

	Display version information.

=head2 --forward-agent

	Forward the ssh-agent socket.

Nigel Kukard's avatar
Nigel Kukard committed
=head2 --knock <HOST:PORT>

	Knock on HOST:PORT to gain access.

=head2 --rsync <[USER@]HOST:/path> <DEST>
Nigel Kukard's avatar
Nigel Kukard committed

	Use rsync to rsync data from remote server to DEST. This can be specified either way around.

=head2 --libvirt-vnc <HOST:PORT>

	Connect to remote VNC server HOST:PORT.

=cut
Nigel Kukard's avatar
Nigel Kukard committed

# Grab options
my %optctl = ();
GetOptions(\%optctl,
	"help|?",
	"version",

	# TODO: debug is not implemented, make sure displayHelp is updated
	"debug",

	# TODO: Improve globbing before adding it to displayHelp
	"globbing",

	"forward-agent",

Nigel Kukard's avatar
Nigel Kukard committed
	"knock=s",

	"rsync",

	"libvirt-vnc=s",
Nigel Kukard's avatar
Nigel Kukard committed
) or exit 1;

# Check for help
if (defined($optctl{'help'})) {
	displayHelp();
	exit 0;
}

# Check for version
if (defined($optctl{'version'})) {
	displayVersion();
	exit 0;
}

Nigel Kukard's avatar
Nigel Kukard committed
# Check if we using rsync instead of SSH
my $useRsync;
Nigel Kukard's avatar
Nigel Kukard committed
my @rsyncParams;
if (defined(my $rsyncHost = $optctl{'rsync'})) {
	$useRsync = $rsyncHost;
}

# Check if we using libvirt vnc instead of SSH
my $libvirtVNC;
if (defined(my $vmName = $optctl{'libvirt-vnc'})) {
	if (! -x '/usr/bin/ssvncviewer') {
		logger('ERROR',color('magenta')."To use --libvirt-vnc you need to install ssvncviewer. Hint: apt-get install ssvnc".
				color('reset'));
		exit 1;
	}
	$libvirtVNC = $vmName;
}

# Check if we should be doing port knocking
my ($knockHost,$knockPort);
if (defined(my $knock = $optctl{'knock'})) {
	# If so, split off the host and the port
	($knockHost,$knockPort) = split(':',$knock);
	if (!defined($knockPort)) {
		logger('ERROR',color('magenta')."Port knock specifications should be in the format of HOST:PORT".color('reset'));
		exit 1;
	}
}


# Check for option combinations
if (defined($useRsync) && defined($libvirtVNC)) {
	logger('ERROR',color('magenta')."Options --rsync and --libvirt-monitor cannot be used together".color('reset'));
	exit 1;
}


# Variables we may set below
my $loginUsername;

Nigel Kukard's avatar
Nigel Kukard committed
my $hostSpec;
if (defined($useRsync)) {
Nigel Kukard's avatar
Nigel Kukard committed
	foreach my $param (@ARGV) {
		# Look for the : param
		if ($param =~ /:/) {
Nigel Kukard's avatar
Nigel Kukard committed
			# Assing hostSpec to the first part of the tag
			($hostSpec) = split(/:/,$param);
			push(@rsyncParams,$param);
Nigel Kukard's avatar
Nigel Kukard committed
		# Else just add it
		} else {
			push(@rsyncParams,$param);
		}
	}
	# Make sure we got a hostSpec
	if (!defined($hostSpec)) {
		logger('ERROR',color('magenta')."awit-ssh --rsync needs a <HOST:/path> parameter to be specified on the command line".
Nigel Kukard's avatar
Nigel Kukard committed
				color('reset'));
		exit 1;
	}

} else {
	$hostSpec = shift(@ARGV) // "";
}
my ($loginHost,$loginPort) = split(':',$hostSpec);
if (defined($loginHost)) {
	# Suck in username if specified
Nigel Kukard's avatar
Nigel Kukard committed
	my ($userBit,$hostBit) = split('@',$loginHost);
	if (defined($hostBit)) {
		$loginUsername = $userBit;
		$loginHost = $hostBit;
	}
} else {
	logger('ERROR',color('magenta')."No hostname provided".color('reset'));
Nigel Kukard's avatar
Nigel Kukard committed
# Make sure we save the hostname
my $realLoginHost = $loginHost;
# Port forwarding/bouncing
my ($forwardHost,$forwardPort,$forwardUsername,@forwardPortExtra);
Nigel Kukard's avatar
Nigel Kukard committed
=head1 CONFIG FILE

The following options are read from C<~/.awit-ssh.conf>, each set of options is organized in an inifile [section].

=cut


# Check for config and read
my $configFile = $ENV{"HOME"}.'/.awit-ssh.conf';
if (! -f $configFile) {
	print STDERR "No configuration file found. Please answer the questions below to generate it.\n\n";

	tie %iniSetup, 'Config::IniFiles';
Nigel Kukard's avatar
Nigel Kukard committed

=head2 [server]

LDAP server options.

=head3 B<uri>=LDAP_URI

Set the LDAP server URI, for example C<ldaps://example.com>.

=head3 B<base>=BASE_DN

Set the LDAP server base DN to use, for example C<dc=example,dc=com>.

=cut

	$iniSetup{server} = {};
	$iniSetup{server}{uri} = prompt("Your LDAP URI     : ", '-tty');
	$iniSetup{server}{uri} =~ s/^uri=//;
	$iniSetup{server}{base} = prompt("Your LDAP Base    : ", '-tty');
	$iniSetup{server}{base} =~ s/^base=//;
	tied(%iniSetup)->WriteConfig($configFile) or die "Could not write settings to new configuration file.";
	untie %iniSetup;
} else {
	# Flag, so we don't echo the same output lower down immediately after a setup.
	$iniSetup{server} = 'installed';
}
my $config = Config::IniFiles->new(-file => $configFile);

my $ldapURI = $config->val("server","uri");
if (!defined($ldapURI) || $ldapURI eq "") {
	logger('ERROR',color('magenta')."Server URI not defined in config file".color('reset'));
	exit 1;
}

my $ldapBase = $config->val("server","base");
if (!defined($ldapBase) || $ldapBase eq "") {
	logger('ERROR',color('magenta')."Server base DN not defined in config file".color('reset'));
Nigel Kukard's avatar
Nigel Kukard committed

=head2 [pkcs11]

Options for integration into PKCS11, this allows you to login to servers users a smartcard.

=head3 B<provider>=PKCS11_PROVIDER

Libary path of the PKCS11 provider. For example I</usr/lib/x86_64-linux-gnu/pkcs11/opensc-pkcs11.so>.

=cut

my $pkcsProvider = $config->val("pkcs11","provider");
if (defined($pkcsProvider) && $pkcsProvider ne "") {
	if (! -f $pkcsProvider) {
		logger('ERROR',color('magenta')."PKCS11 provider '$pkcsProvider' does not exist".color('reset'));
		exit 1;
	}
}

if (%iniSetup) {
	print STDERR "LDAP server URI   : $ldapURI\n";
	print STDERR "LDAP server base  : $ldapBase\n";
}
# Try get name automatically
my $pwent = getpwnam($ENV{'USER'});
(my $username) = split(/,/,$pwent->gecos);
if (!defined($username) || $username eq "") {
	print STDERR "WARNING: Cannot determine your name, set your gecos field.\n\n";
	$username = prompt("Your LDAP CN      : ", '-tty');
} else {
	print STDERR "Your LDAP CN      : $username (passwd->gecos)\n";
Nigel Kukard's avatar
Nigel Kukard committed

# Sort out LDAP password

my $password;
# Lets try kwallet
my ($kwalletObject,$kwalletHandle);
# IF removed, lets rather run this in its own scope...
{
	my $dbus = Net::DBus->find();

	# Grab the kwallet service off DBus
	my $kwalletService;
	eval {
		$kwalletService = $dbus->get_service('org.kde.kwalletd');
	};
	if (!defined($kwalletService)) {
		logger('WARNING',color('magenta')."Kwallet not found on DBus".color('reset'));
		goto KWALLET_END;
	}
	$kwalletObject = $kwalletService->get_object('/modules/kwalletd','org.kde.KWallet');
	# Grab a handle to the network wallet
	my $networkWalletName = $kwalletObject->networkWallet();
	$kwalletHandle = $kwalletObject->open($networkWalletName,0,$NAME);
	$password = $kwalletObject->readPassword($kwalletHandle,"ldap","password",$NAME);
# If kwallet returned nothing, try read from terminal
Robert Spencer's avatar
Robert Spencer committed
if (!defined($password) || $password eq "") {
	$password = prompt("Your LDAP Password: ", '-echo' => "*", '-tty');
Nigel Kukard's avatar
Nigel Kukard committed
print STDERR "\n";


my $ldap = Net::LDAP->new($ldapURI,
Nigel Kukard's avatar
Nigel Kukard committed
#	'debug' => 15
);
if (!defined($ldap)) {
	logger('ERROR',color('magenta')."Failed to setup LDAP object '%s'".color('reset'),$@);
Nigel Kukard's avatar
Nigel Kukard committed
	exit 2;
}

# Bind
my $mesg = $ldap->bind("cn=$username,ou=Users,$ldapBase",password => $password);
Nigel Kukard's avatar
Nigel Kukard committed

# Search
$mesg = $ldap->search(
	base => "ou=Servers,$ldapBase",
	filter => "(|(cn=$loginHost)(awitLoginHostAlias=$loginHost))",
Nigel Kukard's avatar
Nigel Kukard committed
);
# Check for error
if (my $mesgCode = $mesg->code()) {
	if ($mesgCode eq "No such object") {
		logger('ERROR',color('magenta')."LDAP returned '%s', this more than likely means a Username/Password error or your BaseDN is wrong.".color('reset'),$mesgCode);
		logger('ERROR',color('magenta')."LDAP returned '%s'".color('reset'),$mesgCode);
Nigel Kukard's avatar
Nigel Kukard committed
	exit 2;
}


Nigel Kukard's avatar
Nigel Kukard committed


# If no matches
my @ldapResults = $mesg->entries();
my $ldapNumResults = @ldapResults;
my $ldapEntry;
if ($ldapNumResults < 1) {
	logger('NOTICE',color('bold red')."No LDAP results, using the hostname provided on the commandline".color('reset'));
Nigel Kukard's avatar
Nigel Kukard committed

} elsif ($ldapNumResults == 1) {
	$ldapEntry = $ldapResults[0];

} elsif ($ldapNumResults > 1) {
	logger('WARNING',color('red')."Found multiple entries!".color('reset'));
	print STDERR "\n";
	foreach my $key (sort(keys %{$mesg->as_struct()})) {
		logger('MENU '.$counter,"  ".color('red')."%s".color('reset'),$key);
		$counter++;
Nigel Kukard's avatar
Nigel Kukard committed
	}
	my $menuSelection = prompt("Your selection [1-$ldapNumResults,q]: ",
		'-onechar',
		'-require' => {
			"Invalid Value - Your selection [1-$ldapNumResults,q]: " => sub {
				my $val = $_;
				return (
						# Check if is numeric and its within range
						$val =~ /^\d$/ &&
						$val > 0 &&
						$val <= $ldapNumResults
					) || (
						# Else our only other option we accept is 'q'
						$val eq "q"
					);
			}
		}
	);
	if ($menuSelection eq "q") {
Nigel Kukard's avatar
Nigel Kukard committed
		print STDERR "\nExiting...\n";
Nigel Kukard's avatar
Nigel Kukard committed
	print STDERR "\n";
	$menuSelection = $menuSelection->{'value'} - 1;
	$ldapEntry = $ldapResults[$menuSelection];
Nigel Kukard's avatar
Nigel Kukard committed
}

print STDERR "\n";

# Check if we got a result and modify our connection details
if ($ldapEntry) {
	my $ldapEntryName = $ldapEntry->get_value('cn');
	logger('INFO',"Found server entry '".color('green')."$ldapEntryName".color('reset')."'");
Nigel Kukard's avatar
Nigel Kukard committed

Nigel Kukard's avatar
Nigel Kukard committed
	# TODO: Ability to select between mulitple awitLoginHost's separated by ,'s

	# Check if we need to set the port knocking host & port
	if (my $ldapLoginKnockHost = $ldapEntry->get_value('awitLoginKnockHost')) {
		logger('INFO',"  - Knock host ".color('green')."%s".color('reset')." (awitLoginKnockHost)",$ldapLoginKnockHost);
		$knockHost //= $ldapLoginKnockHost;
	}
	if (my $ldapLoginKnockPort = $ldapEntry->get_value('awitLoginKnockPort')) {
		logger('INFO',"  - Knock port ".color('green')."%s".color('reset')." (awitLoginKnockPort)",$ldapLoginKnockPort);
		$knockPort //= $ldapLoginKnockPort;
	}

	# Check if we need to set the port forwarding info
	if (my $ldapLoginForwardHost = $ldapEntry->get_value('awitLoginForwardHost')) {
		logger('INFO',"  - Forward host ".color('green')."%s".color('reset')." (awitLoginForwardHost)",$ldapLoginForwardHost);
		$forwardHost //= $ldapLoginForwardHost;
		$forwardPort = 22;
	}
	if (my $ldapLoginForwardPort = $ldapEntry->get_value('awitLoginForwardPort')) {
		logger('INFO',"  - Forward port ".color('green')."%s".color('reset')." (awitLoginForwardPort)",$ldapLoginForwardPort);
		$forwardPort = $ldapLoginForwardPort;
	}
	if (my $ldapLoginForwardUsername = $ldapEntry->get_value('awitLoginForwardUsername')) {
		logger('INFO',"  - Forward user ".color('green')."%s".color('reset')." (awitLoginForwardUsername)",$ldapLoginForwardUsername);
		$forwardUsername //= $ldapLoginForwardUsername;
	}

	if (my $ldapLoginForwardPortExtra = $ldapEntry->get_value('awitLoginForwardPortExtra', 'asref' => 1)) {
		my @tmpList = ();
		# Check if this is an array ref or not
		if (ref($ldapLoginForwardPortExtra) eq "ARRAY") {
			@tmpList = @{$ldapLoginForwardPortExtra};
		} else {
			@tmpList = ($ldapLoginForwardPortExtra);
		}
		# Output all the port forwards
		foreach my $item (@tmpList) {
			my ($localPort,$destHost,$destPort) = split(/:/,$item);
			# Check localPort
			if (!defined($localPort) || $localPort < 8000) {
				logger('WARNING',"  - Forward port extra ".color('red')."%s".color('reset').
						" (awitLoginForwardPortExtra) is INVALID, localPort check failed",$item);
				goto PFEC1;
			}
			# Check destHost
			if (!defined($destHost)) {
				logger('WARNING',"  - Forward port extra ".color('red')."%s".color('reset').
						" (awitLoginForwardPortExtra) is INVALID, destHost check failed",$item);
				goto PFEC1;
			}
			# Check destPort
			if (!defined($destPort) || $destPort < 1) {
				logger('WARNING',"  - Forward port extra ".color('red')."%s".color('reset').
						" (awitLoginForwardPortExtra) is INVALID, destPort check failed",$item);
				goto PFEC1;
			}
			# Add port forwarding to our list
			push(@forwardPortExtra,{'localPort' => $localPort, 'destHost' => $destHost, 'destPort' => $destPort});

			logger('INFO',"  - Forward port extra ".color('green')."%s".color('reset')." (awitLoginForwardPortExtra)",$item);
PFEC1:
		}
	}

Nigel Kukard's avatar
Nigel Kukard committed
	# Check if we need to set the host
	if (defined($optctl{'globbing'})) {
		$loginHost = $ldapEntryName;
	}
Nigel Kukard's avatar
Nigel Kukard committed
	if (my $ldapLoginHost = $ldapEntry->get_value('awitLoginHost')) {
		logger('INFO',"  - Host ".color('green')."%s".color('reset')." (awitLoginHost)",$ldapLoginHost);
		$loginHost = $ldapLoginHost;
Nigel Kukard's avatar
Nigel Kukard committed
	}

	# Check if we need to set the port
	if (my $ldapLoginPort = $ldapEntry->get_value('awitLoginPort')) {
		logger('INFO',"  - Port ".color('green')."%s".color('reset')." (awitLoginPort)",$ldapLoginPort);
		$loginPort = $ldapLoginPort;
Nigel Kukard's avatar
Nigel Kukard committed
	}

	# Check if we need to set the username
	if (my $ldapLoginUsername = $ldapEntry->get_value('awitLoginUsername')) {
Nigel Kukard's avatar
Nigel Kukard committed
		logger('INFO',"  - User ".color('green')."%s".color('reset')." (awitLoginUsername)",$ldapLoginUsername);
		$loginUsername //= $ldapLoginUsername;
Nigel Kukard's avatar
Nigel Kukard committed
	# Check if we have a description
	if (my $ldapDescription = $ldapEntry->get_value('description')) {
		logger('INFO',"Description");
		foreach my $line (split(/\n/,$ldapDescription)) {
			logger('INFO',"    ".color('green')."%s".color('reset'),$line);
Nigel Kukard's avatar
Nigel Kukard committed
		}
		# Hack'ish ... look if the description mentions dss is required...
		if ($ldapDescription =~ /needs ssh-dss/i) {
			$needDSS = 1;
		}
Nigel Kukard's avatar
Nigel Kukard committed
	}

	# Check if we have a wiki page
	if (my $ldapLoginWikiPage = $ldapEntry->get_value('awitLoginWikiPage')) {
Nigel Kukard's avatar
Nigel Kukard committed
		logger('INFO',"Wiki Page");
		logger('INFO',"    ".color('green')."%s".color('reset'),$ldapLoginWikiPage);
Nigel Kukard's avatar
Nigel Kukard committed
	}

	print STDERR "\n";
}


# If we have kwalletObject and kwalletHandle defined, store the password as we've given awit-ssh-client permission to access
# kwallet
if (defined($kwalletObject) && defined($kwalletHandle)) {
	$kwalletObject->writePassword($kwalletHandle,"ldap","password",$password,$NAME);
}


# Check if we need to do port knocking
if (defined($knockHost)) {
	# Make sure we have a port knocking port
	if (!defined($knockPort)) {
Nigel Kukard's avatar
Nigel Kukard committed
		logger('ERROR',color('bold red')."No port knocking port defined!".color('reset'));
Nigel Kukard's avatar
Nigel Kukard committed

	logger('NOTICE',"Port knocking '".color('green')."%s".color('reset')."' on port '".color('green')."%s".color('reset')."'...",
			$knockHost,$knockPort);
	# Do the port knock
	my $sock = IO::Socket::INET6->new(
		PeerAddr => $knockHost,
		PeerPort => $knockPort,
		Proto => 'tcp',
		Timeout => 3
	);
	# We should get a failure of "Connection refused", if not ERR
	if (defined($sock) || $! ne "Connection refused") {
Nigel Kukard's avatar
Nigel Kukard committed
		logger('ERROR',color('bold red')."Port knocking failed!".color('reset'));
Nigel Kukard's avatar
Nigel Kukard committed
	logger('INFO',"Port knocking success!");
	print STDERR "\n";
}
Nigel Kukard's avatar
Nigel Kukard committed


my @sshArgs = ();
my @sshArgsPortForwards = ();
Nigel Kukard's avatar
Nigel Kukard committed

if (defined($pkcsProvider) && $pkcsProvider ne "") {
	push(@sshArgs,'-I',$pkcsProvider);
Nigel Kukard's avatar
Nigel Kukard committed
	logger('NOTICE',color('blue')."Enabling smartcard/token authentication.".color('reset'));
	print STDERR "\n";
# Notify user we'll be forwarding his authentication agent
if (defined($optctl{'forward-agent'})) {
	logger('NOTICE',color('red')."Forwarding authentication agent!".color('reset'));
	print STDERR "\n";
}

# Only push the config file override to SSH if the config file exists in the users homedir
if (-f (my $sshConfigFile = $ENV{"HOME"}.'/.ssh/config')) {
	push(@sshArgs,'-F',$sshConfigFile);
}

# If the server is ancient, we need to enable DSS
if (defined($needDSS)) {
	logger('WARNING',color('red')."Host needs ssh-dss".color('reset'));
	push(@sshArgs,'-o','PubkeyAcceptedKeyTypes=+ssh-dss');
	push(@sshArgs,'-o','HostbasedKeyTypes=+ssh-dss');
	push(@sshArgs,'-o','HostKeyAlgorithms=+ssh-dss');
}

# Try our key only, we should never need to fall back to password
push(@sshArgs,'-o','PreferredAuthentications=publickey');
push(@sshArgs,'-o','StrictHostKeyChecking=ask');

# Use TCP keepalive
push(@sshArgs,'-o','TCPKeepAlive=yes');
push(@sshArgs,'-o','ServerAliveInterval=5');
push(@sshArgs,'-o','ServerAliveCountMax=24'); # 120s

# Timeout for our connect
push(@sshArgs,'-o','ConnectTimeout=30');
# Fail if we cannot forward ports
push(@sshArgs,'-o','ExitOnForwardFailure=yes');

# Check if we're doing port forwarding...
foreach my $item (@forwardPortExtra) {
	push(@sshArgsPortForwards,'-L',sprintf('%s:%s:%s',$item->{'localPort'},$item->{'destHost'},$item->{'destPort'}));

	logger('NOTICE',color('magenta')."Forwarding port '".color('reset').$item->{'localPort'}.color('magenta').
			"' on localhost to '".color('reset').$item->{'destHost'}.color('magenta')."' port '" .color('reset').
			$item->{'destPort'}.color('magenta')."'\n");
}


# Fixup environment before we start to run SSH
local $ENV{'LANG'} = "en_US.UTF-8";
delete($ENV{'LC_ALL'});
delete($ENV{'LC_TIME'});
delete($ENV{'LC_CTYPE'});


# Setup TMPDIR, we prefer XDG_RUNTIME_DIR as its protected in /run/user/$UID/
my $TMPDIR = $ENV{'XDG_RUNTIME_DIR'} // $ENV{'TMPDIR'} // '/tmp';


# Sockets we may use
our $forwardSocket;
our $libvirtSocket;
# Children PID's we may create
our $forwardChild;
our $libvirtChild;

# Check if we're forwarding, we need to work a few things out...
if (defined($forwardHost)) {

	logger('NOTICE',"Forwarding '".color('green').$realLoginHost.color('reset')."' via host '".color('green').$loginHost.
			color('reset')."'" .(defined($loginPort) ? " on port '".color('green')."$loginPort".color('reset')."'" : "") .
			"...\n\n\n");

	# Default to port 22 if the login port is not defined
	my $destPort = $forwardPort // 22;

	# Add forward socket name
	$forwardSocket = "$TMPDIR/awit-ssh-forward-".sha1_hex("$forwardHost:$destPort $$").".sock";

	# Build up our forwarding process args into this...
	my @forwardArgs = ();

Nigel Kukard's avatar
Nigel Kukard committed
	# TODO: Allow the use of multiple forwarded ports by separating them with ,'s
	# The first port will be assumed as the SSH port, all other ports will be forwarded via TCP/IP and reported in terminal

	# Add on port we're forwarding
	push(@forwardArgs,'-L',"$forwardSocket:$forwardHost:$forwardPort");

	# Check if we have a port defined, if so specify it
	if (defined($loginPort)) {
		push(@forwardArgs,'-p',$loginPort);
	}

	# Check if we have a different username defined to login as
	if (defined($loginUsername)) {
		push(@forwardArgs,'-l',$loginUsername);
	}

	# Explicitly disable control master for the main forwarding process
	push(@forwardArgs,'-o','ControlMaster=no');

	# Fork off child to establish the main connection
	$forwardChild = fork();
	if (!$forwardChild) {

		# Exec ssh
		if (!exec('/usr/bin/ssh',
			@sshArgs,
			@forwardArgs,
			# Use basic compression
			'-o','Compression=yes',
			# All we're doing here is forwarding the port...
			'-N',
			$loginHost
		)) {

			logger('ERROR',color('magenta')."Forwarding SSH process failed to start".color('reset'));

			exit 1;
		}
	}
};
# Install signal handlers to cleanup if we get a TERM or INT
local $SIG{TERM} = local $SIG{INT} = \&cleanup;


# Check if we're forwarding to a socket...
if (defined($forwardSocket)) {

	# Loop waiting for the socket to be created
	my $delay = 30;
	while (! -e $forwardSocket && $delay > 0) {
		$delay--;
		sleep 1;
	}

	if ($delay) {

		# Check if we need to specify the username
		push(@sshArgs,'-l',$forwardUsername) if (defined($forwardUsername));

		logger('NOTICE',"Connecting to host '".color('green')."$forwardHost".color('reset')."'" .
				(defined($forwardPort) ? " on port '".color('green')."$forwardPort".color('reset')."'" : "") . "...\n\n\n");

Nigel Kukard's avatar
Nigel Kukard committed
		# Check what operation we're doing
		if (defined($useRsync)) {
Nigel Kukard's avatar
Nigel Kukard committed
			# Build SSH command
			my $sshCmd = join(' ','/usr/bin/ssh',
				@sshArgs,
				# Override where we connecting to
				'-o',"ProxyCommand='nc -U $forwardSocket'",
Nigel Kukard's avatar
Nigel Kukard committed
				# Explicitly disable control master
				'-o','ControlMaster=no',
			);
			# Run rsync
			system('/usr/bin/rsync',
Nigel Kukard's avatar
Nigel Kukard committed
				'-e',$sshCmd,
				@rsyncParams
			);

		} else {
			# Fire up SSH
			system('/usr/bin/ssh',
				@sshArgs,
				@sshArgsPortForwards,
Nigel Kukard's avatar
Nigel Kukard committed
				# Override where we connecting to
				'-o',"ProxyCommand=nc -U $forwardSocket",
				# Explicitly disable control master
				'-o','ControlMaster=no',
				$realLoginHost
			);
		}

		# Unlink socket and unset it to designate we exited normally
		unlink($forwardSocket);
		undef($forwardSocket);

	} else {
		logger('ERROR',color('magenta')."Forward socket not connected, aborting!".color('reset'));
	}


# Normal SSH connection
} else {

	logger('NOTICE',"Connecting to host '".color('green')."$loginHost".color('reset')."'" .
			(defined($loginPort) ? " on port '".color('green')."$loginPort".color('reset')."'" : "") . "...\n\n\n");

	# Make sure we get asked for control master connections...
	push(@sshArgs,'-o','ControlMaster=autoask');
	push(@sshArgs,'-o',"ControlPath=$TMPDIR/awit-ssh-master-%C");

	# Check if we have a different username defined to login as
	if (defined($loginUsername)) {
		push(@sshArgs,'-l',$loginUsername);
	}

	# Check if we have a port defined, if so specify it
	if (defined($loginPort)) {
		push(@sshArgs,'-p',$loginPort);
	}

	# Check if we're doing rsync...
	if (defined($useRsync)) {
Nigel Kukard's avatar
Nigel Kukard committed
		# Build SSH command
		my $sshCmd = join(' ','/usr/bin/ssh',
			@sshArgs,
			# Use basic compression
			'-o','Compression=yes',
		);
		# Run rsync
		system('/usr/bin/rsync',
Nigel Kukard's avatar
Nigel Kukard committed
			'-e',$sshCmd,
			@rsyncParams
		);

	# Check if we're doing libvirt monitor port forwarding...
	} elsif (defined($libvirtVNC)) {
		# Split off host and port
		my ($vncHost,$vncDisplay) = split(':',$libvirtVNC);
		if (!defined($vncHost) || $vncHost ne "127.0.0.1" || !defined($vncDisplay)) {
				logger('ERROR',color('magenta')."Libvirt VNC socket looks invalid '%s'".color('reset'),$libvirtVNC);
				exit 1;
		}
		# VNC port is the display plus 5900
		my $vncPort = 5900 + $vncDisplay;

		# Add forward socket name
		$libvirtSocket = "$TMPDIR/awit-ssh-vnc-".sha1_hex("$loginHost:".($loginPort // 22).":$libvirtVNC $$").".sock";

		my @libvirtArgs = ();

		# Add on port we're forwarding
		push(@libvirtArgs,'-L',"$libvirtSocket:$vncHost:$vncPort");

		# Fork off child to establish the main connection
		$libvirtChild = fork();
		if (!$libvirtChild) {
			# Don't use signals for this child
			undef($SIG{'TERM'});
			undef($SIG{'INT'});

			# Exec ssh
			if (!exec('/usr/bin/ssh',
				@sshArgs,
				'-o','ControlMaster=no',
				@libvirtArgs,
				# Use basic compression
				'-o','Compression=yes',
				# All we're doing here is forwarding the port...
				'-N',
				$loginHost
			)) {

				logger('ERROR',color('magenta')."Libvirt VNC unix forwarding SSH process failed to start".color('reset'));

				exit 1;
			}
		}

		# Loop waiting for the socket to be created
		my $delay = 30;
		while (! -e $libvirtSocket && $delay > 0) {
			$delay--;
			sleep 1;
		}

		# If we still have timeout ticks left, then we connected, hopefully successfully
		if ($delay) {
			system('/usr/bin/ssvncviewer',
Nigel Kukard's avatar
Nigel Kukard committed
#					'-encodings','copyrect tight hextile zlib corre rre raw',
#					'-encodings','tight hextile zlib corre rre raw',
#					'-quality','7',
					# This is handled by ssh
Nigel Kukard's avatar
Nigel Kukard committed
#					'-compresslevel','0',
#					'-16bpp',
					$libvirtSocket);
		} else {
			logger('ERROR',color('magenta')."Libvirt socket not connected, aborting!".color('reset'));
		}


Nigel Kukard's avatar
Nigel Kukard committed
	# Normal SSH
	} else {

		# Check if we're forwarding our agent
		if ($optctl{'forward-agent'}) {
			# FIXME - check if our keys expire
			push(@sshArgs,'-A');
		}

Nigel Kukard's avatar
Nigel Kukard committed
		system('/usr/bin/ssh',
			@sshArgs,
			@sshArgsPortForwards,
Nigel Kukard's avatar
Nigel Kukard committed
			# Use basic compression
			'-o','Compression=yes',
			$loginHost
		);
	}
Nigel Kukard's avatar
Nigel Kukard committed

Nigel Kukard's avatar
Nigel Kukard committed
exit 0;



#
# Internal Functions
#

# Cleanup function
sub cleanup
{
	# Kill the children
	if ($forwardChild && kill(0,$forwardChild)) {
		kill('TERM',$forwardChild);
		# Wait for it to die
		waitpid($forwardChild,-1);
	}
	if ($libvirtChild && kill(0,$libvirtChild)) {
		kill('TERM',$libvirtChild);
		# Wait for it to die
		waitpid($libvirtChild,-1);
	}
	# Unlink sockets
	if ($forwardSocket) {
		unlink($forwardSocket);
	}
	if ($libvirtSocket) {
		unlink($libvirtSocket);
	}
	# Check if we exiting abnormally...
	if ($forwardSocket || $libvirtSocket) {
		print STDERR "\nExiting...\n";
		exit 1;
	}

	exit 0;
}



Nigel Kukard's avatar
Nigel Kukard committed
# Log something
sub logger
{
	my ($level,$arg1,@args) = @_;


Nigel Kukard's avatar
Nigel Kukard committed
	printf(STDERR '%-7s: '.$arg1.color('reset')."\n",$level,@args);
Nigel Kukard's avatar
Nigel Kukard committed
}


# Display version
sub displayVersion
{
	print("Version: $VERSION\n");
Nigel Kukard's avatar
Nigel Kukard committed
}


# Display usage
sub displayHelp
{
	print(STDERR<<EOF);
Usage: $0 <options> [USER@]HOST
       $0 <options> --knock HOST:PORT [USER@]HOST
       $0 <options> --libvirt-vnc HOST:PORT [USER@]HOST
       $0 <options> --rsync -- <rsync options> [USER@]HOST:/path/file.name /tmp
Nigel Kukard's avatar
Nigel Kukard committed

    General Options:
      --help                             What you're seeing now.
      --version                          Display version.
    Agent Fowarding:
      --forward-agent                    Forward SSH agent socket.

    Port Knocking:
      --knock HOST:PORT                  Port knock a host to get access.

Nigel Kukard's avatar
Nigel Kukard committed
    Secure Copy: (using rsync)
      --rsync ...                        Run rsync instead of ssh, passing all
Nigel Kukard's avatar
Nigel Kukard committed
                                         command line parameters after the host
                                         to it. HOST is used for searching
                                         LDAP.
Nigel Kukard's avatar
Nigel Kukard committed

    Libvirt:
      --libvirt-vnc ...                  Connect to the qemu machines VNC.

Nigel Kukard's avatar
Nigel Kukard committed
EOF
Nigel Kukard's avatar
Nigel Kukard committed
__END__

=head1 AUTHORS

Nigel Kukard E<lt>nkukard@allworld.itE<gt>, Robert Spencer E<lt>rspencer@allworld.itE<gt>