#!/usr/bin/perl -w
# $Id: proxy.perl,v 1.14 2000/11/03 21:59:03 rcaputo Exp $

# NTLM-aware proxy, since noone else seems to be.
# Waider April 2001

use strict;

use Socket;
use MIME::Base64;
use Crypt::ECB;
use Digest::MD4;
use Getopt::Long;
use POE qw(Wheel::ListenAccept Wheel::ReadWrite Driver::SysRW Filter::Stream
           Wheel::SocketFactory
          );
                                        # serial number for logging connections
my $log_id = 0;

                                        # Location of logfile
# Redirections are in the form:
#  listen_address:listen_port-connect_address:connect_port
my @redirects = ();
my $proxy = "";

# This stuff should really be determined as much on the fly as
# possible, with the exception of DOMAIN. Although we could look for a local
# SAMBA installation and grab the workgroup from that.
my $host = `hostname`; # This may have to be a valid PC name.
chomp( $host );
$host =~ s/\..*$//;
my $domain = '';
my $user = $ENV{'LOGNAME'};
my $LOG = $ENV{'HOME'} . "/tmp/proxy-$host.log";

GetOptions( 'domain:s' => \$domain, 'proxy:s' => \$proxy );

die "Need a domain\nStopped " unless $domain;
die "Need a proxy\nStopped " unless $proxy;

push @redirects, "0.0.0.0:8080-$proxy"; # ugly

# hack hack. This should work by converting Auth: NTLM to Auth: Basic and
# passing the proxy-auth request back to the connector.
print "What's the password for $user? ";
`stty -echo`;
my $passwd = <>;
`stty echo`;
chomp( $passwd );
print "\n";

print "forking...\n";
if ( fork() != 0 ) {
  exit;
}

open( LOG, ">>$LOG" ) || warn "Failed to open $LOG: $!";
#*LOG = *STDERR;
select( LOG ); $|= 1; select( STDOUT );

###############################################################################
# This is a stream-based proxy session.  It passes data between two
# sockets, and that's about all.

#------------------------------------------------------------------------------
# Create a proxy session to take over the connection.

sub session_create {
  my ($handle, $peer_host, $peer_port, $remote_addr, $remote_port) = @_;

  POE::Session->new( _start         => \&session_start,
                     _stop          => \&session_stop,
                     client_input   => \&session_client_input,
                     client_error   => \&session_client_error,
					 client_flush   => \&session_client_flush,
                     server_connect => \&session_server_connect,
                     server_input   => \&session_server_input,
                     server_error   => \&session_server_error,
					 server_flush   => \&session_server_flush,

                     # ARG0, ARG1, ARG2, ARG3, ARG4
                     [ $handle, $peer_host, $peer_port,
                       $remote_addr, $remote_port
                     ]
                   );
}

#------------------------------------------------------------------------------
# Accept POE's standard _start event.  Try to establish the client
# side of the proxy session.

sub session_start {
  my ($heap, $socket, $peer_host, $peer_port, $remote_addr, $remote_port) =
    @_[HEAP, ARG0, ARG1, ARG2, ARG3, ARG4];

  $heap->{'log'} = ++$log_id;

  $peer_host = inet_ntoa($peer_host);
  print LOG scalar( localtime( time )) . "[$heap->{'log'}] Accepted connection from $peer_host:$peer_port\n";

  $heap->{peer_host} = $peer_host;
  $heap->{peer_port} = $peer_port;
  $heap->{remote_addr} = $remote_addr;
  $heap->{remote_port} = $remote_port;

  $heap->{state} = 'connecting';
  $heap->{queue} = [];

  # Authorization state
  $heap->{auth} = 0;

  # Handle queued data properly
  $heap->{server_data_remaining} = 0;
  $heap->{client_data_remaining} = 0;
  $heap->{server_closed} = 0;
  $heap->{client_closed} = 0;

  $heap->{wheel_client} = POE::Wheel::ReadWrite->new
    ( Handle     => $socket,
      Driver     => POE::Driver::SysRW->new,
      Filter     => POE::Filter::Stream->new,
      InputState => 'client_input',
      ErrorState => 'client_error',
	  FlushedState => 'client_flush',
    );

  $heap->{wheel_server} = POE::Wheel::SocketFactory->new
    ( RemoteAddress  => $remote_addr,
      RemotePort     => $remote_port,
      SuccessState   => 'server_connect',
      FailureState   => 'server_error',
	  FlushedState   => 'server_flush',
    );
}

