#!/usr/local/bin/perl
# Handles remote_* function calls by a faster method. When first called
# as a CGI, forks and starts listening on a port which is returned to the
# client. From then on, direct TCP connections can be made to this port
# to send requests and get replies.

$main::allow_rpc_only = 1;
BEGIN { push(@INC, "."); };
use WebminCore;
use POSIX;
use Socket;
$force_lang = $default_lang;
&init_config();

my $DEFAULT_RPC_TIMEOUT = 60;
my $DEFAULT_RPC_IDLE_FACTOR = 6;

print "Content-type: text/plain\n\n";

# Can this user make remote calls?
if (!&webmin_user_can_rpc()) {
	print "0 Invalid user for RPC\n";
	exit;
	}

# Will IPv6 work?
&get_miniserv_config(\%miniserv);
$use_ipv6 = 0;
if ($miniserv{'ipv6'}) {
	eval "use Socket6";
	$use_ipv6 = 1 if (!$@);
	}

# Find a free port
$port = $miniserv{'port'} || 10000;
$aerr = &allocate_socket(MAIN, $use_ipv6 ? MAIN6 : undef, \$port);
if ($aerr) {
	print "0 $aerr\n";
	exit;
	}
if (open(RANDOM, "/dev/urandom")) {
	my $tmpsid;
	read(RANDOM, $tmpsid, 16);
	$sid = lc(unpack('h*', $tmpsid));
	close RANDOM;
	}
else {
	$sid = time()*$$;
	}
$version = &get_webmin_version();
print "1 $port $sid $version\n";

# Timeout
my $rpc_idle_factor = $gconfig{'rpc_idle_factor'} || $DEFAULT_RPC_IDLE_FACTOR;
if ($rpc_idle_factor !~ /^\d+$/ || $rpc_idle_factor < 1) {
	$rpc_idle_factor = $DEFAULT_RPC_IDLE_FACTOR;
	}
my $config_rpc_timeout = $gconfig{'rpc_timeout'} || $DEFAULT_RPC_TIMEOUT;
if ($config_rpc_timeout !~ /^\d+$/ || $config_rpc_timeout < 1) {
	$config_rpc_timeout = $DEFAULT_RPC_TIMEOUT;
	}

# Fork and listen for calls ..
$pid = fork();
if ($pid < 0) {
	die "fork() failed : $!";
	}
elsif ($pid) {
	exit;
	}
untie(*STDIN);
untie(*STDOUT);

# Accept the TCP connection
my $rmask;
vec($rmask, fileno(MAIN), 1) = 1;
if ($use_ipv6) {
	vec($rmask, fileno(MAIN6), 1) = 1;
	}
$sel = select($rmask, undef, undef, $config_rpc_timeout);
if ($sel <= 0) {
	print STDERR "fastrpc[$$]: accept timed out\n"
		if ($gconfig{'rpcdebug'});
	exit;
	}
if (vec($rmask, fileno(MAIN), 1)) {
	$acptaddr = accept(SOCK, MAIN);
	}
elsif ($use_ipv6 && vec($rmask, fileno(MAIN6), 1)) {
	$acptaddr = accept(SOCK, MAIN6);
	}
else {
	die "No connection on any socket!";
	}
die "accept failed : $!" if (!$acptaddr);
my $oldsel = select(SOCK);
$| = 1;
select($oldsel);

