#!/usr/bin/perl
#
# login program to invoke PPP.
# RADIUS accounting is NOT handled by this; it is handled by /etc/ppp/
# ip-up and ip-down which are invoked when the TCP/IP connection is up.

# version 0.1   November 5 1996
# clean up the code, minor features.

# version 0.02  May 8 1996
#
# start implementing other types of logins, not only Framed.
# Also honor static IP addresses.
#
# version 0.01  April 1 1996
#
# - ignore RADIUS server requests for Framed-User, just
#   do PPP.  Later, this should be honored.  For now,
#   just use RADIUS for authentication; it's much simpler.
#   Always use dynamic addresses.
#

use strict;
use GDBM_File;

#### CONFIGURATION SECTION ##################################################

# Local IP address for the PPP connection.
my $ip_address_local = "203.176.0.3";

# First IP address for this terminal server, if dynamic addressing
# is requested, or if nothing is specified for Framed-IP-Address.
my $ip_address_begin = "203.176.0.161";

# IP translation factor; subtract this value from radclient before adding
# the beginning IP address.
my $ip_translate_factor = 32;

# Debugging to screen?
my $debug = 1;

# PPP parameters:

# Async map - this one escapes only XON and XOFF characters.
my $asyncmap = "0x000A0000";

# MTU and MRU. 296 is good for interactive performance,
# but larger ones will lead to less overhead for file transfers.
# Maximum is 1500.
my ($mtu, $mru) = (296, 296);

# If we're using proxy ARP, set this to "proxyarp", else leave it blank.
# my $proxyarp = "proxyarp";
my $proxyarp = "";

# Login host for non-framed connections.
# This should only be an IP address, since that's what
# Login-IP-Host should be.
my $login_host = "203.176.0.4"; # marikit.iphil.net

# Programs and files.
my $prog_pppd     = "/usr/sbin/pppd";
my $prog_radacct  = "/usr/local/lib/radiusclient-ng/radacct";
my $prog_rlogin   = "/usr/bin/rlogin";
my $prog_telnet   = "/bin/telnet";
my $prog_tcpclear = "/bin/telnet -e ''";
my $prog_tty      = "/usr/bin/tty";
my $prog_who      = "/usr/bin/who";

my $path_portinfo         = "/var/ipoint/acct/portinfo";
my $path_radiusclient_map = "/etc/radclient/port-id-map";

#############################################################################

# Main program.

print "Starting.\n" if ($debug);

# Run 'who am i' to determine the current port.
my $port = `$prog_tty`;
chomp ($port);

# Translate port numbers to numbers for RADIUS.
# This translation is done again by radacct, but it may be useful here.
# Remove if CPU time is a problem.

my ($portid, $line);
open (H, $path_radiusclient_map);
while (($line = <H>) && (!$portid))
{
    my @info = split (/\s+/, $line);
    $portid = $info[1] if ($info[0] eq $port);
}
close (H);

if ($debug)
{
    # Print out all the RADIUS variables.
    my @el = grep (/^RADIUS/, keys (%ENV));
    my $e;
    foreach $e (@el)
    {
	print "$e = " . $ENV{$e} . "\n";
    }
}

# If the service type is Framed, then give them PPP.
# SLIP is not implemented (and will probably never be).

my $username = $ENV{"RADIUS_USER_NAME"};

# Generate a "unique" string for the session ID.
my $sessionid = "$$" . time ();