#------------------------------------------------------------------------------
# Stop the session, and remove all wheels.

sub session_stop {
  my $heap = $_[HEAP];

  print LOG scalar( localtime( time )) . "[$heap->{'log'}] Closing redirection session\n";
  $heap->{auth} = 0;

  delete $heap->{wheel_client};
  delete $heap->{wheel_server};
}

#------------------------------------------------------------------------------
# Received input from the client.  Pass it to the server.

sub session_client_input {
  my ($heap, $input) = @_[HEAP, ARG0];

  if ( !($heap->{auth} )) {
	my $head = $heap->{'client_header'} || "";
	$head .= $input;

	$heap->{'client_header'} = $head;
	if ( $head !~ /^\r$/m ) {
	  return;
	}

	my $body;
	( $head, $body ) = $head =~ m/\A(.*?\n)\r\n(.*)\Z/ms;
	$body ||= "";
	my $greeting = encode_base64( greeting());
	$greeting =~ s/[\r\n]//g;
	$heap->{'saved_header'} = $head;
	$heap->{'saved_body'} = $body;
	$head .= "Proxy-Connection: Keep-Alive\r\n";
	$head .= "Proxy-Authorization: NTLM " . $greeting . "\r\n";
	$input = "$head\r\n$body";
	$heap->{auth}++;
  }

  if ($heap->{state} eq 'connecting') {
    push @{$heap->{queue}}, $input;
  } else {
    (exists $heap->{wheel_server}) && $heap->{wheel_server}->put($input);
  }
  $heap->{server_data_remaining} = 1;
}

#------------------------------------------------------------------------------
# Received an error from the client.  Shut down the connection.

sub session_client_error {
  my ($heap, $operation, $errnum, $errstr) = @_[HEAP, ARG0, ARG1, ARG2];

  if ($errnum) {
    print LOG scalar( localtime( time )),
	  " [$heap->{'log'}] Client connection encountered ",
		"$operation error $errnum: $errstr\n";
  }
  else {
    print LOG scalar( localtime( time )),
	  " [$heap->{'log'}] Client closed connection.\n";
	$heap->{auth} = 0;
  }
                                        # stop the wheels
  $heap->{client_closed} = 1;
  if ( !$heap->{server_data_remaining} ) {
	delete $heap->{wheel_client};
	delete $heap->{wheel_server};
  }
}

sub session_client_flush {
  my $heap = $_[HEAP];
  $heap->{client_data_remaining} = 0;
  if ( $heap->{server_closed}) {
	delete $heap->{wheel_client};
	delete $heap->{wheel_server};
  }
}

#------------------------------------------------------------------------------
# The connection to the server has been successfully established.
# Begin passing data through.

sub session_server_connect {
  my ($kernel, $session, $heap, $socket) = @_[KERNEL, SESSION, HEAP, ARG0];

  my ($local_port, $local_addr) = unpack_sockaddr_in(getsockname($socket));
  $local_addr = inet_ntoa($local_addr);
  print LOG scalar( localtime( time )),
	"[$heap->{'log'}] Established forward from local ",
	  "$local_addr:$local_port to remote ",
		$heap->{remote_addr}, ':', $heap->{remote_port}, "\n";

  # It's important here to delete the old server wheel before creating
  # the new one.  Why?  Because otherwise the right side of the assign
  # is evaluated first.  What's this mean?  It means that the
  # ReadWrite wheel's selects get registered, and then the selects get
  # taken away when the SocketFactory is destroyed.  In a nutshell:
  # the ReadWrite never receives select events.

  delete $heap->{wheel_server};

  # It might be cleaner just to have three different wheels in this
  # session, but I originally was trying to be clever.

  $heap->{wheel_server} = POE::Wheel::ReadWrite->new
    ( Handle     => $socket,
      Driver     => POE::Driver::SysRW->new,
      Filter     => POE::Filter::Stream->new,
      InputState => 'server_input',
      ErrorState => 'server_error',
    );

  $heap->{state} = 'connected';
  foreach my $pending (@{$heap->{queue}}) {
    $kernel->call($session, 'client_input', $pending);
  }
  $heap->{queue} = [];
}