my $rcount = 0;
my %xfer_kids;
while(1) {
	# Clean up the list of waiting sub-processes
	foreach my $p (keys %xfer_kids) {
		my $waited_pid = waitpid($p, POSIX::WNOHANG());
		delete($xfer_kids{$p}) if ($waited_pid > 0 || $waited_pid == -1);
		}

	# Wait for the request. Wait longer if this isn't the first one
	my $rmask;
	vec($rmask, fileno(SOCK), 1) = 1;
	my $timeout = $rcount
			? $config_rpc_timeout * $rpc_idle_factor
			: $config_rpc_timeout;
	my $sel = select($rmask, undef, undef, $timeout);
	if ($sel <= 0) {
		# Don't kill the control session while a tcpwrite/tcpread is
		# running
		my $nk = scalar(keys %xfer_kids);
		if ($nk) {
			print STDERR "fastrpc[$$]: idle timeout, but $nk ".
				     "transfer(s) active: ".
				     join(",", sort keys %xfer_kids)."\n"
				if ($gconfig{'rpcdebug'});
			next;
			}
		print STDERR "fastrpc[$$]: session timed out\n"
			if ($gconfig{'rpcdebug'});
		last;
		}

	my $line = <SOCK>;
	last if (!$line);
	my ($len, $auth) = split(/\s+/, $line);
	die "Invalid session ID" if ($auth ne $sid);
	my $rawarg;
	while(length($rawarg) < $len) {
		my $got;
		my $rv = read(SOCK, $got, $len - length($rawarg));
		exit if ($rv <= 0);
		$rawarg .= $got;
		}
	print STDERR "fastrpc[$$]: raw $rawarg\n" if ($gconfig{'rpcdebug'});
	my $dumper = substr($rawarg, 0, 5) eq '$VAR1' ? 1 : 0;
	my $arg = &unserialise_variable($rawarg);

	# Process it
	my $rv;
	if ($arg->{'action'} eq 'ping') {
		# Just respond with an OK
		print STDERR "fastrpc[$$]: ping\n" if ($gconfig{'rpcdebug'});
		$rv = { 'status' => 1 };
		}
	elsif ($arg->{'action'} eq 'check') {
		# Check if some module is supported
		print STDERR "fastrpc[$$]: check $arg->{'module'}\n" if ($gconfig{'rpcdebug'});
		$rv = { 'status' => 1,
			'rv' => &foreign_check($arg->{'module'}, undef, undef,
					       $arg->{'api'}) };
		}
	elsif ($arg->{'action'} eq 'config') {
		# Get the config for some module
		print STDERR "fastrpc[$$]: config $arg->{'module'}\n" if ($gconfig{'rpcdebug'});
		my %config = &foreign_config($arg->{'module'});
		$rv = { 'status' => 1, 'rv' => \%config };
		}
	elsif ($arg->{'action'} eq 'write') {
		# Transfer data to a my temp file
		my $file = $arg->{'file'} ? $arg->{'file'} :
			      $arg->{'name'} ? &tempname($arg->{'name'}) :
					       &tempname();
		print STDERR "fastrpc[$$]: write $file\n" if ($gconfig{'rpcdebug'});
		open(FILE, ">$file");
		binmode(FILE);
		print FILE $arg->{'data'};
		close(FILE);
		$rv = { 'status' => 1, 'rv' => $file };
		}
	elsif ($arg->{'action'} eq 'tcpwrite') {
		# Transfer data to a my temp file over TCP connection
		my $file = $arg->{'file'} ? $arg->{'file'} :
			      $arg->{'name'} ? &tempname($arg->{'name'}) :
					       &tempname();
		print STDERR "fastrpc[$$]: tcpwrite $file\n" if ($gconfig{'rpcdebug'});
		my $tsock = time().$$;
		my $tsock6 = $use_ipv6 ? time().$$."v6" : undef;
		my $tport = $port + 1;
		&allocate_socket($tsock, $tsock6, \$tport);
		my $cpid = fork();
		if (!defined($cpid)) {
			$rv = { 'status' => 0, 'rv' => "fork() failed : $!" };
			}
		elsif ($cpid == 0) {
			close(SOCK);
			close(MAIN);
			close(MAIN6) if ($use_ipv6);
			# Accept connection in separate process
			print STDERR "fastrpc[$$]: tcpwrite $file port $tport\n"
				if ($gconfig{'rpcdebug'});
			my $rmask;
			vec($rmask, fileno($tsock), 1) = 1;
			if ($use_ipv6) {
				vec($rmask, fileno($tsock6), 1) = 1;
				}
			my $sel = select($rmask, undef, undef, 30);
			exit if ($sel <= 0);
			if (vec($rmask, fileno($tsock), 1)) {
				accept(TRANS, $tsock) || exit;
				}
			elsif ($use_ipv6 && vec($rmask, fileno($tsock6), 1)) {
				accept(TRANS, $tsock6) || exit;
				}
			print STDERR "fastrpc[$$]: tcpwrite $file accepted\n" if ($gconfig{'rpcdebug'});
			my $buf;
			my $err;
			if (open(FILE, ">$file")) {
				binmode(FILE);
				print STDERR "fastrpc[$$]: tcpwrite $file writing\n" if ($gconfig{'rpcdebug'});
				my $bs = &get_buffer_size();
				while(read(TRANS, $buf, $bs) > 0) {
					my $ok = (print FILE $buf);
					if (!$ok) {
						$err = "Write to $file failed : $!";
						last;
						}
					}
				close(FILE);
				print STDERR "fastrpc[$$]: tcpwrite $file written\n" if ($gconfig{'rpcdebug'});
				}
			else {
				print STDERR "fastrpc[$$]: tcpwrite $file open failed $!\n" if ($gconfig{'rpcdebug'});
				$err = "Failed to open $file : $!";
				}
			print TRANS $err ? "$err\n" : "OK\n";
			close(TRANS);
			exit;
			}
		else {
			$xfer_kids{$cpid} = 1;
			print STDERR "fastrpc[$$]: tcpwrite $file started\n"
				if ($gconfig{'rpcdebug'});
			$rv = { 'status' => 1, 'rv' => [ $file, $tport ] };
			}
		close($tsock);
		close($tsock6) if ($use_ipv6);
		}
	elsif ($arg->{'action'} eq 'read') {
		# Transfer data from a file
		print STDERR "fastrpc[$$]: read $arg->{'file'}\n" if ($gconfig{'rpcdebug'});
		my ($data, $got);
		open(FILE, "<$arg->{'file'}");
		binmode(FILE);
		my $bs = &get_buffer_size();
		while(read(FILE, $got, $bs) > 0) {
			$data .= $got;
			}
		close(FILE);
		$rv = { 'status' => 1, 'rv' => $data };
		}
	elsif ($arg->{'action'} eq 'tcpread') {
		# Transfer data from a file over TCP connection
		print STDERR "fastrpc[$$]: tcpread $arg->{'file'}\n" if ($gconfig{'rpcdebug'});
		if (-d $arg->{'file'}) {
			$rv = { 'status' => 1, 'rv' => [ undef, "$arg->{'file'} is a directory" ] };
			}
		elsif (!open(FILE, "<$arg->{'file'}")) {
			$rv = { 'status' => 1, 'rv' => [ undef, "Failed to open $arg->{'file'} : $!" ] };
			}
		else {
			binmode(FILE);
			my $tsock = time().$$;
			my $tsock6 = $use_ipv6 ? time().$$."v6" : undef;
			my $tport = $port + 1;
			&allocate_socket($tsock, $tsock6, \$tport);
			my $cpid = fork();
			if (!defined($cpid)) {
				$rv = { 'status' => 0, 'rv' => "fork() failed : $!" };
				}
			elsif ($cpid == 0) {
				close(SOCK);
				close(MAIN);
				close(MAIN6) if ($use_ipv6);
				# Accept connection in separate process
				my $rmask;
				vec($rmask, fileno($tsock), 1) = 1;
				if ($use_ipv6) {
					vec($rmask, fileno($tsock6), 1) = 1;
					}
				my $sel = select($rmask, undef, undef, 30);
				exit if ($sel <= 0);
				if (vec($rmask, fileno($tsock), 1)) {
					accept(TRANS, $tsock) || exit;
					}
				elsif (vec($rmask, fileno($tsock6), 1)) {
					accept(TRANS, $tsock6) || exit;
					}
				my $buf;
				while(read(FILE, $buf, 1024) > 0) {
					print TRANS $buf;
					}
				close(FILE);
				close(TRANS);
				exit;
				}
			else {
				$xfer_kids{$cpid} = 1;
				print STDERR "fastrpc[$$]: tcpread $arg->{'file'} ".
					     "started\n"
					     	if ($gconfig{'rpcdebug'});
				$rv = { 'status' => 1,
					'rv' => [ $arg->{'file'}, $tport ] };
				}
			close(FILE);
			close($tsock);
			close($tsock6) if ($use_ipv6);
			}
		}
	elsif ($arg->{'action'} eq 'require') {
		# require a library
		print STDERR "fastrpc[$$]: require $arg->{'module'}/$arg->{'file'}\n" if ($gconfig{'rpcdebug'});
		eval {
			&foreign_require($arg->{'module'},
					 $arg->{'file'});
			};
		if ($@) {
			print STDERR "fastrpc[$$]: require error $@\n" if ($gconfig{'rpcdebug'});
			$rv = { 'status' => 0, 'rv' => $@ };
			}
		else {
			print STDERR "fastrpc[$$]: require done\n" if ($gconfig{'rpcdebug'});
			$rv = { 'status' => 1 };
			}
		}
	elsif ($arg->{'action'} eq 'call') {
		# execute a function
		print STDERR "fastrpc[$$]: call $arg->{'module'}::$arg->{'func'}(",join(",", @{$arg->{'args'}}),")\n" if ($gconfig{'rpcdebug'});
		my @rv;
		eval {
			local $main::error_must_die = 1;
			@rv = &foreign_call($arg->{'module'},
					    $arg->{'func'},
					    @{$arg->{'args'}});
			};
		if ($@) {
			print STDERR "fastrpc[$$]: call error $@\n" if ($gconfig{'rpcdebug'});
			$rv = { 'status' => 0, 'rv' => $@ };
			}
		elsif (@rv == 1) {
			$rv = { 'status' => 1, 'rv' => $rv[0] };
			}
		else {
			$rv = { 'status' => 1, 'arv' => \@rv };
			}
		print STDERR "fastrpc[$$]: call $arg->{'module'}::$arg->{'func'} done = ",join(",", @rv),"\n" if ($gconfig{'rpcdebug'});
		}
	elsif ($arg->{'action'} eq 'eval') {
		# eval some perl code
		print STDERR "fastrpc[$$]: eval $arg->{'module'} $arg->{'code'}\n" if ($gconfig{'rpcdebug'});
		my $erv;
		if ($arg->{'module'}) {
			my $pkg = $arg->{'module'};
			$pkg =~ s/[^A-Za-z0-9]/_/g;
			$erv = eval "package $pkg;\n".
				   $arg->{'code'}."\n";
			}
		else {
			$erv = eval $arg->{'code'};
			}
		print STDERR "fastrpc[$$]: eval $arg->{'module'} $arg->{'code'} done = $rv error = $@\n" if ($gconfig{'rpcdebug'});
		if ($@) {
			$rv = { 'status' => 0, 'rv' => $@ };
			}
		else {
			$rv = { 'status' => 1, 'rv' => $erv };
			}
		}
	elsif ($arg->{'action'} eq 'quit') {
		print STDERR "fastrpc[$$]: quit\n" if ($gconfig{'rpcdebug'});
		$rv = { 'status' => 1 };
		}
	else {
		print STDERR "fastrpc[$$]: unknown $arg->{'action'}\n" if ($gconfig{'rpcdebug'});
		$rv = { 'status' => 0 };
		}
	$rawrv = &serialise_variable($rv, $dumper);

	# Send back to the client
	print SOCK length($rawrv),"\n";
	print SOCK $rawrv;
	last if ($arg->{'action'} eq 'quit');
	$rcount++;
	}

