#!/usr/bin/perl
######################################################################
# 
# http://www.unspecific.com/spa/
# 
#
# Copyright (c) 2005, MadHat (madhat@unspecific.com)
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
#   * Redistributions of source code must retain the above copyright
#     notice, this list of conditions and the following disclaimer.
#   * Redistributions in binary form must reproduce the above copyright
#     notice, this list of conditions and the following disclaimer in
#     the documentation and/or other materials provided with the distribution.
#   * Neither the name of MadHat Productions nor the names of its
#     contributors may be used to endorse or promote products derived
#     from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
######################################################################
# config file for the server
my $server_conf = '/usr/local/etc/spa.conf';

# passphrase for decrypting GPG packets (TMP Solution)
my $passphrase = "simplenomad";

######################################################################
#  You should not change anything below here unless you
#    know what you are doing

use GnuPG::Interface;
use Sys::Syslog qw(:DEFAULT setlogsock);

my $VERSION = 0.4;
my %loaded, %config;

# If we receive a HUP, we reload all configs
#    user_config, server_config, firewall state
$SIG{HUP}=\&reload_conf;

# Call reload to load config the first time
&reload_conf;

if ($config{DEBUG}) {
  if (setlogsock('unix')) {
    openlog('spad_engine', '', LOG_USER);
    syslog(LOG_INFO, 'Launching spa_engine', '');
  } else {
    die("Failed to open syslog");
  }
}


syslog(LOG_INFO, 'OPENING FIFO', '') if ($config{DEBUG});
open(FIFO, "< /var/spa/spa") or syslog(LOG_INFO, "Can't open FIFO: $!", '');

syslog(LOG_INFO, 'READING FIFO', '') if ($config{DEBUG});
# while wwe have data coming from the spad
while (<FIFO>) {
  # data passed via FIFO (/var/spa/spa)
  # src_ip data_size data
  # OR
  # src_ip dst_port
  syslog(LOG_INFO, "Adding " . length($_) . " bytes of new data from FIFO",
'') if ($config{DEBUG} > 2);
  if ($config{DEBUG} > 4) {
    # If we are running in DEBUG mode, display the packet in HEX in the
    # syslog, 8 bytes per line
    my @bytes = split('', $_);
    my $raw_data = '';
    for (0..$#bytes) {
      $raw_data .= " " . unpack('H*', $bytes[$_]);
      if (($_ + 1) % 8 == 0 and $raw_data) {
        syslog(LOG_INFO, "PACKET:%s", $raw_data);
        undef $raw_data;
      }
    }
    syslog(LOG_INFO, "PACKET:%s", $raw_data);
  }

  $data .= $_;
  if (!$length) {
    @test_data = split(' ', $_);
    $length = $test_data[1];
  }
  if ($test_data[1] =~ /^d{1,5}$/) {
    syslog(LOG_INFO, "FIFO reported $test_data[1] bytes of data sent", '')
       if ($config{DEBUG} > 2);
    my $diff = abs(length($_) - $test_data[1]);
    syslog(LOG_INFO, "FIFO is off by %d bytes", length($_) - $test_data[1])
       if ($config{DEBUG} > 2);
  } elsif (length($data) >= $length) {
    syslog(LOG_INFO, "Looks like we got it all, processing", '')
       if ($config{DEBUG} > 2);
    &process($data);
    $data = '';
    $length = '';
  } else {
    syslog(LOG_INFO, "Total Data Collected: %d bytes (expecting $length bytes)",
      length($data)) if ($config{DEBUG} > 2);
    next;
  }
}

syslog(LOG_INFO, 'CLOSING FIFO', '') if ($config{DEBUG});

if (&save_user_config($config{UserConf}) == -1) {
  syslog(LOG_INFO, 'ERROR WRITING CONFIG', '');
}

exit 0;

