#!/usr/bin/perl -w

use strict;

use Net::Ping;
use Sys::Syslog;
use File::Basename;
use File::stat;

my $iface = "eth2";
my $NAME = basename $0;
my $verbose = 0;
my $apache_uid = 48;
my $apache_gid = 48;

log_info("Daemon started up");
# Loop forever... it's a daemon afterall
while ( 1 == 1 ) {
  &check_old_authed();
  &check_new_authed();
  sleep 10;
}
# Ok so this won't be called ever... at least not until I write a sig handler.
log_info("Daemon exiting");

# Look for already authed wireles IPs/MAC combos, do a ping to see
#  if they're live, and make sure arp shows known MAC.  If either 
#  it doesn't ping or mac doesn't match, remove iptables rules for
#  the IP/MAC combo.
# This makes sure that a host's access is removed ASAP after a host goes offline.
#  this should minimize the chances of someone being able to jump onto the wireless net after an
#  authenticated user goes offline.
sub check_old_authed {
  my $file = "/tmp/known_wireless";
  my $write_file = 0;
  my %known_pairs = ();

  if (-f "$file" ) {
    open(KNOWN,"$file") || my_die("Can't open known file $file",1);
    while(<KNOWN>) {
      chomp;
      if (/(10.0.1.\d+)\s([\d|\w]+:[\d|\w]+:[\d|\w]+:[\d|\w]+:[\d|\w]+:[\d|\w]+)/) {
        my $ip = $1;
        my $mac = $2;
        if (!ping_host($ip,$mac)) {
          revoke_auth($ip,$mac);
          $write_file = 1;
        } else {
          $known_pairs{$ip}=$mac;
        }
      }
    }
    close(KNOWN);
    if ($write_file) {
      write_out_known(%known_pairs);
    }
  }
}

# Subroutine to look for the expected files in /tmp that are written out by the auth.cgi module,
#  make sure they're ok, then run the iptables script to give the ip/mac combo net access.
sub check_new_authed {
  my $added = 0;
  my %known_hosts = ();
   my $known_file = "/tmp/known_wireless";
  open(LS, "/bin/ls -1 /tmp/new_auth*|") ||  my_die("Can't run /bin/ls -1 /tmp/new_auth*",1);
  while (<LS>) {
    my $ip = "";
    my $mac = "";
    chomp;
    my $file = $_;
    # Do a quick test to make sure the file is owned by the right uid/gid.. quasi security.
    # Possible DOS attack if user could touch file first.  Not an issue on my setup, I don't
    # have other users that shell into my box.
    my $info = stat($file);
    my $file_uid = $info->uid;
    my $file_gid = $info->gid;
    if ( ($file_uid != $apache_uid) || ($file_gid != $apache_gid) ) {
      log_info("saw file with improper uid/gid: uid: $file_uid gid: $file_gid");
    } else {
      if ($file =~ /new_auth(\d+\.\d+\.\d+\.\d+)/) {
        $ip = $1;
        open(FILE,"$file") || my_die("Couldn't open file $file",1);
        $mac = <FILE>;
        chomp $mac;
        close(FILE);
      }
    }
    unlink($file) || my_die("Couldn't unlink file $file\n",1);
    if ( ("$ip" ne "") && ("$mac" ne "") ) {
      grant_auth($ip,$mac);
      $known_hosts{$ip}=$mac;
      $added=1;
    }
  }
  if ($added) {
    if ( -f "$known_file" ) {
      open(IN,"$known_file") || my_die("Can't open file $known_file",1);
      while (<IN>){
        if (/(10.0.1.\d+)\s([\d|\w]+:[\d|\w]+:[\d|\w]+:[\d|\w]+:[\d|\w]+:[\d|\w]+)/) {
          $known_hosts{$1}=$2;
        }
      }
    }
    write_out_known(%known_hosts);
  }
}

# My die routine to write into to syslog first for admin to troubleshoot.
sub my_die {
  my $msg = shift;
  my $fatal = shift;
  if ($fatal) {
    my $date = log_date();
    openlog("$date $NAME", "cons,pid", "user");
    syslog("local1|info","fatal: $msg");
    closelog();
    exit 1;
  }
}

# Subroutine that pings hosts passed to it, returns 0 if the host is either not online or
#  if the mac seen in the arp table after the ping doesn't match what's expected.
sub ping_host {
  my $ip = shift;
  my $mac = shift;
  my $seen_mac = "";

  print "Pinging host $ip $mac" if $verbose;
  my $p = Net::Ping->new();
  if ($p->ping($ip,7)) {
    $p->close();
    open(ARP,"/sbin/arp $ip|") || my_die("Couldn't arp host $ip",0);
    while (<ARP>) {
      next if /^Address/;
      if (/^$ip\s+ether\s+([\d|\w]+:[\d|\w]+:[\d|\w]+:[\d|\w]+:[\d|\w]+:[\d|\w]+)\s+.*$iface/) {
        $seen_mac = $1;
      }
    }
    close(ARP);
    if ("$seen_mac" eq "$mac") {
      print "ok\n" if $verbose;
      return 1;
    }
  }
  $p->close();
  print "no answer\n" if $verbose;
  return 0;
}

# Logs to syslog and calls script that removes iptables rules allowing passed ip/mac pair 
#  net access.
sub revoke_auth {
  my $ip = shift;
  my $mac = shift;
  print "About to run /usr/sbin/wireless-forwarding deny $ip $mac\n" if $verbose;
  system("/usr/sbin/wireless-forwarding deny $ip $mac");
  log_info("revoked internet access from $ip $mac");
}

# Logs to syslog and calls script that adds iptables rules allowing passed ip/mac pair 
#  net access.
sub grant_auth {
  my $ip = shift;
  my $mac = shift;
  print "About to run /usr/sbin/wireless-forwarding allow $ip $mac\n" if $verbose;
  system("/usr/sbin/wireless-forwarding allow $ip $mac");
  open(CONF,">/tmp/auth_done$ip$mac");
  chown($apache_uid,$apache_gid,("/tmp/auth_done$ip$mac"));
  print CONF "done";
  close(CONF);
  log_info("Granted internet access to $ip $mac");
}

# Writes the known ip/mac pairs to the /tmp/known_wireless file
sub write_out_known {
  my %known = @_;
  my $file = "/tmp/known_wireless";

  unlink($file) || my_die("Couldn't unlink $file",1);
  open(KNOWN,">$file") || my_die("Couldn't open file $file for writing",1);
  foreach my $ip (keys %known) {
    print KNOWN "$ip $known{$ip}\n";
  }
  close(KNOWN);
  chmod(600,$file);
}

# Subroutine to log syslog info... saved some typing making it a sub
sub log_info {
  my $msg = shift;
  my $date = log_date();
  openlog("$date $NAME", "cons,pid", "user");
  syslog("local1|info","info: $msg");
  closelog();
}

sub log_date {
  my $date = localtime(time());
  $date =~ s/^\w+\s//g;
  $date =~ s/\s\d\d\d\d$//g;
  return $date;
}