# allocate_socket(handle, ipv6-handle, &port)
sub allocate_socket
{
my ($fh, $fh6, $port) = @_;
my $proto = getprotobyname('tcp');
if (!socket($fh, PF_INET, SOCK_STREAM, $proto)) {
	return "socket failed : $!";
	}
setsockopt($fh, SOL_SOCKET, SO_REUSEADDR, pack("l", 1));
if ($fh6) {
	if (!socket($fh6, PF_INET6(), SOCK_STREAM, $proto)) {
		return "socket6 failed : $!";
		}
	setsockopt($fh6, SOL_SOCKET, SO_REUSEADDR, pack("l", 1));
	setsockopt($fh6, 41, 26, pack("l", 1));	# IPv6 only
	}
while(1) {
	$$port++;
	if ($$port < 0 || $$port > 65535) {
		return "Failed to allocate a free port number: $$port";
		}
	$pack = pack_sockaddr_in($$port, INADDR_ANY);
	next if (!bind($fh, $pack));
	if ($fh6) {
		$pack6 = pack_sockaddr_in6($$port, in6addr_any());
		next if (!bind($fh6, $pack6));
		}
	last;
	}
listen($fh, SOMAXCONN) || return "listen failed : $!";
if ($fh6) {
	listen($fh6, SOMAXCONN) || return "listen6 failed : $!";
	}
return undef;
}