######################################################################
# Process the packet from the FIFO
# 
sub process {
  my ($buffer) = @_;
  my $new_seq;
  syslog(LOG_INFO, "Processing %d bytes of FIFO Data", length($buffer))
    if ($config{DEBUG});
  if ($config{DEBUG} > 4) {
    # If we are running in DEBUG mode, display the packet in HEX in the
    # syslog, 8 bytes per line
    my @bytes = split('', $buffer);
    for (0..$#bytes) {
      $raw_data .= " " . unpack('H*', $bytes[$_]);
      if (($_ + 1) % 8 == 0 and $raw_data) {
        syslog(LOG_INFO, "PACKET:%s", $raw_data);
        undef $raw_data;
      }
    }
    syslog(LOG_INFO, "PACKET:%s", $raw_data);
    undef $raw_data;
  }
  $buffer =~ /^([\d\.]+) (\d+) (.+)$/s;
  my $src_ip = $1;
  my $length = $2;
  my $gpg = $3;
  if ( $gpg ) {
    syslog(LOG_INFO, 'Data in FIFO looks encrypted', '') if ($config{DEBUG});
    if ($src_ip and $config{DEBUG}) {
      syslog(LOG_INFO, "Received " . length($gpg) . " bytes of data from: $src_ip", '');
    }
    my $gnupg = GnuPG::Interface->new();
    syslog(LOG_INFO, "Setting homedir for gnupg: $config{GPGDir}", '')
      if ($config{DEBUG});
    $gnupg->options->hash_init( homedir => $config{GPGDir} );
    # added $passphrase_fh -SN
    my ( $input, $output, $error, $passphrase_fh, $status_fh ) = (
      IO::Handle->new(),
      IO::Handle->new(),
      IO::Handle->new(),
      IO::Handle->new(),
      IO::Handle->new(), # added -SN
    );
    syslog(LOG_INFO, 'Setting handles for gnupg', '') if ($config{DEBUG});
    my $handles = GnuPG::Handles->new(
      stdin  => $input,
      stdout => $output,
      stderr => $error,
      passphrase => $passphrase_fh, # set handle -SN
      status => $status_fh,
    );
    syslog(LOG_INFO, 'Launching gnupg', '') if ($config{DEBUG});
    my $pid = $gnupg->decrypt( handles => $handles );
    print $passphrase_fh $passphrase; # load in passphrase -SN
    close $passphrase_fh;             # close handle -SN
    syslog(LOG_INFO, "Launched gnupg %s", $pid) if ($config{DEBUG});
    print $input $gpg;
    close $input; # <== moved this before reading the output -SN
    waitpid $pid, 0;
    my @plaintext = <$output>;
    my @error = <$error>;

    my @stat = <$status_fh>;
    close $output;
    close $error;
    close $status_fh;
    if (@error) {
      for my $e (@error) {
        syslog(LOG_INFO, "GnuPG Error: %s", $e) if ($config{DEBUG} > 2);
        if ($e =~ /using \w+ key ID (\w+)/) {
          $gpg_id = $1;
        } elsif ($gpg_id and ($e =~ /Good signature from/)) {
          $good = 1;
        }
      }
      if (!$gpg_id or !$good) { 
        syslog(LOG_INFO, "Unable to verify signature", );
        return;
      }
    }
    if (@plaintext and $config{DEBUG}) {
      syslog(LOG_INFO, "DECODED with gnupg decrypt: %s", join(' ', @plaintext));
    }
    if (@stat and $config{DEBUG} > 2) {
      for my $m (@stat) {
        syslog(LOG_INFO, "GnuPG status: %s", $m);
      }
    }

    my ($time, $seq, $user_ip, $mode, $data) 
       = split(':', join(' ', @plaintext));
    syslog(LOG_INFO, "Decrypted data: Time %s Seq %s IP %s Mode %s Data %s",
       $time, $seq, $user_ip, $mode, $data) if ($config{DEBUG});
    if (!$time) {
      return;
    }
    if (!$user_ip or $user_ip == 0) {
      $user_ip = $src_ip;
    }

    my @now = gmtime();
    $now[5] = $now[5] + 1900;
    for ($i = 0; $i < 5; $i++) {
      if (length($now[$i]) < 2) {
        $now[$i] = "0$now[$i]";
      }
    }
    my $now = "$now[5]$now[4]$now[3]$now[2]$now[1]$now[0]";
    if (abs($time - $now) > $config{PacketTimeout} ) {
      syslog(LOG_INFO, "packet appeared to be 'old'", '') 
        if ($config{DEBUG});
      syslog(LOG_INFO, "$time vs. $now\n", '') if ($config{DEBUG});
      next; # next fifo
    } 

    my @new_seq = split(',', $seq);
    for my $seq (@new_seq) {
      if ($seq == $config{$gpg_id}{seq1} or $seq == $config{$gpg_id}{seq2}) {
        $new_seq = $new_seq[1] . "," . $new_seq[2];
        last;
      } else {
        syslog(LOG_INFO, "ERROR R:$seq S1:$config{$gpg_id}{seq1} S2:$config{$gpg_id}{seq2} ", '') if ($config{DEBUG});
        next;
      }
    }
    if (!$new_seq) {
      syslog(LOG_INFO, 'Sequence numbers did not match', '')
        if ($config{DEBUG});
      next; # next fifo
    } else {
      $config{$gpg_id}{seq1} = $config{$gpg_id}{seq2};
      $config{$gpg_id}{seq2} = $new_seq[2];
    }

    # save config
    if (&save_user_config($config{UserConf}) == -1) {
      syslog(LOG_INFO, 'ERROR WRITING CONFIG', '');
    }

    # Modes 
    # 1 Firewall
    #   IP port
    # 2 spa defined scrpts (eventually)
    #   callback
    # 3 user defined
    #   /usr/local/bin/command
    if ($config{$gpg_id}{$mode}) {
      # if Mode is the firewall flipper
      if ($mode == 1) {
        # flip the firewall
        my $rc = &flipfw($user_ip, $data, 2);
        # if it opened the firewall
        if ($rc == 1) {
          syslog( LOG_INFO, "initializing timeout counter ($user_ip -> $data)\n", '') if ($config{DEBUG});
          $loaded{$user_ip}{$data} = time;
        # if it closed the firewall
        } elsif($rc == -1) {
          syslog( LOG_INFO, "clearing timeout counter ($user_ip -> $data)\n",
            '') if ($config{DEBUG});
          undef($loaded{$user_ip}{$data});
        }
        &save_fw_state($config{FirewallState});
      } elsif ($mode == 2) {
      } elsif ($mode == 3) {
        # exec a command
        my @commands = split (',', $config{$gpg_id}{$mode});
        for my $cmd (@commands) {
          if ($cmd eq $data ) {
            system($data);
          }
        }
      } else {
        next; # next fifo
      }
    } else {
      syslog( LOG_INFO, "Command request not allowed by $gpg_id\n",'');
    }
  } else {
    my ($src_ip, $dst_port) = @data;
    if ($config{DEBUG} > 2) {
      syslog( LOG_INFO, $#data + 1 . " pieces of data in FIFO\n",'');
      for my $ip (sort keys %loaded) {
        syslog( LOG_INFO, "Listing: $ip\n",'');
        for my $port (sort keys %{$loaded{$ip}}) {
          syslog( LOG_INFO, "    $key -=> $loaded{$ip}{$port}\n",'');
        }
      }
    }
    if ($loaded{$src_ip}{$dst_port}) {
      if (&flipfw($src_ip, $dst_port, 1) == 1) {
        if ( $loaded{$src_ip}{$dst_port} - time > $config{FirewallTimeout}) {
          my $rc = &flipfw($src_ip, $data, 2);
          if ($rc == -1) {
            syslog( LOG_INFO, "closed fw due to timeout\n",'') 
              if ($config{DEBUG});
            undef $loaded{$src_ip}{$dst_port};
          }
        } else {
          syslog( LOG_INFO, "updated last time port was used\n",'')
            if ($config{DEBUG});
          $loaded{$src_ip}{$dst_port} = time;
        }
      } else {
        undef $loaded{$src_ip}{$dst_port};
      }
      &save_fw_state($config{FirewallState});
    } else {
      syslog( LOG_INFO, "packet didn't match\n",'') if ($config{DEBUG});
    }
  }
}

######################################################################
# Loading of the server config
# 
sub load_server_config {
  my($file) = @_;
  syslog( LOG_INFO,  "SERVER CONFIG: loading from $file\n",'') 
    if ($config{DEBUG});
  open(FH, $file) or syslog(LOG_CRIT, "SERVER CONFIG ERROR: Unable to open $file: $!\n", '');
  while (<FH>) {
    chomp;
    next if (/^#/ or /^\s*$/);
    my ($key, $value) = split(' ', $_);
    $config{$key} = $value;
  }
}
######################################################################
sub load_user_config {
  # User config Format
  # gpg_id local_username seq_no2,seq_no3 mode:data [mode:data]
  # 987654 local_user seq_no1,seq_no2 1:21,110,445 2:spa_scripts 3:/usr/local/bin/blah,/usr/bin/blah
  my($file) = @_;
  syslog( LOG_INFO,  "USER CONFIG: loading from $file\n",'')
    if ($config{DEBUG});
  open(FH, $file) or syslog(LOG_CRIT, "USER CONFIG ERROR: Unable to open $file: $!\n", '');
  while (<FH>) {
    chomp;
    next if (/^#/ or /^\s*$/);
    my($gpg_id, $user, $seq, @data) = split(' ', $_);
    $config{$gpg_id}{user} = $user;
    my $c = 0;
    for $inseq (split(',', $seq)) {
      $c++;
      $config{$gpg_id}{"seq$c"} = $inseq;
    }
    for $command (@data) {
      my ($mode, $allowed) = split(':', $command);
      $config{$gpg_id}{$mode} = $allowed;
    }
  }
  if ($config{DEBUG} > 2) {
    for my $gpg_id (sort keys %config) {
      syslog( LOG_INFO,  "Loading: $gpg_id\n",'');
      for my $key (sort keys %{$config{$gpg_id}}) {
        syslog( LOG_INFO,  "    $key -=> $config{$gpg_id}{$key}\n",'') if ($key);
      }
    }
  }
}

######################################################################
#
sub save_user_config {
  my($file) = @_;
  # User config Format
  # gpg_id local_username seq_no2,seq_no3 mode:data [mode:data]
  syslog( LOG_INFO,  "Write Config\n",'') if ($config{DEBUG});
  open(FH, ">$file") or return -1;
  for my $gpg_id (sort keys %config) {
    # since %config has data other than user config, we test
    # for GPGID format before writing 
    next if ($gpg_id !~ /^[0-9A-F]{8}$/);
    syslog( LOG_INFO,  "Writing: $gpg_id\n",'') if ($config{DEBUG});
    print FH "$gpg_id $config{$gpg_id}{user} $config{$gpg_id}{seq2},$config{$gpg_id}{seq2} 1:$config{$gpg_id}{1} 2:$config{$gpg_id}{2} 3:$config{$gpg_id}{3}\n";
    if ($config{DEBUG} > 1) {
      syslog( LOG_INFO,  "$gpg_id $config{$gpg_id}{user} $config{$gpg_id}{seq1},$config{$gpg_id}{seq2} 1:$config{$gpg_id}{1} 2:$config{$gpg_id}{2} 3:$config{$gpg_id}{3}\n",'');
      for my $key (sort keys %{$config{$gpg_id}}) {
        syslog( LOG_INFO,  "    $key -=> $config{$gpg_id}{$key}\n",'');
      }
    }
  }
  close(FH);
}

######################################################################
sub flipfw {
  my ($src, $dpt, $return) = @_;
  # src => src ip
  # dpt => dst port
  # return, default == 1
  #  1 => return state == -1 closed, 0 error, 1 open
  #  2 => flip and return == -1 closed, 0 error, 1 open
  if ($return != 2) { $return = 1; }
  if ($dpt !~ /^\d{1,5}$/ or
      $src !~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/) {
    syslog( LOG_INFO,  "Sent bad data\n", '') if ($config{DEBUG});
    return 0;
  }
  if ($config{IPFW} =~ /(iptables|ipchains)$/) {
    $list = `$config{IPFW} -L --line-numbers -n`;
  } elsif ($config{IPFW} =~ /ipfw$/ ) {
    $list = `$config{IPFW} list`;
  } else {
    syslog( LOG_INFO,  "unknown IPFW variable\n", '') if ($config{DEBUG});
    return 0;
  }
  @list = split "\n", $list;
  for my $line (@list) {
    next if ($line =~ /^\s*$/);
    next if ($line =~ /^num/);
    syslog(LOG_INFO, "$line\n", '') if ($config{DEBUG} > 2);
    if (
       ($config{IPFW}=~ /(iptables|ipchains)$/ ) and
       ($line =~ /^Chain input /i) 
      ) {
      ($C, $i) = split(' ', $line);
      $chain = $i;
      syslog(LOG_INFO, "Found chain: $chain\n", '') 
        if ($config{DEBUG} > 1);
      next;
    } elsif (
        ($line =~ /^Chain /) and
        ($config{IPFW} =~ /(iptables|ipchains)$/)
      ) {
      if ($chain and !$rule) {
        syslog(LOG_INFO, " Port is not open on FW\n", '') 
          if ($config{DEBUG});
        if ($return == 2) {
          syslog(LOG_INFO, "opening port (tcp) $src -> here:$dpt\n", '')
            if ($config{DEBUG});
          syslog( LOG_INFO,  "$config{IPFW} -I $chain 1 -p tcp --dport $dpt --src $src -j ACCEPT\n", '')
            if ($config{DEBUG} > 2);
          $rc = `$config{IPFW} -I $chain 1 -p tcp --dport $dpt --src $src -j ACCEPT`;
          syslog(LOG_INFO, $rc, '') if ($rc and $config{DEBUG} > 2);
          return 1;
        } else {
          syslog(LOG_INFO, "port is closed from $src to here:$dpt", '') 
            if ($config{DEBUG} > 1);
          return -1;
        }
      } elsif ($chain and $rule) {
        syslog(LOG_INFO,  "Port is open on FW\n", '') if ($config{DEBUG});
        if ($return == 2) {
          syslog(LOG_INFO,  "closing port (tcp) $src -> here:$dpt", '')
            if ($config{DEBUG});
          syslog(LOG_INFO,  "$config{IPFW} -D $chain $rule\n", '')
            if ($config{DEBUG} > 2);
          $rc = `$config{IPFW} -D $chain $rule`;
          syslog(LOG_INFO,  $rc, '') if ($rc and $config{DEBUG} > 2);
          return -1;
        } else {
          syslog(LOG_INFO, "port is open from $src to here:$dpt", '') 
            if ($config{DEBUG} > 1);
          return 1;
        }
      }
      $chain = '';
      next;
    } elsif ( $config{IPFW} =~ /ipfw$/ and $rule) {
      if ($return == 2) {
        syslog(LOG_INFO,  "closing port (tcp) $src -> here:$dpt", '')
          if ($config{DEBUG});
        syslog( LOG_INFO,  "$config{IPFW} delete $rule\n", '')
          if ($config{DEBUG} > 2);
        $rc = `$config{IPFW} delete $rule`;
        syslog(LOG_INFO, $rc, '') if ($rc and $config{DEBUG} > 2);
        return -1;
      } else {
        syslog(LOG_INFO, "port is open from $src to here:$dpt", '') 
          if ($config{DEBUG} > 1);
        return 1;
      }
    } elsif ( $config{IPFW} =~ /ipfw$/ and !$rule) {
      $chain = 1;
    }
    if ($chain) {
      if ($config{IPFW} =~ /(iptables|ipchains)$/) {
        ($num, $target, $prot, $opt, $source, $destination, @other) = split(' ', $line);
      } elsif ($config{IPFW} =~ /ipfw$/) {
        ($num, $act, $prot, $from, $source, $to, $destination, $dstport, @other) = split(' ', $line);
      }
      push @num, $num;
      if ($source eq $src and (
             ($other[1] eq "dpt:$dpt" and $config{IPFW} =~ /iptables$/) or
             ($other[2] == $dpt and $config{IPFW} =~ /ipchains$/) or
             ($dstport == $dpt and  $config{IPFW} =~ /ipfw$/)
           )
         ) {
        $rule = $num;
      }
    }
  }
  if ($return == 2) {
    syslog(LOG_INFO, "opening port (tcp) $src -> here:$dpt", '') 
      if ($config{DEBUG});
    $insert = 0;
    while ($num[$insert] == $insert + 1) {
      $insert++;
    }
    $insert++;
    syslog(LOG_INFO, "ipfw add $insert allow tcp from $src to any $dpt\n", '')
      if ($config{DEBUG} > 2);
    $rc = `ipfw add $insert allow tcp from $src to any $dpt`;
    syslog(LOG_INFO,  $rc, '') if ($config{DEBUG});
    return 1;
  } else {
    syslog(LOG_INFO, "port is closed from $src to here:$dpt", '') 
      if ($config{DEBUG} > 1);
    return -1;
  }
}

######################################################################
sub save_fw_state {
  my ($file) = @_;
  open(FW, ">$file") or return -1;
  for my $ip (sort keys %loaded) {
    for my $port (sort keys %{$loaded{$ip}}) {
      print FW "$ip $port $loaded{$ip}{$port}\n";
    }
  }
  close(FW);
}

######################################################################
sub load_fw_state {
  my ($file) = @_;
  open(FW, "$fw") or return -1;
  while (<FW>) {
    my ($ip, $port, $time) = split(' ');
    if (&flipfw($ip, $port, 1) == 1) {
      $loaded{$ip}{$port} = $time;
    }
  }
  close(FW);
}

######################################################################
sub reload_conf {
  undef %config;
  undef %loaded;
  &load_user_config($config{UserConf});
  &load_server_config($server_conf);
  &load_fw_state($config{FirewallState});
}