#------------------------------------------------------------------------------
# Received input from the server.  Pass it to the client.

sub session_server_input {
  my ($heap, $input) = @_[HEAP, ARG0];

  if ( $heap->{auth} == 1 ) {
	my $head = $heap->{'server_header'} || "";
	$head .= $input;

	$heap->{'server_header'} = $head;
	if ( $head !~ /^\r$/m ) {
	  return;
	}

	my $body;
	( $head, $body ) = $head =~ m/\A(.*)^\r$(.*)\Z/ms;

	# Now look for the auth headers
	my ( $ntlm ) = $head =~ m/Proxy-Authenticate: NTLM (.*)\r$/m;
	if ( defined( $ntlm )) {
	  $ntlm = decode_base64( $ntlm );
	  my $nonce = substr( $ntlm, 8 + 1 + 7 + 2 + 2 + 2 + 2, 8 );

	  #  Do the magic dance
	  my $resp = do_ntlm_magic( $passwd, $nonce );

	  # Need to build a new header
	  my $response = encode_base64( response( $resp ));
	  $response =~ s/[\r\n]//g; # GAH

	  $head = $heap->{'saved_header'};
	  $body = $heap->{'saved_body'};
	  $head .= "Proxy-Connection: Keep-Alive\r\n";
	  $head .= "Proxy-Authorization: NTLM $response\r\n\r\n$body";
	  $heap->{auth}++;

	  $heap->{wheel_server}->put($head);

	  return;
	} else {
	  $heap->{auth} = 0;
	  $input = $head . "\r\n" . $body;
	}
  }

  (exists $heap->{wheel_client}) && $heap->{wheel_client}->put($input);
  $heap->{client_data_remaining} = 1;
}

#------------------------------------------------------------------------------
# Received an error from the server.  Shut down the connection.

sub session_server_error {
  my ($heap, $operation, $errnum, $errstr) = @_[HEAP, ARG0, ARG1, ARG2];

  if ($errnum) {
    print LOG scalar( localtime( time )) . "[$heap->{'log'}] Server connection encountered ",
           "$operation error $errnum: $errstr\n"
         ;
  } else {
    print LOG scalar( localtime( time )) . "[$heap->{'log'}] Server closed connection.\n";
  }

  # flag the connection for shutdown
  $heap->{server_closed} = 1;
                                        # stop the wheels
  if ( !$heap->{client_data_remaining} ) {
	delete $heap->{wheel_client};
	delete $heap->{wheel_server};
  }
}

sub session_server_flush {
  my $heap = $_[HEAP];
  $heap->{server_data_remaining} = 0;
  if ( $heap->{client_closed}) {
	delete $heap->{delete_server};
	delete $heap->{wheel_client};
  }
}

###############################################################################
# This is a stream-based proxy server.  It listens on tcp ports, and
# spawns connectors to hop down from the firewall.

sub server_create {
  my ($local_address, $local_port, $remote_address, $remote_port) = @_;

  POE::Session->new( _start         => \&server_start,
                     _stop          => \&server_stop,
                     accept_success => \&server_accept_success,
                     accept_failure => \&server_accept_failure,

                     # ARG0, ARG1, ARG2, ARG3
                     [ $local_address,  $local_port,
                       $remote_address, $remote_port
                     ]
                   );
}

#------------------------------------------------------------------------------
# Start the server.  This records where the server should connect and
# creates the listening socket.

