#!/usr/bin/perl -W
###############################################################################
#
# doorman.pl - Use inexpensive USB RFID reader to allow access to a physical
#              portal controlled by an inexpensive USB relay, via a background
#              HTTP portal for authentication.
#
#################################################################[ AJC 2018 ]##
use strict;
use Linux::Input;
use Data::Dumper;
use WWW::Mechanize;
use File::Basename qw{basename};
use IO::Select;

###############################################################################
#
# Configuration directoves
#
###############################################################################

# Direct /dev link to device.  Use by-id when possible
my $device = "/dev/input/by-id/usb-13ba_Barcode_Reader-event-kbd";

my $usbrelay = "/usr/bin/usbrelay";
my $usbrelay_device = "3X9XI_1";

# The URL of the authentication server
my $url = "http://doors.pfv.turnsys.net/";

# Location of syslog's userspace logger utility
my $rsyslog = "/usr/bin/logger";

# Card IDs that bypass network auth
my %backdoors = (

  "0009399422" => 'Charles NW',
  "0009106067" => 'Josef C',

);

# For logging syslog messages to stdout as well as syslog.
my $debug = "true";
#my $debug = "false";

###############################################################################
#
# Subroutines
#
###############################################################################

#################################################
# logger - Send logs to syslog, or stdout if
#          DEBUG is defined.
#################################################
sub logger {

  my $logtext = shift;
  if ($debug eq "true") { print localtime()  . " " . basename($0) . ": " . "$logtext\n"  };
  system( $rsyslog . " \"" . basename($0) . ": " . "$logtext\"" );
  
}

#################################################
# unlock_door - Handle the USB relay to unlock
#               actuator.
#################################################
sub unlock_door() {
  
  system $usbrelay . " " . $usbrelay_device . "=1 >/dev/null 2>&1";
  sleep 10;
  system $usbrelay . " " . $usbrelay_device . "=0 >/dev/null 2>&1";
  logger "Locking door.";

  return 0;

}

#################################################
# authenticate - check backdoors and legit HTTP
#                source to see if ID is valid.
#                Unlocks door if appropriate.
#################################################
sub authenticate {

  my $id = shift;

  if ( defined $backdoors{$id} ) {

    logger $backdoors{$id} . " override key accepted.  Unlocking door.";
    unlock_door;

  } else {

    # Open a connection to auth server.
    my $connection = WWW::Mechanize->new( autocheck => 0, quiet => 1);
    $connection->get ($url . $id);
  
    # Touch a file named as the card ID number to make the card work.
    if ($connection->status == 200) {

      logger "Card $id is valid.  Unlocking door.";
      unlock_door;

    } else {

      # File is not cleanly returned, so it's invalid.
      logger "Card $id is invalid! (" . $connection->status() . ")";

    }

  }

}

#################################################
# keytranslate - Reduce the returned keypress 
#                value, and translate into the
#                actual number pressed.
#################################################
sub keytranslate {

  my $keypress = shift;

  return 1 if ($keypress == 30);
  return 2 if ($keypress == 31);
  return 3 if ($keypress == 32);
  return 4 if ($keypress == 33);
  return 5 if ($keypress == 34);
  return 6 if ($keypress == 35);
  return 7 if ($keypress == 36);
  return 8 if ($keypress == 37);
  return 9 if ($keypress == 38);
  return 0 if ($keypress == 39);
  return $keypress;

}


###############################################################################
# 
# main - This is where the whole thing comes together.
#
###############################################################################

# scoped values for loop work
my $oldvalue = 0;
my $dupe_flag = 0;
my $id = "";
my $done_flag = 0;

# Open all USB input devices, and stash in a hash until needed.
my @devicenames = map { "/dev/input/by-id/" . basename($_) } </dev/input/by-id/*>;
my %devices;

my $selector = IO::Select->new();

foreach (@devicenames) {
  my $device = Linux::Input->new($_);
  $selector->add($device->fh);
  $devices{$device->fh} = $device;
}

logger basename($0) . " started.";

system $usbrelay . " " . $usbrelay_device . "=0 >/dev/null 2>&1";


# stay in an infinite loop.
while ( 1 ) {

  # Do this if we have a device with some input
  while (my @fh = $selector->can_read) {

    # For each file handle that has stuff to read
    foreach my $fh (@fh) {

      my $input = $devices{$fh};

      # Read the input that's waiting
      while (my @events = $input->poll(0.01)) {

        # Convert keystrokes to useful data
        foreach (@events) {
          # break  up the input struct into useful scalars.
          my $code = %$_{code};
          my $value = %$_{value};
          my $type = %$_{type};     
          my $number = keytranslate($value & 127);

          # This seems to indicate a KEYP/KEYDOWN event
          if ( $code == 4 && $type == 4 ) {


            # If we just saw this, it's KEYUP now, so ignore
            if ($value == $oldvalue && $dupe_flag == 0) {
              $dupe_flag = 1;
              next
            } else {
              $dupe_flag = 0;
            }

            # 0-9 for number, 40 for end of string.
            if ( $number > 9 && $number != 40) { logger "Problem reading ID\n"; next} 

            if ( $number == 40) {
              $done_flag = 1;
            } else {
              $id .= $number;
            }
            
            $oldvalue = $value;

          } # if ($code == 4 && $type == 4)

        }  # foreach (@events)
     
      } # while (my @events = $input->poll(0.01))

    } # foreach my $fh (@fh)

    # Do something with the completed code.
    if ( $done_flag == 1) {

      logger "ID $id scanned.";

      authenticate($id); 
     
      $id = "";
      $done_flag = 0;

    } else {

      next

    }

  } # while (my @fh = $selector->can_read)


} # while (1)