#!/usr/bin/perl -w
# mrelayd -- act as TCP forwarder for telnet services

# This is based on the code in the Perl Cookbook, 17.18
# Some changes have been made specifically to support nPulse
# Where changes have been made, the original code is simply
# commented out.

# Connection status messages are output to STDERR

$|=1;
use strict;                 # require declarations
use Getopt::Long;           # for option processing
use Net::hostent;           # by-name interface for host info
use IO::Socket;             # for creating server and client sockets
use POSIX ":sys_wait_h";    # for reaping our dead children

require "./preferences.pl"; # required for nPulse

my (
    %Children,              # hash of outstanding child processes
    $REMOTE,                # whom we connect to on the outside
    $LOCAL,                 # where we listen to on the inside
    $SERVICE,               # our service name or port number
    $proxy_server,          # the socket we accept() from
    $ME,                    # basename of this program
);

($ME = $0) =~ s,.*/,,;      # retain just basename of script name

### New Code ###
my $PIDFILE = shift;
write_pid($PIDFILE);        # Record the pid for start/stop
#################

check_args();               # processing switches
start_proxy();              # launch our own server
service_clients();          # wait for incoming
die "NOT REACHED";          # you can't get here from there

### New Code ###
# save the pid to allow nPulse the ability to stop/start this process
sub write_pid {
my $pidfile = shift or return;
open(PID, ">$pidfile") or return;
print PID $$;
close(PID);
}
#################

# process command line switches using the extended
# version of the getopts library.
sub check_args {

### New Code ###
use Sys::Hostname;
my $hostname = hostname() || 'localhost';
my $host = gethostbyname($hostname);
my $addr = shift @{$host->addr_list};
$LOCAL   = inet_ntoa($addr).':31415';
$SERVICE = 'telnet';

### Original Code ###
#    GetOptions(
#        "remote=s"    => \$REMOTE,
#        "local=s"     => \$LOCAL,
#        "service=s"   => \$SERVICE,
#    ) or die <<EOUSAGE;
#    usage: $0 [ --remote host ] [ --local interface ] [ --service service ]   
#EOUSAGE
#    die "Need remote"                   unless $REMOTE;
#    die "Need local or service"         unless $LOCAL || $SERVICE;
}

# begin our server 
sub start_proxy {
    my @proxy_server_config = (
      Proto     => 'tcp',
      Reuse     => 1,
      Listen    => SOMAXCONN,
    );
    push @proxy_server_config, LocalPort => $SERVICE if $SERVICE;
    push @proxy_server_config, LocalAddr => $LOCAL   if $LOCAL;
    $proxy_server = IO::Socket::INET->new(@proxy_server_config)
                    or die "can't create proxy server: $@";
    print STDERR "[mrelayd: Proxy server on ", ($LOCAL || $SERVICE), " initialized.]\n";
}


sub service_clients { 
    my (
        $local_client,              # someone internal wanting out
        $lc_info,                   # local client's name/port information
        $remote_server,             # the socket for escaping out
        @rs_config,                 # temp array for remote socket options
        $rs_info,                   # remote server's name/port information
        $kidpid,                    # spawned child for each connection
    );

    $SIG{CHLD} = \&REAPER;          # harvest the moribund

    accepting();

    # an accepted connection here means someone inside wants out
    while ($local_client = $proxy_server->accept()) {

### New Code ###
# Get the relay command from the Java Client
# relay host_address port_number
# Ignore the port number and force to telnet only

       my($relay_cmd,$relay_host,$relay_port) = split(/ /,<$local_client>);
       $REMOTE = $relay_host.':23';
################

        $lc_info = peerinfo($local_client);
        set_state("servicing local $lc_info");
        printf STDERR "[mrelayd: Connect from $lc_info]\n";

        @rs_config = (
            Proto     => 'tcp',
            PeerAddr  => $REMOTE,
            Type      => SOCK_STREAM,
        );
        push(@rs_config, PeerPort => $SERVICE) if $SERVICE;

        print STDERR "[mrelayd: Connecting to $REMOTE...";
        set_state("connecting to $REMOTE");                 # see below
        $remote_server = IO::Socket::INET->new(@rs_config)
                         or die "remote server: $@";
        print STDERR "done]\n";

        $rs_info = peerinfo($remote_server);
        set_state("connected to $rs_info");

        $kidpid = fork();
        die "Cannot fork" unless defined $kidpid;
        if ($kidpid) {
            $Children{$kidpid} = time();            # remember his start time
            close $remote_server;                   # no use to master
            close $local_client;                    # likewise
            next;                                   # go get another client
        } 

        # at this point, we are the forked child process dedicated
        # to the incoming client.  but we want a twin to make i/o
        # easier.

        close $proxy_server;                        # no use to slave

        $kidpid = fork(); 
        die "Cannot fork" unless defined $kidpid;

        # now each twin sits around and ferries lines of data.
        # see how simple the algorithm is when you can have
        # multiple threads of control?

        # this is the fork's parent, the master's child
        if ($kidpid) {              
            set_state("$rs_info --> $lc_info");
            select($local_client) || die "Unable to select local_client"; $| = 1;

### Original Code ###
#            print while <$remote_server>;

### New Code ###
# This isn't the most efficient way to do unbuffered I/O, but it works!
            my $byte;
            while (sysread($remote_server, $byte, 1) == 1) {
                print $byte;
            }
################

            print STDERR "Done with remote_server\n";
            kill('TERM', $kidpid);      # kill my twin cause we're done
            } 
        # this is the fork's child, the master's grandchild
        else {                      
            set_state("$rs_info <-- $lc_info");
            select($remote_server) || die "Unable to select remote_server"; $| = 1;

### Original Code ###
#            print while <$local_client>;

### New Code ###
# This isn't the most efficient way to do unbuffered I/O, but it works!
            my $byte;
            while (sysread($local_client, $byte, 1) == 1) {
                print $byte;
            }
################

            print STDERR "Done with local_client\n";
            kill('TERM', getppid());    # kill my twin cause we're done
        } 
        exit;                           # whoever's still alive bites it
    } continue {
        accepting();
    } 
}

# helper function to produce a nice string in the form HOST:PORT
sub peerinfo {
    my $sock = shift;
    my $hostinfo = gethostbyaddr($sock->peeraddr);
    return sprintf("%s:%s", 
                    $hostinfo->name || $sock->peerhost, 
                    $sock->peerport);
} 

# reset our $0, which on some systems make "ps" report
# something interesting: the string we set $0 to!
sub set_state { $0 = "$ME [@_]" } 

# helper function to call set_state
sub accepting {
#    set_state("accepting proxy for " . ($REMOTE || $SERVICE));
    set_state("accepting proxy for $SERVICE");
}

# somebody just died.  keep harvesting the dead until 
# we run out of them.  check how long they ran.
sub REAPER { 
    my $child;
    my $start;
    while (($child = waitpid(-1,WNOHANG)) > 0) {
        if ($start = $Children{$child}) {
            my $runtime = time() - $start;
            printf STDERR "[Child $child ran %dm%ss]\n", 
                $runtime / 60, $runtime % 60;
            delete $Children{$child};
        } else {
            print STDERR "[Bizarre kid $child exited $?]\n";
        } 
    }
    # If I had to choose between System V and 4.2, I'd resign. --Peter Honeyman
    $SIG{CHLD} = \&REAPER; 
};
