#!/usr/bin/perl
# awit-ssh - SSH initiator which searches LDAP for host details
# Copyright (c) 2016, AllWorldIT
#
# 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/>.

use strict;
use warnings;

use Getopt::Long;
# 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 Term::ReadKey
if (!eval {require Term::ReadKey; 1;}) {
	print STDERR "You're missing Term::ReadKey, try 'apt-get install libterm-readkey-perl'\n";
	exit 1;
}



my $VERSION = "0.0.1";


print(STDERR "AWIT-SSH-Client v$VERSION - Copyright (c) 2016, AllWorldIT\n\n");



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

	"debug",

	"knock=s",
) or exit 1;

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

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

# Pull in hostname
my $hostSpec = shift(@ARGV) // "";
my ($host,$port) = split(':',$hostSpec);
if (!defined($host)) {
	logger('ERROR',"No hostname provided\n");
	displayHelp();
	exit 1;
}




# Check if we should be doing port knocking
if (defined(my $knock = $optctl{'knock'})) {
	# If so, split off the host and the port
	my ($host,$port) = split(':',$knock);
	if (!defined($port)) {
		logger('ERROR',"Port knock specifications should be in the format of HOST:PORT");
		exit 1;
	}
	print STDERR "Port knocking '$host' on port '$port'...";
	# Do the port knock
	my $sock = IO::Socket::INET6->new(
		PeerAddr => $host,
		PeerPort => $port,
		Proto => 'tcp',
		Timeout => 3
	);
	# We should get a failure of "Connection refused", if not ERR
	if (defined($sock) || $! ne "Connection refused") {
		print STDERR "FAILED\n";
		exit 1;
	}
	print STDERR "success\n";
}


print STDERR "Your LDAP CN      : ";
my $username = <STDIN>;

print STDERR "Your LDAP Password: ";
# Don't echo password
Term::ReadKey::ReadMode('noecho');
chomp(my $password = <STDIN>);
# Turn echo back on
Term::ReadKey::ReadMode(0);
print STDERR "\n";



my $ldap = Net::LDAP->new('ldaps://XXX',
#	'debug' => 15
);
if (!defined($ldap)) {
	logger('ERROR',"Failed to setup LDAP object '%s'",$@);
	exit 2;
}

# Bind
my $mesg = $ldap->bind("cn=$username,ou=Users,YYY",password => $password);

# Search
$mesg = $ldap->search(
	base => "ou=Servers,ZZZ",
	filter => "(|(cn=$host)(awitLoginHost=$host)(awitLoginHostAlias=$host))",
);
# Check for error
if ($mesg->code()) {
	logger('ERROR',"LDAP returned '%s'",$mesg->error());
	exit 2;
}


# Some flags we may need
my $needDSS;


# If no matches
my @ldapResults = $mesg->entries();
my $ldapNumResults = @ldapResults;
my $ldapEntry;
if ($ldapNumResults < 1) {
	logger('NOTICE',"No LDAP results, using defaults");

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

} elsif ($ldapNumResults > 1) {
	logger('WARNING',"Found multiple entries!");
	while (my ($cn,$item) = %{$mesg->as_struct()}) {
		logger('WARNING',"  %s",$item->{'cn'});
	}
	exit 3;
}

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 '$ldapEntryName'");

	# Check if we need to set the host
	if (my $ldapLoginHost = $ldapEntry->get_value('awitLoginHost')) {
		logger('INFO',"  - Host %s (awitLoginHost)",$ldapLoginHost);
		$host = $ldapLoginHost;
	}

	# Check if we need to set the port
	if (my $ldapLoginPort = $ldapEntry->get_value('awitLoginPort')) {
		logger('INFO',"  - Port %s",$ldapLoginPort);
		$port = $ldapLoginPort;
	}

	# Check if we have a description
	if (my $ldapDescription = $ldapEntry->get_value('description')) {
		logger('INFO',"Description");
		foreach my $line (split(/\n/,$ldapDescription)) {
			logger('INFO',"    %s",$line);
		}
		# Hack'ish ... look if the description mentions dss is required...
		if ($ldapDescription =~ /needs ssh-dss/i) {
			$needDSS = 1;
		}
	}

	# Check if we have a wiki page
	if (my $ldapLoginWikiPage = $ldapEntry->get_value('awitLoginWikiPage')) {
		logger('INFO',"Wiki Page '%s'",$ldapLoginWikiPage);
	}

	print STDERR "\n";
}




# TODO for forawarding




my @sshArgs = ();

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

# If the server is ancient, we need to enable DSS
if (defined($needDSS)) {
	push(@sshArgs,'-o','PubkeyAcceptedKeyTypes=+ssh-dss');
	push(@sshArgs,'-o','HostbasedKeyTypes=+ssh-dss');
	push(@sshArgs,'-o','HostKeyAlgorithms=+ssh-dss');
}

logger('NOTICE',"Connecting to host '$host'" . (defined($port) ? " on port '$port'" : "") . "...\n\n\n");
exec('/usr/bin/ssh',
		'-F',$ENV{"HOME"}.'/.ssh/config',
		# Try our key only, we should never need to fall back to password
		'-o','PreferredAuthentications=publickey',
		'-o','StrictHostKeyChecking=yes',
		# Use TCP keepalive
		'-o','TCPKeepAlive=5',
		'-o','ServerAliveInterval=5',
		'-o','ConnectTimeout=30',
		# Use basic compression
		'-o','Compression=yes',
		'-o','CompressionLevel=1',
		# Fail if we cannot forward ports
		'-o','ExitOnForwardFailure=yes',
		'-o','ControlMaster=ask',
		'-o','ControlPath=~/.ssh/awit-ssh-master-%C',
		@sshArgs,
		$host
);

exit 0;



#
# Internal Functions
#



# Log something
sub logger
{
	my ($level,$arg1,@args) = @_;


	printf(STDERR '%-7s: '.$arg1."\n",$level,@args);
}


# Display version
sub displayVersion
{
	print("Version: $VERSION\n");
}


# Display usage
sub displayHelp
{
	print(STDERR<<EOF);
Usage: $0 <options>

    General Options:
      --help                             What you're seeing now.
      --version                          Display version.
      --debug                            Enable debugging.

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