#!/usr/bin/perl
#
# proxy_over_ssl -- proxy just about anything over a generic ssl proxy
#
$rcsid = '$Id: proxy_over_ssl,v 1.14 2004/12/26 17:59:39 dgregor Exp $';
#
# Copyright (c) 1998-2000 Daniel J. Gregor, Jr., All rights reserved.
# 
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
# 3. All advertising materials mentioning features or use of this software
#    must display the following acknowledgement:
# 	This product includes software developed by Daniel J. Gregor, Jr.
# 4. The name of Daniel J. Gregor, Jr. may not be used to endorse or promote
#    products derived from this software without specific prior written
#    permission.
# 
# THIS SOFTWARE IS PROVIDED BY DANIEL J. GREGOR, JR. ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL DANIEL J. GREGOR, JR. BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
# manual auth code by Karl N. Matthias -- 2000

$stdin = \*STDIN;
$stdout = \*STDOUT;

use IO::Socket;
use IO::Select;
use FileHandle;
use POSIX;
use Getopt::Std;

$buffsize = 2048;
$blockonwrite = 1;

$progname = "proxy_over_ssl";
($ver = $rcsid) =~ s/^.*\S+,v\s+([0-9.]+)\s+.*$/$1/;

$usage = << "EOF";
usage:
	$progname [-a <user agent string>] [-u <proxy auth user>]
		[-w <proxy auth passwd>] [-W <proxy auth passwd file>]
		[-H <proxy host>] [-P <proxy port>] 
		<dest host> [<dest port>]

	$progname -h
		Display help information

	$progname -v
		Display version information
EOF

$opt_H = "proxy";
$opt_P = 8080;

$opt_v = undef; 	# verbose
$opt_d = undef; 	# debug
$opt_q = undef; 	# quiet
$opt_A = undef; 	# proxy authorization
$opt_a = "$progname $ver";	# User-Agent
$opt_u = (getpwuid(geteuid()))[0];	# default proxy auth username
$opt_w = undef;		# proxy auth password (insecure! use -W <file>, instead)
$opt_W = undef;

Getopt::Std::getopt('APHpauwW'); # || die $usage;

if (defined($opt_h)) {
	die $usage;
}

if (defined($opt_v)) {
	print "$progname: $rcsid\n";
	exit 0;
}

if (@ARGV != 1 && @ARGV != 2) {
	die "$progname: invalid number of arguments\n$usage\n";
}

$host = shift(@ARGV);
if (@ARGV > 0) {
	$port = shift(@ARGV);
} else  {
	$port = 80;
}

if (defined($opt_w) && defined($opt_W)) {
	die "$progname: the options -w and -W are mutually exclusive\n$usage\n";
}

if (defined($opt_W)) {
	if (!defined($passwd_file = new FileHandle($opt_W, "r"))) {
		die "$progname: Can't open file $opt_W for reading: $!\n"; 
	}
	$opt_w = <$passwd_file>;
	chomp($opt_w);
	$passwd_file->close();
}

# Call the routine to try the proxy
$status = &ProxyConnect(\$proxy);

print(STDERR "Connected to endpoint host $host:$port...\n") unless $opt_q;
	
LOSEHEADERS:
while (defined($status = $proxy->getline())) {
	$status =~ s/[\n\r]+//g;
	last LOSEHEADERS
		if $status =~ m/^$/;
}

fcntl($proxy, F_SETFL(), O_NONBLOCK());
fcntl($stdin, F_SETFL(), O_NONBLOCK());
fcntl($stdout, F_SETFL(), O_NONBLOCK());

$read_set = new IO::Select;
$read_set->add($proxy);
$read_set->add($stdin);
$write_set = new IO::Select;

$stdinbuf = "";
$proxybuf = "";

