#!/usr/bin/perl -w

$rcsid = q$Id: ppp-connect 1.1 Sat, 19 Jun 1999 19:19:22 +0100 paul $;

use strict;
#no strict 'vars';
use vars qw($rcsid $opt_help $opt_version $opt_verbose),
    qw($opt_max_idle_time $opt_max_time $opt_min_time),
    qw($opt_chatdir $opt_chatfiles $opt_chat_timeout $opt_serial_rate),
    qw($opt_sleep_time $opt_max_dials $opt_max_time_per_attempt),
    qw($opt_sampling_interval $opt_logfile $opt_config_file),
    qw($opt_chat_verbose $opt_monitor_idle $opt_pjob_interval),
    qw(@user_options @root_options),
    qw($progname $is_setuid),
    qw($prev_event $prev_time),
    qw(@pids_launched $pppd_pid $pppd_start_time $ppp_device),
    qw(%pid_desc);

use FileHandle;
use Getopt::Long;
use POSIX "sys_wait_h";

# --------------------------------------------------------------------

sub logmsg {
#    system(qw(/usr/bin/logger -s -t), $basename, @_);
    print @_, "\n";
    print DIALOUT_LOG @_, "\n";
}

sub verbose {
   if ($opt_verbose) {logmsg @_};
}


sub timing_announce {
    my $new_event = shift;
    my ($time_elapsed, $mins, $secs, $new_time);
    
    $new_time = time;
    if (defined $prev_time) {
        $time_elapsed = $new_time - $prev_time;
        $mins = int($time_elapsed / 60);
        $secs = $time_elapsed % 60;
        $secs = sprintf("%02d", $secs);
        logmsg "$new_event ($mins:$secs after $prev_event)";
    } else {
        logmsg $new_event;
    }
    $prev_time = $new_time;
    $prev_event = $new_event;
}

sub kill_pppd {
    if (defined $pppd_pid) {
        # Kill it!
        if (kill('TERM', $pppd_pid)) {
            timing_announce "PPPD killed with signal";
        } else {
            timing_announce "PPPD kill failed with error $!";
        }
        undef $pppd_pid;
    }
}

sub snuff_it {
    my $exitcode = shift;
    my $exit_message = shift;

    if (defined $pppd_pid) {
        kill_pppd();
        logmsg $exit_message;
    } else {
        timing_announce $exit_message;
    }
    if(defined &main::before_exit) {
        eval '&before_exit' or die "before_exit command failed: $@, stopped";
    }
    exit $exitcode;
}

sub signal_handler {
    my $signal = shift;

    snuff_it(-1, "Shutting down after signal $signal");
}

sub com_launch {
    my $wait_time = shift;
    my $uid = shift;
    my $gid = shift;
    my $pid = fork;
    
    unless (defined $pid) {
        snuff_it(-1, "Failed to fork: $!, stopped");
    }
    if ($pid == 0) {
        # We're the child
        $) = $( = $gid;
        $> = $< = $uid;
        sleep $wait_time;
        exec @_;
        # Exec must have failed
        die "Exec of " . join(' ', @_) . " failed: $!, stopped";
    }
    # We're the parent
    logmsg "PID $pid: ", join(' ', @_);
    push @pids_launched, $pid;
    $pid_desc{$pid} = join(" ", @_);
    return $pid;
}

sub launch {
    my $wait_time = shift;
    return com_launch($wait_time, $>, $), @_);
}

sub su_launch {
    my $wait_time = shift;
    my $user = shift;
    
#    unless (($user, $group) = ($usergroup =~ /^(.+),(.+)$/)) {
#      $user = $group = $usergroup;
#    }
    my ($login, $pass, $uid, $gid);
    unless (($login, $pass, $uid, $gid) = getpwnam($user)) {
      snuff_it(-1, "Failed to get user information for $user: $!, stopped");
    }
    return com_launch($wait_time, $uid, $gid, @_);
}

sub is_pppd_dead {
    unless (defined $pppd_pid) {
        return 1;
    }
    my $pid;
    while (($pid = waitpid(-1, WNOHANG)) != 0) {
        my $error_code = $? >> 8;
	my $error_desc;
	if ($error_code) {
	    $error_desc = "died with error number $error_code";
	} else {
	    $error_desc = "completed successfully"
	}
        if ($pid == $pppd_pid) {
            undef $pppd_pid;
	    if ($error_code) {
                timing_announce "PPPD $error_desc";
	     }
            return 1;
        } else {
            timing_announce "PID $pid (" . $pid_desc{$pid} . ") $error_desc";
        }
    }
    return 0;
}

