#!/usr/bin/perl

#    login_monitor
#    Copyright (C) 2003 by Tim Niemueller <tim@niemueller.de>
#    Website: http://www.niemueller.de/software/perl/loginmonitor/
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.

#    Created  : 04.04.2003


#### Modules
use strict;
use Getopt::Long;
use POSIX qw(setsid);
use Fcntl ':flock';

#### Constants, just to make code readable
my $NAME="login_monitor";
my $VERSION="0.1";
my $max_idle="00:05";
my $who="who";
my $shell="bash";

#### Get parameters
my %params=();
GetOptions("maxidle=i" => \$params{'maxidle'},
           "d" => \$params{'d'},
           "s" => \$params{'s'},
           "h" => \$params{'h'},
           "t:i" => \$params{'t'},
           "ignore:s" => \$params{'ignore'},
           "shell:s" => \$params{'shell'},
           ) or usage();
if ( $params{'h'} ) {
  usage();
}

if ($params{'t'} eq "") {
  $params{'t'} = 180;
}
if ($params{'maxidle'} eq "") {
  $params{'maxidle'} = $max_idle;
}
if ($params{'shell'} eq "") {
  $params{'shell'} = $shell;
}
if ($params{'s'}) {
  use Sys::Syslog qw(:DEFAULT setlogsock);
  setlogsock('unix');
  openlog("login_monitor", "", "authpriv");
}

# Where should we create the lock file?
my $lockfile = "/var/lock/login_monitor.lock";

#### Signals
$SIG{'TERM'} = \&terminate_daemon;
$SIG{'HUP'} = \&check_status;

#### Globals to make it quick and dirty
my $oldaddr = "";

#### Main Program, main() like

daemonize();
create_lock();

while (1) {
  my $na = check_status();
  sleep $params{'t'};
}


#### Subs related to time handling and alarms
sub check_status {

  my $output=qx(LANG=C $who -l -u);
  my @out=split(/\n/, $output);

  for (@out) {
    my ($user, $tty, $month, $day, $time, $idle, $host) = split(/\s+/);
    # logmsg("info", "Checking: $user $tty $month $day $time $idle $host");
    next if ($idle eq ".");
    next if ($host eq ""); # local login will not get kicked

    if (($params{'ignore'} eq "") || ($host !~ /$params{'ignore'}/)) {
      my ($idlehour, $idlemin) = split(/:/, $idle, 2);
      $idlemin += $idlehour * 60;
      my ($maxidlehour, $maxidlemin) = split(/:/, $params{'maxidle'}, 2);
      $maxidlemin += $maxidlehour * 60;
      if ($idlemin > $maxidlemin) {
        # user is logged in too long from non allowed hosts
        # Find shells for this user and kick 'em
        my $pids=qx(ps ax | grep "$tty" | grep "$params{'shell'}" | grep -v grep | awk '{ print \$1 }');
        my @pids=split(/\n/, $pids);
        logmsg("info", "Going to kick user \"$user\", PIDs: ".join(", ", @pids));
        if (scalar(@pids) == 0) {
          logmsg("warning", "Cannot kick user \"$user\", no PIDs found matching shell \"$params{'shell'}\"");
        }
        foreach my $p (@pids) {
          system("kill -9 $p");
          # logmsg("info", "Would kick $user, $p, $idle, $params{'maxidle'}");
        }
        my $pids=qx(ps ax | grep "$tty" | grep "$params{'shell'}" | grep -v grep | awk '{ print \$1 }');
        my @pids=split(/\n/, $pids);
        if (scalar(@pids) > 0) {
          logmsg("warning", "Shell processes for user \"$user\" left after kick. Next try in $params{'t'} seconds");
        } else {
          logmsg("info", "User \"$user\" successfully kicked");
        }
      }
    }
  }
}


#### Subs related to basic program stuff (daemon, fifo, lock etc.)

# Could be modified to syslog for example
sub logmsg {
  my $priority = shift;
  my $msg = shift;

  if ($params{'d'}) {
    my $now=localtime();
    my $msgrep = sprintf($msg, @_);
    print "$now ($priority): $msgrep\n";
  } else {
    if ($params{'s'}) {
      syslog($priority, $msg, @_);
    }
  }
}

# logs the errors and exits the program
sub failure_exit {
  logmsg($_[0]);
  die $_[0];
}


# Disconnects from console
sub daemonize {
  if (! $params{'d'}) {
    my $pid;
    chdir '/' or failure_exit("Can't chdir to /: $!");
    open STDIN, '/dev/null' or failure_exit("daemonize: Can't read /dev/null: $!");
    open STDOUT, '>/dev/null' or failure_exit("daemonize: Can't write to /dev/null: $!");
    defined($pid = fork) or failure_exit("Can't fork: $!");
    exit 0 if $pid;
    setsid() or failure_exit("Can't start a new session: $!");
    print LOCK $$;
  }
  logmsg("info", "$NAME startup successful");
}

# creates the lockfile
sub create_lock {
  open(LOCK, ">$lockfile");
  my $ok = flock(LOCK, LOCK_EX | LOCK_NB);
  failure_exit("It seems that another instance is already running") if (! $ok);
}


# removes the lockfile
sub remove_lock {
  flock(LOCK, LOCK_UN);
  close(LOCK);
  unlink $lockfile;
}

# Terminates daemon closing ISDN connection,
# used as signal handler
sub terminate_daemon {
  alarm 0;                      # Stop timer
  remove_lock();
  logmsg("info", "Stopped");
  if ($params{'s'}) {
    closelog();
  }
  exit;
}

# prints some basic usage message
sub usage {
  print "login_monitor v$VERSION\n",
        "Copyright (C) 2003 by Tim Niemueller\n\n",
        "Monitors remote user logins and kicks when too long idle\n",
        "Usage: login_monnitor [options]\n\n",
        "where options are:\n",
        "--shell s  : Kill all process with name s on users tty on kick\n",
        "--ignore i : Ignore all users whose remote host contains i\n",
        "-t t       : check every t seconds if connection parameters have changed\n",
        "             Dafault: 180 seconds\n",
        "-d         : Debug mode. Do not fork to background, log output to STDOUT\n",
        "-s         : Use Syslog for logging. Default is no logging.\n",
        "\n";
  exit 1;
}
 

### END.