while (1) {
	($r_ready, $w_ready) =
		IO::Select->select($read_set, $write_set, undef, undef);

	foreach $sock (@$r_ready) {
		if ($sock == $proxy) {
			if (!sysread($proxy, $proxybuf, $buffsize)) {
				die "Connection closed by remote host: $!\n"
					unless !$proxy->eof;
				die "Error reading from proxy socket: $!\n"
					unless $! eq "Resource temporarily unavailable";
			}
			
			if (length($proxybuf) > 0 &&
					!syswrite($stdout, $proxybuf)) {
				die "Error writing to stdout: $!\n"
					unless $! eq "Resource temporarily unavailable";

if ($blockonwrite) {
				# screw this, block until we can write!
				warn "Setting non-blocking IO on STDOUT\n" if $opt_v;
				fcntl($stdout, F_SETFL(), ~O_NONBLOCK());
				if (!syswrite($stdout, $proxybuf)) {
					die "Error writing to stdout: $!\n";
				}
				warn "Clearing non-blocking IO on STDOUT\n" if $opt_v;
				fcntl($stdout, F_SETFL(), O_NONBLOCK());

} else {

				# Remove proxy from the $read_set list, so
				# that we don't overwrite $proxybuf
				# We'll add it later. ;-0
				warn "Removing PROXY from read_set\n" if $opt_v;
				$read_set->remove($proxy);
				$write_set->add($stdout);
}
			}
		}
		if ($sock == $stdin) {
			if (!sysread($stdin, $stdinbuf, $buffsize)) {
				if (eof($stdin)) {
					print(STDERR "STDIN closed by local host\n") if $opt_v;
					exit(0);
				}
				die "Error reading from STDIN socket: $!\n"
					unless $! eq "Resource temporarily unavailable";
			}
			
			if (length($stdinbuf) > 0 &&
					!syswrite($proxy, $stdinbuf)) {
				die "Error writing to proxy: $!\n"
					unless $! eq "Resource temporarily unavailable";

if ($blockonwrite) {

				# screw this, block until we can write!
				warn "Setting non-blocking IO on PROXY\n" if $opt_v;
				fcntl($proxy, F_SETFL(), ~O_NONBLOCK());
				if (!syswrite($proxy, $stdinbuf)) {
					die "Error writing to proxy: $!\n";
				}
				warn "Clearing non-blocking IO on PROXY\n" if $opt_v;
				fcntl($proxy, F_SETFL(), O_NONBLOCK());

} else {

				# Remove STDIN from the $read_set list, so
				# that we don't overwrite $stdinbuf
				# We'll add it later. ;-0
				warn "Removing STDIN from read_set\n" if $opt_v;
				$read_set->remove($stdin);
				$write_set->add($proxy);
}
			}
		}
	}

	foreach $sock (@$w_ready) {
		if ($sock == $stdout) {
			if (!syswrite($stdout, $proxybuf, length($proxybuf))) {
				die "Error writing to stdout: $!\n"
					unless $! eq "Resource temporarily unavailable";

				# screw this, block until we can write!
				fcntl($stdout, F_SETFL(), ~O_NONBLOCK());
				if (!syswrite($stdout, $proxybuf,
					length($proxybuf))) {
					die "Error writing to stdout: $!\n";
				}
				fcntl($stdout, F_SETFL(), O_NONBLOCK());
			} else {
				warn("Adding PROXY to read_set\n") if $opt_v;
				$read_set->add($proxy);
				$write_set->remove($stdout);
			}
		}
		if ($sock == $stdin) {
			if (!syswrite($proxy, $stdinbuf)) {
				die "Error writing to proxy: $!\n"
					unless $! eq "Resource temporarily unavailable";

				# screw this, block until we can write!
				fcntl($proxy, F_SETFL(), ~O_NONBLOCK());
				if (!syswrite($proxy, $stdinbuf,
					length($stdinbuf))) {
					die "Error writing to proxy: $!\n";
				}
				fcntl($proxy, F_SETFL(), O_NONBLOCK());

			} else {
				warn("Adding STDIN to read_set\n") if $opt_v;
				$read_set->add($stdin);
				$write_set->remove($proxy);
			}
		}
	}
}