sub get_usage {
    my $ppp_device = shift;
    
    open DEV, "/proc/net/dev"
        or snuff_it(-1, "Couldn't open dev file: $!");
    my $usage = 0;
    LINE: while (<DEV>) {
        chomp;
        my @dev = split(/\s+/);
        unless ($dev[0]) {
            shift @dev;
        }
        if ($dev[0] eq "$ppp_device\:") {
            $usage += $dev[1]; # Receive
            $usage += $dev[6]; # Transmit
        }
    }
    close DEV or snuff_it(-1, "Couldn't close dev file: $!");
    return $usage;
}

# --------------------------------------------------------------------
# --------------------------------------------------------------------
# --------------------------------------------------------------------

# Switch off buffering.

STDOUT->autoflush(1);

# CD to somewhere everyone can reach

chdir("/tmp") or die "Couldn't even cd to /tmp: $!, stopped";

# --------------------------------------------------------------------

$opt_help = 0;
$opt_version = 0;

$Getopt::Long::passthrough = 1;
GetOptions(qw(help version))
    or die "Failed to parse command line options, stopped";


if ($opt_help) {
    print "Sorry, I haven't written help yet.\n";
    print "You don't really need any options, but if you do, use the source\n";
    print "cheers, paul\@hedonism.demon.co.uk\n";
    exit;
}

if ($opt_version) {
    print "RCS version: $rcsid\n";
    exit;
}

# --------------------------------------------------------------------

$is_setuid = scalar( $< != $> or $( != $) );
if ($is_setuid) {
    $ENV{PATH} = q(/usr/bin:/bin);
}

$opt_max_idle_time = 180;
$opt_logfile = '/var/local/log/dialout';
$opt_chatdir = '/etc/local/dialout/chatfiles';
$opt_sleep_time = 30;
$opt_max_dials = 12;
$opt_max_time_per_attempt = 180; # usually not needed, chat dies first.
$opt_sampling_interval = 10;
$opt_pjob_interval = 30;
$opt_chat_timeout = 60;
$opt_max_time = 3600; # An hour
$opt_min_time = 300; # Five minutes
$opt_serial_rate = 115200;
$opt_monitor_idle = 0;
$opt_verbose = 1;
$opt_chat_verbose = 0;

@user_options = 
    (qw(verbose! monitor_idle!),
     qw(max-idle-time=i sleep-time=i max-time=i min-time=i),
     qw(max-dials=i max-time-per-attempt=i),
     qw(chatfiles=s chat-timeout=i sampling-interval=i),
     qw(pjob-interval=i chat-verbose! serial-rate=i));

@root_options = 
    (qw(chatdir=s logfile=s));

$opt_config_file = "default";

if (!$is_setuid) {
    $Getopt::Long::passthrough = 1;
    GetOptions(qw(config-file=s))
        or die "Failed to parse command line options, stopped";
}

{
    my $config_file;
    if ($opt_config_file eq "default") {
        $config_file = '/etc/local/dialout/ppp-config';
    } else {
        $config_file = $opt_config_file;
    }
    
    if (-e $config_file) {
        unless (do $config_file) {
          print "$@\n";
          die "Failed reading config file $config_file, stopped";
	}
    } elsif ($opt_config_file ne "default") {
        die "Config file $config_file doesn't exist, stopped";
    }
}

$Getopt::Long::passthrough = 0;
GetOptions(@user_options, $is_setuid ? () : @root_options)
    or die "Failed to parse command line options, stopped";

if (scalar(@ARGV) > 0) {
    die "Unparsed arguments on command line: \""
        . join(' ', @ARGV) . "\", stopped";
}

if ($opt_max_time < $opt_min_time) {
    die "Doesn't make sense to have max-time < min-time, stopped";
}

if ($opt_chatfiles eq "") {
    die "Chatfile list empty, stopped";
}

my @chatfiles = split(/,/, $opt_chatfiles);

my $file;

foreach $file (@chatfiles) {
   unless ($file =~ /^[-a-z]+$/) {
       die "Invalid chatfile name $file, stopped";
   }
   unless (-f "$opt_chatdir/$file") {
       die "Chatfile $opt_chatdir/$file doesn't exist, stopped";
   }
}

# --------------------------------------------------------------------

open DIALOUT_LOG, ">>$opt_logfile"
    or die "Couldn't open log file for appending: $!, stopped";
flock DIALOUT_LOG, (2 | 4) # Exclusive, nonblocking
    or die "Couldn't get lock on log file: $!, stopped";
seek DIALOUT_LOG, 0, 2 # Seek to the end now we've got the lock.
    or die "Couldn't seek to end of log file: $!, stopped";
DIALOUT_LOG->autoflush(1);

$is_setuid = scalar( $< != $> or $( != $) );

