The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Apache::HTTunnel::Handler ;

use strict ;
use File::FDkeeper ;
use Socket ;
use IO::Socket::INET ;
use Carp ;

use Apache2::RequestRec ;
use Apache2::RequestIO ;
use Apache2::RequestUtil ;
use Apache2::Response ;
use APR::Table ;
use Apache2::Const qw(:common :http) ;


my $fdk = undef ;


sub handler {
	my $r = shift ;

	my $slog = $r->log() ;

	if ($r->method() ne 'POST'){
		return Apache2::Const::HTTP_METHOD_NOT_ALLOWED() ;
	}
	my $code = OK ;
	my $resp = undef ;
	my $timeout = 0 ;
	my $extra = undef ;
	eval {
		if (! defined($fdk)){
			# Connect to the file descriptor storage process
			my $fifo = $r->dir_config('HTTunnelFifo') ;
			$slog->info("HTTunnel Handler: Creating File::FDkeeper\@$fifo...") ;
			$fdk = new File::FDkeeper(Peer => $fifo) ;
			$slog->notice("HTTunnel Handler: File::FDkeeper\@$fifo created") ;
		}

		# Now let's process the current request
		my $path_info = $r->path_info() ;
		$path_info =~ s/^\/// ;
		$path_info =~ s/\/$// ;
		my @params = split(/\//, $path_info) ;
	
		my $cmd = shift @params ;
		$slog->info("HTTunnel Handler: Processing '$cmd' command ($path_info)") ;
		if ($cmd eq 'connect'){
			($resp, $timeout, $extra) = connect_cmd($r, @params) ;
		}
		elsif ($cmd eq 'read'){
			($resp, $timeout, $extra) = read_cmd($r, @params) ;
		}
		elsif ($cmd eq 'write'){
			($resp, $timeout, $extra) = write_cmd($r, @params) ;
		}
		elsif ($cmd eq 'close'){
			($resp, $timeout, $extra) = close_cmd($r, @params) ;
		}
		else {
			die("Invalid command $cmd ($path_info)") ;
		}
	} ;
	if ($@){
		# TODO: Handle APR::Error
		$resp = 'err'. $@ ;
		$slog->error("HTTunnel Handler: $@") ;
	}
	else {
		if (defined($extra)){
			$extra .= ':' ;
		}
		if ($timeout){
			$resp = 'okt' . $extra ;
		}
		elsif (length($resp) == 0){
			$resp = 'okn' . $extra ;
		}
		else {
			$resp = 'okd' . $extra . $resp ;
		}
	}

	$r->print($resp) or
		$slog->error("HTTunnel Handler: Error writing response to client: $!") ;
	$r->rflush() ;

	my $cnt = $fdk->cnt() ;
	$slog->info("HTTunnel Handler: $cnt handles remaining in Keeper") ;

	return OK ;
}


sub connect_cmd {
	my $r = shift ;
	my @params = @_ ;

	my $slog = $r->log() ;
	my $proto = shift @params ;
	my $host = shift @params ;
	my $port = shift @params ;
	my $timeout = shift @params || 15 ;
	my $max_timeout = $r->dir_config('HTTunnelMaxConnectTimeout') || 15 ;
	if ($timeout > $max_timeout){
		$slog->notice("HTTunnel Handler: Requested connect timeout ($timeout) decreased " .
			"to HTTunnelMaxConnectTimeout ($max_timeout)") ;
		$timeout = $max_timeout ;
	}

	check_access($r, $host, $port) ;
	$slog->info("HTTunnel Handler: Connecting to $host:$port...") ;

	my $sock = undef ;
	my $peer_info = undef ;
	eval {
		local $SIG{ALRM} = sub {die "timeout\n"} ;
		alarm($timeout) ;
		$sock = new IO::Socket::INET(
			Proto => $proto,
			PeerAddr => $host,
			PeerPort => $port,
		) ;
		die("Error connecting to $host:$port: $!") unless defined($sock) ;

		if ($proto eq 'tcp'){
			my $peer = getpeername($sock) ;
			my ($port, $addr) = sockaddr_in($peer) ;
			$peer_info = join(':', inet_ntoa($addr), $port) ;
		}

		alarm(0) ;
	} ;
	if ($@){
		if ($@ eq "timeout\n"){
			$slog->notice("HTTunnel Handler: Connection to $host:$port timed out " .
				"after $timeout seconds.") ;
			return (undef, 0) ;
		}
		else {
			alarm(0) ;
			die("$@\n") ;
		}
	}

	die("Can't connect to $host:$port: $!") unless $sock ;
	$slog->notice("HTTunnel Handler: Connected to $host:$port") ;

	$slog->notice("HTTunnel Handler: Putting filehandle...") ;
	my $fhid = $fdk->put($sock) ;
	$slog->notice("HTTunnel Handler: Filehandle '$fhid' put") ;

	return ($fhid, 0, $peer_info) ;
}


sub check_access {
	my $r = shift ;
	my $host = shift ;
	my $port = shift ;

	my $slog = $r->log() ;
	my $rules = $r->dir_config('HTTunnelAllowedTunnels') or die("HTTunnelAllowedTunnels not defined in Apache configuration file") ;
	$rules =~ s/^\s+// ;
	$rules =~ s/\s+$// ;
	my @rules = split(/\s*,\s*/, $rules) ;
	foreach my $r (@rules){
		$slog->debug("HTTunnel Handler: Allowed (raw): $r") ;
	}

	my %allowed = () ;
	foreach my $r (@rules){
		my ($hosts, $ports) = split(/\s*=>\s*/, $r) ;
		my @hosts = split(/\s*\|\s*/, $hosts) ;
		my @ports = split(/\s*\|\s*/, $ports) ;

		foreach my $h (@hosts){
			foreach my $p (@ports){
				my @addrs = ($h) ;
				if ($h ne '*'){
					my @info = gethostbyname($h) ;
					for (my $i = 4 ; $i < scalar(@info) ; $i++){
						push @addrs, inet_ntoa($info[$i]) ;
					}
				}
				foreach my $a (@addrs){
					$allowed{"$a:$p"} = 1 ;
				}
			}
		}
	}
	foreach my $r (@rules){
		$slog->debug("HTTunnel Handler: Allowed (expanded): $r") ;
	}

	if (($allowed{"$host:$port"})||($allowed{"$host:*"})||
		($allowed{"*:$port"})||($allowed{"*:*"})){
		$slog->notice("HTTunnel Handler: $host:$port is allowed by configuration") ;
	}
	else{
		die("Permission denied for $host:$port") ;
	}
}


sub read_cmd {
	my $r = shift ;
	my @params = @_ ;

	my $slog = $r->log() ;
	my $fhid = shift @params ;
	my $proto = shift @params ;
	my $len = shift @params ;
	my $timeout = shift @params || 15 ;
	my $max_len = $r->dir_config('HTTunnelMaxReadLength') || 131072 ;
	if ($len > $max_len){
		$slog->notice("HTTunnel Handler: Requested read length ($len) decreased " .
			"to HTTunnelMaxReadLength ($max_len)") ;
		$len = $max_len ;
	}
	my $max_timeout = $r->dir_config('HTTunnelMaxReadTimeout') || 15 ;
	if ($timeout > $max_timeout){
		$slog->notice("HTTunnel Handler: Requested read timeout ($timeout) decreased " .
			"to HTTunnelMaxReadTimeout ($max_timeout)") ;
		$timeout = $max_timeout ;
	}

	my $data = undef ;
	$slog->notice("HTTunnel Handler: Getting filehandle '$fhid'...") ;
	my $fh = $fdk->get($fhid) or die("Unknown filehandle '$fhid'") ;
	$slog->notice("HTTunnel Handler: Filehandle '$fhid' gotten") ;

	my $timed_out = 0 ;
	my $peer_info = undef ;
	eval {
		local $SIG{ALRM} = sub {die "timeout\n"} ;
		alarm($timeout) ;
		$slog->info("HTTunnel Handler: Reading up to $len bytes from filehandle '$fhid'") ;
		if ($proto eq 'udp'){
			my $peer = undef ;
			($peer, $data) = recv_from($fh, $len) ;
			my ($port, $addr) = sockaddr_in($peer) ;
			$peer_info = join(':', inet_ntoa($addr), $port) ;
		}
		else{
			$data = read_from($fh, $len) ;
		}
		if (! defined($data)){
			$slog->notice("HTTunnel Handler: EOF detected on filehandle '$fhid'") ;
		}
		else {
			my $l = length($data) ;
			$slog->notice("HTTunnel Handler: Read $l bytes from filehandle '$fhid'") ;
		}

		alarm(0) ;
	} ;
	if ($@){
		if ($@ eq "timeout\n"){
			$slog->notice("HTTunnel Handler: Read timed out on purpose after $timeout seconds.") ;
			$timed_out = 1 ;
		}
		else {
			alarm(0) ;
			die("$@\n") ;
		}
	}

	return ($data, $timed_out, $peer_info) ;	
}


sub write_cmd {
	my $r = shift ;
	my @params = @_ ;

	my $slog = $r->log() ;
	my $fhid = shift @params ;
	my $proto = shift @params ;

	my $cl = $r->headers_in->{'Content-Length'} ;
	defined($cl) or die("Content-Length is not defined") ;
	$slog->notice("HTTunnel Handler: Content-Length is $cl bytes") ;
	my $data = '' ;

	$slog->notice("HTTunnel Handler: Getting filehandle '$fhid'...") ;
	my $fh = $fdk->get($fhid) or die("Unknown filehandle '$fhid'") ;
	$slog->notice("HTTunnel Handler: Filehandle '$fhid' gotten") ;


	my $left = $cl ;
	$slog->info("HTTunnel Handler: Reading $cl bytes from request input...") ;
	while ($left > 0){
		my $cnt = $r->read($data, $left) ;
		$cnt or die("Unexpected EOF from request input ($left bytes missing)") ;
		$slog->notice("HTTunnel Handler: Read $cl bytes from request input...") ;

		$left -= length($data) ;
	}

	my $l = length($data) ;
	$slog->info("HTTunnel Handler: Writing $l bytes to filehandle '$fhid'...") ;
	if ($proto eq 'udp'){
		send_to($fh, $data) ;
	}
	else {
		write_to($fh, $data) ;
	}
	$slog->notice("HTTunnel Handler: Wrote $l bytes to filehandle '$fhid'") ;

	return (undef, 0) ;
}


sub close_cmd {
	my $r = shift ;
	my @params = @_ ;

	my $slog = $r->log() ;
	my $fhid = shift @params ;

	$slog->info("HTTunnel Handler: Deleting filehandle '$fhid'...") ;
	$fdk->del($fhid) or die("Unknown filehandle '$fhid'") ;
	$slog->info("HTTunnel Handler: Filehandle '$fhid' deleted") ;

	return (undef, 0) ;
}


sub read_from {
	my $h = shift ;
	my $bufsize = shift || 0 ;

	my $buf = '' ;
	my $res = $h->sysread($buf, $bufsize) ;
	if ($res < 0){
		croak("sysread error: $!") ;
	}
	elsif ($res == 0){
		return undef ;
	}
	else {
		return $buf ;
	}
}


sub recv_from {
	my $h = shift ;
	my $bufsize = shift || 0 ;

	my $buf = '' ;
	my $res = recv($h, $buf, $bufsize, 0) ;
	if (! defined($res)){
		croak("sysread error: $!") ;
	}
	else {
		return ($res, $buf) ;
	}
}


sub write_to {
	my $h = shift ;
	my $buf = shift ;

	my $res = print $h $buf ;
	if (! $res){
		croak("print error: $!") ;
	}
}


sub send_to {
	my $h = shift ;
	my $buf = shift ;

	my $peer = getpeername($h) ;
	my $res = send($h, $buf, 0, $peer) ;

	if (! defined($res)){
		croak("send error: $!") ;
	}
}



1 ;


__DATA__