# b64encode hacked from:
# base64.pl -- A perl package to handle MIME-style BASE64 encoding
# A. P. Barrett <barrett@ee.und.ac.za>, October 1993
# $Revision: 1.14 $$Date: 2004/12/26 17:59:39 $
# Second-hand hacked from Deej's ez-ip.pl script since he did all the
# work of modularizing it. :) --karmat
sub b64encode
{
	local ($_) = @_;
	local ($chunk);
	local ($result);

	$base64_alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.
					   'abcdefghijklmnopqrstuvwxyz'.
					   '0123456789+/';
	$base64_pad = '=';

	$uuencode_alphabet = q|`!"#$%&'()*+,-./0123456789:;<=>?|.
					   '@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_'; # double that '\\'!


	# Build some strings for use in tr/// commands.
	# Some uuencodes use " " and some use "`", so we handle both.
	# We also need to protect backslashes.
	($tr_uuencode = " ".$uuencode_alphabet) =~ s/\\/\\\\/;
	$tr_base64 = "A".$base64_alphabet;

	# break into chunks of 45 input chars, use perl's builtin
	# uuencoder to convert each chunk to uuencode format,
	# then kill the leading "M", translate to the base64 alphabet,
	# and finally append a newline.
	while (s/^((.|\n){45})//) {
		#warn "in:$&:\n";
		$chunk = substr(pack("u", $&), $[+1, 60);
		#warn "packed	:$chunk:\n";
		eval qq{
			\$chunk =~ tr|$tr_uuencode|$tr_base64|;
		};
		#warn "translated:$chunk:\n";
		$result .= $chunk . "\n";
	}

	# any leftover chars go onto a shorter line
	# with uuencode padding converted to base64 padding
	if ($_ ne "") {
		#warn "length ".length($_)." \$_:$_:\n";
		#warn "enclen ", int((length($_)+2)/3)*4 - (45-length($_))%3, "\n";
		$chunk = substr(pack("u", $_), $[+1,
						int((length($_)+2)/3)*4 - (45-length($_))%3);
		#warn "chunk:$chunk:\n";
		eval qq{
			\$chunk =~ tr|$tr_uuencode|$tr_base64|;
		};
		#warn "translated:$chunk:\n";
		$result .= $chunk . ($base64_pad x ((60 - length($chunk)) % 4)) . "\n";
	}

	# return result
	$result;
}

# Go about actually connecting to the proxy
sub ProxyConnect {
	my($junk) = @_;
	local $status;

	$proxy = \$junk;

	# first, we build the proxy authentication string if we were provided
	# with authentication information
	if (defined($opt_w)) {
		$proxy_auth = "Basic " .  &b64encode($opt_u . ":" . $opt_w);
		chomp($proxy_auth);
	}

	print(STDERR "Trying proxy at $opt_H:$opt_P...\n") unless $opt_q;
	
	$proxy = new IO::Socket::INET (
		PeerAddr => $opt_H,
		PeerPort => $opt_P,
		Proto => 'tcp',
		);
	
	die "Error connecting to proxy at $opt_H:$opt_P: $!\n" unless $proxy;
	
	$proxy->autoflush(1);
	
	print(STDERR "Connected to proxy at $opt_H:$opt_P.\n") unless $opt_q;

	$proxy->print("CONNECT $host:$port HTTP/1.0\r\n");
	print(STDERR "CONNECT $host:$port HTTP/1.0\r\n") if defined($opt_d);

	$proxy->print("Proxy-authorization: $proxy_auth\r\n")
		if defined($proxy_auth);
	print(STDERR "Proxy-authorization: $proxy_auth\r\n")
		if defined($proxy_auth) && defined($opt_d);

	$proxy->print("User-Agent: $opt_a\r\n") if defined($opt_a);
	print(STDERR "User-Agent: $opt_a\r\n")
		if defined($opt_a) && defined($opt_d);

	$proxy->print("\r\n");	# end of headers
	
	$status = $proxy->getline();
	$status =~ s/[\n\r]+//g;

	if (!($status =~ m?^HTTP/1\.. 200?)) {
		if(($status =~ m?^HTTP/1\.. 407?) or 
		    ($status =~ m?^HTTP/1\.. 401?)) {
			# Open the tty so that we can get input and output from
			# the terminal itself script.  Gets around ssh having 
			# control  of the terminal when we are called as a proxy
			# for ssh.
			$ttyout = new FileHandle (">/dev/tty") or 
				die "$0: Can't open your TTY for writing: $!\n";
			$ttyin = new FileHandle ("</dev/tty") or 
				die "$0: Can't open your TTY for reading: $!\n";
	
			$msg = "Proxy auth failed!  Enter Password: ";
			$ttyout->print($msg);
			$ttyout->autoflush(1); # Required or doesn't work
	
			system("stty -echo < /dev/tty");
			$mypass = $ttyin->getline();
			system("stty echo < /dev/tty");
			chop($mypass);

			# erase the message that we printed
			$ttyout->print("\b \b" x length($msg));

			$ttyout->close();
			$ttyin->close();

			$opt_w = $mypass;
	
			$proxy->close();
			&ProxyConnect(\$proxy);
	
		} elsif ($opt_v) {
			die "$progname: Error from proxy: \"$status\"\n" .
				join("", $proxy->getlines) . "\n";
		} else {
			die "$progname: Error from proxy: \"$status\"\n";
		}
	}


	return $status;
}