sub server_start {
  my ($heap, $local_addr, $local_port, $remote_addr, $remote_port) =
    @_[HEAP, ARG0, ARG1, ARG2, ARG3];

  print LOG scalar( localtime( time )) . "+ Redirecting $local_addr:$local_port to $remote_addr:$remote_port\n";
                                        # remember the redirect's details
  $heap->{local_addr}  = $local_addr;
  $heap->{local_port}  = $local_port;
  $heap->{remote_addr} = $remote_addr;
  $heap->{remote_port} = $remote_port;
                                        # create a socket factory
  $heap->{server_wheel} = POE::Wheel::SocketFactory->new
    ( BindAddress    => $local_addr,      # bind to this address
      BindPort       => $local_port,      # and bind to this port
      Reuse          => 'yes',            # reuse immediately
      SuccessState   => 'accept_success', # generate this event on connection
      FailureState   => 'accept_failure', # generate this event on error
    );
}

#------------------------------------------------------------------------------
# Accept POE's standard _stop event, and log that the redirection
# server has stopped.

sub server_stop {
  my $heap = $_[HEAP];
  delete $heap->{server_wheel};
  print LOG scalar( localtime( time )) . "- Redirection from $heap->{local_addr}:$heap->{local_port} to ",
         "$heap->{remote_addr}:$heap->{remote_port} has stopped.\n"
       ;
}

#------------------------------------------------------------------------------
# Pass the accepted socket (with peer address information) to the
# session creator, with information about where it should connect.

sub server_accept_success {
  my ($heap, $socket, $peer_addr, $peer_port) = @_[HEAP, ARG0, ARG1, ARG2];
  &session_create( $socket, $peer_addr, $peer_port,
                   $heap->{remote_addr}, $heap->{remote_port}
                 );
}

#------------------------------------------------------------------------------
# The server encountered an error.  Log it, but don't stop.

sub server_accept_failure {
  my ($heap, $operation, $errnum, $errstr) = @_[HEAP, ARG0, ARG1, ARG2];

  print LOG scalar( localtime( time )) . "! Redirection from $heap->{local_addr}:$heap->{local_port} to ",
         "$heap->{remote_addr}:$heap->{remote_port} encountered $operation ",
         "error $errnum: $errstr\n"
       ;
}

###############################################################################
# Parse the redirects, and create a server session for each.

foreach my $redirect (@redirects) {
  my ($local_address, $local_port, $remote_address, $remote_port) =
    split(/[-:]+/, $redirect);

  &server_create($local_address, $local_port, $remote_address, $remote_port);
}

$poe_kernel->run();

exit;

sub greeting {
  my $greeting;
  $greeting = "NTLMSSP\0";
  $greeting .= pack( "V", 0x01 ); # message number
  $greeting .= pack( "V", 0xb203 ); # version of some sort
  $greeting .= pack( "v", length( $domain ));
  $greeting .= pack( "v", length( $domain ));
  $greeting .= pack( "V", length( $host ) + 0x20 );
  $greeting .= pack( "v", length( $host ));
  $greeting .= pack( "v", length( $host ));
  $greeting .= pack( "V", 0x20 );
  $greeting .= $host;
  $greeting .= $domain;

  $greeting;
}