if ($is_setuid) {
    logmsg "Started dialout for user: ", scalar(getpwuid($<)), ": ", scalar(gmtime);
} else {
    logmsg "Started non-setuid dialout: ", scalar(gmtime);
}

# --------------------------------------------------------------------

if(defined &main::before_connect) {
    eval '&before_connect' or die "before_connect command failed: $@, stopped";
}

foreach (qw(HUP INT QUIT TRAP ABRT IOT BUS FPE KILL USR1 USR2 PIPE ALRM), 
    qw(TERM STKFLT STOP URG XCPU XFSZ IO)) {
    $SIG{$_} = \&signal_handler;
}

my $dial_attempts = 0;

 DIAL_ATTEMPT: while (1) {
     if ($dial_attempts >= $opt_max_dials) {
         snuff_it(-1, "Maximum dial attempts reached");
     }
     if ($dial_attempts > 0) {
         sleep $opt_sleep_time;
     }
     # Pull the chatfile from one end
     my $chatfile = shift @chatfiles;
     # Push it on the other.
     push @chatfiles, $chatfile;
     my $connect_script = 
         join(" ",
              "/usr/sbin/chat",
              "-t", $opt_chat_timeout,
              $opt_chat_verbose ? ('-v') : (),
              '-f', ($opt_chatdir . "/" . $chatfile));
     $dial_attempts++;
     timing_announce "starting PPPD";
     $pppd_pid = 
         launch(0, qw(/usr/sbin/pppd /dev/modem), $opt_serial_rate,
                 qw(defaultroute -detach), 
                'connect', $connect_script);
     $pppd_start_time = time;
     logmsg "PPP pid $pppd_pid (attempt $dial_attempts, \"$chatfile\")";
     # Now wait either for PPPD to die or a route to be found
     while (1) {
         if (is_pppd_dead()) {
             # It's already been announced.
             next DIAL_ATTEMPT;
         }
#    verbose "Opening /proc/net/route"; # Really too verbose. 
         open ROUTE, "/proc/net/route" 
             or snuff_it(-1, "Couldn't open route file: $!, stopped");
         my @route_names = split(/\t/, <ROUTE>);
         while (<ROUTE>) {
             chomp;
             my $i = 0;
             my %route;
             my $value;
             foreach $value (split(/\t/)) {
                 $route{$route_names[$i]} = $value;
                 $i++;
             }
             if ($route{Mask} eq '00000000') {
                 timing_announce "default route found";
                 $ppp_device = $route{Iface};
                 last DIAL_ATTEMPT;
             }
         }
         close ROUTE or snuff_it(-1, "Couldn't close route file, error $!");
         if (time - $pppd_start_time > $opt_max_time_per_attempt) {
             logmsg "PPPD seems to be wedged, shooting...";
             kill_pppd();
             next DIAL_ATTEMPT;
         }
#    verbose "Failed to find route, sleeping...";
         sleep 2;
     }
 }

# We're now connected.
logmsg "Dialout started: ", scalar(gmtime($pppd_start_time));

if(defined &main::once_connected) {
    eval '&once_connected' or die "once_connected command failed: $@, stopped";
}

# Now wait for some idle time.

my $prev_usage = get_usage($ppp_device);
my $initial_deadline = $pppd_start_time + $opt_min_time;
my $time_now = time;
my $use_deadline = $time_now + $opt_max_idle_time;
my $pjob_deadline = $time_now + $opt_pjob_interval;
my $final_deadline = $pppd_start_time + $opt_max_time;
SAMPLE: while (1) {
    sleep($opt_sampling_interval);
    if (is_pppd_dead()) {
        snuff_it(-1, "PPPD is dead");
    }
    $time_now = time;
    if ($time_now >= $final_deadline) {
        snuff_it(0, "PPPD maximum dialout time reached");
    }
    my $usage = get_usage($ppp_device);
    if ($usage > $prev_usage) {
        if ($opt_monitor_idle) {
            logmsg "Still working with ", $use_deadline - $time_now, 
                " seconds to go";
        }
        $prev_usage = $usage;
        $use_deadline = $time_now + $opt_max_idle_time;
    } else {
        if ($opt_monitor_idle) {
            logmsg "all quiet with ", $use_deadline - $time_now, 
                " seconds to go";
        }
        if ($time_now >= $initial_deadline && $time_now >= $use_deadline) {
            snuff_it(0, "PPPD long idle time detected.");
        }
    }
    if ($time_now >= $pjob_deadline) {
        if (defined &main::do_pjob) {
            eval '&do_pjob' or snuff_it(0, "pjob command failed: $@.");
        }
	$pjob_deadline = $time_now + $opt_pjob_interval;
    }
}