if ($ENV{"RADIUS_SERVICE_TYPE"} =~ /^Framed$/)
{
   
# Use the specified IP address, or generate one if none is specified,
# or a dynamic one requested.  Or, let the user negotiate the address.

    my $ip_address = $ENV{"RADIUS_FRAMED_IP_ADDRESS"};

    if (!$ip_address || ($ip_address eq "255.255.255.254"))
    {
	my @ipn = split (/\./, $ip_address_begin);
	$ipn[3] += $portid - $ip_translate_factor;
	$ip_address = join ('.', @ipn);

	if ($debug)
	{
	    print "port: $port\n";
	    print "portid: $portid\n";
	    print "ip_translate_factor: $ip_translate_factor\n";
	    print "ip_address: $ip_address\n";
	    print "mru: $mru\n";
	}

    }
    elsif ($ip_address eq "255.255.255.255")
    {
	# Clear it out so that pppd will let the remote end specify the
	# IP address.
	$ip_address = "";
    }

    # Override the specified MTU.
    $mtu = $ENV{"RADIUS_FRAMED_MTU"} if $ENV{"RADIUS_FRAMED_MTU"};

    # If no compression is specified, turn it off.
    my $compress;
    if (!$ENV{"RADIUS_FRAMED_COMPRESSION"})
    {
	$compress = "-vj";
    }

# Fix up the parameters to be passed to ip-up.  Include Framed-Route.
# Escape spaces with %20's.

    # Split up the framed route into multiple parts.
    # Separate the different given routes with bars.
    my $routelist = join ("@", map {$ENV{$_}}
			     grep {/^RADIUS_FRAMED_ROUTE/} keys (%ENV)
			    );
    $routelist =~ s/ /%20/g;

    my $param = join (':', $sessionid, $username, $port, $portid,
		      $ENV{"RADIUS_SESSION_TIMEOUT"}, $routelist);

		      
# Run pppd through exec, so that it grabs hold of the terminal
# and catches disconnections.

    # Portmaster-style prompt.
    if ($ENV{"RADIUS_SESSION_TIMEOUT"})
    {
	print "Session timeout: " . $ENV{"RADIUS_SESSION_TIMEOUT"} .
	    " seconds.\n";
    }
    print "PPP session from ($ip_address_local) to $ip_address beginning....";
    my $pppdcmd =
	"$prog_pppd $ip_address_local:$ip_address modem crtscts " .
	"asyncmap $asyncmap lock -detach $compress " .
	"ipparam $param mtu $mtu mru $mru $proxyarp";

    exec ($pppdcmd);
}
elsif ($ENV{"RADIUS_SERVICE_TYPE"} =~ /Login/)
{
    # Warning:  This code has not been tested as well as the PPP version,
    # as of now (19961107).

    # Determine what host to connect to.
    if (($ENV{"RADIUS_LOGIN_IP_HOST"} eq "0.0.0.0") ||
	!defined ($ENV{"RADIUS_LOGIN_IP_HOST"}))
    {
	# Do nothing, it's already specified above in the config section.
    }
    elsif ($ENV{"RADIUS_LOGIN_IP_HOST"} eq "255.255.255.255")
    {
	# The user should be able to choose.  Prompt the user.
	print "Host to connect to?  ";
	$login_host = <STDIN>;
	chomp ($login_host);
    }
    else
    {
	# Use what's specified by the RADIUS server.
	$login_host = $ENV{"RADIUS_LOGIN_IP_HOST"};
    }
    
    # Log into a host.  Default to telnet.  Do the accounting
    # now, since the target of the login wouldn't know how to
    # account for it.

    # Start accounting.  Send the record.
    open  (H, "| $prog_radacct") || die ("Cannot run $prog_radacct");

    my $login_service = $ENV{"RADIUS_LOGIN_SERVICE"};

    my $cmd =
	"Acct-Session-ID = \"$sessionid\"\n" .
	"User-Name = \"$username\"\n" .
	"Acct-Status-Type = Start\n" .
        "Acct-Authentic = RADIUS\n" .
        "Service-Type = Login\n" .
        "Login-Service = " . $login_service . "\n" .
        "Login-IP-Host = $login_host\n";

    print H $cmd;
    close (H);

    # Time.
    my $timestart = time ();

    # What protocol are we running?
    my ($prog_run, $login_port);

    if ($login_service eq "Rlogin")
    {
	$prog_run = $prog_rlogin;
    }
    elsif ($login_service eq "Telnet")
    {
	$prog_run = $prog_telnet;
	$login_port = $ENV{"RADIUS_LOGIN_PORT"};
    }
    elsif ($login_service eq "TCP-Clear")
    {
	$prog_run = $prog_tcpclear;
	$login_port = $ENV{"RADIUS_LOGIN_PORT"};
    }

    # Store the user information into portinfo.  We need to
    # manually fork, since we have to know the PID of the program.

    my $pid = fork ();
    if ($pid == 0)
    {
	# Child.  Run the program.
	# print "Connecting to $login_host:\n";
	my $cmd = "$prog_run $login_host $login_port";
	exec ("$cmd");
    }
    else
    {
	# Parent.  
	# Create the portinfo record, which needs the pid of the program
	# to kill.
	# The IP address is all zero, as it is not applicable here.
	# Store the time now, and the Session-Timeout.

	my %db_portinfo;

	tie (%db_portinfo, "GDBM_File", $path_portinfo, GDBM_WRCREAT, 0600);
	$db_portinfo{$portid} =
	    join (':', $username, "Login/$login_service",
		  "0.0.0.0", $pid, $timestart, $ENV{"RADIUS_SESSION_TIMEOUT"});
	untie (%db_portinfo);

	# Wait for the session to finish.
	waitpid ($pid, 0);
    }
    # Stop.  Send the record.
    open  (H, "| $prog_radacct") || die ("Cannot run $prog_radacct");

    my $timespent = time () - $timestart;

    my $cmd =
	"Acct-Session-ID = \"$sessionid\"\n" .
	"User-Name = \"$username\"\n" .
	"Acct-Status-Type = Stop\n" .
        "Acct-Authentic = RADIUS\n" .
        "Service-Type = Login\n" .
        "Login-Service = " . $login_service . "\n" .
        "Login-IP-Host = $login_host\n" .
        "Acct-Session-Time = $timespent\n";

    print H $cmd;
    close (H);

    # Remove the record from portinfo.
    my %db_portinfo;
    tie (%db_portinfo, "GDBM_File", $path_portinfo, GDBM_WRCREAT, 0600);
    delete $db_portinfo{$portid};
    untie (%db_portinfo);
}

### END ####