sub response {
  my $resp = shift;
  my $data = "";

  $data .= "NTLMSSP\0";
  $data .= pack( "V", 0x03 );
  $data .= pack( "v", 0x18 );
  $data .= pack( "v", 0x18 );
  $data .= pack( "v", 0x40 + length( $domain ) * 2 + length( $user ) * 2 +
				 length( $host ) * 2 );
  $data .= pack( "v", 0x0 );

  $data .= pack( "v", 0x18 );
  $data .= pack( "v", 0x18 );
  $data .= pack( "v", 0x40 + length( $domain ) * 2 + length( $user ) * 2 +
				 length( $host ) * 2 + 0x18 );
  $data .= pack( "v", 0x0 );

  $data .= pack( "v", length( $domain ) * 2 );
  $data .= pack( "v", length( $domain ) * 2 );
  $data .= pack( "v", 0x40 );
  $data .= pack( "v", 0x0 );

  $data .= pack( "v", length( $user ) * 2 );
  $data .= pack( "v", length( $user ) * 2 );
  $data .= pack( "v", 0x40 + length( $domain ) * 2 );
  $data .= pack( "v", 0x0 );

  $data .= pack( "v", length( $host ) * 2 );
  $data .= pack( "v", length( $host ) * 2 );
  $data .= pack( "v", 0x40 + length( $domain ) * 2 + length( $user ) * 2 );
  $data .= pack( "v", 0x0 );
  $data .= pack( "V", 0x0 );

  $data .= pack( "V", 0x40 + length( $domain ) * 2 + length( $user ) * 2 +
				 length( $host ) * 2 + 0x18 + 0x18 );
  $data .= pack( "V", 0x8201 );

  # Fake unicode conversion
  my $d = $domain; $d =~ s/(?<=.)/\0/g;
  $data .= $d;
  my $u = $user; $u =~ s/(?<=.)/\0/g;
  $data .= $u;
  my $h = $host; $h =~ s/(?<=.)/\0/g;
  $data .= $h;

  # add in the magic ingredient
  $data .= $resp;

  $data;
}

# NTLM challenge/response "nonce"ense.
# Note that some of this is a bit wasteful as the password doesn't
# change, but I figure it's useful to have a general-purpose NTLM
# thingy lying around.
sub do_ntlm_magic {
  my ( $passw, $nonce ) = @_;

  # This is the magic string: KGS!@#$%
  # On this shall ye found your encryption. I'm not a cryptographer,
  # and even /I/ know that this should really be "squeamish ossifrage".
  my $magic = "\x4b\x47\x53\x21\x40\x23\x24\x25";

  my $cr = Crypt::ECB->new;
  $cr->cipher( "DES" ) || die $cr->errstring;

  # Hello strong key, let's emasculate you by cutting you down to 14
  # chars and making you all caps. This brings down the potential
  # keyspace considerably, since it treats the following as identical
  # passwords:
  #
  # squeamish ossifrage
  # Squeamish Ossifrage
  # SQUEAMISH OSSI      <--- what they all boil down to
  #
  # I don't know what the valid character range is, either, but I note
  # that the key-generating code, cribbed from elsewhere, cuts off the
  # high bit, meaning you're stuck with low ascii. Assuming it's
  # limited to what you can type on a keyboard, you're talking about
  # 40 non-alpha + 26 alpha = 66, so your keyspace is going to be 66^14
  # possible keys, I guess.
  my $lmpw = uc( substr( $passw . "\x0" x 14, 0, 14 ));

  $cr->key( make_key( substr( $lmpw, 7 )));
  my $lmhpw = $cr->encrypt( $magic );
  $cr->key( make_key( substr( $lmpw, 0, 7 )));
  $lmhpw = $cr->encrypt( $magic ) . $lmhpw . ( "\0" x 5 );

  # fake unicode conversion. I don't know if real conversion is necessary.
  my $ntpw = $passw;
  $ntpw =~ s/(?<=.)/\0/g;
  my $context = new Digest::MD4;
  $context->add( $ntpw );
  my $nthpw = $context->digest . ( "\0" x 5 );

  # Now, we do a silly little dance!
  my $result = "";
  for my $hash ( $lmhpw, $nthpw ) {
	$cr->key( make_key( substr( $hash, 0, 7)));
	$result .= $cr->encrypt( $nonce );
	$cr->key( make_key( substr( $hash, 7, 7)));
	$result .= $cr->encrypt( $nonce );
	$cr->key( make_key( substr( $hash, 14, 7)));
	$result .= $cr->encrypt( $nonce );
  }

  # And that's it.
  $result;
}

# Make a 64-bit parity-coded block from a 56-bit key
sub make_key {
  my $key = shift;
  my $x;
  my $r = unpack( "B*", $key ); # convert to binary
  $r =~ s/(.......)/$1 . (1 - ((($x = $1) =~ tr|1|1|) % 2))/gxe; # gnee
  $r = pack( "B*", $r ); # and back again

  $r;
}
