瀏覽代碼

server side schema, scripts, daemons, etc.

* incomplete. The process of making these files free/libre/open source
  is a work in process. These will not work 'out of the box' as of now
clementinecomputing 6 年之前
父節點
當前提交
c5e1423183

+ 276 - 0
server/daemons/avls_server/avls_server.pl

@@ -0,0 +1,276 @@
+#!/usr/bin/perl -Tw 
+#
+# Copyright (c) 2019 Clementine Computing LLC.
+# 
+# This file is part of PopuFare.
+# 
+# PopuFare is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# PopuFare 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 Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with PopuFare.  If not, see <https://www.gnu.org/licenses/>.
+#
+
+require 5.002; 
+use strict; 
+use Socket; 
+use Switch;
+use Carp;
+use DBI;
+use FileHandle;
+use Fcntl;
+use Compress::Zlib;
+
+use POSIX;
+
+my $database_path = 'DBI:SQLite:dbname=../bus.sqlite';
+my $database_user = '';
+my $database_pass = '';
+
+my $bind_ip  	  = '127.0.0.1';
+my $bind_port	  = 2857;
+
+#----------------------------------------------Ugly exception handling logic using closures and anonymous functions----
+#-------------------------------------------This is in there to deal with the fact that CreditCall uses the die("error")
+#-------------------------------------------function instead of returning an error message in many cases...
+
+#       This utility function returns the passed string sans any leading or trailing whitespace.
+#
+sub strip_whitespace
+{
+	my $str = shift;        #grab our first parameter
+        
+        $str =~ s/^\s+//;       #strip leading whitespace
+        $str =~ s/\s+$//;       #strip trailing whitespace
+                        
+        return $str;            #return the improved string
+}
+                                
+
+#    This function takes two coderef parameters, the second of which is usually an explicit call to the 
+# 'catch' function which itself takes a coderef parameter.  This allows the code employing this suite of 
+# functions to look somewhat like a conventional exception handling mechanism:
+#
+# try
+# {
+#    do_something_that_might_die();
+# }
+# catch
+# {
+#   my $errmsg = $_;
+#   log_the_error_message($errmsg);
+#   perform_some_cleanup();
+# };
+#
+# DO NOT FORGET THAT LAST SEMICOLON, EVERYTHING GOES TO HELL IF YOU DO!
+#   
+sub try(&$)
+{
+	my ($attempt, $handler) = @_;
+  
+	eval
+	{
+		&$attempt;
+	};
+        
+	if($@)
+	{
+		do_catch($handler);
+	}
+}
+
+#    This function strips off the whitespace from the exception message reported by die()
+# and places the result into the default variable such that the code in the catch block can
+# just examine $_ to figure out what the cause of the error is, or to display or log
+# the error message.
+#
+sub do_catch(&$)
+{
+	my ($handler) = @_;
+
+	local $_ = strip_whitespace($@);
+	
+	&$handler;
+}
+
+#    This just takes an explicit coderef and returns it unharmed.  The only
+# purpose of this is so the try/catch structure looks pretty and familiar.                                          
+#
+sub catch(&) {$_[0]}
+
+#--------------------------------------------------------------------------------------------------------------------
+
+
+#my $DebugMode = 1;
+my $DebugMode = 0;
+
+#	This function only executes the passed code reference if the global variable $DebugMode is non-zero.
+# The reason for this is that any calculation (like a FooBar::ComplexObject->toString call) will not be
+# performed if we are not in debug mode, sort of like a very limited form of lazy evaluation.
+#
+sub ifdebug(&@)
+{
+	my ($cmd) = @_;
+	&$cmd() if($DebugMode);
+}
+
+
+sub StoreAvls
+{
+	my $client_query = $_[0];
+	chomp($client_query);
+
+	my $dbh = DBI->connect($database_path, $database_user, $database_pass)
+		or die "Couldn't connect to database: " . DBI->errstr;
+	my $sth_avls = $dbh->prepare('INSERT INTO avls_data (equip_num, driver, paddle, route, trip, stop, chirp_time, latitude, longitude, heading, velocity) VALUES (?, ?, ?, ?, ?, ?, FROM_UNIXTIME(?), ?, ?, ?, ?)')
+		or die "Couldn't prepare statement: " . $dbh->errstr;
+	#store avls data
+	$sth_avls->execute(split("\t", $client_query))  # Execute the query
+		or die "Couldn't execute statement: " . $sth_avls->errstr;
+	
+	$sth_avls->finish;
+	$dbh->disconnect;
+}
+
+
+sub handle_client() 
+{
+	close SERVER;
+	CLIENT->autoflush(1);
+
+	my $linebuffer;
+	
+	while($linebuffer = <CLIENT>) 
+	{
+		StoreAvls($linebuffer);
+	}	#while data from client
+
+	close CLIENT;
+}
+
+
+my $waitedpid = 0; 
+my $sigreceived = 0;
+
+
+sub REAPER 
+{ 
+	while (($waitedpid = waitpid(-1, WNOHANG))>0) { }
+	$SIG{CHLD} = \&REAPER; # loathe sysV 
+	$sigreceived = 1;
+} 
+
+
+sub spawn
+{ 
+	my $coderef = shift; 						#grab the first parameter
+	unless (@_ == 0 && $coderef && ref($coderef) eq 'CODE') 	#verify that it consists of a non-null block of executable perl code
+	{ 
+		confess "usage: spawn CODEREF"; 			#complain if this is not the case
+	} 
+	my $pid; 
+	if (!defined($pid = fork)) 					#attempt a fork, remembering the returned PID value
+	{
+		close CLIENT;
+		return; 						#failed to fork, we'd better close the client
+	} 
+	elsif ($pid) 							#If the returned process ID is non-zero, that indicates that we are the parent process
+	{ 
+		return; # i'm the parent 
+	}  
+	else								#otherwise, if the returned process ID is 0, that means we're the child process
+	{
+		exit &$coderef();					#in which case, we want to execute the child handler that was passed in, and then
+									#exit this (child) process when we've finished our conversation(s) with the
+									#other (client) end of the socket.
+	}
+} 
+
+#----------------------------------------------------------------------
+#   Local network settings for Inter-Process communication.
+#----------------------------------------------------------------------
+my $proto = getprotobyname('tcp'); 
+my $addr = sockaddr_in( $bind_port ,inet_aton($bind_ip));;
+#----------------------------------------------------------------------
+
+my $max_retries = 10;		#Maximum number of address-binding retries before we give up.
+my $retry_count = $max_retries;	#number of retries left...
+my $retry_delay = 3;		#number of seconds to wait between retries at binding to our designated IPC address
+my $got_network = 0;		#flag to let us know that we can quit retrying once we have gotten a valid listening socket
+
+while( ($retry_count > 0) && (!$got_network) )
+{
+	try	#Try and allocate a socket, bind it to our IPC address, and set it to listen for connections
+	{
+		socket(SERVER,PF_INET,SOCK_STREAM,$proto) || die "socket: $!"; 
+		setsockopt(SERVER, SOL_SOCKET, SO_REUSEADDR,  1);
+		bind (SERVER, $addr) || die "bind: $!"; 
+		listen(SERVER,5) || die "listen: $!";
+		$got_network = 1;
+	}
+	catch	#If that didn't work for some reason, log the error, clean up, and prepair to retry
+	{
+		my $errmsg = $_;	#Remember the error message
+		
+		close(SERVER);		#Clean up the server socket if it needs it
+		
+		#Decrement our remaining retry counter
+		$retry_count = $retry_count - 1;
+		
+		#Log the message to our debug log
+		print "Failed to allocate socket, will retry $retry_count times: $errmsg\n";
+		
+		#Wait a reasonable period before trying again
+		sleep $retry_delay;
+	};
+}
+
+if($got_network)	#If we met with success binding to the network, report it
+{
+	my $logmsg = "Socket setup successful.  Listening for clients at $bind_ip:$bind_port\n";
+	
+	print $logmsg;	
+	
+}
+else			#If we ran out of patience and gave up, report that as well and exit
+{
+	my $errmsg = "Could not allocate and bind listening socket at $bind_ip:$bind_port after $max_retries attempts.\n";
+	
+	die $errmsg;
+}
+
+#    Set up our signal handler which will clean up defunct child processes and let the main
+# accept() loop know that the reason accept returned was due to a signal, not a legit connection.
+$SIG{CHLD} = \&REAPER; 
+
+#This for loop is efficient, but confusting, so I'll break it down by clause
+#
+#    The first clause ($sigreceived = 0) clears the signal received flag that will be set if the 
+# accept() call was interrupted by a signal.  This clause runs once before the first run of the loop
+#
+#    The second clause is the test clause, it will process the contents of the loop if EITHER
+# accept() has returned (presumably generating a valid file handle for the CLIENT end of the
+# socket, OR the signal received flag is set (thus accept would have returned early without
+# having actually accepted a connection.
+#
+#    The third clause (the 'incrementer') is run after each time the body is executed, before the 
+# test clause is executed again (deciding whether to run the body or drop out...  This test
+# clause will close the parent process' copy of the CLIENT file handle since (see body below)
+# after the body executes, all communication with the socket referred to by that file handle
+# will be carried out by the spawned child process.  This frees the parent's copy of the CLIENT
+# file handle to be used again in the parent process for the next accepted incoming connection.
+
+for ( $sigreceived = 0; accept(CLIENT,SERVER) || $sigreceived; $sigreceived = 0, close CLIENT) 
+{ 
+	next if $sigreceived; 			#If we were interrupted by a signal, there is no real client, just go back and try to accept a new one
+	print "connection received.\n";		#Print a diagnostic message confirming that we have made a connection
+	spawn sub {handle_client();};		#fork() off a child process that will handle communication with the socket pointed to by the CLIENT file handle 
+} 
+

+ 244 - 0
server/daemons/avls_server/ridelogic_avlsd

@@ -0,0 +1,244 @@
+#!/usr/bin/perl -Tw 
+#
+# Copyright (c) 2019 Clementine Computing LLC.
+# 
+# This file is part of PopuFare.
+# 
+# PopuFare is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# PopuFare 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 Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with PopuFare.  If not, see <https://www.gnu.org/licenses/>.
+#
+
+require 5.002; 
+use strict; 
+use Socket; 
+use Switch;
+use Carp;
+use DBI;
+use FileHandle;
+use Fcntl;
+use Compress::Zlib;
+use Getopt::Long qw(:config no_ignore_case);
+use POSIX;
+use RideLogic;
+
+my $database_path = 'DBI:mysql:busdb';
+my $database_user = '';
+my $database_pass = '';
+
+my $bind_ip  	  = '127.0.0.1';
+my $bind_port	  = 2857;
+
+#--------------------------------------------------------------------------------------------------------------------
+
+my $DebugMode = 0;
+
+#	This function only executes the passed code reference if the global variable $DebugMode is non-zero.
+# The reason for this is that any calculation (like a FooBar::ComplexObject->toString call) will not be
+# performed if we are not in debug mode, sort of like a very limited form of lazy evaluation.
+#
+sub ifdebug(&@)
+{
+	my ($cmd) = @_;
+	&$cmd() if($DebugMode);
+}
+
+
+sub StoreAvls
+{
+	my $client_query = $_[0];
+	chomp($client_query);
+
+	my $dbh = DBI->connect($database_path, $database_user, $database_pass)
+		or die "Couldn't connect to database: " . DBI->errstr;
+	my $sth_avls = $dbh->prepare('INSERT INTO avls_data (equip_num, driver, paddle, route, trip, stop, chirp_time, latitude, longitude, heading, velocity) VALUES (?, ?, ?, ?, ?, ?, FROM_UNIXTIME(?), ?, ?, ?, ?)')
+		or die "Couldn't prepare statement: " . $dbh->errstr;
+	#store avls data
+	$sth_avls->execute(split("\t", $client_query))  # Execute the query
+		or die "Couldn't execute statement: " . $sth_avls->errstr;
+	
+	$sth_avls->finish;
+	$dbh->disconnect;
+}
+
+
+sub handle_client() 
+{
+	close SERVER;
+	CLIENT->autoflush(1);
+
+	my $linebuffer;
+	
+	while($linebuffer = <CLIENT>) 
+	{
+		StoreAvls($linebuffer);
+	}	#while data from client
+
+	close CLIENT;
+}
+
+
+my $waitedpid = 0; 
+my $sigreceived = 0;
+
+
+sub REAPER 
+{ 
+	while (($waitedpid = waitpid(-1, WNOHANG))>0) { }
+	$SIG{CHLD} = \&REAPER; # loathe sysV 
+	$sigreceived = 1;
+} 
+
+
+sub spawn
+{ 
+	my $coderef = shift; 						#grab the first parameter
+	unless (@_ == 0 && $coderef && ref($coderef) eq 'CODE') 	#verify that it consists of a non-null block of executable perl code
+	{ 
+		confess "usage: spawn CODEREF"; 			#complain if this is not the case
+	} 
+	my $pid; 
+	if (!defined($pid = fork)) 					#attempt a fork, remembering the returned PID value
+	{
+		close CLIENT;
+		return; 						#failed to fork, we'd better close the client
+	} 
+	elsif ($pid) 							#If the returned process ID is non-zero, that indicates that we are the parent process
+	{ 
+		return; # i'm the parent 
+	}  
+	else								#otherwise, if the returned process ID is 0, that means we're the child process
+	{
+		exit &$coderef();					#in which case, we want to execute the child handler that was passed in, and then
+									#exit this (child) process when we've finished our conversation(s) with the
+									#other (client) end of the socket.
+	}
+} 
+
+sub show_help_and_exit {
+  print "usage:\n";
+  print "  [-i]       interactive, do not daemonize\n";
+  print "  [-c cfg]   use cfg as config file (default to " . $RideLogic::RIDELOGIC_DAEMON_CONF . ") \n";
+  print "  [-h]       show help (this screen)\n";
+  exit;
+}
+
+#----------------------------------------------------------------------
+#
+#----------------------------------------------------------------------
+my $daemonize   = 1;
+my $interactive = 0;
+my $show_help   = 0;
+my $cfg_file    = $RideLogic::RIDELOGIC_DAEMON_CONF;
+GetOptions(
+  'i|interactive'   => \$interactive,
+  'c|config=s'      => \$cfg_file,
+  'h|help'          => \$show_help );
+show_help_and_exit() if ($show_help);
+
+$daemonize=0 if ($interactive);
+
+
+#----------------------------------------------------------------------
+#   Local network settings for Inter-Process communication.
+#----------------------------------------------------------------------
+my $proto = getprotobyname('tcp'); 
+my $addr = sockaddr_in( $bind_port ,inet_aton($bind_ip));;
+#----------------------------------------------------------------------
+
+my $max_retries = 10;		#Maximum number of address-binding retries before we give up.
+my $retry_count = $max_retries;	#number of retries left...
+my $retry_delay = 3;		#number of seconds to wait between retries at binding to our designated IPC address
+my $got_network = 0;		#flag to let us know that we can quit retrying once we have gotten a valid listening socket
+
+my %CFG_VAR;
+
+read_config($cfg_file, \%CFG_VAR) if ($cfg_file);
+
+my $logfile     = ($CFG_VAR{"RIDELOGIC_DAEMON_LOG_DIR"} || $RideLogic::RIDELOGIC_DAEMON_LOG_DIR) . "/ridelogic_avlsd.log";
+my $pidfile     = ($CFG_VAR{"RIDELOGIC_DAEMON_PID_DIR"} || $RideLogic::RIDELOGIC_DAEMON_PID_DIR) . "/ridelogic_avlsd.pid";
+
+daemonize($logfile, $pidfile) if ($daemonize);
+
+# set our pipes to be piping hot
+$|=1;
+
+
+while( ($retry_count > 0) && (!$got_network) )
+{
+	try	#Try and allocate a socket, bind it to our IPC address, and set it to listen for connections
+	{
+		socket(SERVER,PF_INET,SOCK_STREAM,$proto) || die "socket: $!"; 
+		setsockopt(SERVER, SOL_SOCKET, SO_REUSEADDR,  1);
+		bind (SERVER, $addr) || die "bind: $!"; 
+		listen(SERVER,5) || die "listen: $!";
+		$got_network = 1;
+	}
+	catch	#If that didn't work for some reason, log the error, clean up, and prepair to retry
+	{
+		my $errmsg = $_;	#Remember the error message
+		
+		close(SERVER);		#Clean up the server socket if it needs it
+		
+		#Decrement our remaining retry counter
+		$retry_count = $retry_count - 1;
+		
+		#Log the message to our debug log
+		print "Failed to allocate socket, will retry $retry_count times: $errmsg\n";
+		
+		#Wait a reasonable period before trying again
+		sleep $retry_delay;
+	};
+}
+
+if($got_network)	#If we met with success binding to the network, report it
+{
+	my $logmsg = "Socket setup successful.  Listening for clients at $bind_ip:$bind_port\n";
+	
+	print $logmsg;	
+	
+}
+else			#If we ran out of patience and gave up, report that as well and exit
+{
+	my $errmsg = "Could not allocate and bind listening socket at $bind_ip:$bind_port after $max_retries attempts.\n";
+	
+	die $errmsg;
+}
+
+#    Set up our signal handler which will clean up defunct child processes and let the main
+# accept() loop know that the reason accept returned was due to a signal, not a legit connection.
+$SIG{CHLD} = \&REAPER; 
+
+#This for loop is efficient, but confusting, so I'll break it down by clause
+#
+#    The first clause ($sigreceived = 0) clears the signal received flag that will be set if the 
+# accept() call was interrupted by a signal.  This clause runs once before the first run of the loop
+#
+#    The second clause is the test clause, it will process the contents of the loop if EITHER
+# accept() has returned (presumably generating a valid file handle for the CLIENT end of the
+# socket, OR the signal received flag is set (thus accept would have returned early without
+# having actually accepted a connection.
+#
+#    The third clause (the 'incrementer') is run after each time the body is executed, before the 
+# test clause is executed again (deciding whether to run the body or drop out...  This test
+# clause will close the parent process' copy of the CLIENT file handle since (see body below)
+# after the body executes, all communication with the socket referred to by that file handle
+# will be carried out by the spawned child process.  This frees the parent's copy of the CLIENT
+# file handle to be used again in the parent process for the next accepted incoming connection.
+
+for ( $sigreceived = 0; accept(CLIENT,SERVER) || $sigreceived; $sigreceived = 0, close CLIENT) 
+{ 
+	next if $sigreceived; 			#If we were interrupted by a signal, there is no real client, just go back and try to accept a new one
+	print "connection received.\n";		#Print a diagnostic message confirming that we have made a connection
+	spawn sub {handle_client();};		#fork() off a child process that will handle communication with the socket pointed to by the CLIENT file handle 
+} 
+

+ 688 - 0
server/daemons/billing_server/billing_server.pl

@@ -0,0 +1,688 @@
+#!/usr/bin/perl -Tw 
+#
+# Copyright (c) 2019 Clementine Computing LLC.
+# 
+# This file is part of PopuFare.
+# 
+# PopuFare is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# PopuFare 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 Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with PopuFare.  If not, see <https://www.gnu.org/licenses/>.
+#
+
+require 5.002; 
+use strict; 
+use Socket; 
+use Switch;
+use Carp;
+use DBI;
+use Date::Calc qw(:all);
+use FileHandle;
+use Fcntl;
+use Digest::MD5 qw(md5 md5_hex md5_base64);
+
+use POSIX;
+
+use Data::Dumper;
+
+use OrgDB;
+
+my $ORG = "ORG";
+
+my $database_path = 'DBI:mysql:busdb';
+my $database_user = '';
+my $database_pass = '';
+
+my $bind_ip  	  = '127.0.0.1';
+my $bind_port	  = 2455;
+
+my $logfile       = '/home/bus/log/billing_log.log';
+
+sub unix_to_readable_time {
+  my $unix_time = shift;
+  my @a = localtime($unix_time);
+  return sprintf('%d-%02d-%02d %02d:%02d:%02d', (1900+$a[5]), (1+$a[4]), $a[3], $a[2], $a[1], $a[0]);
+}
+
+#----------------------------------------------Ugly exception handling logic using closures and anonymous functions----
+#-------------------------------------------This is in there to deal with the fact that CreditCall uses the die("error")
+#-------------------------------------------function instead of returning an error message in many cases...
+
+#       This utility function returns the passed string sans any leading or trailing whitespace.
+#
+sub strip_whitespace
+{
+	my $str = shift;        #grab our first parameter
+        
+        $str =~ s/^\s+//;       #strip leading whitespace
+        $str =~ s/\s+$//;       #strip trailing whitespace
+                        
+        return $str;            #return the improved string
+}
+                                
+
+#    This function takes two coderef parameters, the second of which is usually an explicit call to the 
+# 'catch' function which itself takes a coderef parameter.  This allows the code employing this suite of 
+# functions to look somewhat like a conventional exception handling mechanism:
+#
+# try
+# {
+#    do_something_that_might_die();
+# }
+# catch
+# {
+#   my $errmsg = $_;
+#   log_the_error_message($errmsg);
+#   perform_some_cleanup();
+# };
+#
+# DO NOT FORGET THAT LAST SEMICOLON, EVERYTHING GOES TO HELL IF YOU DO!
+#   
+sub try(&$)
+{
+	my ($attempt, $handler) = @_;
+	
+	eval
+	{
+		&$attempt;
+	};
+	
+	if($@)
+	{
+		do_catch($handler);
+	}
+}
+
+#    This function strips off the whitespace from the exception message reported by die()
+# and places the result into the default variable such that the code in the catch block can
+# just examine $_ to figure out what the cause of the error is, or to display or log
+# the error message.
+#
+sub do_catch(&$)
+{
+	my ($handler) = @_;
+	
+	local $_ = strip_whitespace($@);
+	
+	&$handler;
+}
+
+#    This just takes an explicit coderef and returns it unharmed.  The only
+# purpose of this is so the try/catch structure looks pretty and familiar.                                          
+#
+sub catch(&) {$_[0]}
+
+#--------------------------------------------------------------------------------------------------------------------
+
+
+#my $DebugMode = 1;
+my $DebugMode = 0;
+
+# This function only executes the passed code reference if the global variable $DebugMode is non-zero.
+# The reason for this is that any calculation (like a FooBar::ComplexObject->toString call) will not be
+# performed if we are not in debug mode, sort of like a very limited form of lazy evaluation.
+#
+sub ifdebug(&@)
+{
+	my ($cmd) = @_;
+	&$cmd() if($DebugMode);
+}
+
+sub ExpirePass {
+  my $dbh = shift;
+  my $cardid = shift;
+  my $dummy_passid = shift;
+  my $ride_time = shift;
+  my @oldrow = @_;
+
+  local $dbh->{RaiseError};
+  local $dbh->{PrintError};
+
+  $dbh->{RaiseError} = 1;
+  $dbh->{PrintError} = 1;
+
+  $dbh->begin_work;
+
+  # get passes to expire for a cardid
+  my $query = $dbh->prepare("select p.user_pass_id, p.queue_order, p.rule, p.nrides_remain, p.nday_expiration, rc.ruleclass
+                               from user_pass p, rule_class rc
+                              where p.logical_card_id = ? and p.active = 1 and p.expired = 0 and
+                                    ( ( rc.ruleclass = 'NDAY' and p.nday_expiration < now() ) or
+                                    ( rc.ruleclass = 'NRIDE' and p.nrides_remain <= 0 ) or
+                                    ( rc.rulename = 'PREACTIVE' ) ) ");
+  $query->execute($cardid);
+  if ($query->rows == 0) { $dbh->commit; return; }
+
+  my $href = $query->fetchrow_hashref;
+  my $passid = $href->{'user_pass_id'};
+  my $current_q_num = $href->{'queue_order'};
+
+  # expire old pass
+  my $audit_pass_id = audit_user_pass_start($dbh, $passid, "billing_server: ExpirePass: deactivating and expiring pass");
+  $query = $dbh->prepare("update user_pass set active = 0, expired = 1, deactivated = now() where user_pass_id = ?");
+  $query->execute($passid);
+  audit_user_pass_end($dbh, $passid, $audit_pass_id);
+
+  # activate new pass
+  $query = $dbh->prepare("select p.user_pass_id, p.rule, p.nday_orig, p.nday_expiration, p.nrides_orig, p.queue_order, rc.ruleclass
+                            from user_pass p, rule_class rc
+                           where p.logical_card_id = ? 
+                             and p.expired = 0 and p.rule = rc.rulename
+                             and p.queue_order = ( select min(t.queue_order) 
+                                                   from user_pass t 
+                                                  where t.logical_card_id = ? 
+                                                    and t.queue_order > ? 
+                                                    and t.expired = 0) ");
+  $query->execute($cardid, $cardid, $current_q_num);
+
+  # no passes left, put in reject rule, finish transaction
+  if ($query->rows == 0) {
+    $query = $dbh->prepare("lock tables active_rider_table write");
+    $query->execute();
+    $query = $dbh->prepare("insert into active_rider_table (logical_card_id, rfid_token, mag_token, rule_name, rule_param, deleted, notes)
+                                                    values (?,?,?,?,?,?,?)");
+    $query->execute($cardid, @oldrow[1,2], $ORG . '-REJECT', 'reject', 0, $oldrow[7]);
+    $dbh->commit;
+    $query = $dbh->prepare("unlock tables");
+    $query->execute();
+    return;
+  }
+
+  # else make new pass active and update art with new pass
+  $href = $query->fetchrow_hashref;
+
+  my $pass_param = '';
+  if      ($href->{'ruleclass'} eq 'NRIDE') { 
+    $pass_param = $href->{'nrides_orig'}; 
+  } elsif ($href->{'ruleclass'} eq 'NDAY')  { 
+    $pass_param = $href->{'nday_orig'}; 
+    $pass_param .= " " . $href->{'nday_expiration'} if $href->{'nday_expiration'}; 
+  }
+
+  $audit_pass_id = audit_user_pass_start($dbh, $href->{'user_pass_id'}, "billing_server: ExpirePass: activating pass");
+  $query = $dbh->prepare("update user_pass set active = 1, activated = ? where user_pass_id = ?");
+  $query->execute($ride_time, $href->{'user_pass_id'} );
+  audit_user_pass_end($dbh, $href->{'user_pass_id'}, $audit_pass_id);
+
+  $query = $dbh->prepare("lock tables active_rider_table write");
+  $query->execute();
+  $query = $dbh->prepare("insert into active_rider_table (logical_card_id, rfid_token, mag_token, rule_name, rule_param, deleted, notes)
+                                                  values (?,?,?,?,?,?,?)");
+  $query->execute($cardid, @oldrow[1,2], $href->{'rule'}, $pass_param, 0, $oldrow[7]);
+  $dbh->commit;
+  $query = $dbh->prepare("unlock tables");
+  $query->execute();
+
+}
+
+sub AdvanceRiderPass {
+  my $dbh = shift;
+  my $logical_card_id = shift;
+  my $billing_cksum = shift;
+  my $billing_ride_time = shift;
+  my $billing_action = shift;
+  my $billing_rule = shift;
+
+  local $dbh->{RaiseError};
+  local $dbh->{PrintError};
+
+  $dbh->{RaiseError} = 1;
+  $dbh->{PrintError} = 1;
+
+  $dbh->begin_work;
+
+  my $sth_find = $dbh->prepare('SELECT active_rider_table.logical_card_id, 	active_rider_table.rfid_token, 
+				       active_rider_table.mag_token, 	        active_rider_table.rule_name, 
+				       active_rider_table.rule_param, 	        active_rider_table.deleted, 
+				       active_rider_table.parent_entity, 	active_rider_table.notes,
+                                       active_rider_table.seq_num
+				  FROM active_rider_table
+				 WHERE logical_card_id = ? 
+				   AND NOT(deleted) 
+				   AND seq_num = (SELECT max(seq_num) FROM active_rider_table WHERE logical_card_id = ?) ');
+  $sth_find->execute($logical_card_id, $logical_card_id);
+  if ($sth_find->rows != 1) { $dbh->commit; return; }
+
+  #@oldrow:
+  #0. logical_card_id
+  #1. rfid_token
+  #2. mag_token
+  #3. rule_name
+  #4. rule_param
+  #5. deleted
+  #6. parent_entity
+  #7. notes
+  #8. seq_num
+  my @oldrow = $sth_find->fetchrow_array();
+
+  my $sth_pass = $dbh->prepare("select p.user_pass_id, p.nrides_remain, p.nday_orig, p.nday_expiration, p.rule
+                                  from user_pass p, user_card c
+                                 where p.logical_card_id = ? 
+                                   and c.logical_card_id = p.logical_card_id
+                                   and c.active = 1
+                                   and p.active = 1 
+                                   and p.expired = 0 
+                                   and p.activated <= ?");
+  $sth_pass->execute($logical_card_id, $billing_ride_time);
+  if ($sth_pass->rows != 1) { 
+    if (uc($billing_action) ne "REJECT") {
+      my $sth = $dbh->prepare("insert into diagnostic_log (loglvl, message) 
+        values ('warning', concat('billing_server: logical_card_id ', ?, ', billing_cksum ', ?, ', art seq_num ', ?, ', dropping billing entry: no matching pass entry') ) ");
+      $sth->execute($logical_card_id, $billing_cksum, $oldrow[8]);
+    }
+    $dbh->commit; 
+    return; 
+  }
+
+  my $pass = $sth_pass->fetchrow_hashref;
+
+  my $t = $dbh->prepare("select ruleclass from rule_class where rulename = ?");
+  $t->execute($pass->{'rule'});
+  my $rule_class = 'OTHER';
+  if ($t->rows == 1) { 
+    $rule_class = $t->fetchrow_hashref->{'ruleclass'}; 
+  } elsif ($t->rows < 1) {
+    my $sth = $dbh->prepare("insert into diagnostic_log (loglvl, message) 
+      values ('warning', concat('billing_server: logical_card_id ', ?, ', billing_cksum ', ?, ', art seq_num ', ?, ', no rule class found, dropping billing entry') ) ");
+    $sth->execute($logical_card_id, $billing_cksum, $oldrow[8]);
+    $dbh->commit; 
+    return; 
+  } else {
+    my $sth = $dbh->prepare("insert into diagnostic_log (loglvl, message) 
+      values ('warning', concat('billing_server: logical_card_id ', ?, ', billing_cksum ', ?, ', art seq_num ', ?, ', multiple rule classes found, dropping billing entry') ) ");
+    $sth->execute($logical_card_id, $billing_cksum, $oldrow[8]);
+    $dbh->commit; 
+    return; 
+  }
+
+  if (uc($billing_action) eq "REJECT") {
+    # bus not sync'd? 
+    $dbh->commit;
+  } elsif ($oldrow[3] ne $pass->{'rule'}) {
+    # raise warning?
+    my $sth = $dbh->prepare("insert into diagnostic_log (loglvl, message) 
+      values ('warning', concat('billing_server: logical_card_id ',?,', billing_cksum ',?,', art seq_num ',?,', rule mismatch(1): art rule \"',?,'\" != user_pass_id ',?,' rule \"',?,'\"') )");
+    $sth->execute($logical_card_id, $billing_cksum, $oldrow[8], $oldrow[3], $pass->{'user_pass_id'}, $pass->{'rule'});
+    $dbh->commit;
+  } elsif ($billing_rule ne $pass->{'rule'}) {
+    # bus got out of sync with art?  give user this pass at the risk to prevent against
+    # decrementing an nride when an nday (or something else) was reported
+    my $sth = $dbh->prepare("insert into diagnostic_log (loglvl, message) 
+      values ('warning', concat('billing_server: logical_card_id ',?,', billing_cksum ',?,', art seq_num ',?,', rule mismatch(2): billing rule \"',?,'\" != user_pass_id ',?,' rule \"',?,'\"' ) )");
+    $sth->execute($logical_card_id, $billing_cksum, $oldrow[8], $billing_rule, $pass->{'user_pass_id'}, $pass->{'rule'});
+    $dbh->commit;
+  } elsif ( $rule_class eq 'NRIDE') {
+
+    my $cur_rides = (($pass->{'nrides_remain'} > 0) ? ($pass->{'nrides_remain'}-1) : 0 );
+    $oldrow[4] = $cur_rides;
+
+    my $audit_pass_id = audit_user_pass_start($dbh, $pass->{'user_pass_id'}, "billing_server: AdvanceRiderPass: updating nride");
+    my $q = $dbh->prepare('update user_pass set nrides_remain = ?, lastused = ? where user_pass_id = ?');
+    $q->execute($cur_rides, $billing_ride_time, $pass->{'user_pass_id'});
+    audit_user_pass_end($dbh, $pass->{'user_pass_id'}, $audit_pass_id);
+
+    # expire passes will take care of it if #rides == 0
+    if ($cur_rides>0) {
+      $q = $dbh->prepare("lock tables active_rider_table write");
+      $q->execute();
+      $q = $dbh->prepare('insert into active_rider_table (logical_card_id, rfid_token, mag_token, rule_name, rule_param, deleted, parent_entity, notes) 
+        values (?, ?, ?,?, ?, ?, ?, ?)');
+      $q->execute(@oldrow[0..7]);
+    }
+    $dbh->commit;
+    if ($cur_rides>0) { $q = $dbh->prepare("unlock tables"); $q->execute(); }
+
+  } elsif ($rule_class eq 'NDAY') {
+
+    # update user_pass with expiration and update active_rider_table with new param
+    if (!$pass->{'nday_expiration'}) {
+      my $audit_pass_id = audit_user_pass_start($dbh, $pass->{'user_pass_id'}, "billing_server: AdvanceRiderPass: updating nday");
+      my $q = $dbh->prepare("update user_pass 
+                                set nday_expiration = addtime( adddate(convert(date(?), datetime), nday_orig), '2:30'), firstused = ?, lastused = ? 
+                              where user_pass_id = ?");
+      $q->execute($billing_ride_time, $billing_ride_time, $billing_ride_time, $pass->{'user_pass_id'});
+      audit_user_pass_end($dbh, $pass->{'user_pass_id'}, $audit_pass_id);
+
+      $oldrow[4] = $pass->{'nday_orig'} . " " . join('-', Add_Delta_Days(Today, $pass->{'nday_orig'} )) . " 2:30:00";
+
+      $q = $dbh->prepare("lock tables active_rider_table write"); $q->execute();
+      my $sth_new_expires = $dbh->prepare('INSERT INTO active_rider_table (logical_card_id, rfid_token, mag_token, rule_name, rule_param, deleted, parent_entity, notes) 
+        VALUES (?, ?, ?, ?, ?, ?, ?, ?)');
+      $sth_new_expires->execute(@oldrow[0..7]);
+
+      $dbh->commit;
+      $q = $dbh->prepare("unlock tables");
+      $q->execute();
+
+    } else {  # else just update last used
+
+      my $audit_pass_id = audit_user_pass_start($dbh, $pass->{'user_pass_id'}, "billing_server: AdvanceRiderPass: updating nday (lastused only)");
+      my $q = $dbh->prepare("update user_pass set lastused = ? where user_pass_id = ? and (lastused is null or lastused < ?)");
+      $q->execute($billing_ride_time, $pass->{'user_pass_id'}, $billing_ride_time);
+      audit_user_pass_end($dbh, $pass->{'user_pass_id'}, $audit_pass_id);
+      $dbh->commit;
+    }
+
+  } else {
+    # domain card, do nothing
+    my $audit_pass_id = audit_user_pass_start($dbh, $pass->{'user_pass_id'}, "billing_server: AdvanceRiderPass: updating domain (lastused only)");
+    my $q = $dbh->prepare("update user_pass set lastused = ? where user_pass_id = ? and (lastused is null or lastused < ?)");
+    $q->execute($billing_ride_time, $pass->{'user_pass_id'}, $billing_ride_time);
+    audit_user_pass_end($dbh, $pass->{'user_pass_id'}, $audit_pass_id);
+    $dbh->commit;
+  }
+
+  ExpirePass( $dbh, $logical_card_id, $pass->{'user_pass_id'}, $billing_ride_time, @oldrow );
+
+}
+
+sub ServerReply
+{
+	my $client_query = $_[0];
+	
+	$/="\n";
+	chomp($client_query);
+	my $response = "";
+	
+	my $client_query_md5 = md5_hex($client_query);
+	
+	my $dbh = DBI->connect($database_path, $database_user, $database_pass)
+		or die "Couldn't connect to database: " . DBI->errstr;
+	my $sth ;
+	my $loglvl ;
+	my $message ;
+	my $logmsg ;
+
+	if ($client_query =~ m/^[\s\x00]*$/)
+	{
+		$logmsg .= "Ignoring spurious blank line.\n";
+		$response .= "IGN\t" . $client_query_md5 . "\n";
+	}
+	elsif ($client_query =~ m/^\!/) #error
+	{
+		$loglvl = "error";
+		$message = $client_query;
+		$message =~  s/^.//;
+
+		try {
+			$sth = $dbh->prepare('INSERT IGNORE INTO diagnostic_log  (loglvl, message) VALUES (?, ?)')
+				or die "Couldn't prepare statement: " . $dbh->errstr;
+	
+			$sth->execute($loglvl, $message)             # Execute the query
+				or die "Couldn't execute statement: " . $sth->errstr;
+		}
+		catch {
+			$logmsg .= $_ . "\n";
+			$response .= "IGN\t" . $client_query_md5 . "\n";
+		};
+		
+		if ($sth->rows < 1) {
+			$response .= "DUP\t" . $client_query_md5 . "\n";
+		} else {
+			$response .= "ACK\t" . $client_query_md5 . "\n";	
+		}
+	}
+	elsif ($client_query =~ m/^\*/) #warning
+	{
+		$loglvl = "warning";
+		$message = $client_query;
+		$message =~  s/^.//;
+
+		try {
+			$sth = $dbh->prepare('INSERT IGNORE INTO diagnostic_log  (loglvl, message) VALUES (?, ?)')
+				or die "Couldn't prepare statement: " . $dbh->errstr;
+	
+			$sth->execute($loglvl, $message)             # Execute the query
+				or die "Couldn't execute statement: " . $sth->errstr;
+		}
+		catch {
+			$logmsg .= $_ . "\n";
+			$response .= "IGN\t" . $client_query_md5 . "\n";
+		};
+			
+		if ($sth->rows < 1) {
+			$response .= "DUP\t" . $client_query_md5 . "\n";
+		} else {
+			$response .= "ACK\t" . $client_query_md5 . "\n";	
+		}
+	}
+	elsif ($client_query =~ m/^\#/) #debug
+	{
+		$loglvl = "debug";
+		$message = $client_query;
+		$message =~  s/^.//;
+
+		try {
+			$sth = $dbh->prepare('INSERT IGNORE INTO diagnostic_log  (loglvl, message) VALUES (?, ?)')
+				or die "Couldn't prepare statement: " . $dbh->errstr;
+	
+			$sth->execute($loglvl, $message)             # Execute the query
+				or die "Couldn't execute statement: " . $sth->errstr;
+		}
+		catch {
+			$logmsg .= $_ . "\n";
+			$response .= "IGN\t" . $client_query_md5 . "\n";
+		};
+		
+		if ($sth->rows < 1) {
+			$response .= "DUP\t" . $client_query_md5 . "\n";
+		} else {
+			$response .= "ACK\t" . $client_query_md5 . "\n";	
+		}
+	}
+	elsif ($client_query =~ m/^(?:[^\t]*\t)+[^\t]*/)	#look for a list of optionally blank tab-delimited fields
+	{
+		my @client_values = split(/[\t]/, $client_query, -1);	#the -1 keeps split from trimming trailing blank fields
+			#0. 	equip_num
+			#1. 	driver
+			#2. 	paddle
+			#3. 	route
+			#4. 	trip
+			#5. 	stop
+			#6. 	ride_time
+			#7. 	latitude
+			#8. 	longitude
+			#9. 	action
+			#10.	rule
+			#11.	ruleparam
+			#12.	reason
+			#13.	credential
+			#14.	logical_card_id
+			#15.	cash_value
+			#16.	stop_name
+			#17.	(unused by DB) usec	
+		
+                my $duplicate_billing_entry=0;
+		try {
+                        $sth = $dbh->prepare('select count(*) num from billing_log where ride_time = FROM_UNIXTIME(?) and conf_checksum = ?') or die "Couldn't prepare statement: " . $dbh->errstr;
+                        $sth->execute($client_values[6], $client_query_md5) or die "Couldn't execute statement: " . $sth->errstr;
+
+                        $duplicate_billing_entry=1 if ($sth->fetchrow_arrayref->[0] > 0);
+
+                        if (!$duplicate_billing_entry) {
+			        $sth = $dbh->prepare('REPLACE INTO billing_log (conf_checksum, equip_num, driver, paddle, route, trip, stop, ride_time, latitude, longitude, action, rule, ruleparam, reason, credential, logical_card_id, cash_value, stop_name) VALUES (?, ?, ?, ?, ?, ?, ?, FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
+				        or die "Couldn't prepare statement: " . $dbh->errstr;
+		
+			        $sth->execute($client_query_md5, @client_values[0..16])             # Execute the query
+				        or die "Couldn't execute statement: " . $sth->errstr;
+                        }
+		}
+		catch {
+			$logmsg .= $_ . "\n";
+			$response .= "IGN\t" . $client_query_md5 . "\n";
+		};
+		
+                if ($duplicate_billing_entry) 
+                {
+			$response .= "DUP\t" . $client_query_md5 . "\n";
+                } elsif ($sth->rows == 1)  #if the billing log update was sucessful and wasn't a duplicate
+		{
+			AdvanceRiderPass($dbh, $client_values[14], $client_query_md5, unix_to_readable_time($client_values[6]), $client_values[9], $client_values[10]);
+			$response .= "ACK\t" . $client_query_md5 . "\n";
+		} 
+                #elsif ($sth->rows > 1) 
+                #{
+		#	$response .= "DUP\t" . $client_query_md5 . "\n";
+		#} 
+                else 
+                {
+			$logmsg .= "Error inserting $client_query_md5 $client_query into billing_log\n" ;
+		}
+	}
+	else
+	{
+		$logmsg .= "Malformed log entry \"$client_query\".\n";
+		$response .= "IGN\t" . $client_query_md5 . "\n";
+	}
+	print $logmsg if $logmsg;
+	
+	return $response;
+}
+
+
+sub handle_client() 
+{
+	close SERVER;
+	CLIENT->autoflush(1);
+	
+	my $linebuffer;
+	
+	while($linebuffer = <CLIENT>) 
+	{
+                open LOGFH, ">>$logfile";
+                print LOGFH $linebuffer;
+                close LOGFH;
+
+		print CLIENT ServerReply($linebuffer);
+	}	#while data from client
+	
+	close CLIENT;
+}
+
+
+my $waitedpid = 0; 
+my $sigreceived = 0;
+
+
+sub REAPER 
+{ 
+	while (($waitedpid = waitpid(-1, WNOHANG))>0) { }
+	$SIG{CHLD} = \&REAPER; # loathe sysV 
+	$sigreceived = 1;
+} 
+
+
+sub spawn
+{ 
+	my $coderef = shift; 						#grab the first parameter
+	unless (@_ == 0 && $coderef && ref($coderef) eq 'CODE') 	#verify that it consists of a non-null block of executable perl code
+	{ 
+		confess "usage: spawn CODEREF"; 			#complain if this is not the case
+	} 
+	my $pid; 
+	if (!defined($pid = fork)) 					#attempt a fork, remembering the returned PID value
+	{
+		close CLIENT;
+		return; 						#failed to fork, we'd better close the client
+	} 
+	elsif ($pid) 							#If the returned process ID is non-zero, that indicates that we are the parent process
+	{ 
+		return; # i'm the parent 
+	}  
+	else								#otherwise, if the returned process ID is 0, that means we're the child process
+	{
+		exit &$coderef();					#in which case, we want to execute the child handler that was passed in, and then
+									#exit this (child) process when we've finished our conversation(s) with the
+									#other (client) end of the socket.
+	}
+} 
+
+#----------------------------------------------------------------------
+#   Local network settings for Inter-Process communication.
+#----------------------------------------------------------------------
+my $proto = getprotobyname('tcp'); 
+my $addr = sockaddr_in( $bind_port ,inet_aton($bind_ip));;
+#----------------------------------------------------------------------
+
+my $max_retries = 10;		#Maximum number of address-binding retries before we give up.
+my $retry_count = $max_retries;	#number of retries left...
+my $retry_delay = 3;		#number of seconds to wait between retries at binding to our designated IPC address
+my $got_network = 0;		#flag to let us know that we can quit retrying once we have gotten a valid listening socket
+
+while( ($retry_count > 0) && (!$got_network) )
+{
+	try	#Try and allocate a socket, bind it to our IPC address, and set it to listen for connections
+	{
+		socket(SERVER,PF_INET,SOCK_STREAM,$proto) || die "socket: $!"; 
+		setsockopt(SERVER, SOL_SOCKET, SO_REUSEADDR,  1);
+		bind (SERVER, $addr) || die "bind: $!"; 
+		listen(SERVER,5) || die "listen: $!";
+		$got_network = 1;
+	}
+	catch	#If that didn't work for some reason, log the error, clean up, and prepair to retry
+	{
+		my $errmsg = $_;	#Remember the error message
+		
+		close(SERVER);		#Clean up the server socket if it needs it
+		
+		#Decrement our remaining retry counter
+		$retry_count = $retry_count - 1;
+		
+		#Log the message to our debug log
+		print "Failed to allocate socket, will retry $retry_count times: $errmsg\n";
+		
+		#Wait a reasonable period before trying again
+		sleep $retry_delay;
+	};
+}
+
+if($got_network)	#If we met with success binding to the network, report it
+{
+	my $logmsg = "Socket setup successful.  Listening for clients at $bind_ip:$bind_port\n";
+	
+	print $logmsg;	
+	
+}
+else			#If we ran out of patience and gave up, report that as well and exit
+{
+	my $errmsg = "Could not allocate and bind listening socket at $bind_ip:$bind_port after $max_retries attempts.\n";
+	
+	die $errmsg;
+}
+
+#    Set up our signal handler which will clean up defunct child processes and let the main
+# accept() loop know that the reason accept returned was due to a signal, not a legit connection.
+$SIG{CHLD} = \&REAPER; 
+
+#This for loop is efficient, but confusting, so I'll break it down by clause
+#
+#    The first clause ($sigreceived = 0) clears the signal received flag that will be set if the 
+# accept() call was interrupted by a signal.  This clause runs once before the first run of the loop
+#
+#    The second clause is the test clause, it will process the contents of the loop if EITHER
+# accept() has returned (presumably generating a valid file handle for the CLIENT end of the
+# socket, OR the signal received flag is set (thus accept would have returned early without
+# having actually accepted a connection.
+#
+#    The third clause (the 'incrementer') is run after each time the body is executed, before the 
+# test clause is executed again (deciding whether to run the body or drop out...  This test
+# clause will close the parent process' copy of the CLIENT file handle since (see body below)
+# after the body executes, all communication with the socket referred to by that file handle
+# will be carried out by the spawned child process.  This frees the parent's copy of the CLIENT
+# file handle to be used again in the parent process for the next accepted incoming connection.
+
+for ( $sigreceived = 0; accept(CLIENT,SERVER) || $sigreceived; $sigreceived = 0, close CLIENT) 
+{ 
+	next if $sigreceived; 			#If we were interrupted by a signal, there is no real client, just go back and try to accept a new one
+	print "connection received.\n";		#Print a diagnostic message confirming that we have made a connection
+	spawn sub {handle_client();};		#fork() off a child process that will handle communication with the socket pointed to by the CLIENT file handle 
+} 
+

+ 655 - 0
server/daemons/billing_server/ridelogic_billingd

@@ -0,0 +1,655 @@
+#!/usr/bin/perl -Tw 
+#
+# Copyright (c) 2019 Clementine Computing LLC.
+# 
+# This file is part of PopuFare.
+# 
+# PopuFare is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# PopuFare 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 Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with PopuFare.  If not, see <https://www.gnu.org/licenses/>.
+#
+
+require 5.002; 
+use strict; 
+use Socket; 
+use Switch;
+use Carp;
+use DBI;
+use Date::Calc qw(:all);
+use FileHandle;
+use Fcntl;
+use Digest::MD5 qw(md5 md5_hex md5_base64);
+use Getopt::Long qw(:config no_ignore_case);
+use POSIX;
+use Data::Dumper;
+use RideLogic;
+
+my $ORG = "ORG";
+
+my $database_path = 'DBI:mysql:busdb';
+my $database_user = '';
+my $database_pass = '';
+
+my $bind_ip  	  = '127.0.0.1';
+my $bind_port	  = 2455;
+
+my $billing_logfile;
+
+sub unix_to_readable_time {
+  my $unix_time = shift;
+  my @a = localtime($unix_time);
+  return sprintf('%d-%02d-%02d %02d:%02d:%02d', (1900+$a[5]), (1+$a[4]), $a[3], $a[2], $a[1], $a[0]);
+}
+
+my $DebugMode = 0;
+
+# This function only executes the passed code reference if the global variable $DebugMode is non-zero.
+# The reason for this is that any calculation (like a FooBar::ComplexObject->toString call) will not be
+# performed if we are not in debug mode, sort of like a very limited form of lazy evaluation.
+#
+sub ifdebug(&@)
+{
+	my ($cmd) = @_;
+	&$cmd() if($DebugMode);
+}
+
+sub ExpirePass {
+  my $dbh = shift;
+  my $cardid = shift;
+  my $dummy_passid = shift;
+  my $ride_time = shift;
+  my @oldrow = @_;
+
+  local $dbh->{RaiseError};
+  local $dbh->{PrintError};
+
+  $dbh->{RaiseError} = 1;
+  $dbh->{PrintError} = 1;
+
+  $dbh->begin_work;
+
+  # get passes to expire for a cardid
+  my $query = $dbh->prepare("select p.user_pass_id, p.queue_order, p.rule, p.nrides_remain, p.nday_expiration, rc.ruleclass
+                               from user_pass p, rule_class rc
+                              where p.logical_card_id = ? and p.active = 1 and p.expired = 0 and
+                                    ( ( rc.ruleclass = 'NDAY' and p.nday_expiration < now() ) or
+                                    ( rc.ruleclass = 'NRIDE' and p.nrides_remain <= 0 ) or
+                                    ( rc.rulename = 'PREACTIVE' ) ) ");
+  $query->execute($cardid);
+  if ($query->rows == 0) { $dbh->commit; return; }
+
+  my $href = $query->fetchrow_hashref;
+  my $passid = $href->{'user_pass_id'};
+  my $current_q_num = $href->{'queue_order'};
+
+  # expire old pass
+  my $audit_pass_id = audit_user_pass_start($dbh, $passid, "billing_server: ExpirePass: deactivating and expiring pass");
+  $query = $dbh->prepare("update user_pass set active = 0, expired = 1, deactivated = now() where user_pass_id = ?");
+  $query->execute($passid);
+  audit_user_pass_end($dbh, $passid, $audit_pass_id);
+
+  # activate new pass
+  $query = $dbh->prepare("select p.user_pass_id, p.rule, p.nday_orig, p.nday_expiration, p.nrides_orig, p.queue_order, rc.ruleclass
+                            from user_pass p, rule_class rc
+                           where p.logical_card_id = ? 
+                             and p.expired = 0 and p.rule = rc.rulename
+                             and p.queue_order = ( select min(t.queue_order) 
+                                                   from user_pass t 
+                                                  where t.logical_card_id = ? 
+                                                    and t.queue_order > ? 
+                                                    and t.expired = 0) ");
+  $query->execute($cardid, $cardid, $current_q_num);
+
+  # no passes left, put in reject rule, finish transaction
+  if ($query->rows == 0) {
+    $query = $dbh->prepare("lock tables active_rider_table write");
+    $query->execute();
+    $query = $dbh->prepare("insert into active_rider_table (logical_card_id, rfid_token, mag_token, rule_name, rule_param, deleted, notes)
+                                                    values (?,?,?,?,?,?,?)");
+    $query->execute($cardid, @oldrow[1,2], $ORG . '-REJECT', 'reject', 0, $oldrow[7]);
+    $dbh->commit;
+    $query = $dbh->prepare("unlock tables");
+    $query->execute();
+    return;
+  }
+
+  # else make new pass active and update art with new pass
+  $href = $query->fetchrow_hashref;
+
+  my $pass_param = '';
+  if      ($href->{'ruleclass'} eq 'NRIDE') { 
+    $pass_param = $href->{'nrides_orig'}; 
+  } elsif ($href->{'ruleclass'} eq 'NDAY')  { 
+    $pass_param = $href->{'nday_orig'}; 
+    $pass_param .= " " . $href->{'nday_expiration'} if $href->{'nday_expiration'}; 
+  }
+
+  $audit_pass_id = audit_user_pass_start($dbh, $href->{'user_pass_id'}, "billing_server: ExpirePass: activating pass");
+  $query = $dbh->prepare("update user_pass set active = 1, activated = ? where user_pass_id = ?");
+  $query->execute($ride_time, $href->{'user_pass_id'} );
+  audit_user_pass_end($dbh, $href->{'user_pass_id'}, $audit_pass_id);
+
+  $query = $dbh->prepare("lock tables active_rider_table write");
+  $query->execute();
+  $query = $dbh->prepare("insert into active_rider_table (logical_card_id, rfid_token, mag_token, rule_name, rule_param, deleted, notes)
+                                                  values (?,?,?,?,?,?,?)");
+  $query->execute($cardid, @oldrow[1,2], $href->{'rule'}, $pass_param, 0, $oldrow[7]);
+  $dbh->commit;
+  $query = $dbh->prepare("unlock tables");
+  $query->execute();
+
+}
+
+sub AdvanceRiderPass {
+  my $dbh = shift;
+  my $logical_card_id = shift;
+  my $billing_cksum = shift;
+  my $billing_ride_time = shift;
+  my $billing_action = shift;
+  my $billing_rule = shift;
+
+  local $dbh->{RaiseError};
+  local $dbh->{PrintError};
+
+  $dbh->{RaiseError} = 1;
+  $dbh->{PrintError} = 1;
+
+  $dbh->begin_work;
+
+  my $sth_find = $dbh->prepare('SELECT active_rider_table.logical_card_id, 	active_rider_table.rfid_token, 
+				       active_rider_table.mag_token, 	        active_rider_table.rule_name, 
+				       active_rider_table.rule_param, 	        active_rider_table.deleted, 
+				       active_rider_table.parent_entity, 	active_rider_table.notes,
+                                       active_rider_table.seq_num
+				  FROM active_rider_table
+				 WHERE logical_card_id = ? 
+				   AND NOT(deleted) 
+				   AND seq_num = (SELECT max(seq_num) FROM active_rider_table WHERE logical_card_id = ?) ');
+  $sth_find->execute($logical_card_id, $logical_card_id);
+  if ($sth_find->rows != 1) { $dbh->commit; return; }
+
+  #@oldrow:
+  #0. logical_card_id
+  #1. rfid_token
+  #2. mag_token
+  #3. rule_name
+  #4. rule_param
+  #5. deleted
+  #6. parent_entity
+  #7. notes
+  #8. seq_num
+  my @oldrow = $sth_find->fetchrow_array();
+
+  my $sth_pass = $dbh->prepare("select p.user_pass_id, p.nrides_remain, p.nday_orig, p.nday_expiration, p.rule
+                                  from user_pass p, user_card c
+                                 where p.logical_card_id = ? 
+                                   and c.logical_card_id = p.logical_card_id
+                                   and c.active = 1
+                                   and p.active = 1 
+                                   and p.expired = 0 
+                                   and p.activated <= ?");
+  $sth_pass->execute($logical_card_id, $billing_ride_time);
+  if ($sth_pass->rows != 1) { 
+    if (uc($billing_action) ne "REJECT") {
+      my $sth = $dbh->prepare("insert into diagnostic_log (loglvl, message) 
+        values ('warning', concat('billing_server: logical_card_id ', ?, ', billing_cksum ', ?, ', art seq_num ', ?, ', dropping billing entry: no matching pass entry') ) ");
+      $sth->execute($logical_card_id, $billing_cksum, $oldrow[8]);
+    }
+    $dbh->commit; 
+    return; 
+  }
+
+  my $pass = $sth_pass->fetchrow_hashref;
+
+  my $t = $dbh->prepare("select ruleclass from rule_class where rulename = ?");
+  $t->execute($pass->{'rule'});
+  my $rule_class = 'OTHER';
+  if ($t->rows == 1) { 
+    $rule_class = $t->fetchrow_hashref->{'ruleclass'}; 
+  } elsif ($t->rows < 1) {
+    my $sth = $dbh->prepare("insert into diagnostic_log (loglvl, message) 
+      values ('warning', concat('billing_server: logical_card_id ', ?, ', billing_cksum ', ?, ', art seq_num ', ?, ', no rule class found, dropping billing entry') ) ");
+    $sth->execute($logical_card_id, $billing_cksum, $oldrow[8]);
+    $dbh->commit; 
+    return; 
+  } else {
+    my $sth = $dbh->prepare("insert into diagnostic_log (loglvl, message) 
+      values ('warning', concat('billing_server: logical_card_id ', ?, ', billing_cksum ', ?, ', art seq_num ', ?, ', multiple rule classes found, dropping billing entry') ) ");
+    $sth->execute($logical_card_id, $billing_cksum, $oldrow[8]);
+    $dbh->commit; 
+    return; 
+  }
+
+  if (uc($billing_action) eq "REJECT") {
+    # bus not sync'd? 
+    $dbh->commit;
+  } elsif ($oldrow[3] ne $pass->{'rule'}) {
+    # raise warning?
+    my $sth = $dbh->prepare("insert into diagnostic_log (loglvl, message) 
+      values ('warning', concat('billing_server: logical_card_id ',?,', billing_cksum ',?,', art seq_num ',?,', rule mismatch(1): art rule \"',?,'\" != user_pass_id ',?,' rule \"',?,'\"') )");
+    $sth->execute($logical_card_id, $billing_cksum, $oldrow[8], $oldrow[3], $pass->{'user_pass_id'}, $pass->{'rule'});
+    $dbh->commit;
+  } elsif ($billing_rule ne $pass->{'rule'}) {
+    # bus got out of sync with art?  give user this pass at the risk to prevent against
+    # decrementing an nride when an nday (or something else) was reported
+    my $sth = $dbh->prepare("insert into diagnostic_log (loglvl, message) 
+      values ('warning', concat('billing_server: logical_card_id ',?,', billing_cksum ',?,', art seq_num ',?,', rule mismatch(2): billing rule \"',?,'\" != user_pass_id ',?,' rule \"',?,'\"' ) )");
+    $sth->execute($logical_card_id, $billing_cksum, $oldrow[8], $billing_rule, $pass->{'user_pass_id'}, $pass->{'rule'});
+    $dbh->commit;
+  } elsif ( $rule_class eq 'NRIDE') {
+
+    my $cur_rides = (($pass->{'nrides_remain'} > 0) ? ($pass->{'nrides_remain'}-1) : 0 );
+    $oldrow[4] = $cur_rides;
+
+    my $audit_pass_id = audit_user_pass_start($dbh, $pass->{'user_pass_id'}, "billing_server: AdvanceRiderPass: updating nride");
+    my $q = $dbh->prepare('update user_pass set nrides_remain = ?, lastused = ? where user_pass_id = ?');
+    $q->execute($cur_rides, $billing_ride_time, $pass->{'user_pass_id'});
+    audit_user_pass_end($dbh, $pass->{'user_pass_id'}, $audit_pass_id);
+
+    # expire passes will take care of it if #rides == 0
+    if ($cur_rides>0) {
+      $q = $dbh->prepare("lock tables active_rider_table write");
+      $q->execute();
+      $q = $dbh->prepare('insert into active_rider_table (logical_card_id, rfid_token, mag_token, rule_name, rule_param, deleted, parent_entity, notes) 
+        values (?, ?, ?,?, ?, ?, ?, ?)');
+      $q->execute(@oldrow[0..7]);
+    }
+    $dbh->commit;
+    if ($cur_rides>0) { $q = $dbh->prepare("unlock tables"); $q->execute(); }
+
+  } elsif ($rule_class eq 'NDAY') {
+
+    # update user_pass with expiration and update active_rider_table with new param
+    if (!$pass->{'nday_expiration'}) {
+      my $audit_pass_id = audit_user_pass_start($dbh, $pass->{'user_pass_id'}, "billing_server: AdvanceRiderPass: updating nday");
+      my $q = $dbh->prepare("update user_pass 
+                                set nday_expiration = addtime( adddate(convert(date(?), datetime), nday_orig), '2:30'), firstused = ?, lastused = ? 
+                              where user_pass_id = ?");
+      $q->execute($billing_ride_time, $billing_ride_time, $billing_ride_time, $pass->{'user_pass_id'});
+      audit_user_pass_end($dbh, $pass->{'user_pass_id'}, $audit_pass_id);
+
+      $oldrow[4] = $pass->{'nday_orig'} . " " . join('-', Add_Delta_Days(Today, $pass->{'nday_orig'} )) . " 2:30:00";
+
+      $q = $dbh->prepare("lock tables active_rider_table write"); $q->execute();
+      my $sth_new_expires = $dbh->prepare('INSERT INTO active_rider_table (logical_card_id, rfid_token, mag_token, rule_name, rule_param, deleted, parent_entity, notes) 
+        VALUES (?, ?, ?, ?, ?, ?, ?, ?)');
+      $sth_new_expires->execute(@oldrow[0..7]);
+
+      $dbh->commit;
+      $q = $dbh->prepare("unlock tables");
+      $q->execute();
+
+    } else {  # else just update last used
+
+      my $audit_pass_id = audit_user_pass_start($dbh, $pass->{'user_pass_id'}, "billing_server: AdvanceRiderPass: updating nday (lastused only)");
+      my $q = $dbh->prepare("update user_pass set lastused = ? where user_pass_id = ? and (lastused is null or lastused < ?)");
+      $q->execute($billing_ride_time, $pass->{'user_pass_id'}, $billing_ride_time);
+      audit_user_pass_end($dbh, $pass->{'user_pass_id'}, $audit_pass_id);
+      $dbh->commit;
+    }
+
+  } else {
+    # domain card, do nothing
+    my $audit_pass_id = audit_user_pass_start($dbh, $pass->{'user_pass_id'}, "billing_server: AdvanceRiderPass: updating domain (lastused only)");
+    my $q = $dbh->prepare("update user_pass set lastused = ? where user_pass_id = ? and (lastused is null or lastused < ?)");
+    $q->execute($billing_ride_time, $pass->{'user_pass_id'}, $billing_ride_time);
+    audit_user_pass_end($dbh, $pass->{'user_pass_id'}, $audit_pass_id);
+    $dbh->commit;
+  }
+
+  ExpirePass( $dbh, $logical_card_id, $pass->{'user_pass_id'}, $billing_ride_time, @oldrow );
+
+}
+
+sub ServerReply
+{
+	my $client_query = $_[0];
+	
+	$/="\n";
+	chomp($client_query);
+	my $response = "";
+	
+	my $client_query_md5 = md5_hex($client_query);
+	
+	my $dbh = DBI->connect($database_path, $database_user, $database_pass)
+		or die "Couldn't connect to database: " . DBI->errstr;
+	my $sth ;
+	my $loglvl ;
+	my $message ;
+	my $logmsg ;
+
+	if ($client_query =~ m/^[\s\x00]*$/)
+	{
+		$logmsg .= "Ignoring spurious blank line.\n";
+		$response .= "IGN\t" . $client_query_md5 . "\n";
+	}
+	elsif ($client_query =~ m/^\!/) #error
+	{
+		$loglvl = "error";
+		$message = $client_query;
+		$message =~  s/^.//;
+
+		try {
+			$sth = $dbh->prepare('INSERT IGNORE INTO diagnostic_log  (loglvl, message) VALUES (?, ?)')
+				or die "Couldn't prepare statement: " . $dbh->errstr;
+	
+			$sth->execute($loglvl, $message)             # Execute the query
+				or die "Couldn't execute statement: " . $sth->errstr;
+		}
+		catch {
+			$logmsg .= $_ . "\n";
+			$response .= "IGN\t" . $client_query_md5 . "\n";
+		};
+		
+		if ($sth->rows < 1) {
+			$response .= "DUP\t" . $client_query_md5 . "\n";
+		} else {
+			$response .= "ACK\t" . $client_query_md5 . "\n";	
+		}
+	}
+	elsif ($client_query =~ m/^\*/) #warning
+	{
+		$loglvl = "warning";
+		$message = $client_query;
+		$message =~  s/^.//;
+
+		try {
+			$sth = $dbh->prepare('INSERT IGNORE INTO diagnostic_log  (loglvl, message) VALUES (?, ?)')
+				or die "Couldn't prepare statement: " . $dbh->errstr;
+	
+			$sth->execute($loglvl, $message)             # Execute the query
+				or die "Couldn't execute statement: " . $sth->errstr;
+		}
+		catch {
+			$logmsg .= $_ . "\n";
+			$response .= "IGN\t" . $client_query_md5 . "\n";
+		};
+			
+		if ($sth->rows < 1) {
+			$response .= "DUP\t" . $client_query_md5 . "\n";
+		} else {
+			$response .= "ACK\t" . $client_query_md5 . "\n";	
+		}
+	}
+	elsif ($client_query =~ m/^\#/) #debug
+	{
+		$loglvl = "debug";
+		$message = $client_query;
+		$message =~  s/^.//;
+
+		try {
+			$sth = $dbh->prepare('INSERT IGNORE INTO diagnostic_log  (loglvl, message) VALUES (?, ?)')
+				or die "Couldn't prepare statement: " . $dbh->errstr;
+	
+			$sth->execute($loglvl, $message)             # Execute the query
+				or die "Couldn't execute statement: " . $sth->errstr;
+		}
+		catch {
+			$logmsg .= $_ . "\n";
+			$response .= "IGN\t" . $client_query_md5 . "\n";
+		};
+		
+		if ($sth->rows < 1) {
+			$response .= "DUP\t" . $client_query_md5 . "\n";
+		} else {
+			$response .= "ACK\t" . $client_query_md5 . "\n";	
+		}
+	}
+	elsif ($client_query =~ m/^(?:[^\t]*\t)+[^\t]*/)	#look for a list of optionally blank tab-delimited fields
+	{
+		my @client_values = split(/[\t]/, $client_query, -1);	#the -1 keeps split from trimming trailing blank fields
+			#0. 	equip_num
+			#1. 	driver
+			#2. 	paddle
+			#3. 	route
+			#4. 	trip
+			#5. 	stop
+			#6. 	ride_time
+			#7. 	latitude
+			#8. 	longitude
+			#9. 	action
+			#10.	rule
+			#11.	ruleparam
+			#12.	reason
+			#13.	credential
+			#14.	logical_card_id
+			#15.	cash_value
+			#16.	stop_name
+			#17.	(unused by DB) usec	
+		
+                my $duplicate_billing_entry=0;
+		try {
+                        $sth = $dbh->prepare('select count(*) num from billing_log where ride_time = FROM_UNIXTIME(?) and conf_checksum = ?') or die "Couldn't prepare statement: " . $dbh->errstr;
+                        $sth->execute($client_values[6], $client_query_md5) or die "Couldn't execute statement: " . $sth->errstr;
+
+                        $duplicate_billing_entry=1 if ($sth->fetchrow_arrayref->[0] > 0);
+
+                        if (!$duplicate_billing_entry) {
+			        $sth = $dbh->prepare('REPLACE INTO billing_log (conf_checksum, equip_num, driver, paddle, route, trip, stop, ride_time, latitude, longitude, action, rule, ruleparam, reason, credential, logical_card_id, cash_value, stop_name) VALUES (?, ?, ?, ?, ?, ?, ?, FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
+				        or die "Couldn't prepare statement: " . $dbh->errstr;
+		
+			        $sth->execute($client_query_md5, @client_values[0..16])             # Execute the query
+				        or die "Couldn't execute statement: " . $sth->errstr;
+                        }
+		}
+		catch {
+			$logmsg .= $_ . "\n";
+			$response .= "IGN\t" . $client_query_md5 . "\n";
+		};
+		
+                if ($duplicate_billing_entry) 
+                {
+			$response .= "DUP\t" . $client_query_md5 . "\n";
+                } elsif ($sth->rows == 1)  #if the billing log update was sucessful and wasn't a duplicate
+		{
+			AdvanceRiderPass($dbh, $client_values[14], $client_query_md5, unix_to_readable_time($client_values[6]), $client_values[9], $client_values[10]);
+			$response .= "ACK\t" . $client_query_md5 . "\n";
+		} 
+                #elsif ($sth->rows > 1) 
+                #{
+		#	$response .= "DUP\t" . $client_query_md5 . "\n";
+		#} 
+                else 
+                {
+			$logmsg .= "Error inserting $client_query_md5 $client_query into billing_log\n" ;
+		}
+	}
+	else
+	{
+		$logmsg .= "Malformed log entry \"$client_query\".\n";
+		$response .= "IGN\t" . $client_query_md5 . "\n";
+	}
+	print $logmsg if $logmsg;
+	
+	return $response;
+}
+
+
+sub handle_client() 
+{
+	close SERVER;
+	CLIENT->autoflush(1);
+	
+	my $linebuffer;
+	
+	while($linebuffer = <CLIENT>) 
+	{
+                if ($billing_logfile =~ /^([^\0]+)$/) {
+                        my $untainted_billing_logfile = $1;
+                        sysopen ( my $fh , $untainted_billing_logfile, O_WRONLY|O_APPEND|O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH );
+                        print $fh $linebuffer;
+                        close $fh;
+                }
+
+		print CLIENT ServerReply($linebuffer);
+	}	#while data from client
+	
+	close CLIENT;
+}
+
+
+my $waitedpid = 0; 
+my $sigreceived = 0;
+
+
+sub REAPER 
+{ 
+	while (($waitedpid = waitpid(-1, WNOHANG))>0) { }
+	$SIG{CHLD} = \&REAPER; # loathe sysV 
+	$sigreceived = 1;
+} 
+
+
+sub spawn
+{ 
+	my $coderef = shift; 						#grab the first parameter
+	unless (@_ == 0 && $coderef && ref($coderef) eq 'CODE') 	#verify that it consists of a non-null block of executable perl code
+	{ 
+		confess "usage: spawn CODEREF"; 			#complain if this is not the case
+	} 
+	my $pid; 
+	if (!defined($pid = fork)) 					#attempt a fork, remembering the returned PID value
+	{
+		close CLIENT;
+		return; 						#failed to fork, we'd better close the client
+	} 
+	elsif ($pid) 							#If the returned process ID is non-zero, that indicates that we are the parent process
+	{ 
+		return; # i'm the parent 
+	}  
+	else								#otherwise, if the returned process ID is 0, that means we're the child process
+	{
+		exit &$coderef();					#in which case, we want to execute the child handler that was passed in, and then
+									#exit this (child) process when we've finished our conversation(s) with the
+									#other (client) end of the socket.
+	}
+} 
+
+sub show_help_and_exit {
+  print "usage:\n";
+  print "  [-i]       interactive, do not daemonize\n";
+  print "  [-c cfg]   use cfg as config file (default to " . $RideLogic::RIDELOGIC_DAEMON_CONF . ") \n";
+  print "  [-h]       show help (this screen)\n";
+  exit;
+}
+
+#----------------------------------------------------------------------
+#
+#----------------------------------------------------------------------
+my $daemonize   = 1;
+my $interactive = 0;
+my $show_help   = 0;
+my $cfg_file    = $RideLogic::RIDELOGIC_DAEMON_CONF;
+GetOptions(
+  'i|interactive'   => \$interactive,
+  'c|config=s'      => \$cfg_file,
+  'h|help'          => \$show_help );
+show_help_and_exit() if ($show_help);
+
+$daemonize=0 if ($interactive);
+
+
+#----------------------------------------------------------------------
+#   Local network settings for Inter-Process communication.
+#----------------------------------------------------------------------
+my $proto = getprotobyname('tcp'); 
+my $addr = sockaddr_in( $bind_port ,inet_aton($bind_ip));;
+#----------------------------------------------------------------------
+
+my $max_retries = 10;		#Maximum number of address-binding retries before we give up.
+my $retry_count = $max_retries;	#number of retries left...
+my $retry_delay = 3;		#number of seconds to wait between retries at binding to our designated IPC address
+my $got_network = 0;		#flag to let us know that we can quit retrying once we have gotten a valid listening socket
+
+my %CFG_VAR;
+
+read_config($cfg_file, \%CFG_VAR) if ($cfg_file);
+
+my $logfile             = ($CFG_VAR{"RIDELOGIC_DAEMON_LOG_DIR"} || $RideLogic::RIDELOGIC_DAEMON_LOG_DIR) . "/ridelogic_billingd.log";
+   $billing_logfile     = ($CFG_VAR{"RIDELOGIC_DAEMON_LOG_DIR"} || $RideLogic::RIDELOGIC_DAEMON_LOG_DIR) . "/billing_log";
+my $pidfile             = ($CFG_VAR{"RIDELOGIC_DAEMON_PID_DIR"} || $RideLogic::RIDELOGIC_DAEMON_PID_DIR) . "/ridelogic_billingd.pid";
+
+daemonize($logfile, $pidfile) if ($daemonize);
+
+# set our pipes to be piping hot
+$|=1;
+
+
+while( ($retry_count > 0) && (!$got_network) )
+{
+	try	#Try and allocate a socket, bind it to our IPC address, and set it to listen for connections
+	{
+		socket(SERVER,PF_INET,SOCK_STREAM,$proto) || die "socket: $!"; 
+		setsockopt(SERVER, SOL_SOCKET, SO_REUSEADDR,  1);
+		bind (SERVER, $addr) || die "bind: $!"; 
+		listen(SERVER,5) || die "listen: $!";
+		$got_network = 1;
+	}
+	catch	#If that didn't work for some reason, log the error, clean up, and prepair to retry
+	{
+		my $errmsg = $_;	#Remember the error message
+		
+		close(SERVER);		#Clean up the server socket if it needs it
+		
+		#Decrement our remaining retry counter
+		$retry_count = $retry_count - 1;
+		
+		#Log the message to our debug log
+		print "Failed to allocate socket, will retry $retry_count times: $errmsg\n";
+		
+		#Wait a reasonable period before trying again
+		sleep $retry_delay;
+	};
+}
+
+if($got_network)	#If we met with success binding to the network, report it
+{
+	my $logmsg = "Socket setup successful.  Listening for clients at $bind_ip:$bind_port\n";
+	
+	print $logmsg;	
+	
+}
+else			#If we ran out of patience and gave up, report that as well and exit
+{
+	my $errmsg = "Could not allocate and bind listening socket at $bind_ip:$bind_port after $max_retries attempts.\n";
+	
+	die $errmsg;
+}
+
+#    Set up our signal handler which will clean up defunct child processes and let the main
+# accept() loop know that the reason accept returned was due to a signal, not a legit connection.
+$SIG{CHLD} = \&REAPER; 
+
+#This for loop is efficient, but confusting, so I'll break it down by clause
+#
+#    The first clause ($sigreceived = 0) clears the signal received flag that will be set if the 
+# accept() call was interrupted by a signal.  This clause runs once before the first run of the loop
+#
+#    The second clause is the test clause, it will process the contents of the loop if EITHER
+# accept() has returned (presumably generating a valid file handle for the CLIENT end of the
+# socket, OR the signal received flag is set (thus accept would have returned early without
+# having actually accepted a connection.
+#
+#    The third clause (the 'incrementer') is run after each time the body is executed, before the 
+# test clause is executed again (deciding whether to run the body or drop out...  This test
+# clause will close the parent process' copy of the CLIENT file handle since (see body below)
+# after the body executes, all communication with the socket referred to by that file handle
+# will be carried out by the spawned child process.  This frees the parent's copy of the CLIENT
+# file handle to be used again in the parent process for the next accepted incoming connection.
+
+for ( $sigreceived = 0; accept(CLIENT,SERVER) || $sigreceived; $sigreceived = 0, close CLIENT) 
+{ 
+	next if $sigreceived; 			#If we were interrupted by a signal, there is no real client, just go back and try to accept a new one
+	print "connection received.\n";		#Print a diagnostic message confirming that we have made a connection
+	spawn sub {handle_client();};		#fork() off a child process that will handle communication with the socket pointed to by the CLIENT file handle 
+} 
+

+ 708 - 0
server/daemons/billing_server/ridelogic_billingd_using_api

@@ -0,0 +1,708 @@
+#!/usr/bin/perl -Tw 
+#
+# Copyright (c) 2019 Clementine Computing LLC.
+# 
+# This file is part of PopuFare.
+# 
+# PopuFare is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# PopuFare 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 Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with PopuFare.  If not, see <https://www.gnu.org/licenses/>.
+#
+
+require 5.002; 
+use strict; 
+use Socket; 
+use Switch;
+use Carp;
+
+use FileHandle;
+use Fcntl;
+use Digest::MD5 qw(md5 md5_hex md5_base64);
+use Getopt::Long qw(:config no_ignore_case);
+use POSIX;
+use Time::Local;
+use Data::Dumper;
+
+use RideLogic;
+use RideLogicAPIQueryWrapper;
+
+my $PROGNAME = "ridelogic_billingd_using_api";
+
+my $HOST;
+my $DB;
+my $DBUSER;
+my $DBPASS;
+my $DSN;
+
+my $ORG = "ORG";
+
+my $bind_ip  	  = '127.0.0.1';
+my $bind_port	  = 2455;
+
+my $billing_logfile;
+my $debug_logfile;
+
+my $REJECT_RULE = $ORG . "-REJECT";
+
+sub unix_to_readable_time {
+  my $unix_time = shift;
+  my @a = localtime($unix_time);
+  return sprintf('%d-%02d-%02d %02d:%02d:%02d', (1900+$a[5]), (1+$a[4]), $a[3], $a[2], $a[1], $a[0]);
+}
+
+sub readable_time_cmp {
+        my $ldate = shift;
+        my $rdate = shift;
+
+        $ldate =~ m/^(\d+)-(\d+)-(\d+) (\d+):(\d+):(\d+)$/;
+        my $lunx = timelocal($6, $5, $4, $3, $2 - 1, $1);
+
+        $rdate =~ m/^(\d+)-(\d+)-(\d+) (\d+):(\d+):(\d+)$/;
+        my $runx = timelocal($6, $5, $4, $3, $2 - 1, $1);
+
+        return $lunx - $runx;
+}
+
+sub get_readable_expiration_date {
+        my $readable_date = shift;
+        my $ndays = shift;
+
+        $readable_date =~ m/^(\d+)-(\d+)-(\d+) (\d+):(\d+):(\d+)$/;
+        my $t_unix = timelocal($6, $5, $4, $3, $2 - 1, $1);
+
+        my $s = $ndays*60*60*24;
+        my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime($t_unix + $s);
+        return strftime('%Y-%m-%d %H:%M:%S', 0, 30, 2, $mday, $mon, $year);
+}
+
+my $DebugMode = 0;
+
+# This function only executes the passed code reference if the global variable $DebugMode is non-zero.
+# The reason for this is that any calculation (like a FooBar::ComplexObject->toString call) will not be
+# performed if we are not in debug mode, sort of like a very limited form of lazy evaluation.
+#
+sub ifdebug(&@)
+{
+	my ($cmd) = @_;
+	&$cmd() if($DebugMode);
+}
+
+sub ExpirePass {
+        my $rldbh = shift;
+        my $cardid = shift;
+
+        # get active pass
+        my %rhash;
+#        $rldbh->get_user_pass(\%rhash, 
+        $rldbh->GetUserPass(\%rhash, 
+          {
+            CardId => $cardid, 
+            Active => 1 
+          }
+        );
+
+        my $pass_id = $rhash{'PassId'};
+        my $uc_type = uc($rhash{'Type'});
+
+        if ($pass_id) 
+        {
+              # if the pass exists and has an expired nday, no nrides left 
+              # or is a preactivated card, expire it
+                my $now_str = strftime("%Y-%m-%d %H:%M:%S", localtime());
+                if (   ( ($uc_type eq 'NDAY')  and ( readable_time_cmp($rhash{'NDayExpiration'}, $now_str) <= 0 ) ) 
+                    or ( ($uc_type eq 'NRIDE') and ( $rhash{'NRideRemain'} <= 0) )
+                    or   ($uc_type eq 'PREACTIVE')  )
+                {
+
+                       $rldbh->deactivate_user_pass($pass_id);
+                       $rldbh->activate_user_card_pass($cardid);
+                       $rldbh->insert_active_rider_table( { logical_card_id => $cardid } );
+                }
+        }
+
+}
+
+sub use_nride {
+        my $rldbh = shift;
+        my $pass_entry = shift;
+        my $art_entry = shift;
+        my $billing_ride_time = shift;
+
+        my $cur_rides = (($pass_entry->{'NRideRemain'} > 0) ? ($pass_entry->{'NRideRemain'}-1) : 0 );
+
+        my %update_pass_param = 
+          (
+            nrides_remain => $cur_rides,
+            lastused => $billing_ride_time
+          );
+
+        if ( !$pass_entry->{'FirstUsed'} or
+             (readable_time_cmp($billing_ride_time, $pass_entry->{'FirstUsed'}) < 0) )
+        {
+                $update_pass_param{'firstused'} = $billing_ride_time ;
+        }
+
+        $rldbh->update_user_pass($pass_entry->{'PassId'}, \%update_pass_param);
+
+        if ($cur_rides > 0)
+        {
+                $rldbh->insert_active_rider_table( { logical_card_id => $pass_entry->{'CardId'} }  );
+        }
+}
+
+
+sub use_nday {
+        my $rldbh = shift;
+        my $pass_entry = shift;
+        my $art_entry = shift;
+        my $billing_ride_time = shift;
+
+        my %update_pass_param = 
+          (
+            lastused => $billing_ride_time
+          );
+
+        if ( !$pass_entry->{'FirstUsed'} or 
+             (readable_time_cmp($billing_ride_time, $pass_entry->{'FirstUsed'}) < 0) )
+        {
+                $update_pass_param{'firstused'} = $billing_ride_time;
+        }
+
+        if (!$pass_entry->{'NDayExpiration'})
+        {
+                my $nday_exp = get_readable_expiration_date($billing_ride_time, $pass_entry->{'NDayOrig'}); 
+                $update_pass_param{'nday_expiration'} = $nday_exp;
+                $rldbh->update_user_pass($pass_entry->{'PassId'}, \%update_pass_param);
+                $rldbh->insert_active_rider_table( { logical_card_id => $pass_entry->{'CardId'} } );
+        }
+        else
+        {
+                $rldbh->update_user_pass($pass_entry->{'PassId'}, \%update_pass_param);
+        }
+
+}
+
+sub update_domain_card {
+        my $rldbh = shift;
+        my $pass_entry = shift;
+        my $billing_ride_time = shift;
+
+        my %update_pass_param;
+        if ( !$pass_entry->{'LastUsed'} or 
+             (readable_time_cmp($billing_ride_time, $pass_entry->{'LastUsed'}) > 0) )
+        {
+                $update_pass_param{'lastused'} = $billing_ride_time;
+        }
+
+        if ( !$pass_entry->{'FirstUsed'} or
+             (readable_time_cmp($billing_ride_time, $pass_entry->{'FirstUsed'}) < 0) )
+        {
+                $update_pass_param{'firstused'} = $billing_ride_time;
+        }
+
+        if (scalar(keys(%update_pass_param)) > 0)
+        {
+                $rldbh->update_user_pass($pass_entry->{'PassId'}, \%update_pass_param );
+        }
+
+}
+
+
+sub art_pass_mismatch {
+        my $rldbh = shift;
+        my $logical_card_id = shift;
+        my $billing_cksum = shift;
+        my $billing_ride_time = shift;
+        my $billing_action = shift;
+        my $billing_rule = shift;
+
+        my $pass_entry = shift;
+        my $art_entry = shift;
+
+        my $mismatch = 0;
+        my $reason;
+
+        # order matters
+        if ( !$pass_entry->{'PassId'} ) 
+        {
+
+                if ( uc($billing_rule) ne $REJECT_RULE )
+                {
+                        $reason = "Billing entry has rule \"$billing_rule\" but no passes on card";
+                        $mismatch = 1;
+                }
+                elsif ( uc($art_entry->{'rule_name'}) ne $REJECT_RULE )
+                {
+                        $reason = "art rule \"" . $art_entry->{'rule_name'} . "\" with no passes on card";
+                        $mismatch = 1;
+                }
+
+        }
+        elsif (uc($art_entry->{'rule_name'}) ne uc($pass_entry->{'Rule'})) 
+        {
+                $mismatch = 1;
+                $reason = "art rule \"" . $art_entry->{'rule_name'} . "\"";
+                $reason .= " != pass rule \"" . $pass_entry->{'Rule'} . "\"";
+        } 
+        elsif ( uc($billing_rule) ne uc($pass_entry->{'Rule'}) ) 
+        {
+
+                # unless its a passback reject, we have a mismatch
+                if ( (uc($billing_action) ne 'REJECT') or (uc($billing_rule) ne 'PASSBACK') )
+                {
+                        # bus got out of sync with art?  give user this pass to protect against
+                        # decrementing an nride when an nday (or something else) was reported
+                        $mismatch = 1;
+                        $reason = "billing rule \"$billing_rule\" != pass rule \"". $pass_entry->{'Rule'} ."\"";
+                }
+
+        } 
+
+        if ($mismatch)
+        {
+                $rldbh->diagnostic_log("warning",
+                  "$PROGNAME: cardid $logical_card_id, " .
+                  "cksum $billing_cksum, " .
+                  "passid " . ($pass_entry->{'PassId'} || "n/a" ) . " " .
+                  "seq_num " . ($art_entry->{'seq_num'} || "n/a") . ", " .
+                  "mismatch ($reason): " .
+                  "bill. rule \"$billing_rule\", " .
+                  "pass rule \"" . ($pass_entry->{'Rule'} || "n/a") . "\", " .
+                  "art rule \"" . ($art_entry->{'rule_name'} || "n/a") . "\""
+                );
+
+        }
+
+        return $mismatch;
+}
+
+sub AdvanceRiderPass {
+        my $rldbh = shift;
+        my $logical_card_id = shift;
+        my $billing_cksum = shift;
+        my $billing_ride_time = shift;
+        my $billing_action = shift;
+        my $billing_rule = shift;
+
+        return 1 if !$logical_card_id;
+
+        my %art_entry;
+        $rldbh->get_active_rider_table(\%art_entry, 
+          { 
+            logical_card_id => $logical_card_id 
+          }
+        );
+
+        if ( !$art_entry{'seq_num'} )
+        {
+                $rldbh->diagnostic_log('warning', "No seq_num found in billing_log for $logical_card_id");
+                return 0;
+        }
+
+        my %pass_entry;
+#        $rldbh->get_user_pass(\%pass_entry, 
+        $rldbh->GetUserPass(\%pass_entry, 
+          {
+            CardId => $logical_card_id,
+            Active => 1
+          }
+        );
+
+        return 0 if (art_pass_mismatch($rldbh, 
+                                       $logical_card_id, 
+                                       $billing_cksum, 
+                                       $billing_ride_time, 
+                                       $billing_action, 
+                                       $billing_rule, 
+                                       \%pass_entry, 
+                                       \%art_entry));
+
+        # we only allow a pass to be used when it's an accept and the database is consistent for this pass
+        if (uc($billing_action) eq 'ACCEPT')
+        {
+                my $uc_type = uc($pass_entry{'Type'});
+
+                if ( $uc_type eq 'NRIDE') 
+                { 
+                        use_nride($rldbh, \%pass_entry, \%art_entry, $billing_ride_time); 
+                } 
+                elsif ( $uc_type eq 'NDAY')  
+                { 
+                        use_nday($rldbh, \%pass_entry, \%art_entry, $billing_ride_time);  
+                } 
+                else # domain card
+                { 
+                        update_domain_card($rldbh, \%pass_entry, $billing_ride_time); 
+                }
+
+        }
+        else
+        {
+                # update first used/last used?
+        }
+
+        ExpirePass( $rldbh, $logical_card_id );
+
+        return 1;
+}
+
+sub ServerReply
+{
+	my $client_query = $_[0];
+	
+	$/="\n";
+	chomp($client_query);
+	my $response = "";
+	
+	my $client_query_md5 = md5_hex($client_query);
+	
+        my $rldbh = RideLogicAPIQueryWrapper->connect($DSN, $DBUSER, $DBPASS) || die "ERROR: could not connect to DB";
+        $rldbh->raise_error( 1 );
+
+	my $sth ;
+	my $loglvl ;
+	my $message ;
+	my $logmsg ;
+
+	if ($client_query =~ m/^[\s\x00]*$/)
+	{
+		$logmsg .= "Ignoring spurious blank line.\n";
+		$response .= "IGN\t" . $client_query_md5 . "\n";
+	}
+	elsif ($client_query =~ m/^\!/) #error
+	{
+		$loglvl = "error";
+		$message = $client_query;
+		$message =~  s/^.//;
+
+		try {
+                        $rldbh->diagnostic_log($loglvl, $message) 
+                                or die "Couldn't write to diagnostic log: " . $rldbh->errstr;
+			$response .= "ACK\t" . $client_query_md5 . "\n";	
+		}
+		catch {
+			$logmsg .= $_ . "\n";
+			$response .= "IGN\t" . $client_query_md5 . "\n";
+		};
+		
+	}
+	elsif ($client_query =~ m/^\*/) #warning
+	{
+		$loglvl = "warning";
+		$message = $client_query;
+		$message =~  s/^.//;
+
+		try {
+                        $rldbh->diagnostic_log($loglvl, $message)
+                                or die "Couldn't write to diagnostic log: " . $rldbh->errstr;
+			$response .= "ACK\t" . $client_query_md5 . "\n";	
+		}
+		catch {
+			$logmsg .= $_ . "\n";
+			$response .= "IGN\t" . $client_query_md5 . "\n";
+		};
+	}
+	elsif ($client_query =~ m/^\#/) #debug
+	{
+		$loglvl = "debug";
+		$message = $client_query;
+		$message =~  s/^.//;
+
+		try {
+                        $rldbh->diagnostic_log($loglvl, $message)
+                                or die "Couldn't write to diagnostic log: " . $rldbh->errstr;
+			$response .= "ACK\t" . $client_query_md5 . "\n";	
+		}
+		catch {
+			$logmsg .= $_ . "\n";
+			$response .= "IGN\t" . $client_query_md5 . "\n";
+		};
+	}
+	elsif ($client_query =~ m/^(?:[^\t]*\t)+[^\t]*/)	#look for a list of optionally blank tab-delimited fields
+	{
+		my @client_values = split(/[\t]/, $client_query, -1);	#the -1 keeps split from trimming trailing blank fields
+			#0. 	equip_num
+			#1. 	driver
+			#2. 	paddle
+			#3. 	route
+			#4. 	trip
+			#5. 	stop
+			#6. 	ride_time
+			#7. 	latitude
+			#8. 	longitude
+			#9. 	action
+			#10.	rule
+			#11.	ruleparam
+			#12.	reason
+			#13.	credential
+			#14.	logical_card_id
+			#15.	cash_value
+			#16.	stop_name
+			#17.	(unused by DB) usec	
+		
+		try {
+                        my $duplicate_billing_entry = 
+                          $rldbh->check_dup_billing_log($client_values[6], $client_query_md5);
+
+                        if (!$duplicate_billing_entry) {
+                                $rldbh->insert_billing_log($client_query_md5, @client_values[0..16]);
+
+#                                $rldbh->lock_common();
+#                                $rldbh->begin_work();
+                                $rldbh->begin_locked_transaction_common();
+
+                                my $r = 
+                                  AdvanceRiderPass($rldbh,                                    # db handle
+                                                   $client_values[14],                        # logical_card_id 
+                                                   $client_query_md5,                         # billing log md5
+                                                   unix_to_readable_time($client_values[6]),  # ride_time (readable)
+                                                   $client_values[9],                         # action (e.g. ACCEPT/REJECT)
+                                                   $client_values[10]);                       # rule
+                                $response .=  "ACK\t" . $client_query_md5 . "\n";
+
+#                                $rldbh->commit();
+#                                $rldbh->unlock();
+                                $rldbh->unlock_commit();
+
+                        }
+                        else
+                        {
+                                $response .= "DUP\t" . $client_query_md5 . "\n";
+                        } 
+
+		}
+		catch {
+#                        $rldbh->rollback();
+#                        $rldbh->unlock();
+                        $rldbh->unlock_rollback();
+
+			$logmsg .= $_ . "\n";
+			$response .= "IGN\t" . $client_query_md5 . "\n";
+
+		};
+
+	}
+	else
+	{
+		$logmsg .= "Malformed log entry \"$client_query\".\n";
+		$response .= "IGN\t" . $client_query_md5 . "\n";
+	}
+	print $logmsg if $logmsg;
+	
+	return $response;
+}
+
+sub debug_print
+{
+        my $line = shift;
+        if ($debug_logfile =~ /^([^\0]+)$/) {
+                my $untainted_debug_logfile = $1;
+                sysopen ( my $fh , $untainted_debug_logfile, O_WRONLY|O_APPEND|O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH );
+                print $fh $line . "\n";
+                close $fh;
+	}
+}
+
+
+sub handle_client() 
+{
+	close SERVER;
+	CLIENT->autoflush(1);
+	
+	my $linebuffer;
+	
+	while($linebuffer = <CLIENT>) 
+	{
+                if ($billing_logfile =~ /^([^\0]+)$/) {
+                        my $untainted_billing_logfile = $1;
+                        sysopen ( my $fh , $untainted_billing_logfile, O_WRONLY|O_APPEND|O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH );
+                        print $fh $linebuffer;
+                        close $fh;
+                }
+
+		print CLIENT ServerReply($linebuffer);
+	}	#while data from client
+	
+	close CLIENT;
+}
+
+
+my $waitedpid = 0; 
+my $sigreceived = 0;
+
+
+sub REAPER 
+{ 
+	while (($waitedpid = waitpid(-1, WNOHANG))>0) { }
+	$SIG{CHLD} = \&REAPER; # loathe sysV 
+	$sigreceived = 1;
+} 
+
+
+sub spawn
+{ 
+	my $coderef = shift; 						#grab the first parameter
+	unless (@_ == 0 && $coderef && ref($coderef) eq 'CODE') 	#verify that it consists of a non-null block of executable perl code
+	{ 
+		confess "usage: spawn CODEREF"; 			#complain if this is not the case
+	} 
+	my $pid; 
+	if (!defined($pid = fork)) 					#attempt a fork, remembering the returned PID value
+	{
+		close CLIENT;
+		return; 						#failed to fork, we'd better close the client
+	} 
+	elsif ($pid) 							#If the returned process ID is non-zero, that indicates that we are the parent process
+	{ 
+		return; # i'm the parent 
+	}  
+	else								#otherwise, if the returned process ID is 0, that means we're the child process
+	{
+		exit &$coderef();					#in which case, we want to execute the child handler that was passed in, and then
+									#exit this (child) process when we've finished our conversation(s) with the
+									#other (client) end of the socket.
+	}
+} 
+
+sub show_help_and_exit {
+  print "usage:\n";
+  print "  [-i]       interactive, do not daemonize\n";
+  print "  [-c cfg]   use cfg as config file (default to " . $RideLogic::RIDELOGIC_DAEMON_CONF . ") \n";
+  print "  [-h]       show help (this screen)\n";
+  exit;
+}
+
+#----------------------------------------------------------------------
+#
+#----------------------------------------------------------------------
+my $daemonize   = 1;
+my $interactive = 0;
+my $show_help   = 0;
+my $cfg_file      = $RideLogic::RIDELOGIC_DAEMON_CONF;
+my $api_cfg_file  = $RideLogic::RIDELOGIC_API_CONF;
+GetOptions(
+  'i|interactive'   => \$interactive,
+  'c|config=s'      => \$cfg_file,
+  'h|help'          => \$show_help );
+show_help_and_exit() if ($show_help);
+
+$daemonize=0 if ($interactive);
+
+#----------------------------------------------------------------------
+#   Local network settings for Inter-Process communication.
+#----------------------------------------------------------------------
+my $proto = getprotobyname('tcp'); 
+my $addr = sockaddr_in( $bind_port ,inet_aton($bind_ip));;
+#----------------------------------------------------------------------
+
+my $max_retries = 10;		#Maximum number of address-binding retries before we give up.
+my $retry_count = $max_retries;	#number of retries left...
+my $retry_delay = 3;		#number of seconds to wait between retries at binding to our designated IPC address
+my $got_network = 0;		#flag to let us know that we can quit retrying once we have gotten a valid listening socket
+
+my %CFG_VAR;
+
+read_config($cfg_file, \%CFG_VAR)     if ($cfg_file);
+read_config($api_cfg_file, \%CFG_VAR) if ($api_cfg_file);
+
+my $logfile             = ($CFG_VAR{"RIDELOGIC_DAEMON_LOG_DIR"} || $RideLogic::RIDELOGIC_DAEMON_LOG_DIR) . "/ridelogic_billingd.log";
+   $billing_logfile     = ($CFG_VAR{"RIDELOGIC_DAEMON_LOG_DIR"} || $RideLogic::RIDELOGIC_DAEMON_LOG_DIR) . "/billing_log";
+my $pidfile             = ($CFG_VAR{"RIDELOGIC_DAEMON_PID_DIR"} || $RideLogic::RIDELOGIC_DAEMON_PID_DIR) . "/ridelogic_billingd.pid";
+   $debug_logfile       = ($CFG_VAR{"RIDELOGIC_DAEMON_LOG_DIR"} || $RideLogic::RIDELOGIC_DAEMON_LOG_DIR) . "/billing_debug_log";
+
+$HOST    = $CFG_VAR{'RIDELOGIC_DB_SERVER'};
+$DB      = $CFG_VAR{'RIDELOGIC_DB'};
+$DBUSER  = $CFG_VAR{'RIDELOGIC_DB_USERNAME'};
+$DBPASS  = $CFG_VAR{'RIDELOGIC_DB_PASSWORD'};
+$DSN     = "dbi:mysql:host=" . $HOST . ";database=" . $DB;
+
+
+#my $RLDBH = RideLogicAPIQueryWrapper->connect($DSN, $DBUSER, $DBPASS) || die "ERROR: could not connect to DB";
+#my ($query, $result, $row);
+#$RLDBH->raise_error( 1 );
+
+
+daemonize($logfile, $pidfile) if ($daemonize);
+
+# set our pipes to be piping hot
+$|=1;
+
+
+while( ($retry_count > 0) && (!$got_network) )
+{
+	try	#Try and allocate a socket, bind it to our IPC address, and set it to listen for connections
+	{
+		socket(SERVER,PF_INET,SOCK_STREAM,$proto) || die "socket: $!"; 
+		setsockopt(SERVER, SOL_SOCKET, SO_REUSEADDR,  1);
+		bind (SERVER, $addr) || die "bind: $!"; 
+		listen(SERVER,5) || die "listen: $!";
+		$got_network = 1;
+	}
+	catch	#If that didn't work for some reason, log the error, clean up, and prepair to retry
+	{
+		my $errmsg = $_;	#Remember the error message
+		
+		close(SERVER);		#Clean up the server socket if it needs it
+		
+		#Decrement our remaining retry counter
+		$retry_count = $retry_count - 1;
+		
+		#Log the message to our debug log
+		print "Failed to allocate socket, will retry $retry_count times: $errmsg\n";
+		
+		#Wait a reasonable period before trying again
+		sleep $retry_delay;
+	};
+}
+
+if($got_network)	#If we met with success binding to the network, report it
+{
+	my $logmsg = "Socket setup successful.  Listening for clients at $bind_ip:$bind_port\n";
+	
+	print $logmsg;	
+	
+}
+else			#If we ran out of patience and gave up, report that as well and exit
+{
+	my $errmsg = "Could not allocate and bind listening socket at $bind_ip:$bind_port after $max_retries attempts.\n";
+	
+	die $errmsg;
+}
+
+#    Set up our signal handler which will clean up defunct child processes and let the main
+# accept() loop know that the reason accept returned was due to a signal, not a legit connection.
+$SIG{CHLD} = \&REAPER; 
+
+#This for loop is efficient, but confusting, so I'll break it down by clause
+#
+#    The first clause ($sigreceived = 0) clears the signal received flag that will be set if the 
+# accept() call was interrupted by a signal.  This clause runs once before the first run of the loop
+#
+#    The second clause is the test clause, it will process the contents of the loop if EITHER
+# accept() has returned (presumably generating a valid file handle for the CLIENT end of the
+# socket, OR the signal received flag is set (thus accept would have returned early without
+# having actually accepted a connection.
+#
+#    The third clause (the 'incrementer') is run after each time the body is executed, before the 
+# test clause is executed again (deciding whether to run the body or drop out...  This test
+# clause will close the parent process' copy of the CLIENT file handle since (see body below)
+# after the body executes, all communication with the socket referred to by that file handle
+# will be carried out by the spawned child process.  This frees the parent's copy of the CLIENT
+# file handle to be used again in the parent process for the next accepted incoming connection.
+
+for ( $sigreceived = 0; accept(CLIENT,SERVER) || $sigreceived; $sigreceived = 0, close CLIENT) 
+{ 
+	next if $sigreceived; 			#If we were interrupted by a signal, there is no real client, just go back and try to accept a new one
+	print "connection received.\n";		#Print a diagnostic message confirming that we have made a connection
+	spawn sub {handle_client();};		#fork() off a child process that will handle communication with the socket pointed to by the CLIENT file handle 
+} 
+

+ 426 - 0
server/daemons/buspass_server/buspass_server.pl

@@ -0,0 +1,426 @@
+#!/usr/bin/perl -Tw 
+#
+# Copyright (c) 2019 Clementine Computing LLC.
+# 
+# This file is part of PopuFare.
+# 
+# PopuFare is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# PopuFare 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 Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with PopuFare.  If not, see <https://www.gnu.org/licenses/>.
+#
+
+require 5.002; 
+use strict; 
+use Socket; 
+use Switch;
+use Carp;
+use DBI;
+use FileHandle;
+use Fcntl;
+use Compress::Zlib;
+
+use POSIX;
+
+my $database_path = 'DBI:mysql:busdb';
+my $database_user = '';
+my $database_pass = '';
+
+my $bind_ip  	  = '127.0.0.1';
+my $bind_port	  = 7277;
+
+#----------------------------------------------Ugly exception handling logic using closures and anonymous functions----
+#-------------------------------------------This is in there to deal with the fact that CreditCall uses the die("error")
+#-------------------------------------------function instead of returning an error message in many cases...
+
+#       This utility function returns the passed string sans any leading or trailing whitespace.
+#
+sub strip_whitespace
+{
+	my $str = shift;        #grab our first parameter
+        
+        $str =~ s/^\s+//;       #strip leading whitespace
+        $str =~ s/\s+$//;       #strip trailing whitespace
+                        
+        return $str;            #return the improved string
+}
+                                
+
+#    This function takes two coderef parameters, the second of which is usually an explicit call to the 
+# 'catch' function which itself takes a coderef parameter.  This allows the code employing this suite of 
+# functions to look somewhat like a conventional exception handling mechanism:
+#
+# try
+# {
+#    do_something_that_might_die();
+# }
+# catch
+# {
+#   my $errmsg = $_;
+#   log_the_error_message($errmsg);
+#   perform_some_cleanup();
+# };
+#
+# DO NOT FORGET THAT LAST SEMICOLON, EVERYTHING GOES TO HELL IF YOU DO!
+#   
+sub try(&$)
+{
+	my ($attempt, $handler) = @_;
+  
+	eval
+	{
+		&$attempt;
+	};
+        
+	if($@)
+	{
+		do_catch($handler);
+	}
+}
+
+#    This function strips off the whitespace from the exception message reported by die()
+# and places the result into the default variable such that the code in the catch block can
+# just examine $_ to figure out what the cause of the error is, or to display or log
+# the error message.
+#
+sub do_catch(&$)
+{
+	my ($handler) = @_;
+
+	local $_ = strip_whitespace($@);
+	
+	&$handler;
+}
+
+#    This just takes an explicit coderef and returns it unharmed.  The only
+# purpose of this is so the try/catch structure looks pretty and familiar.                                          
+#
+sub catch(&) {$_[0]}
+
+#--------------------------------------------------------------------------------------------------------------------
+
+
+#my $DebugMode = 1;
+my $DebugMode = 0;
+
+#	This function only executes the passed code reference if the global variable $DebugMode is non-zero.
+# The reason for this is that any calculation (like a FooBar::ComplexObject->toString call) will not be
+# performed if we are not in debug mode, sort of like a very limited form of lazy evaluation.
+#
+sub ifdebug(&@)
+{
+	my ($cmd) = @_;
+	&$cmd() if($DebugMode);
+}
+
+
+sub ServerReply
+{
+	my $client_query = $_[0];
+	chomp($client_query);
+	my $response = "";
+	my $hangup_flag=0;
+
+	#Turning this on will use FLUSH instead of ZFLUSH, which is much slower
+	my $do_legacy_flush = 0;
+
+	switch ($client_query) 
+	{
+		case /^QUERY\t[0-9][0-9]*$/ 
+		{
+			my $sequence_number = $client_query;
+			$sequence_number =~  s/^QUERY\t//;
+
+			my $dbh = DBI->connect($database_path, $database_user, $database_pass)
+				or die "Couldn't connect to database: " . DBI->errstr;
+			
+			#A query to check for the validity of the queried sequence number
+			my $seqcheck = $dbh->prepare('SELECT seq_num FROM active_rider_table WHERE seq_num = ?') or die "Couldn't prepare statement: " . $dbh->errstr;
+			
+			#Prepare to send records
+			my $sth = $dbh->prepare('SELECT deleted, seq_num, logical_card_id, mag_token, rfid_token, rule_name, rule_param FROM active_rider_table a1 WHERE seq_num = ' .
+						'(SELECT MAX(seq_num) FROM active_rider_table a2 WHERE a1.logical_card_id= a2.logical_card_id) AND seq_num > ? ORDER BY seq_num ASC')
+				or die "Couldn't prepare statement: " . $dbh->errstr;
+
+			$seqcheck->execute($sequence_number);
+
+			#Check if the client is on the same page as us
+			#if not, tell them to flush everything and send it all again
+			my $flushdata = 0;
+			if ($sequence_number == 0) 
+			{
+				$flushdata = 1;
+
+				$sth->execute($sequence_number)             # Execute the query
+					or die "Couldn't execute statement: " . $sth->errstr;
+				
+			} 
+			elsif (!$seqcheck->fetchrow_array()) 
+			{
+				$sth->execute(0) # Get everything
+				or die "Couldn't execute statement: " . $sth->errstr;
+				$flushdata = 1;
+			}
+			else
+			{
+				$sth->execute($sequence_number)             # Execute the query
+					or die "Couldn't execute statement: " . $sth->errstr;
+			}
+
+			# Read the matching records and print them out
+			# $data[0] = deleted
+			# $data[1] = seq_num
+			# $data[2] = logical_card_id 
+			# $data[3] = mag_token
+			# $data[4] = rfid_token
+			# $data[5] = rule_name
+			# $data[6] = rule_param
+			my @data ;
+			
+			
+			#If we are doing a flush
+			if($flushdata)
+			{
+				if($do_legacy_flush)
+				{
+					$response .= "FLUSH\n" if $flushdata;
+					while (@data = $sth->fetchrow_array()) 
+					{
+						if (!$data[0]) 
+						{
+							$data[3] = "" unless defined $data[3];		#populate any NULL mag_token with ""
+							$data[4] = "" unless defined $data[4];		#populate any NULL rfid_token with ""
+							$data[6] = "" unless defined $data[6];		#populate any NULL rule_param with ""
+							$response .= "UPDATE\t$data[1]\t$data[2]\t$data[3]\t$data[4]\t$data[5]\t$data[6]\n"; 
+						}
+					}
+					$response .= "FLUSHDONE\n" if $flushdata;
+				}
+				else
+				{
+					my $z = deflateInit( -Level => Z_BEST_COMPRESSION ) or die "Cannot create a deflation stream\n";
+					my $size = 0;
+					my $dat = "";
+					my ($zout, $stat);
+					my $cmpdat;
+				
+					while (@data = $sth->fetchrow_array()) 
+					{
+						if (!$data[0]) 
+						{
+							$data[3] = "" unless defined $data[3];		#populate any NULL mag_token with ""
+							$data[4] = "" unless defined $data[4];		#populate any NULL rfid_token with ""
+							$data[6] = "" unless defined $data[6];		#populate any NULL rule_param with ""
+							$dat .= "UPDATE\t$data[1]\t$data[2]\t$data[3]\t$data[4]\t$data[5]\t$data[6]\n"; 
+						}
+					}
+				
+					($zout, $stat) = $z->deflate($dat);
+					$stat == Z_OK or die "deflation failed...";
+					$cmpdat = $zout;
+				
+					($zout, $stat) = $z->flush();
+					$stat == Z_OK or die "deflation failed...";
+					$cmpdat .= $zout;
+				
+					$size = $z->total_out();
+				
+					$response .= "ZFLUSH\t$size\n";
+					$response .= $cmpdat;
+					$response .= "ZFLUSHDONE\n";
+					
+					#Set the "HANG-UP" flag to make the server hang up on a client who has just done a ZFLUSH
+					#so that the client will start a fresh server session with its shiny new database
+					$hangup_flag = 1;
+				}
+				
+			}
+			else
+			{
+				while (@data = $sth->fetchrow_array()) 
+				{
+					if ($data[0]) 
+					{
+						$response .= "DELETE\t$data[1]\t$data[2]\n";
+					} else 
+					{
+						$data[3] = "" unless defined $data[3];		#populate any NULL mag_token with ""
+						$data[4] = "" unless defined $data[4];		#populate any NULL rfid_token with ""
+						$data[6] = "" unless defined $data[6];		#populate any NULL rule_param with ""
+						$response .= "UPDATE\t$data[1]\t$data[2]\t$data[3]\t$data[4]\t$data[5]\t$data[6]\n"; 
+					}
+				}
+			}
+			
+
+			$seqcheck->finish;
+			$sth->finish;
+			$dbh->disconnect;
+		}
+		else
+		{
+                        $response = "ERROR\n" . $client_query;
+		}
+	}
+	
+	if($response eq "")
+	{
+		$response .= "NOP\n";
+	}
+	
+	return ($response, $hangup_flag);
+}
+
+
+sub handle_client() 
+{
+	close SERVER;
+	CLIENT->autoflush(1);
+
+	my $linebuffer;
+	
+	while($linebuffer = <CLIENT>) 
+	{
+		my ($reply, $hangup_flag) = ServerReply($linebuffer);
+		print CLIENT $reply;
+	
+		if($hangup_flag)
+		{
+                        sleep(60);
+			shutdown(CLIENT, 2);
+			close CLIENT;
+			return 0;
+		}
+
+	}	#while data from client
+
+	close CLIENT;
+}
+
+
+my $waitedpid = 0; 
+my $sigreceived = 0;
+
+
+sub REAPER 
+{ 
+	while (($waitedpid = waitpid(-1, WNOHANG))>0) { }
+	$SIG{CHLD} = \&REAPER; # loathe sysV 
+	$sigreceived = 1;
+} 
+
+
+sub spawn
+{ 
+	my $coderef = shift; 						#grab the first parameter
+	unless (@_ == 0 && $coderef && ref($coderef) eq 'CODE') 	#verify that it consists of a non-null block of executable perl code
+	{ 
+		confess "usage: spawn CODEREF"; 			#complain if this is not the case
+	} 
+	my $pid; 
+	if (!defined($pid = fork)) 					#attempt a fork, remembering the returned PID value
+	{
+		close CLIENT;
+		return; 						#failed to fork, we'd better close the client
+	} 
+	elsif ($pid) 							#If the returned process ID is non-zero, that indicates that we are the parent process
+	{ 
+		return; # i'm the parent 
+	}  
+	else								#otherwise, if the returned process ID is 0, that means we're the child process
+	{
+		exit &$coderef();					#in which case, we want to execute the child handler that was passed in, and then
+									#exit this (child) process when we've finished our conversation(s) with the
+									#other (client) end of the socket.
+	}
+} 
+
+#----------------------------------------------------------------------
+#   Local network settings for Inter-Process communication.
+#----------------------------------------------------------------------
+my $proto = getprotobyname('tcp'); 
+my $addr = sockaddr_in( $bind_port ,inet_aton($bind_ip));;
+#----------------------------------------------------------------------
+
+my $max_retries = 10;		#Maximum number of address-binding retries before we give up.
+my $retry_count = $max_retries;	#number of retries left...
+my $retry_delay = 3;		#number of seconds to wait between retries at binding to our designated IPC address
+my $got_network = 0;		#flag to let us know that we can quit retrying once we have gotten a valid listening socket
+
+while( ($retry_count > 0) && (!$got_network) )
+{
+	try	#Try and allocate a socket, bind it to our IPC address, and set it to listen for connections
+	{
+		socket(SERVER,PF_INET,SOCK_STREAM,$proto) || die "socket: $!"; 
+		setsockopt(SERVER, SOL_SOCKET, SO_REUSEADDR,  1);
+		bind (SERVER, $addr) || die "bind: $!"; 
+		listen(SERVER,5) || die "listen: $!";
+		$got_network = 1;
+	}
+	catch	#If that didn't work for some reason, log the error, clean up, and prepair to retry
+	{
+		my $errmsg = $_;	#Remember the error message
+		
+		close(SERVER);		#Clean up the server socket if it needs it
+		
+		#Decrement our remaining retry counter
+		$retry_count = $retry_count - 1;
+		
+		#Log the message to our debug log
+		print "Failed to allocate socket, will retry $retry_count times: $errmsg\n";
+		
+		#Wait a reasonable period before trying again
+		sleep $retry_delay;
+	};
+}
+
+if($got_network)	#If we met with success binding to the network, report it
+{
+	my $logmsg = "Socket setup successful.  Listening for clients at $bind_ip:$bind_port\n";
+	
+	print $logmsg;	
+	
+}
+else			#If we ran out of patience and gave up, report that as well and exit
+{
+	my $errmsg = "Could not allocate and bind listening socket at $bind_ip:$bind_port after $max_retries attempts.\n";
+	
+	die $errmsg;
+}
+
+#    Set up our signal handler which will clean up defunct child processes and let the main
+# accept() loop know that the reason accept returned was due to a signal, not a legit connection.
+$SIG{CHLD} = \&REAPER; 
+
+#This for loop is efficient, but confusting, so I'll break it down by clause
+#
+#    The first clause ($sigreceived = 0) clears the signal received flag that will be set if the 
+# accept() call was interrupted by a signal.  This clause runs once before the first run of the loop
+#
+#    The second clause is the test clause, it will process the contents of the loop if EITHER
+# accept() has returned (presumably generating a valid file handle for the CLIENT end of the
+# socket, OR the signal received flag is set (thus accept would have returned early without
+# having actually accepted a connection.
+#
+#    The third clause (the 'incrementer') is run after each time the body is executed, before the 
+# test clause is executed again (deciding whether to run the body or drop out...  This test
+# clause will close the parent process' copy of the CLIENT file handle since (see body below)
+# after the body executes, all communication with the socket referred to by that file handle
+# will be carried out by the spawned child process.  This frees the parent's copy of the CLIENT
+# file handle to be used again in the parent process for the next accepted incoming connection.
+
+for ( $sigreceived = 0; accept(CLIENT,SERVER) || $sigreceived; $sigreceived = 0, close CLIENT) 
+{ 
+	next if $sigreceived; 			#If we were interrupted by a signal, there is no real client, just go back and try to accept a new one
+	print "connection received.\n";		#Print a diagnostic message confirming that we have made a connection
+	spawn sub {handle_client();};		#fork() off a child process that will handle communication with the socket pointed to by the CLIENT file handle 
+} 
+

+ 393 - 0
server/daemons/buspass_server/ridelogic_buspassd

@@ -0,0 +1,393 @@
+#!/usr/bin/perl -Tw 
+#
+# Copyright (c) 2019 Clementine Computing LLC.
+# 
+# This file is part of PopuFare.
+# 
+# PopuFare is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# PopuFare 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 Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with PopuFare.  If not, see <https://www.gnu.org/licenses/>.
+#
+
+require 5.002; 
+use strict; 
+use Socket; 
+use Switch;
+use Carp;
+use DBI;
+use FileHandle;
+use Fcntl;
+use Compress::Zlib;
+use Getopt::Long qw(:config no_ignore_case);
+use POSIX;
+use RideLogic;
+
+my $database_path = 'DBI:mysql:busdb';
+my $database_user = '';
+my $database_pass = '';
+
+my $bind_ip  	  = '127.0.0.1';
+my $bind_port	  = 7277;
+
+my $DebugMode = 0;
+
+#	This function only executes the passed code reference if the global variable $DebugMode is non-zero.
+# The reason for this is that any calculation (like a FooBar::ComplexObject->toString call) will not be
+# performed if we are not in debug mode, sort of like a very limited form of lazy evaluation.
+#
+sub ifdebug(&@)
+{
+	my ($cmd) = @_;
+	&$cmd() if($DebugMode);
+}
+
+
+sub ServerReply
+{
+	my $client_query = $_[0];
+	chomp($client_query);
+	my $response = "";
+	my $hangup_flag=0;
+
+	#Turning this on will use FLUSH instead of ZFLUSH, which is much slower
+	my $do_legacy_flush = 0;
+
+	switch ($client_query) 
+	{
+		case /^QUERY\t[0-9][0-9]*$/ 
+		{
+			my $sequence_number = $client_query;
+			$sequence_number =~  s/^QUERY\t//;
+
+			my $dbh = DBI->connect($database_path, $database_user, $database_pass)
+				or die "Couldn't connect to database: " . DBI->errstr;
+			
+			#A query to check for the validity of the queried sequence number
+			my $seqcheck = $dbh->prepare('SELECT seq_num FROM active_rider_table WHERE seq_num = ?') or die "Couldn't prepare statement: " . $dbh->errstr;
+			
+			#Prepare to send records
+			my $sth = $dbh->prepare('SELECT deleted, seq_num, logical_card_id, mag_token, rfid_token, rule_name, rule_param FROM active_rider_table a1 WHERE seq_num = ' .
+						'(SELECT MAX(seq_num) FROM active_rider_table a2 WHERE a1.logical_card_id= a2.logical_card_id) AND seq_num > ? ORDER BY seq_num ASC')
+				or die "Couldn't prepare statement: " . $dbh->errstr;
+
+			$seqcheck->execute($sequence_number);
+
+			#Check if the client is on the same page as us
+			#if not, tell them to flush everything and send it all again
+			my $flushdata = 0;
+			if ($sequence_number == 0) 
+			{
+				$flushdata = 1;
+
+				$sth->execute($sequence_number)             # Execute the query
+					or die "Couldn't execute statement: " . $sth->errstr;
+				
+			} 
+			elsif (!$seqcheck->fetchrow_array()) 
+			{
+				$sth->execute(0) # Get everything
+				or die "Couldn't execute statement: " . $sth->errstr;
+				$flushdata = 1;
+			}
+			else
+			{
+				$sth->execute($sequence_number)             # Execute the query
+					or die "Couldn't execute statement: " . $sth->errstr;
+			}
+
+			# Read the matching records and print them out
+			# $data[0] = deleted
+			# $data[1] = seq_num
+			# $data[2] = logical_card_id 
+			# $data[3] = mag_token
+			# $data[4] = rfid_token
+			# $data[5] = rule_name
+			# $data[6] = rule_param
+			my @data ;
+			
+			
+			#If we are doing a flush
+			if($flushdata)
+			{
+				if($do_legacy_flush)
+				{
+					$response .= "FLUSH\n" if $flushdata;
+					while (@data = $sth->fetchrow_array()) 
+					{
+						if (!$data[0]) 
+						{
+							$data[3] = "" unless defined $data[3];		#populate any NULL mag_token with ""
+							$data[4] = "" unless defined $data[4];		#populate any NULL rfid_token with ""
+							$data[6] = "" unless defined $data[6];		#populate any NULL rule_param with ""
+							$response .= "UPDATE\t$data[1]\t$data[2]\t$data[3]\t$data[4]\t$data[5]\t$data[6]\n"; 
+						}
+					}
+					$response .= "FLUSHDONE\n" if $flushdata;
+				}
+				else
+				{
+					my $z = deflateInit( -Level => Z_BEST_COMPRESSION ) or die "Cannot create a deflation stream\n";
+					my $size = 0;
+					my $dat = "";
+					my ($zout, $stat);
+					my $cmpdat;
+				
+					while (@data = $sth->fetchrow_array()) 
+					{
+						if (!$data[0]) 
+						{
+							$data[3] = "" unless defined $data[3];		#populate any NULL mag_token with ""
+							$data[4] = "" unless defined $data[4];		#populate any NULL rfid_token with ""
+							$data[6] = "" unless defined $data[6];		#populate any NULL rule_param with ""
+							$dat .= "UPDATE\t$data[1]\t$data[2]\t$data[3]\t$data[4]\t$data[5]\t$data[6]\n"; 
+						}
+					}
+				
+					($zout, $stat) = $z->deflate($dat);
+					$stat == Z_OK or die "deflation failed...";
+					$cmpdat = $zout;
+				
+					($zout, $stat) = $z->flush();
+					$stat == Z_OK or die "deflation failed...";
+					$cmpdat .= $zout;
+				
+					$size = $z->total_out();
+				
+					$response .= "ZFLUSH\t$size\n";
+					$response .= $cmpdat;
+					$response .= "ZFLUSHDONE\n";
+					
+					#Set the "HANG-UP" flag to make the server hang up on a client who has just done a ZFLUSH
+					#so that the client will start a fresh server session with its shiny new database
+					$hangup_flag = 1;
+				}
+				
+			}
+			else
+			{
+				while (@data = $sth->fetchrow_array()) 
+				{
+					if ($data[0]) 
+					{
+						$response .= "DELETE\t$data[1]\t$data[2]\n";
+					} else 
+					{
+						$data[3] = "" unless defined $data[3];		#populate any NULL mag_token with ""
+						$data[4] = "" unless defined $data[4];		#populate any NULL rfid_token with ""
+						$data[6] = "" unless defined $data[6];		#populate any NULL rule_param with ""
+						$response .= "UPDATE\t$data[1]\t$data[2]\t$data[3]\t$data[4]\t$data[5]\t$data[6]\n"; 
+					}
+				}
+			}
+			
+
+			$seqcheck->finish;
+			$sth->finish;
+			$dbh->disconnect;
+		}
+		else
+		{
+                        $response = "ERROR\n" . $client_query;
+		}
+	}
+	
+	if($response eq "")
+	{
+		$response .= "NOP\n";
+	}
+	
+	return ($response, $hangup_flag);
+}
+
+
+sub handle_client() 
+{
+	close SERVER;
+	CLIENT->autoflush(1);
+
+	my $linebuffer;
+	
+	while($linebuffer = <CLIENT>) 
+	{
+		my ($reply, $hangup_flag) = ServerReply($linebuffer);
+		print CLIENT $reply;
+	
+		if($hangup_flag)
+		{
+                        sleep(60);
+			shutdown(CLIENT, 2);
+			close CLIENT;
+			return 0;
+		}
+
+	}	#while data from client
+
+	close CLIENT;
+}
+
+
+my $waitedpid = 0; 
+my $sigreceived = 0;
+
+
+sub REAPER 
+{ 
+	while (($waitedpid = waitpid(-1, WNOHANG))>0) { }
+	$SIG{CHLD} = \&REAPER; # loathe sysV 
+	$sigreceived = 1;
+} 
+
+
+sub spawn
+{ 
+	my $coderef = shift; 						#grab the first parameter
+	unless (@_ == 0 && $coderef && ref($coderef) eq 'CODE') 	#verify that it consists of a non-null block of executable perl code
+	{ 
+		confess "usage: spawn CODEREF"; 			#complain if this is not the case
+	} 
+	my $pid; 
+	if (!defined($pid = fork)) 					#attempt a fork, remembering the returned PID value
+	{
+		close CLIENT;
+		return; 						#failed to fork, we'd better close the client
+	} 
+	elsif ($pid) 							#If the returned process ID is non-zero, that indicates that we are the parent process
+	{ 
+		return; # i'm the parent 
+	}  
+	else								#otherwise, if the returned process ID is 0, that means we're the child process
+	{
+		exit &$coderef();					#in which case, we want to execute the child handler that was passed in, and then
+									#exit this (child) process when we've finished our conversation(s) with the
+									#other (client) end of the socket.
+	}
+} 
+
+sub show_help_and_exit {
+  print "usage:\n";
+  print "  [-i]       interactive, do not daemonize\n";
+  print "  [-c cfg]   use cfg as config file (default to " . $RideLogic::RIDELOGIC_DAEMON_CONF . ") \n";
+  print "  [-h]       show help (this screen)\n";
+  exit;
+}
+
+#----------------------------------------------------------------------
+#
+#----------------------------------------------------------------------
+my $daemonize   = 1;
+my $interactive = 0;
+my $show_help   = 0;
+my $cfg_file    = $RideLogic::RIDELOGIC_DAEMON_CONF;
+GetOptions(
+  'i|interactive'   => \$interactive,
+  'c|config=s'      => \$cfg_file,
+  'h|help'          => \$show_help );
+show_help_and_exit() if ($show_help);
+
+$daemonize=0 if ($interactive);
+
+
+
+#----------------------------------------------------------------------
+#   Local network settings for Inter-Process communication.
+#----------------------------------------------------------------------
+my $proto = getprotobyname('tcp'); 
+my $addr = sockaddr_in( $bind_port ,inet_aton($bind_ip));;
+#----------------------------------------------------------------------
+
+my $max_retries = 10;		#Maximum number of address-binding retries before we give up.
+my $retry_count = $max_retries;	#number of retries left...
+my $retry_delay = 3;		#number of seconds to wait between retries at binding to our designated IPC address
+my $got_network = 0;		#flag to let us know that we can quit retrying once we have gotten a valid listening socket
+
+my %CFG_VAR;
+
+read_config($cfg_file, \%CFG_VAR) if ($cfg_file);
+
+my $logfile     = ($CFG_VAR{"RIDELOGIC_DAEMON_LOG_DIR"} || $RideLogic::RIDELOGIC_DAEMON_LOG_DIR) . "/ridelogic_buspassd.log";
+my $pidfile     = ($CFG_VAR{"RIDELOGIC_DAEMON_PID_DIR"} || $RideLogic::RIDELOGIC_DAEMON_PID_DIR) . "/ridelogic_buspassd.pid";
+
+daemonize($logfile, $pidfile) if ($daemonize);
+
+# set our pipes to be piping hot
+$|=1;
+
+
+while( ($retry_count > 0) && (!$got_network) )
+{
+	try	#Try and allocate a socket, bind it to our IPC address, and set it to listen for connections
+	{
+		socket(SERVER,PF_INET,SOCK_STREAM,$proto) || die "socket: $!"; 
+		setsockopt(SERVER, SOL_SOCKET, SO_REUSEADDR,  1);
+		bind (SERVER, $addr) || die "bind: $!"; 
+		listen(SERVER,5) || die "listen: $!";
+		$got_network = 1;
+	}
+	catch	#If that didn't work for some reason, log the error, clean up, and prepair to retry
+	{
+		my $errmsg = $_;	#Remember the error message
+		
+		close(SERVER);		#Clean up the server socket if it needs it
+		
+		#Decrement our remaining retry counter
+		$retry_count = $retry_count - 1;
+		
+		#Log the message to our debug log
+		print "Failed to allocate socket, will retry $retry_count times: $errmsg\n";
+		
+		#Wait a reasonable period before trying again
+		sleep $retry_delay;
+	};
+}
+
+if($got_network)	#If we met with success binding to the network, report it
+{
+	my $logmsg = "Socket setup successful.  Listening for clients at $bind_ip:$bind_port\n";
+	
+	print $logmsg;	
+	
+}
+else			#If we ran out of patience and gave up, report that as well and exit
+{
+	my $errmsg = "Could not allocate and bind listening socket at $bind_ip:$bind_port after $max_retries attempts.\n";
+	
+	die $errmsg;
+}
+
+#    Set up our signal handler which will clean up defunct child processes and let the main
+# accept() loop know that the reason accept returned was due to a signal, not a legit connection.
+$SIG{CHLD} = \&REAPER; 
+
+#This for loop is efficient, but confusting, so I'll break it down by clause
+#
+#    The first clause ($sigreceived = 0) clears the signal received flag that will be set if the 
+# accept() call was interrupted by a signal.  This clause runs once before the first run of the loop
+#
+#    The second clause is the test clause, it will process the contents of the loop if EITHER
+# accept() has returned (presumably generating a valid file handle for the CLIENT end of the
+# socket, OR the signal received flag is set (thus accept would have returned early without
+# having actually accepted a connection.
+#
+#    The third clause (the 'incrementer') is run after each time the body is executed, before the 
+# test clause is executed again (deciding whether to run the body or drop out...  This test
+# clause will close the parent process' copy of the CLIENT file handle since (see body below)
+# after the body executes, all communication with the socket referred to by that file handle
+# will be carried out by the spawned child process.  This frees the parent's copy of the CLIENT
+# file handle to be used again in the parent process for the next accepted incoming connection.
+
+for ( $sigreceived = 0; accept(CLIENT,SERVER) || $sigreceived; $sigreceived = 0, close CLIENT) 
+{ 
+	next if $sigreceived; 			#If we were interrupted by a signal, there is no real client, just go back and try to accept a new one
+	print "connection received.\n";		#Print a diagnostic message confirming that we have made a connection
+	spawn sub {handle_client();};		#fork() off a child process that will handle communication with the socket pointed to by the CLIENT file handle 
+} 
+

+ 25 - 0
server/daemons/etc/init.d/ridelogic_avlsd

@@ -0,0 +1,25 @@
+#!/bin/bash
+#
+#chkconfig: 3 99 99
+#description: RideLogic Automatic Vehicle Location Server Daemon
+#
+# Copyright (c) 2019 Clementine Computing LLC.
+# 
+# This file is part of PopuFare.
+# 
+# PopuFare is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# PopuFare 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 Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with PopuFare.  If not, see <https://www.gnu.org/licenses/>.
+#
+
+
+/etc/init.d/ridelogic_daemon_manager ridelogic_avlsd $@

+ 24 - 0
server/daemons/etc/init.d/ridelogic_billingd

@@ -0,0 +1,24 @@
+#!/bin/bash
+#
+#chkconfig: 3 99 99
+#description: RideLogic Billing Daemon
+#
+# Copyright (c) 2019 Clementine Computing LLC.
+# 
+# This file is part of PopuFare.
+# 
+# PopuFare is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# PopuFare 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 Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with PopuFare.  If not, see <https://www.gnu.org/licenses/>.
+#
+
+/etc/init.d/ridelogic_daemon_manager ridelogic_billingd $@

+ 25 - 0
server/daemons/etc/init.d/ridelogic_buspassd

@@ -0,0 +1,25 @@
+#!/bin/bash
+#
+#chkconfig: 3 99 99
+#description: RideLogic Bus Pass Daemon
+#
+# Copyright (c) 2019 Clementine Computing LLC.
+# 
+# This file is part of PopuFare.
+# 
+# PopuFare is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# PopuFare 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 Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with PopuFare.  If not, see <https://www.gnu.org/licenses/>.
+#
+
+
+/etc/init.d/ridelogic_daemon_manager ridelogic_buspassd $@

+ 111 - 0
server/daemons/etc/init.d/ridelogic_daemon_manager

@@ -0,0 +1,111 @@
+#!/bin/bash
+#
+# ridelogic_daemon_manager
+#
+# Copyright (c) 2019 Clementine Computing LLC.
+# 
+# This file is part of PopuFare.
+# 
+# PopuFare is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# PopuFare 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 Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with PopuFare.  If not, see <https://www.gnu.org/licenses/>.
+#
+
+
+. /etc/rc.d/init.d/functions
+
+if [ -f /etc/ridelogic/daemon.conf ]
+then
+        . /etc/ridelogic/daemon.conf
+else
+        echo "Missing config file /etc/ridelogic/daemon.conf, exiting"
+        exit 1
+fi
+
+daemon=( ridelogic_avlsd ridelogic_buspassd ridelogic_billingd ridelogic_hellod ridelogic_versiond )
+
+prog=''
+for d in ${daemon[@]} 
+do
+        if [ "$1" == $d ]
+        then
+                prog=$d
+                break
+        fi
+done
+
+if [ -z "$1" ] || [ -z "$prog" ]
+then
+  echo "Usage: $0 daemon (start|stop|restart|condrestart)"
+  exit 1
+fi
+
+path="$RIDELOGIC_DAEMON_DIR/$prog"
+pidfile="$RIDELOGIC_DAEMON_PID_DIR/$prog.pid"
+logfile="$RIDELOGIC_DAEMON_LOG_DIR/$prog.log"
+
+start() {
+	if [ -e "$pidfile" ]; then
+		if ps -p `cat $pidfile` > /dev/null 2>&1; then
+			echo $path is already running - skipping start
+                        return 0
+		else
+			echo Lockfile $pidfile is junk. Deleting...
+			rm -f $pidfile
+		fi
+        fi
+
+        echo -n Starting $path
+        daemon --user $RIDELOGIC_DAEMON_USER $path 
+        RETVAL=$?
+        echo
+        return $RETVAL
+}
+
+stop() {
+        RETVAL=0
+	if [ -e $pidfile ]; then
+		echo -n Stopping $path
+                killproc $path
+                RETVAL=$?
+		rm -f $pidfile
+	else 
+		echo -n $path is not running
+	fi
+        echo
+        return $RETVAL
+}
+
+case $2 in
+	start )
+                start
+	        ;;
+	stop )
+                stop
+               	;;
+	restart )
+		stop
+		start
+	        ;;
+        status )
+                status -p ${pidfile} $path
+                ;;
+	condrestart )
+                if [ -f "$pidfile" ] ; then
+                        stop
+                        start
+                fi
+	        ;;
+	* )
+		echo "Usage: $prog (start|stop|restart|condrestart|status)"
+		;;
+esac

+ 25 - 0
server/daemons/etc/init.d/ridelogic_hellod

@@ -0,0 +1,25 @@
+#!/bin/bash
+#
+#chkconfig: 3 99 99
+#description: RideLogic Hello Daemon
+# 
+# Copyright (c) 2019 Clementine Computing LLC.
+# 
+# This file is part of PopuFare.
+# 
+# PopuFare is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# PopuFare 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 Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with PopuFare.  If not, see <https://www.gnu.org/licenses/>.
+#
+
+
+/etc/init.d/ridelogic_daemon_manager ridelogic_hellod $@

+ 25 - 0
server/daemons/etc/init.d/ridelogic_versiond

@@ -0,0 +1,25 @@
+#!/bin/bash
+#
+#chkconfig: 3 99 99
+#description: RideLogic Version Daemon
+#
+# Copyright (c) 2019 Clementine Computing LLC.
+# 
+# This file is part of PopuFare.
+# 
+# PopuFare is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# PopuFare 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 Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with PopuFare.  If not, see <https://www.gnu.org/licenses/>.
+#
+
+
+/etc/init.d/ridelogic_daemon_manager ridelogic_versiond $@

+ 56 - 0
server/daemons/etc/ridelogic/api.conf

@@ -0,0 +1,56 @@
+# config file for RideLogic web api
+#
+# Copyright (c) 2019 Clementine Computing LLC.
+# 
+# This file is part of PopuFare.
+# 
+# PopuFare is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# PopuFare 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 Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with PopuFare.  If not, see <https://www.gnu.org/licenses/>.
+#
+
+
+# api password
+RIDELOGIC_API_PASS=####
+
+# default credential information
+RIDELOGIC_DEFAULT_MAG_TRACK=2
+RIDELOGIC_DEFAULT_RF_LENGTH=26
+RIDELOGIC_DEFAULT_RF_SITE=137
+
+# default group membership and hash algorithm
+RIDELOGIC_DEFAULT_GROUP_NAME=####
+RIDELOGIC_DEFAULT_HASH_ALGORITHM=sha1
+
+# default session token length
+RIDELOGIC_TOKEN_LENGTH=32
+
+# maximum number of characters to a field allowed
+RIDELOGIC_MAX_FIELD_LENGTH=256
+
+# 120 seconds to fulfill api request
+RIDELOGIC_STALE_SEC_SESSION=120
+
+# Allow a day window after registration
+# for account activation (86400=24*60*60)
+RIDELOGIC_STALE_SEC_REGISTRATION=86400
+
+# Allow a two day window to reset password
+# after a password reset attempt (172800=48*60*60)
+RIDELOGIC_STALE_SEC_PASSWORD_RESET=172800
+
+# DB information
+RIDELOGIC_DB=####
+RIDELOGIC_DB_SERVER=####
+RIDELOGIC_DB_USERNAME=####
+RIDELOGIC_DB_PASSWORD=####
+

+ 27 - 0
server/daemons/etc/ridelogic/daemon.conf

@@ -0,0 +1,27 @@
+#
+#
+# Copyright (c) 2019 Clementine Computing LLC.
+# 
+# This file is part of PopuFare.
+# 
+# PopuFare is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# PopuFare 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 Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with PopuFare.  If not, see <https://www.gnu.org/licenses/>.
+#
+
+
+RIDELOGIC_DAEMON_USER=bus
+RIDELOGIC_DAEMON_DIR=/usr/bin
+RIDELOGIC_DAEMON_LOG_DIR=/etc/ridelogic/logs
+RIDELOGIC_DAEMON_PID_DIR=/etc/ridelogic/run
+
+

+ 245 - 0
server/daemons/hello_daemon/hello_daemon.pl

@@ -0,0 +1,245 @@
+#!/usr/bin/perl -Tw 
+#
+# Copyright (c) 2019 Clementine Computing LLC.
+# 
+# This file is part of PopuFare.
+# 
+# PopuFare is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# PopuFare 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 Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with PopuFare.  If not, see <https://www.gnu.org/licenses/>.
+#
+
+require 5.002; 
+use strict; 
+use Socket; 
+use Carp;
+use FileHandle;
+use Fcntl;
+
+use POSIX;
+
+my $bind_ip  	  = '127.0.0.1';
+my $bind_port	  = 3556;
+
+#----------------------------------------------Ugly exception handling logic using closures and anonymous functions----
+#-------------------------------------------This is in there to deal with the fact that CreditCall uses the die("error")
+#-------------------------------------------function instead of returning an error message in many cases...
+
+#       This utility function returns the passed string sans any leading or trailing whitespace.
+#
+sub strip_whitespace
+{
+	my $str = shift;        #grab our first parameter
+        
+        $str =~ s/^\s+//;       #strip leading whitespace
+        $str =~ s/\s+$//;       #strip trailing whitespace
+                        
+        return $str;            #return the improved string
+}
+                                
+
+#    This function takes two coderef parameters, the second of which is usually an explicit call to the 
+# 'catch' function which itself takes a coderef parameter.  This allows the code employing this suite of 
+# functions to look somewhat like a conventional exception handling mechanism:
+#
+# try
+# {
+#    do_something_that_might_die();
+# }
+# catch
+# {
+#   my $errmsg = $_;
+#   log_the_error_message($errmsg);
+#   perform_some_cleanup();
+# };
+#
+# DO NOT FORGET THAT LAST SEMICOLON, EVERYTHING GOES TO HELL IF YOU DO!
+#   
+sub try(&$)
+{
+	my ($attempt, $handler) = @_;
+  
+	eval
+	{
+		&$attempt;
+	};
+        
+	if($@)
+	{
+		do_catch($handler);
+	}
+}
+
+#    This function strips off the whitespace from the exception message reported by die()
+# and places the result into the default variable such that the code in the catch block can
+# just examine $_ to figure out what the cause of the error is, or to display or log
+# the error message.
+#
+sub do_catch(&$)
+{
+	my ($handler) = @_;
+
+	local $_ = strip_whitespace($@);
+	
+	&$handler;
+}
+
+#    This just takes an explicit coderef and returns it unharmed.  The only
+# purpose of this is so the try/catch structure looks pretty and familiar.                                          
+#
+sub catch(&) {$_[0]}
+
+#--------------------------------------------------------------------------------------------------------------------
+
+
+#my $DebugMode = 1;
+my $DebugMode = 0;
+
+#	This function only executes the passed code reference if the global variable $DebugMode is non-zero.
+# The reason for this is that any calculation (like a FooBar::ComplexObject->toString call) will not be
+# performed if we are not in debug mode, sort of like a very limited form of lazy evaluation.
+#
+sub ifdebug(&@)
+{
+	my ($cmd) = @_;
+	&$cmd() if($DebugMode);
+}
+
+sub handle_client() 
+{
+	close SERVER;
+	CLIENT->autoflush(1);
+
+	print CLIENT 'Hello.';
+	
+	close CLIENT;
+}
+
+
+my $waitedpid = 0; 
+my $sigreceived = 0;
+
+
+sub REAPER 
+{ 
+	while (($waitedpid = waitpid(-1, WNOHANG))>0) { }
+	$SIG{CHLD} = \&REAPER; # loathe sysV 
+	$sigreceived = 1;
+} 
+
+
+sub spawn
+{ 
+	my $coderef = shift; 						#grab the first parameter
+	unless (@_ == 0 && $coderef && ref($coderef) eq 'CODE') 	#verify that it consists of a non-null block of executable perl code
+	{ 
+		confess "usage: spawn CODEREF"; 			#complain if this is not the case
+	} 
+	my $pid; 
+	if (!defined($pid = fork)) 					#attempt a fork, remembering the returned PID value
+	{
+		close CLIENT;
+		return; 						#failed to fork, we'd better close the client
+	} 
+	elsif ($pid) 							#If the returned process ID is non-zero, that indicates that we are the parent process
+	{ 
+		return; # i'm the parent 
+	}  
+	else								#otherwise, if the returned process ID is 0, that means we're the child process
+	{
+		exit &$coderef();					#in which case, we want to execute the child handler that was passed in, and then
+									#exit this (child) process when we've finished our conversation(s) with the
+									#other (client) end of the socket.
+	}
+} 
+
+#----------------------------------------------------------------------
+#   Local network settings for Inter-Process communication.
+#----------------------------------------------------------------------
+my $proto = getprotobyname('tcp'); 
+my $addr = sockaddr_in( $bind_port ,inet_aton($bind_ip));;
+#----------------------------------------------------------------------
+
+my $max_retries = 10;		#Maximum number of address-binding retries before we give up.
+my $retry_count = $max_retries;	#number of retries left...
+my $retry_delay = 3;		#number of seconds to wait between retries at binding to our designated IPC address
+my $got_network = 0;		#flag to let us know that we can quit retrying once we have gotten a valid listening socket
+
+while( ($retry_count > 0) && (!$got_network) )
+{
+	try	#Try and allocate a socket, bind it to our IPC address, and set it to listen for connections
+	{
+		socket(SERVER,PF_INET,SOCK_STREAM,$proto) || die "socket: $!"; 
+		setsockopt(SERVER, SOL_SOCKET, SO_REUSEADDR,  1);
+		bind (SERVER, $addr) || die "bind: $!"; 
+		listen(SERVER,5) || die "listen: $!";
+		$got_network = 1;
+	}
+	catch	#If that didn't work for some reason, log the error, clean up, and prepair to retry
+	{
+		my $errmsg = $_;	#Remember the error message
+		
+		close(SERVER);		#Clean up the server socket if it needs it
+		
+		#Decrement our remaining retry counter
+		$retry_count = $retry_count - 1;
+		
+		#Log the message to our debug log
+		print "Failed to allocate socket, will retry $retry_count times: $errmsg\n";
+		
+		#Wait a reasonable period before trying again
+		sleep $retry_delay;
+	};
+}
+
+if($got_network)	#If we met with success binding to the network, report it
+{
+	my $logmsg = "Socket setup successful.  Listening for clients at $bind_ip:$bind_port\n";
+	
+	print $logmsg;	
+	
+}
+else			#If we ran out of patience and gave up, report that as well and exit
+{
+	my $errmsg = "Could not allocate and bind listening socket at $bind_ip:$bind_port after $max_retries attempts.\n";
+	
+	die $errmsg;
+}
+
+#    Set up our signal handler which will clean up defunct child processes and let the main
+# accept() loop know that the reason accept returned was due to a signal, not a legit connection.
+$SIG{CHLD} = \&REAPER; 
+
+#This for loop is efficient, but confusting, so I'll break it down by clause
+#
+#    The first clause ($sigreceived = 0) clears the signal received flag that will be set if the 
+# accept() call was interrupted by a signal.  This clause runs once before the first run of the loop
+#
+#    The second clause is the test clause, it will process the contents of the loop if EITHER
+# accept() has returned (presumably generating a valid file handle for the CLIENT end of the
+# socket, OR the signal received flag is set (thus accept would have returned early without
+# having actually accepted a connection.
+#
+#    The third clause (the 'incrementer') is run after each time the body is executed, before the 
+# test clause is executed again (deciding whether to run the body or drop out...  This test
+# clause will close the parent process' copy of the CLIENT file handle since (see body below)
+# after the body executes, all communication with the socket referred to by that file handle
+# will be carried out by the spawned child process.  This frees the parent's copy of the CLIENT
+# file handle to be used again in the parent process for the next accepted incoming connection.
+
+for ( $sigreceived = 0; accept(CLIENT,SERVER) || $sigreceived; $sigreceived = 0, close CLIENT) 
+{ 
+	next if $sigreceived; 			#If we were interrupted by a signal, there is no real client, just go back and try to accept a new one
+	print "connection received.\n";		#Print a diagnostic message confirming that we have made a connection
+	spawn sub {handle_client();};		#fork() off a child process that will handle communication with the socket pointed to by the CLIENT file handle 
+} 
+

+ 206 - 0
server/daemons/hello_daemon/ridelogic_hellod

@@ -0,0 +1,206 @@
+#!/usr/bin/perl -Tw 
+#
+# Copyright (c) 2019 Clementine Computing LLC.
+# 
+# This file is part of PopuFare.
+# 
+# PopuFare is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# PopuFare 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 Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with PopuFare.  If not, see <https://www.gnu.org/licenses/>.
+#
+
+require 5.002; 
+use strict; 
+use Socket; 
+use Carp;
+use FileHandle;
+use Fcntl;
+use Getopt::Long qw(:config no_ignore_case);
+use POSIX;
+use RideLogic;
+
+my $bind_ip  	  = '127.0.0.1';
+my $bind_port	  = 3556;
+
+my $DebugMode = 0;
+
+#	This function only executes the passed code reference if the global variable $DebugMode is non-zero.
+# The reason for this is that any calculation (like a FooBar::ComplexObject->toString call) will not be
+# performed if we are not in debug mode, sort of like a very limited form of lazy evaluation.
+#
+sub ifdebug(&@)
+{
+	my ($cmd) = @_;
+	&$cmd() if($DebugMode);
+}
+
+sub handle_client() 
+{
+	close SERVER;
+	CLIENT->autoflush(1);
+
+	print CLIENT 'Hello.';
+	
+	close CLIENT;
+}
+
+my $waitedpid = 0; 
+my $sigreceived = 0;
+
+sub REAPER 
+{ 
+	while (($waitedpid = waitpid(-1, WNOHANG))>0) { }
+	$SIG{CHLD} = \&REAPER; # loathe sysV 
+	$sigreceived = 1;
+} 
+
+sub spawn
+{ 
+	my $coderef = shift; 						#grab the first parameter
+	unless (@_ == 0 && $coderef && ref($coderef) eq 'CODE') 	#verify that it consists of a non-null block of executable perl code
+	{ 
+		confess "usage: spawn CODEREF"; 			#complain if this is not the case
+	} 
+	my $pid; 
+	if (!defined($pid = fork)) 					#attempt a fork, remembering the returned PID value
+	{
+		close CLIENT;
+		return; 						#failed to fork, we'd better close the client
+	} 
+	elsif ($pid) 							#If the returned process ID is non-zero, that indicates that we are the parent process
+	{ 
+		return; # i'm the parent 
+	}  
+	else								#otherwise, if the returned process ID is 0, that means we're the child process
+	{
+		exit &$coderef();					#in which case, we want to execute the child handler that was passed in, and then
+									#exit this (child) process when we've finished our conversation(s) with the
+									#other (client) end of the socket.
+	}
+} 
+
+sub show_help_and_exit {
+  print "usage:\n";
+  print "  [-i]       interactive, do not daemonize\n";
+  print "  [-c cfg]   use cfg as config file (default to " . $RideLogic::RIDELOGIC_DAEMON_CONF . ") \n";
+  print "  [-h]       show help (this screen)\n";
+  exit;
+}
+
+#----------------------------------------------------------------------
+#   
+#----------------------------------------------------------------------
+my $daemonize   = 1;
+my $interactive = 0;
+my $show_help   = 0;
+my $cfg_file    = $RideLogic::RIDELOGIC_DAEMON_CONF;
+GetOptions( 
+  'i|interactive'   => \$interactive,
+  'c|config=s'      => \$cfg_file,
+  'h|help'          => \$show_help );
+show_help_and_exit() if ($show_help);
+
+$daemonize=0 if ($interactive);
+
+#----------------------------------------------------------------------
+#   Local network settings for Inter-Process communication.
+#----------------------------------------------------------------------
+my $proto = getprotobyname('tcp'); 
+my $addr = sockaddr_in( $bind_port ,inet_aton($bind_ip));;
+#----------------------------------------------------------------------
+
+my $max_retries = 10;		#Maximum number of address-binding retries before we give up.
+my $retry_count = $max_retries;	#number of retries left...
+my $retry_delay = 3;		#number of seconds to wait between retries at binding to our designated IPC address
+my $got_network = 0;		#flag to let us know that we can quit retrying once we have gotten a valid listening socket
+
+my %CFG_VAR;
+
+read_config($cfg_file, \%CFG_VAR) if ($cfg_file);
+
+my $logfile     = ($CFG_VAR{"RIDELOGIC_DAEMON_LOG_DIR"} || $RideLogic::RIDELOGIC_DAEMON_LOG_DIR) . "/ridelogic_hellod.log";
+my $pidfile     = ($CFG_VAR{"RIDELOGIC_DAEMON_PID_DIR"} || $RideLogic::RIDELOGIC_DAEMON_PID_DIR) . "/ridelogic_hellod.pid";
+
+daemonize($logfile, $pidfile) if ($daemonize);
+
+# set our pipes to be piping hot
+$|=1;
+
+while( ($retry_count > 0) && (!$got_network) )
+{
+	try	#Try and allocate a socket, bind it to our IPC address, and set it to listen for connections
+	{
+		socket(SERVER,PF_INET,SOCK_STREAM,$proto) || die "socket: $!"; 
+		setsockopt(SERVER, SOL_SOCKET, SO_REUSEADDR,  1);
+		bind (SERVER, $addr) || die "bind: $!"; 
+		listen(SERVER,5) || die "listen: $!";
+		$got_network = 1;
+	}
+	catch	#If that didn't work for some reason, log the error, clean up, and prepair to retry
+	{
+		my $errmsg = $_;	#Remember the error message
+		
+		close(SERVER);		#Clean up the server socket if it needs it
+		
+		#Decrement our remaining retry counter
+		$retry_count = $retry_count - 1;
+		
+		#Log the message to our debug log
+		print "Failed to allocate socket, will retry $retry_count times: $errmsg\n";
+		
+		#Wait a reasonable period before trying again
+		sleep $retry_delay;
+	};
+}
+
+if($got_network)	#If we met with success binding to the network, report it
+{
+	my $logmsg = "Socket setup successful.  Listening for clients at $bind_ip:$bind_port\n";
+
+	print $logmsg;	
+	
+}
+else			#If we ran out of patience and gave up, report that as well and exit
+{
+	my $errmsg = "Could not allocate and bind listening socket at $bind_ip:$bind_port after $max_retries attempts.\n";
+	
+	die $errmsg;
+}
+
+#    Set up our signal handler which will clean up defunct child processes and let the main
+# accept() loop know that the reason accept returned was due to a signal, not a legit connection.
+$SIG{CHLD} = \&REAPER; 
+
+#This for loop is efficient, but confusting, so I'll break it down by clause
+#
+#    The first clause ($sigreceived = 0) clears the signal received flag that will be set if the 
+# accept() call was interrupted by a signal.  This clause runs once before the first run of the loop
+#
+#    The second clause is the test clause, it will process the contents of the loop if EITHER
+# accept() has returned (presumably generating a valid file handle for the CLIENT end of the
+# socket, OR the signal received flag is set (thus accept would have returned early without
+# having actually accepted a connection.
+#
+#    The third clause (the 'incrementer') is run after each time the body is executed, before the 
+# test clause is executed again (deciding whether to run the body or drop out...  This test
+# clause will close the parent process' copy of the CLIENT file handle since (see body below)
+# after the body executes, all communication with the socket referred to by that file handle
+# will be carried out by the spawned child process.  This frees the parent's copy of the CLIENT
+# file handle to be used again in the parent process for the next accepted incoming connection.
+
+for ( $sigreceived = 0; accept(CLIENT,SERVER) || $sigreceived; $sigreceived = 0, close CLIENT) 
+{ 
+	next if $sigreceived; 			#If we were interrupted by a signal, there is no real client, just go back and try to accept a new one
+	print "connection received.\n";		#Print a diagnostic message confirming that we have made a connection
+	spawn sub {handle_client();};		#fork() off a child process that will handle communication with the socket pointed to by the CLIENT file handle 
+} 
+

+ 54 - 0
server/daemons/initial_setup.txt

@@ -0,0 +1,54 @@
+#to initially set up daemons on a system:
+# running as root:
+#
+# Copyright (c) 2019 Clementine Computing LLC.
+# 
+# This file is part of PopuFare.
+# 
+# PopuFare is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# PopuFare 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 Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with PopuFare.  If not, see <https://www.gnu.org/licenses/>.
+#
+
+cp ../server_scripts/RideLogic.pm /usr/lib/perl/5.8.8/
+
+mkdir /var/run/ridelogic
+chown bus:bus /var/run/ridelogic
+
+mkdir /var/log/ridelogic
+chown bus:bus /var/log/ridelogic
+
+mkdir /etc/ridelogic
+cp /home/bus/new_bus/devel/server_daemons/etc/ridelogic/* /etc/ridelogic
+cp /home/bus/new_bus/devel/server_daemons/etc/init.d/* /etc/init.d
+
+cp /home/bus/new_bus/devel/server_daemons/avls_server/ridelogic_avlsd /usr/bin
+cp /home/bus/new_bus/devel/server_daemons/billing_server/ridelogic_billingd /usr/bin
+cp /home/bus/new_bus/devel/server_daemons/buspass_server/ridelogic_buspassd /usr/bin
+cp /home/bus/new_bus/devel/server_daemons/hello_daemon/ridelogic_hellod /usr/bin
+cp /home/bus/new_bus/devel/server_daemons/version_server/ridelogic_versiond /usr/bin
+
+chkconfig --add ridelogic_avlsd
+chkconfig --add ridelogic_billingd
+chkconfig --add ridelogic_buspassd
+chkconfig --add ridelogic_hellod
+chkconfig --add ridelogic_versiond
+
+chkconfig --level 3 ridelogic_avlsd on
+chkconfig --level 3 ridelogic_billingd on
+chkconfig --level 3 ridelogic_buspassd on
+chkconfig --level 3 ridelogic_hellod on
+chkconfig --level 3 ridelogic_versiond on
+
+# chkconfig --list | grep ridelogic
+
+

+ 115 - 0
server/daemons/org_server.sh

@@ -0,0 +1,115 @@
+#!/bin/bash
+#
+# Copyright (c) 2019 Clementine Computing LLC.
+# 
+# This file is part of PopuFare.
+# 
+# PopuFare is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# PopuFare 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 Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with PopuFare.  If not, see <https://www.gnu.org/licenses/>.
+#
+
+org_daemon_base_dir="/home/bus/new_bus/devel/server_daemon"
+org_deamon_pid_dir="/tmp"
+org_daemon_log_dir="/home/bus/log"
+
+daemon=( avls_server billing_server buspass_server hello_daemon version_server )
+
+start_org_server() {
+	path=$1
+	lockfile=$2
+        logfile=$3
+	if [ -e "$lockfile" ]; then
+		if ps -p `cat $lockfile` > /dev/null 2>&1; then
+			echo $path is already running - skipping start
+                        return 0
+		else
+			echo Lockfile $lockfile is junk. Deleting...
+			rm -f $lockfile
+			#start_buspass_server
+		fi
+        fi
+
+        if [ -z "$logfile" ]
+        then
+                logfile="/dev/null"
+        fi
+	#else 
+		echo Starting $path
+		$path  > $logfile 2>&1 &
+		echo $! > $lockfile
+	#fi
+}
+
+stop_org_server() {
+	path=$1
+	lockfile=$2
+	if [ -e $lockfile ]; then
+		echo Stopping $path
+		kill `cat $lockfile`
+		rm -f $lockfile
+	else 
+		echo $path is not running
+	fi
+}
+
+echo
+case $1 in
+	start )
+                for d in ${daemon[@]}
+                do
+                        start_server $org_deamon_base_dir/$d.pl $org_daemon_lockfile_dir/$d.pid $org_daemon_logfile_dir/$d.log
+                done
+	;;
+	stop )
+                for d in ${daemon[@]}
+                do
+                        stop_server $org_deamon_base_dir/$d.pl $org_daemon_lockfile_dir/$d.pid 
+                done
+	;;
+	restart )
+		$0 stop
+		$0 start
+	;;
+	* )
+                for d in ${daemon[@]}
+                do
+                        if [ "$d" == $1 ]
+                        then
+                                case "$2" in
+                                        start ) 
+                                                start_server $org_deamon_base_dir/$d.pl $org_daemon_lockfile_dir/$d.pid $org_daemon_logfile_dir/$d.log
+                                                break
+                                        ;;
+                                        stop ) 
+                                                stop_server $org_deamon_base_dir/$d.pl $org_daemon_lockfile_dir/$d.pid 
+                                                break
+                                        ;;
+                                        restart ) 
+                                                stop_server $org_deamon_base_dir/$d.pl $org_daemon_lockfile_dir/$d.pid
+                                                start_server $org_deamon_base_dir/$d.pl $org_daemon_lockfile_dir/$d.pid $org_daemon_logfile_dir/$d.log
+                                                break
+                                        ;;
+                                        * ) 
+                                                break 
+                                                ;;
+                                esac
+                                return 0
+                        fi
+                done
+                
+		echo "Usage:"
+		echo "    $0 start"
+		echo "    $0 stop"
+		echo "    $0 restart"
+		;;
+esac

+ 339 - 0
server/daemons/version_server/ridelogic_versiond

@@ -0,0 +1,339 @@
+#!/usr/bin/perl -Tw 
+#
+# Copyright (c) 2019 Clementine Computing LLC.
+# 
+# This file is part of PopuFare.
+# 
+# PopuFare is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# PopuFare 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 Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with PopuFare.  If not, see <https://www.gnu.org/licenses/>.
+#
+
+require 5.002; 
+use strict; 
+use Socket; 
+use Carp;
+use DBI;
+use FileHandle;
+use Fcntl;
+use Getopt::Long qw(:config no_ignore_case);
+use POSIX;
+use RideLogic;
+
+my $database_path = 'DBI:mysql:busdb';
+my $database_user = '';
+my $database_pass = '';
+
+my $bind_ip  	  = '127.0.0.1';
+my $bind_port	  = 8377;
+
+my $DebugMode = 0;
+
+#	This function only executes the passed code reference if the global variable $DebugMode is non-zero.
+# The reason for this is that any calculation (like a FooBar::ComplexObject->toString call) will not be
+# performed if we are not in debug mode, sort of like a very limited form of lazy evaluation.
+#
+sub ifdebug(&@)
+{
+	my ($cmd) = @_;
+	&$cmd() if($DebugMode);
+}
+
+sub CheckinServerReply
+{
+	my $client_query = $_[0];
+
+	my $dbh = DBI->connect($database_path, $database_user, $database_pass)
+		or die "Couldn't connect to database: " . DBI->errstr;
+
+	my $sth ;
+	my $logmsg ;
+	my $response = '';
+	
+	my @client_values = split(/[\t]/, $client_query, -1);   #the -1 keeps split from trimming trailing blank fields
+		#0.	viper_num	(0 for Phase II)
+		#1.	equip_num	(usually bogus for Phase I)
+		#2.	eth0_mac	(Effectively a serial number of the SBC (be it Viper, Titan, or some Atom based system)
+		#3.	cell_imei	(Effectively a serial number of the Cell Modem)
+		#4.	cell_imsi	(Effectively a serial number of the SIM card inserted in the modem)	
+		#5.	version_strings (a concatenation of package versions)
+
+	$client_values[0] =~ s/^[^0-9]*//;	#Strip the leading '#' (and anything else non-numeric) from our string
+
+	$sth = $dbh->prepare('INSERT INTO bus_checkin_log (viper_num, equip_num, eth0_mac, cell_imei, cell_imsi, version_data) VALUES (?, ?, ?, ?, ?, ?)');
+
+	#    We explicitly chop this down to the 6 fields we want to insert, rather than passing @client_values as a parameter so
+	#that if some foolish version string goes and contains a tab (this should never happen!) it will be trunctated instead
+	#of the whole update being shitcanned because the array has too many data fields for the quiery...
+	
+	try
+	{
+		$sth->execute(@client_values[0..5]);
+		$response .= "Thanks.\n";
+	}
+	catch
+	{
+		$logmsg .= $_ . "\n";
+		$response .= "Server Side Error.\n";
+	};
+
+	print $logmsg if $logmsg;
+	
+	return $response;
+}
+
+
+sub ServerReply
+{
+	my $client_query = $_[0];
+	
+	$/="\n";
+	chomp($client_query);
+	
+	if ($client_query =~ m/^\#/) #A leading '#' signals a bus_checkin_log entry, rather than an package update checkin
+	{
+		return CheckinServerReply($client_query);
+	}
+
+	my $response = "";
+	
+	my $dbh = DBI->connect($database_path, $database_user, $database_pass)
+		or die "Couldn't connect to database: " . DBI->errstr;
+	my $sth ;
+	my $logmsg ;
+
+	$sth = $dbh->prepare('SELECT client_file, checksum, file_size, file_path, fileversion FROM update_level t1 WHERE (serial = (SELECT serial FROM update_level WHERE client_file = t1.client_file AND (equip_num = 0 OR equip_num = ?) ORDER BY equip_num DESC, serial DESC LIMIT 1)) ORDER BY client_file ASC');
+
+	my @client_values = split(/[\t]/, $client_query, -1);   #the -1 keeps split from trimming trailing blank fields
+		#0.	equip_num
+		#1.	filename=md5sum
+		#2 ...
+
+	my $i;
+	my %filetable = ();
+	
+	for($i = 1; $i < @client_values; $i = $i + 1)
+	{
+		my ($client_file, $client_checksum) = split(/=/, $client_values[$i]);
+		
+		if($client_file && $client_checksum)
+		{
+			$filetable{$client_file} = $client_checksum;
+		}
+	}
+	
+	try
+	{
+		$sth->execute($client_values[0]) or die "Couldn't execute statement: " . $sth->errstr;
+	}
+	catch
+	{
+		$logmsg .= $_ . "\n";	
+	};
+	
+	
+	
+	while(my @data = $sth->fetchrow_array())
+	{
+		#0	client_file
+		#1	checksum
+		#2	file_size
+		#3	file_path
+		#4	fileversion
+	
+		if(defined $filetable{$data[0]} && $filetable{$data[0]} eq $data[1])
+		{
+			#do nothing, the client is up to date	
+		}
+		else
+		{
+			$response .= "$data[0]\t$data[1]\t$data[2]\t$data[3]\t$data[4]\n";
+		}
+	}
+
+	print $logmsg if $logmsg;
+	
+	return $response;
+}
+
+
+sub handle_client() 
+{
+	close SERVER;
+	CLIENT->autoflush(1);
+
+	my $linebuffer;
+	
+	while($linebuffer = <CLIENT>) 
+	{
+		print CLIENT ServerReply($linebuffer);
+	}	#while data from client
+	
+	close CLIENT;
+}
+
+
+my $waitedpid = 0; 
+my $sigreceived = 0;
+
+
+sub REAPER 
+{ 
+	while (($waitedpid = waitpid(-1, WNOHANG))>0) { }
+	$SIG{CHLD} = \&REAPER; # loathe sysV 
+	$sigreceived = 1;
+} 
+
+
+sub spawn
+{ 
+	my $coderef = shift; 						#grab the first parameter
+	unless (@_ == 0 && $coderef && ref($coderef) eq 'CODE') 	#verify that it consists of a non-null block of executable perl code
+	{ 
+		confess "usage: spawn CODEREF"; 			#complain if this is not the case
+	} 
+	my $pid; 
+	if (!defined($pid = fork)) 					#attempt a fork, remembering the returned PID value
+	{
+		close CLIENT;
+		return; 						#failed to fork, we'd better close the client
+	} 
+	elsif ($pid) 							#If the returned process ID is non-zero, that indicates that we are the parent process
+	{ 
+		return; # i'm the parent 
+	}  
+	else								#otherwise, if the returned process ID is 0, that means we're the child process
+	{
+		exit &$coderef();					#in which case, we want to execute the child handler that was passed in, and then
+									#exit this (child) process when we've finished our conversation(s) with the
+									#other (client) end of the socket.
+	}
+} 
+
+sub show_help_and_exit {
+  print "usage:\n";
+  print "  [-i]       interactive, do not daemonize\n";
+  print "  [-c cfg]   use cfg as config file (default to " . $RideLogic::RIDELOGIC_DAEMON_CONF . ") \n";
+  print "  [-h]       show help (this screen)\n";
+  exit;
+}
+
+#----------------------------------------------------------------------
+#
+#----------------------------------------------------------------------
+my $daemonize   = 1;
+my $interactive = 0;
+my $show_help   = 0;
+my $cfg_file    = $RideLogic::RIDELOGIC_DAEMON_CONF;
+GetOptions(
+  'i|interactive'   => \$interactive,
+  'c|config=s'      => \$cfg_file,
+  'h|help'          => \$show_help );
+show_help_and_exit() if ($show_help);
+
+$daemonize=0 if ($interactive);
+
+
+#----------------------------------------------------------------------
+#   Local network settings for Inter-Process communication.
+#----------------------------------------------------------------------
+my $proto = getprotobyname('tcp'); 
+my $addr = sockaddr_in( $bind_port ,inet_aton($bind_ip));;
+#----------------------------------------------------------------------
+
+my $max_retries = 10;		#Maximum number of address-binding retries before we give up.
+my $retry_count = $max_retries;	#number of retries left...
+my $retry_delay = 3;		#number of seconds to wait between retries at binding to our designated IPC address
+my $got_network = 0;		#flag to let us know that we can quit retrying once we have gotten a valid listening socket
+
+my %CFG_VAR;
+
+read_config($cfg_file, \%CFG_VAR) if ($cfg_file);
+
+my $logfile     = ($CFG_VAR{"RIDELOGIC_DAEMON_LOG_DIR"} || $RideLogic::RIDELOGIC_DAEMON_LOG_DIR) . "/ridelogic_versiond.log";
+my $pidfile     = ($CFG_VAR{"RIDELOGIC_DAEMON_PID_DIR"} || $RideLogic::RIDELOGIC_DAEMON_PID_DIR) . "/ridelogic_versiond.pid";
+
+daemonize($logfile, $pidfile) if ($daemonize);
+
+# set our pipes to be piping hot
+$|=1;
+
+
+while( ($retry_count > 0) && (!$got_network) )
+{
+	try	#Try and allocate a socket, bind it to our IPC address, and set it to listen for connections
+	{
+		socket(SERVER,PF_INET,SOCK_STREAM,$proto) || die "socket: $!"; 
+		setsockopt(SERVER, SOL_SOCKET, SO_REUSEADDR,  1);
+		bind (SERVER, $addr) || die "bind: $!"; 
+		listen(SERVER,5) || die "listen: $!";
+		$got_network = 1;
+	}
+	catch	#If that didn't work for some reason, log the error, clean up, and prepair to retry
+	{
+		my $errmsg = $_;	#Remember the error message
+		
+		close(SERVER);		#Clean up the server socket if it needs it
+		
+		#Decrement our remaining retry counter
+		$retry_count = $retry_count - 1;
+		
+		#Log the message to our debug log
+		print "Failed to allocate socket, will retry $retry_count times: $errmsg\n";
+		
+		#Wait a reasonable period before trying again
+		sleep $retry_delay;
+	};
+}
+
+if($got_network)	#If we met with success binding to the network, report it
+{
+	my $logmsg = "Socket setup successful.  Listening for clients at $bind_ip:$bind_port\n";
+	
+	print $logmsg;	
+	
+}
+else			#If we ran out of patience and gave up, report that as well and exit
+{
+	my $errmsg = "Could not allocate and bind listening socket at $bind_ip:$bind_port after $max_retries attempts.\n";
+	
+	die $errmsg;
+}
+
+#    Set up our signal handler which will clean up defunct child processes and let the main
+# accept() loop know that the reason accept returned was due to a signal, not a legit connection.
+$SIG{CHLD} = \&REAPER; 
+
+#This for loop is efficient, but confusting, so I'll break it down by clause
+#
+#    The first clause ($sigreceived = 0) clears the signal received flag that will be set if the 
+# accept() call was interrupted by a signal.  This clause runs once before the first run of the loop
+#
+#    The second clause is the test clause, it will process the contents of the loop if EITHER
+# accept() has returned (presumably generating a valid file handle for the CLIENT end of the
+# socket, OR the signal received flag is set (thus accept would have returned early without
+# having actually accepted a connection.
+#
+#    The third clause (the 'incrementer') is run after each time the body is executed, before the 
+# test clause is executed again (deciding whether to run the body or drop out...  This test
+# clause will close the parent process' copy of the CLIENT file handle since (see body below)
+# after the body executes, all communication with the socket referred to by that file handle
+# will be carried out by the spawned child process.  This frees the parent's copy of the CLIENT
+# file handle to be used again in the parent process for the next accepted incoming connection.
+
+for ( $sigreceived = 0; accept(CLIENT,SERVER) || $sigreceived; $sigreceived = 0, close CLIENT) 
+{ 
+	next if $sigreceived; 			#If we were interrupted by a signal, there is no real client, just go back and try to accept a new one
+	print "connection received.\n";		#Print a diagnostic message confirming that we have made a connection
+	spawn sub {handle_client();};		#fork() off a child process that will handle communication with the socket pointed to by the CLIENT file handle 
+} 
+

+ 373 - 0
server/daemons/version_server/version_daemon.pl

@@ -0,0 +1,373 @@
+#!/usr/bin/perl -Tw 
+#
+# Copyright (c) 2019 Clementine Computing LLC.
+# 
+# This file is part of PopuFare.
+# 
+# PopuFare is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+# 
+# PopuFare 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 Affero General Public License for more details.
+# 
+# You should have received a copy of the GNU Affero General Public License
+# along with PopuFare.  If not, see <https://www.gnu.org/licenses/>.
+#
+
+require 5.002; 
+use strict; 
+use Socket; 
+use Carp;
+use DBI;
+use FileHandle;
+use Fcntl;
+
+use POSIX;
+
+my $database_path = 'DBI:mysql:busdb';
+my $database_user = '';
+my $database_pass = '';
+
+my $bind_ip  	  = '127.0.0.1';
+my $bind_port	  = 8377;
+
+#----------------------------------------------Ugly exception handling logic using closures and anonymous functions----
+#-------------------------------------------This is in there to deal with the fact that CreditCall uses the die("error")
+#-------------------------------------------function instead of returning an error message in many cases...
+
+#       This utility function returns the passed string sans any leading or trailing whitespace.
+#
+sub strip_whitespace
+{
+	my $str = shift;        #grab our first parameter
+        
+        $str =~ s/^\s+//;       #strip leading whitespace
+        $str =~ s/\s+$//;       #strip trailing whitespace
+                        
+        return $str;            #return the improved string
+}
+                                
+
+#    This function takes two coderef parameters, the second of which is usually an explicit call to the 
+# 'catch' function which itself takes a coderef parameter.  This allows the code employing this suite of 
+# functions to look somewhat like a conventional exception handling mechanism:
+#
+# try
+# {
+#    do_something_that_might_die();
+# }
+# catch
+# {
+#   my $errmsg = $_;
+#   log_the_error_message($errmsg);
+#   perform_some_cleanup();
+# };
+#
+# DO NOT FORGET THAT LAST SEMICOLON, EVERYTHING GOES TO HELL IF YOU DO!
+#   
+sub try(&$)
+{
+	my ($attempt, $handler) = @_;
+  
+	eval
+	{
+		&$attempt;
+	};
+        
+	if($@)
+	{
+		do_catch($handler);
+	}
+}
+
+#    This function strips off the whitespace from the exception message reported by die()
+# and places the result into the default variable such that the code in the catch block can
+# just examine $_ to figure out what the cause of the error is, or to display or log
+# the error message.
+#
+sub do_catch(&$)
+{
+	my ($handler) = @_;
+
+	local $_ = strip_whitespace($@);
+	
+	&$handler;
+}
+
+#    This just takes an explicit coderef and returns it unharmed.  The only
+# purpose of this is so the try/catch structure looks pretty and familiar.                                          
+#
+sub catch(&) {$_[0]}
+
+#--------------------------------------------------------------------------------------------------------------------
+
+
+#my $DebugMode = 1;
+my $DebugMode = 0;
+
+#	This function only executes the passed code reference if the global variable $DebugMode is non-zero.
+# The reason for this is that any calculation (like a FooBar::ComplexObject->toString call) will not be
+# performed if we are not in debug mode, sort of like a very limited form of lazy evaluation.
+#
+sub ifdebug(&@)
+{
+	my ($cmd) = @_;
+	&$cmd() if($DebugMode);
+}
+
+sub CheckinServerReply
+{
+	my $client_query = $_[0];
+
+	my $dbh = DBI->connect($database_path, $database_user, $database_pass)
+		or die "Couldn't connect to database: " . DBI->errstr;
+
+	my $sth ;
+	my $logmsg ;
+	my $response = '';
+	
+	my @client_values = split(/[\t]/, $client_query, -1);   #the -1 keeps split from trimming trailing blank fields
+		#0.	viper_num	(0 for Phase II)
+		#1.	equip_num	(usually bogus for Phase I)
+		#2.	eth0_mac	(Effectively a serial number of the SBC (be it Viper, Titan, or some Atom based system)
+		#3.	cell_imei	(Effectively a serial number of the Cell Modem)
+		#4.	cell_imsi	(Effectively a serial number of the SIM card inserted in the modem)	
+		#5.	version_strings (a concatenation of package versions)
+
+	$client_values[0] =~ s/^[^0-9]*//;	#Strip the leading '#' (and anything else non-numeric) from our string
+
+	$sth = $dbh->prepare('INSERT INTO bus_checkin_log (viper_num, equip_num, eth0_mac, cell_imei, cell_imsi, version_data) VALUES (?, ?, ?, ?, ?, ?)');
+
+	#    We explicitly chop this down to the 6 fields we want to insert, rather than passing @client_values as a parameter so
+	#that if some foolish version string goes and contains a tab (this should never happen!) it will be trunctated instead
+	#of the whole update being shitcanned because the array has too many data fields for the quiery...
+	
+	try
+	{
+		$sth->execute(@client_values[0..5]);
+		$response .= "Thanks.\n";
+	}
+	catch
+	{
+		$logmsg .= $_ . "\n";
+		$response .= "Server Side Error.\n";
+	};
+
+	print $logmsg if $logmsg;
+	
+	return $response;
+}
+
+
+sub ServerReply
+{
+	my $client_query = $_[0];
+	
+	$/="\n";
+	chomp($client_query);
+	
+	if ($client_query =~ m/^\#/) #A leading '#' signals a bus_checkin_log entry, rather than an package update checkin
+	{
+		return CheckinServerReply($client_query);
+	}
+
+	my $response = "";
+	
+	my $dbh = DBI->connect($database_path, $database_user, $database_pass)
+		or die "Couldn't connect to database: " . DBI->errstr;
+	my $sth ;
+	my $logmsg ;
+
+	$sth = $dbh->prepare('SELECT client_file, checksum, file_size, file_path, fileversion FROM update_level t1 WHERE (serial = (SELECT serial FROM update_level WHERE client_file = t1.client_file AND (equip_num = 0 OR equip_num = ?) ORDER BY equip_num DESC, serial DESC LIMIT 1)) ORDER BY client_file ASC');
+
+	my @client_values = split(/[\t]/, $client_query, -1);   #the -1 keeps split from trimming trailing blank fields
+		#0.	equip_num
+		#1.	filename=md5sum
+		#2 ...
+
+	my $i;
+	my %filetable = ();
+	
+	for($i = 1; $i < @client_values; $i = $i + 1)
+	{
+		my ($client_file, $client_checksum) = split(/=/, $client_values[$i]);
+		
+		if($client_file && $client_checksum)
+		{
+			$filetable{$client_file} = $client_checksum;
+		}
+	}
+	
+	try
+	{
+		$sth->execute($client_values[0]) or die "Couldn't execute statement: " . $sth->errstr;
+	}
+	catch
+	{
+		$logmsg .= $_ . "\n";	
+	};
+	
+	
+	
+	while(my @data = $sth->fetchrow_array())
+	{
+		#0	client_file
+		#1	checksum
+		#2	file_size
+		#3	file_path
+		#4	fileversion
+	
+		if(defined $filetable{$data[0]} && $filetable{$data[0]} eq $data[1])
+		{
+			#do nothing, the client is up to date	
+		}
+		else
+		{
+			$response .= "$data[0]\t$data[1]\t$data[2]\t$data[3]\t$data[4]\n";
+		}
+	}
+
+	print $logmsg if $logmsg;
+	
+	return $response;
+}
+
+
+sub handle_client() 
+{
+	close SERVER;
+	CLIENT->autoflush(1);
+
+	my $linebuffer;
+	
+	while($linebuffer = <CLIENT>) 
+	{
+		print CLIENT ServerReply($linebuffer);
+	}	#while data from client
+	
+	close CLIENT;
+}
+
+
+my $waitedpid = 0; 
+my $sigreceived = 0;
+
+
+sub REAPER 
+{ 
+	while (($waitedpid = waitpid(-1, WNOHANG))>0) { }
+	$SIG{CHLD} = \&REAPER; # loathe sysV 
+	$sigreceived = 1;
+} 
+
+
+sub spawn
+{ 
+	my $coderef = shift; 						#grab the first parameter
+	unless (@_ == 0 && $coderef && ref($coderef) eq 'CODE') 	#verify that it consists of a non-null block of executable perl code
+	{ 
+		confess "usage: spawn CODEREF"; 			#complain if this is not the case
+	} 
+	my $pid; 
+	if (!defined($pid = fork)) 					#attempt a fork, remembering the returned PID value
+	{
+		close CLIENT;
+		return; 						#failed to fork, we'd better close the client
+	} 
+	elsif ($pid) 							#If the returned process ID is non-zero, that indicates that we are the parent process
+	{ 
+		return; # i'm the parent 
+	}  
+	else								#otherwise, if the returned process ID is 0, that means we're the child process
+	{
+		exit &$coderef();					#in which case, we want to execute the child handler that was passed in, and then
+									#exit this (child) process when we've finished our conversation(s) with the
+									#other (client) end of the socket.
+	}
+} 
+
+#----------------------------------------------------------------------
+#   Local network settings for Inter-Process communication.
+#----------------------------------------------------------------------
+my $proto = getprotobyname('tcp'); 
+my $addr = sockaddr_in( $bind_port ,inet_aton($bind_ip));;
+#----------------------------------------------------------------------
+
+my $max_retries = 10;		#Maximum number of address-binding retries before we give up.
+my $retry_count = $max_retries;	#number of retries left...
+my $retry_delay = 3;		#number of seconds to wait between retries at binding to our designated IPC address
+my $got_network = 0;		#flag to let us know that we can quit retrying once we have gotten a valid listening socket
+
+while( ($retry_count > 0) && (!$got_network) )
+{
+	try	#Try and allocate a socket, bind it to our IPC address, and set it to listen for connections
+	{
+		socket(SERVER,PF_INET,SOCK_STREAM,$proto) || die "socket: $!"; 
+		setsockopt(SERVER, SOL_SOCKET, SO_REUSEADDR,  1);
+		bind (SERVER, $addr) || die "bind: $!"; 
+		listen(SERVER,5) || die "listen: $!";
+		$got_network = 1;
+	}
+	catch	#If that didn't work for some reason, log the error, clean up, and prepair to retry
+	{
+		my $errmsg = $_;	#Remember the error message
+		
+		close(SERVER);		#Clean up the server socket if it needs it
+		
+		#Decrement our remaining retry counter
+		$retry_count = $retry_count - 1;
+		
+		#Log the message to our debug log
+		print "Failed to allocate socket, will retry $retry_count times: $errmsg\n";
+		
+		#Wait a reasonable period before trying again
+		sleep $retry_delay;
+	};
+}
+
+if($got_network)	#If we met with success binding to the network, report it
+{
+	my $logmsg = "Socket setup successful.  Listening for clients at $bind_ip:$bind_port\n";
+	
+	print $logmsg;	
+	
+}
+else			#If we ran out of patience and gave up, report that as well and exit
+{
+	my $errmsg = "Could not allocate and bind listening socket at $bind_ip:$bind_port after $max_retries attempts.\n";
+	
+	die $errmsg;
+}
+
+#    Set up our signal handler which will clean up defunct child processes and let the main
+# accept() loop know that the reason accept returned was due to a signal, not a legit connection.
+$SIG{CHLD} = \&REAPER; 
+
+#This for loop is efficient, but confusting, so I'll break it down by clause
+#
+#    The first clause ($sigreceived = 0) clears the signal received flag that will be set if the 
+# accept() call was interrupted by a signal.  This clause runs once before the first run of the loop
+#
+#    The second clause is the test clause, it will process the contents of the loop if EITHER
+# accept() has returned (presumably generating a valid file handle for the CLIENT end of the
+# socket, OR the signal received flag is set (thus accept would have returned early without
+# having actually accepted a connection.
+#
+#    The third clause (the 'incrementer') is run after each time the body is executed, before the 
+# test clause is executed again (deciding whether to run the body or drop out...  This test
+# clause will close the parent process' copy of the CLIENT file handle since (see body below)
+# after the body executes, all communication with the socket referred to by that file handle
+# will be carried out by the spawned child process.  This frees the parent's copy of the CLIENT
+# file handle to be used again in the parent process for the next accepted incoming connection.
+
+for ( $sigreceived = 0; accept(CLIENT,SERVER) || $sigreceived; $sigreceived = 0, close CLIENT) 
+{ 
+	next if $sigreceived; 			#If we were interrupted by a signal, there is no real client, just go back and try to accept a new one
+	print "connection received.\n";		#Print a diagnostic message confirming that we have made a connection
+	spawn sub {handle_client();};		#fork() off a child process that will handle communication with the socket pointed to by the CLIENT file handle 
+} 
+

+ 376 - 0
server/scripts/RideLogic.pm

@@ -0,0 +1,376 @@
+package RideLogic;
+
+use strict;
+use POSIX;
+
+require Exporter;
+
+use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
+
+$VERSION     = "0.01";
+@ISA         = qw(Exporter);
+
+#@EXPORT      = qw( daemonize
+#                   read_config
+#                   strip_whitespace
+#                   try
+#                   catch );
+#@EXPORT_OK = qw( daemonize
+#                 read_config
+#                 strip_whitespace
+#                 try
+#                 catch );
+#%EXPORT_TAGS = ( DEFAULT => [qw( &daemonize
+#                                 &read_config
+#                                 &strip_whitespace
+#                                 &try
+#                                 &catch )] );
+
+@EXPORT      = qw( audit_user_card_start 
+                   audit_user_card_end 
+                   audit_user_pass_start 
+                   audit_user_pass_end 
+                   audit_users_start
+                   audit_users_end 
+                   audit_admins_start
+                   audit_admins_end 
+                   daemonize
+                   read_config
+                   strip_whitespace
+                   try
+                   catch );
+@EXPORT_OK = qw( audit_user_card_start 
+                 audit_user_card_end 
+                 audit_user_pass_start 
+                 audit_user_pass_end 
+                 audit_users_start
+                 audit_users_end 
+                 audit_admins_start
+                 audit_admins_end 
+                 daemonize
+                 read_config
+                 strip_whitespace
+                 try
+                 catch );
+%EXPORT_TAGS = ( DEFAULT => [qw( &audit_user_card_start 
+                                 &audit_user_card_end 
+                                 &audit_user_pass_start 
+                                 &audit_user_pass_end 
+                                 &audit_users_start
+                                 &audit_users_end
+                                 &audit_admins_start
+                                 &audit_admins_end
+                                 &daemonize
+                                 &read_config
+                                 &strip_whitespace
+                                 &try
+                                 &catch )] );
+
+our @ISA = qw(Exporter);
+
+our $VERSION="0.1";
+
+our $RIDELOGIC_DAEMON_CONF="/etc/ridelogic/daemon.conf";
+our $RIDELOGIC_DAEMON_LOG_DIR="/var/log/ridelogic";
+our $RIDELOGIC_DAEMON_PID_DIR="/var/run/ridelogic";
+
+our $RIDELOGIC_API_CONF="/etc/ridelogic/api.conf";
+
+sub audit_user_pass_start {
+  my $dbh = shift;
+  my $pass_id = shift;
+  my $comment = shift;
+
+  my @field = qw( user_pass_id logical_card_id issued activated firstused lastused nrides_orig nrides_remain nday_orig nday_expiration active rule queue_order comment expired paytype deactivated );
+  my $ins_query = " insert into audit_user_pass ( timestamp, comment, old_" . $field[0];
+  my $tail_ins_query = " select now(), ?, " . ($pass_id ? $field[0] : "null");
+  for (my $i=1; $i<scalar(@field); $i++) {
+    $ins_query .= ", old_" . $field[$i];
+    $tail_ins_query .= ", " . ($pass_id ? $field[$i] : "null" );
+  }
+  $ins_query .= ") ";
+  $tail_ins_query .= ($pass_id ? " from user_pass where user_pass_id = ?" : "");
+  $ins_query .= $tail_ins_query;
+
+  my $query = $dbh->prepare($ins_query);
+  my $response = ($pass_id ? $query->execute($comment, $pass_id) : $query->execute($comment));
+  return ($dbh->last_insert_id(undef, undef, undef, undef));
+}
+
+sub audit_user_pass_end {
+  my $dbh = shift;
+  my $pass_id = shift;
+  my $audit_id = shift;
+
+  my @field = qw( user_pass_id logical_card_id issued activated firstused lastused nrides_orig nrides_remain nday_orig nday_expiration active rule queue_order comment expired paytype deactivated );
+  my $up_query = " update audit_user_pass, user_pass set audit_user_pass.new_" . $field[0] . " = user_pass." . $field[0];
+  for (my $i=1; $i<scalar(@field); $i++) {
+    $up_query .= ", audit_user_pass.new_".$field[$i] . " = user_pass." . $field[$i];
+  }
+  $up_query .= " where audit_user_pass.audit_user_pass_id = ? and user_pass.user_pass_id = ? ";
+
+  my $query = $dbh->prepare($up_query);
+  my $response = $query->execute($audit_id, $pass_id);
+
+}
+
+sub audit_user_card_start {
+  my $dbh = shift;
+  my $card_id = shift;
+  my $comment = shift;
+
+  my @field = qw( logical_card_id mag_token rfid_token comment lastused userid issued firstused group_id active deactivated issuetype );
+  my $ins_query = " insert into audit_user_card ( timestamp, comment, old_" . $field[0];
+  my $tail_ins_query = " select now(), ?, " . ($card_id ? $field[0] : "null");
+  for (my $i=1; $i<scalar(@field); $i++) {
+    $ins_query .= ", old_" . $field[$i];
+    $tail_ins_query .= ", " . ($card_id ? $field[$i] : "null");
+  }
+  $ins_query .= ") ";
+  $tail_ins_query .= ($card_id ? " from user_card where logical_card_id = ?" : "");
+  $ins_query .= $tail_ins_query;
+
+  my $query = $dbh->prepare($ins_query);
+  my $response = ($card_id ? $query->execute($comment, $card_id) : $query->execute($comment) );
+  return ($dbh->last_insert_id(undef, undef, undef, undef));
+}
+
+sub audit_user_card_end {
+  my $dbh = shift;
+  my $card_id = shift;
+  my $audit_id = shift;
+
+  my @field = qw( logical_card_id mag_token rfid_token comment lastused userid issued firstused group_id active deactivated issuetype );
+  my $up_query = " update audit_user_card, user_card set audit_user_card.new_" . $field[0] . " = user_card." . $field[0];
+  for (my $i=1; $i<scalar(@field); $i++) {
+    $up_query .= ", audit_user_card.new_".$field[$i] . " = user_card." . $field[$i];
+  }
+  $up_query .= " where audit_user_card.audit_user_card_id = ? and user_card.logical_card_id = ? ";
+
+  my $query = $dbh->prepare($up_query);
+  my $response = $query->execute($audit_id, $card_id);
+
+}
+
+sub audit_users_start {
+  my $dbh = shift;
+  my $userid = shift;
+  my $comment = shift;
+
+  my @field = qw( username passwordhash userid comment first_name last_name phone email address city state zip created active
+                  shipping_address shipping_city shipping_state shipping_zip shipping_name shipping_country_code shipping_country_name reset_attempts );
+  my $ins_query = " insert into audit_users ( timestamp, comment, old_" . $field[0];
+  my $tail_ins_query = " select now(), ?, " . ($userid ? $field[0] : "null");
+  for (my $i=1; $i<scalar(@field); $i++) {
+    $ins_query .= ", old_".$field[$i];
+    $tail_ins_query .= ", " . ($userid ? $field[$i] : "null");
+  }
+  $ins_query .= ") ";
+  $tail_ins_query .= ($userid ? " from users where userid = ?" : "");
+  $ins_query .= $tail_ins_query;
+
+  my $query = $dbh->prepare($ins_query);
+  my $response = ($userid ? $query->execute($comment, $userid) : $query->execute($comment) );
+  return ($dbh->last_insert_id(undef, undef, undef, undef));
+}
+
+sub audit_users_end {
+  my $dbh = shift;
+  my $userid = shift;
+  my $audit_id = shift;
+
+  my @field = qw( username passwordhash userid comment first_name last_name phone email address city state zip created active
+                  shipping_address shipping_city shipping_state shipping_zip shipping_name shipping_country_code shipping_country_name reset_attempts );
+  my $up_query = " update audit_users,  users set audit_users.new_" . $field[0] . " = users." . $field[0];
+  for (my $i=1; $i<scalar(@field); $i++) {
+    $up_query .= ", audit_users.new_".$field[$i] . " = users." . $field[$i];
+  }
+  $up_query .= " where audit_users.audit_users_id = ? and users.userid = ? ";
+
+  my $query = $dbh->prepare($up_query);
+  my $response = $query->execute($audit_id, $userid);
+
+}
+
+sub audit_admins_start {
+  my $dbh = shift;
+  my $userid = shift;
+  my $comment = shift;
+
+  my @field = qw( username password userid );
+  my $ins_query = " insert into audit_admins ( timestamp, comment, old_" . $field[0];
+  my $tail_ins_query = " select now(), ?, " . ($userid ? $field[0] : "null");
+  for (my $i=1; $i<scalar(@field); $i++) {
+    $ins_query .= ", old_".$field[$i];
+    $tail_ins_query .= ", " . ($userid ? $field[$i] : "null");
+  }
+  $ins_query .= ") ";
+  $tail_ins_query .= ($userid ? " from admins where userid = ?" : "");
+  $ins_query .= $tail_ins_query;
+
+  my $query = $dbh->prepare($ins_query);
+  my $response = ($userid ? $query->execute($comment, $userid) : $query->execute($comment) );
+  return ($dbh->last_insert_id(undef, undef, undef, undef));
+}
+
+sub audit_admins_end {
+  my $dbh = shift;
+  my $userid = shift;
+  my $audit_id = shift;
+
+  my @field = qw( username password userid );
+  my $up_query = " update audit_admins,  admins set audit_admins.new_" . $field[0] . " = admins." . $field[0];
+  for (my $i=1; $i<scalar(@field); $i++) {
+    $up_query .= ", audit_admins.new_".$field[$i] . " = admins." . $field[$i];
+  }
+  $up_query .= " where audit_admins.audit_admins_id = ? and admins.userid = ? ";
+
+  my $query = $dbh->prepare($up_query);
+  my $response = $query->execute($audit_id, $userid);
+
+}
+
+
+#----------------------------------------------Ugly exception handling logic using closures and anonymous functions----
+#-------------------------------------------This is in there to deal with the fact that CreditCall uses the die("error")
+#-------------------------------------------function instead of returning an error message in many cases...
+
+#       This utility function returns the passed string sans any leading or trailing whitespace.
+#
+sub strip_whitespace
+{
+        my $str = shift;        #grab our first parameter
+
+        $str =~ s/^\s+//;       #strip leading whitespace
+        $str =~ s/\s+$//;       #strip trailing whitespace
+
+        return $str;            #return the improved string
+}
+
+
+#    This function takes two coderef parameters, the second of which is usually an explicit call to the
+# 'catch' function which itself takes a coderef parameter.  This allows the code employing this suite of
+# functions to look somewhat like a conventional exception handling mechanism:
+#
+# try
+# {
+#    do_something_that_might_die();
+# }
+# catch
+# {
+#   my $errmsg = $_;
+#   log_the_error_message($errmsg);
+#   perform_some_cleanup();
+# };
+#
+# DO NOT FORGET THAT LAST SEMICOLON, EVERYTHING GOES TO HELL IF YOU DO!
+#
+sub try(&$)
+{
+        my ($attempt, $handler) = @_;
+
+        eval
+        {
+                &$attempt;
+        };
+
+        if($@)
+        {
+                do_catch($handler);
+        }
+}
+
+
+#    This function strips off the whitespace from the exception message reported by die()
+# and places the result into the default variable such that the code in the catch block can
+# just examine $_ to figure out what the cause of the error is, or to display or log
+# the error message.
+#
+sub do_catch(&$)
+{
+        my ($handler) = @_;
+
+        local $_ = strip_whitespace($@);
+
+        &$handler;
+}
+
+#    This just takes an explicit coderef and returns it unharmed.  The only
+# purpose of this is so the try/catch structure looks pretty and familiar.
+#
+sub catch(&) {$_[0]}
+
+
+sub read_config
+{
+        my $cfg_file = shift;
+        my $cfg_href = shift;
+
+        try
+        {
+                open my $fh, "$cfg_file";
+                while (<$fh>) {
+                        next if /^#/;
+                        chomp;
+                        s/^\s+//;
+                        s/\s+$//;
+                        next if !$_;
+                        my ($var, $val) = split(/=/, $_);
+                        $cfg_href->{$var} = $val;
+                }
+                close $fh;
+        }
+        catch
+        {
+                my $errmsg = $_;
+                die "Error processing config file $cfg_file, '$errmsg'.  exiting\n";
+        };
+
+}
+
+sub daemonize
+{
+        my $logfile = shift;
+        my $pidfile = shift;
+        my ($untainted_lf, $untainted_pf);
+
+        $untainted_lf = $1 if ($logfile =~ /^([^\0]+)$/);
+        $untainted_pf = $1 if ($pidfile =~ /^([^\0]+)$/);
+
+        try
+        {
+                chdir '/';
+                umask 0;
+                open STDIN, '/dev/null';
+                open STDOUT, '/dev/null';
+                open STDERR, '/dev/null';
+                my $pid = fork;
+                exit if $pid;
+                setsid;
+
+                if ($untainted_lf)
+                {
+                        close STDOUT;
+                        close STDERR;
+                        sysopen( STDOUT, $untainted_lf, O_WRONLY|O_APPEND|O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH );
+                        sysopen( STDERR, $untainted_lf, O_WRONLY|O_APPEND|O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH );
+                }
+
+                if ($untainted_pf)
+                {
+                        sysopen( my $fh, $untainted_pf, O_WRONLY|O_APPEND|O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH );
+                        #open my $fh, ">$untainted_pf";
+                        print $fh $$, "\n";
+                        close $fh ;
+                }
+        }
+        catch
+        {
+                my $errmsg = $_;
+                die "Failed to daemonize: '$errmsg', exiting\n";
+        };
+
+}
+
+
+return 1;

+ 642 - 0
server/scripts/RideLogicACL.pm

@@ -0,0 +1,642 @@
+package RideLogicACL;
+
+use strict;
+#use RideLogic;
+#use RideLogicAPIQueryWrapper;
+
+use POSIX;
+
+
+require Exporter;
+use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
+
+$VERSION     = "0.01";
+@ISA         = qw( Exporter );
+@EXPORT      = qw( );
+@EXPORT_OK   = qw( );
+%EXPORT_TAGS = ( DEFAULT => [ qw( ) ] );
+
+my $PACKAGE_NAME = "RideLogicACL";
+
+#####
+
+sub new {
+  my $class_name = shift;
+  my $rldbh = shift;
+
+  my %acl;
+  $acl{aro_ref} = {};
+  $acl{aco_ref} = {};
+  $acl{aro_aco_ref} = {};
+
+  my $class = {};
+  $class->{acl_ref} = \%acl;
+  $class->{dbh_ref} = \$rldbh;
+
+  bless($class, $class_name);
+
+  $class->get_tree();
+
+  return $class;
+
+}
+
+#####
+
+sub print_debug {
+  my $self = shift;
+
+  my $acl_href = $self->{acl_href};
+  my $aros_href = $acl_href->{aros_href};
+  my $acos_href = $acl_href->{acos_href};
+  my $aros_acos_href = $acl_href->{aros_acos_href};
+
+  print "aros:\n";
+  foreach my $k (keys(%{$aros_href})) {
+    print "$k " . $aros_href->{$k} . "\n";
+  }
+
+  print "\n";
+
+  print "acos:\n";
+  foreach my $k (keys(%{$acos_href})) {
+    print "$k " . $acos_href->{$k} . "\n";
+  }
+
+  print "\n";
+
+  print "aros_acos:\n";
+  foreach my $k (keys(%{$aros_acos_href})) {
+    print "$k " . $aros_acos_href->{$k} . "\n";
+  }
+
+
+}
+
+#####
+
+sub print_formatted_acl_table {
+  my $self = shift;
+  my $type = shift;
+
+  return undef if !($type =~ /^(aros|acos|aros_acos)$/);
+
+  my $rldbh = ${$self->{dbh_ref}};
+
+  my $query = $rldbh->prepare(" select * from rlapi_" . $type . " order by lft asc");
+  $query->execute();
+
+  my @s;
+  while (my $row = $query->fetchrow_hashref) {
+
+    my $id = $row->{id};
+    my $lft = $row->{lft};
+    my $rght = $row->{rght};
+    my $alias = $row->{alias};
+
+    if (scalar(@s)) {
+      while ( (scalar(@s)>0) && ($s[scalar(@s)-1] < $rght) ) {
+        pop @s;
+      }
+    }
+
+    print " " x (scalar(@s)*2) . "($id)[$lft,$rght] $alias\n";
+    push @s, $rght;
+  }
+
+}
+
+#####
+
+sub print_acl_tree {
+  my $self = shift;
+
+  $self->print_formatted_acl_table("aros");
+  print "\n\n";
+  $self->print_formatted_acl_table("acos");
+  print "\n\n";
+
+  my $rldbh = ${$self->{dbh_ref}};
+
+  my $query = $rldbh->prepare("select id, aros_id, acos_id from rlapi_aros_acos order by id asc");
+  $query->execute();
+  while (my $row = $query->fetchrow_arrayref) {
+    my ($id, $aros_id, $acos_id) = ($row->[0], $row->[1], $row->[2]);
+    print "[$id] ($aros_id, $acos_id)\n";
+  }
+  print "\n\n";
+}
+
+#####
+
+sub get_tree {
+  my $self = shift;
+
+  my $rldbh = ${$self->{dbh_ref}};
+
+  $self->{acl_href} = {};
+  my $acl_href = $self->{acl_href};
+
+  $acl_href->{aros_href} = {};
+  $acl_href->{acos_href} = {};
+  $acl_href->{aros_acos_href} = {};
+  my $aros_href = $acl_href->{aros_href};
+  my $acos_href = $acl_href->{acos_href};
+  my $aros_acos_href = $acl_href->{aros_acos_href};
+
+
+  my $query = $rldbh->prepare("select id, lft, rght, alias from rlapi_aros order by lft asc");
+  $query->execute();
+
+  my @rght_stack;
+  my @alias_stack;
+  while (my $row = $query->fetchrow_hashref) {
+
+    my $id = $row->{id};
+    my $lft = $row->{lft};
+    my $rght = $row->{rght};
+    my $alias = $row->{alias};
+
+    if (scalar(@rght_stack)) {
+      while ( (scalar(@rght_stack)>0) && ($rght_stack[scalar(@rght_stack)-1] < $rght) ) {
+        pop @rght_stack;
+        pop @alias_stack;
+      }
+    }
+
+    push @rght_stack, $rght;
+    push @alias_stack, $alias;
+    $aros_href->{ join('/', @alias_stack) } = $id;
+
+  }
+ 
+  my $query = $rldbh->prepare("select id, lft, rght, alias from rlapi_acos order by lft asc");
+  $query->execute();
+
+  @rght_stack = ();
+  @alias_stack = ();
+  while (my $row = $query->fetchrow_hashref) {
+
+    my $id = $row->{id};
+    my $lft = $row->{lft};
+    my $rght = $row->{rght};
+    my $alias = $row->{alias};
+
+    if (scalar(@rght_stack)) {
+      while ( (scalar(@rght_stack)>0) && ($rght_stack[scalar(@rght_stack)-1] < $rght) ) {
+        pop @rght_stack;
+        pop @alias_stack;
+      }
+    }
+
+    push @rght_stack, $rght;
+    push @alias_stack, $alias;
+    $acos_href->{ join('/', @alias_stack) } = $id;
+  }
+
+  my $query = $rldbh->prepare("select aros_id, acos_id from rlapi_aros_acos ");
+  $query->execute();
+  while (my $row = $query->fetchrow_arrayref) {
+    $aros_acos_href->{ $row->[0] . ":" . $row->[1] } = 1;
+  }
+
+
+}
+
+
+
+#####
+
+sub rlapi_acl_get_effective_id {
+  my $ao = shift;
+  my $ao_href = shift;
+
+  $ao =~ s/^\/*//;
+
+  my @a = split(/\//, $ao);
+  while (scalar(@a)>0) {
+    my $s = join('/', @a);
+    if (defined($ao_href->{$s})) {
+      return $ao_href->{$s};
+    }
+    pop @a;
+  }
+
+  return undef;
+}
+
+#####
+
+sub get_aro_id {
+  my $self = shift;
+  my $aro = shift;
+  my $acl_href = $self->{acl_href};
+  return rlapi_acl_get_effective_id( $aro, $acl_href->{aros_href} );
+}
+
+
+#####
+
+sub get_real_aro_id {
+  my $self = shift;
+  my $aro = shift;
+
+  $aro =~ s/^\///;
+
+  my $acl_href = $self->{acl_href};
+  my $href = $acl_href->{aros_href};
+  return $href->{$aro};
+}
+
+#####
+
+sub get_aco_id {
+  my $self = shift;
+  my $aco = shift;
+  my $acl_href = $self->{acl_href};
+  return rlapi_acl_get_effective_id( $aco, $acl_href->{acos_href} );
+}
+
+#####
+
+sub get_real_aco_id {
+  my $self = shift;
+  my $aco = shift;
+
+  $aco =~ s/^\///;
+
+  my $acl_href = $self->{acl_href};
+  my $href = $acl_href->{acos_href};
+  return $href->{$aco};
+}
+
+
+#####
+
+# inefficient method.  need to update this to have
+# a local version of the tree for efficient collection
+sub get_aro_aco_subtree_by_aro {
+  my $self = shift;
+  my $aro = shift;
+  my $aco_root = shift;
+
+  $aro =~ s/^\///;
+  $aco_root =~ s/^\///;
+
+  my $acl_href = $self->{acl_href};
+  my $aros_href = $acl_href->{aros_href};
+  my $acos_href = $acl_href->{acos_href};
+  my $aros_acos_href = $acl_href->{aros_acos_href};
+
+  my @res;
+
+  my $aco_root_len = length($aco_root);
+
+  my $aro_id = rlapi_acl_get_effective_id($aro, $aros_href);
+  foreach my $k (keys(%$acos_href))
+  {
+    my $sub_aco = substr($k, 0, $aco_root_len);
+
+    next if $sub_aco ne $aco_root;
+    my $aco_id = rlapi_acl_get_effective_id($k, $acos_href);
+    push @res, $k if $aros_acos_href->{ $aro_id . ":" . $aco_id };
+
+  }
+
+  return @res;
+
+}
+
+#####
+
+sub get_aro_aco_subtree_by_aco {
+  my $self = shift;
+  my $aro_root = shift;
+  my $aco = shift;
+
+  $aro_root =~ s/^\///;
+  $aco =~ s/^\///;
+
+  my $acl_href = $self->{acl_href};
+  my $aros_href = $acl_href->{aros_href};
+  my $acos_href = $acl_href->{acos_href};
+  my $aros_acos_href = $acl_href->{aros_acos_href};
+
+  my @res;
+
+  my $aro_root_len = length($aro_root);
+
+  my $aco_id = rlapi_acl_get_effective_id($aco, $acos_href);
+  foreach my $k (keys(%$aros_href))
+  {
+    my $sub_aro = substr($k, 0, $aro_root_len);
+    next if $sub_aro ne $aro_root;
+    my $aro_id = rlapi_acl_get_effective_id($k, $aros_href);
+    push @res, $k if $aros_acos_href->{ $aro_id . ":" . $aco_id };
+  }
+
+  return @res;
+
+}
+
+
+#####
+
+sub has_permission {
+  my $self = shift;
+  my $aro = shift;
+  my $aco = shift;
+
+  my $acl_href = $self->{acl_href};
+  my $aros_href = $acl_href->{aros_href};
+  my $acos_href = $acl_href->{acos_href};
+  my $aros_acos_href = $acl_href->{aros_acos_href};
+
+  my $aro_id = rlapi_acl_get_effective_id($aro, $aros_href);
+  my $aco_id = rlapi_acl_get_effective_id($aco, $acos_href);
+
+  return $aros_acos_href->{ $aro_id . ":" . $aco_id };
+
+}
+
+
+
+#####
+
+sub insert_aro {
+  my $self = shift;
+  my $aro = shift;
+
+  $aro =~ s/^\/*//;
+
+  my $rldbh = ${$self->{dbh_ref}};
+
+  my $acl_href = $self->{acl_href};
+  my $aros_href = $acl_href->{aros_href};
+
+  return undef if ($aros_href->{$aro});
+
+  my $foreign_key = undef;
+
+  my @a = split(/\//, $aro);
+  my $aro_alias = pop @a;
+
+  if (scalar(@a)==0) {
+    my $query = $rldbh->prepare("call rlapi_aros_insert( 1, ?, ?)");
+
+    my $r = $query->execute($foreign_key, $aro_alias);
+
+    my $tquery = $rldbh->prepare("select last_insert_id()");
+    my $r = $tquery->execute();
+
+    my $trow = $tquery->fetchrow_arrayref;
+    my $id = $trow->[0];
+
+    $self->get_tree();
+
+    return $id;
+  }
+
+  my $parent_aro = join('/', @a);
+  return undef if (!$aros_href->{$parent_aro});
+  my $parent_id = $aros_href->{$parent_aro};
+
+
+  my $query = $rldbh->prepare("call rlapi_aros_insert_under( ?, ?, ?)");
+
+  my $r = $query->execute($parent_id, $foreign_key, $aro_alias);
+  #my $id = $rldbh->last_insert_id();
+
+  my $tquery = $rldbh->prepare("select last_insert_id()");
+  $tquery->execute();
+  my $trow = $tquery->fetchrow_arrayref;
+  my $id = $trow->[0];
+
+  $self->get_tree();
+
+  return $id;
+  
+}
+
+#####
+
+sub insert_aco {
+  my $self = shift;
+  my $aco = shift;
+
+  $aco =~ s/^\/*//;
+
+  my $rldbh = ${$self->{dbh_ref}};
+
+  my $acl_href = $self->{acl_href};
+  my $acos_href = $acl_href->{acos_href};
+
+
+  return undef if ($acos_href->{$aco});
+
+  my $foreign_key = undef;
+
+  my @a = split(/\//, $aco);
+  my $aco_alias = pop @a;
+
+  if (scalar(@a)==0) {
+    my $query = $rldbh->prepare("call rlapi_acos_insert( 1, ?, ?)");
+    $query->execute($foreign_key, $aco_alias);
+    #my $id = $rldbh->last_insert_id(undef, undef, undef, undef);
+
+    my $tquery = $rldbh->prepare("select last_insert_id()");
+    $tquery->execute();
+    my $trow = $tquery->fetchrow_arrayref;
+    my $id = $trow->[0];
+
+    $self->get_tree();
+
+    return $id;
+  }
+
+  my $parent_aco = join('/', @a);
+  return undef if (!$acos_href->{$parent_aco});
+  my $parent_id = $acos_href->{$parent_aco};
+
+
+  my $query = $rldbh->prepare("call rlapi_acos_insert_under( ?, ?, ?)");
+  $query->execute($parent_id, $foreign_key, $aco_alias);
+  #my $id = $rldbh->last_insert_id();
+
+  my $tquery = $rldbh->prepare("select last_insert_id()");
+  $tquery->execute();
+  my $trow = $tquery->fetchrow_arrayref;
+  my $id = $trow->[0];
+
+  $self->get_tree();
+
+  return $id;
+  
+}
+
+#####
+
+sub insert_aros_acos {
+  my $self = shift;
+  my $aro = shift;
+  my $aco = shift;
+
+  $aro =~ s/^\///;
+  $aco =~ s/^\///;
+
+  my $rldbh = ${$self->{dbh_ref}};
+
+  my $acl_href = $self->{acl_href};
+  my $aros_href = $acl_href->{aros_href};
+  my $acos_href = $acl_href->{acos_href};
+  my $aros_acos_href = $acl_href->{aros_acos_href};
+
+  my $aro_id = $aros_href->{$aro};
+  my $aco_id = $acos_href->{$aco};
+
+  return undef if !$aro_id or !$aco_id;
+
+  my $query = $rldbh->prepare("select count(id) from rlapi_aros_acos where aros_id = ? and acos_id = ?");
+  $query->execute($aro_id, $aco_id);
+  return undef if $query->fetchrow_arrayref->[0];
+
+  my $query = $rldbh->prepare("insert into rlapi_aros_acos (aros_id, acos_id) values (?, ?)");
+  $query->execute($aro_id, $aco_id);
+
+  my $tquery = $rldbh->prepare("select last_insert_id()");
+  $tquery->execute();
+  my $trow = $tquery->fetchrow_arrayref;
+  my $id = $trow->[0];
+
+  $self->get_tree();
+
+  return $id;
+
+}
+
+
+#####
+
+sub remove_aro_id {
+  my $self = shift;
+  my $aro_id = shift;
+
+  my $rldbh = ${$self->{dbh_ref}};
+
+  my $query = $rldbh->prepare("call rlapi_aros_delete(?)");
+  $query->execute($aro_id);
+
+  $self->get_tree();
+  return 1;
+
+}
+
+#####
+
+sub remove_aco_id {
+  my $self = shift;
+  my $aco_id = shift;
+
+  my $rldbh = ${$self->{dbh_ref}};
+
+  my $query = $rldbh->prepare("call rlapi_acos_delete(?)");
+  $query->execute($aco_id);
+
+  $self->get_tree();
+  return 1;
+
+}
+
+#####
+
+sub remove_aro_aco_id {
+  my $self = shift;
+  my $aro_id = shift,
+  my $aco_id = shift;
+
+  my $rldbh = ${$self->{dbh_ref}};
+
+  my $query = $rldbh->prepare("delete from rlapi_aros_acos where aros_id = ? and acos_id = ?");
+  $query->execute($aro_id, $aco_id);
+
+  $self->get_tree();
+  return 1;
+
+}
+
+#####
+
+sub remove_aro {
+  my $self = shift;
+  my $aro = shift;
+
+  $aro =~ s/\///;
+
+  my $rldbh = ${$self->{dbh_ref}};
+
+  my $acl_href = $self->{acl_href};
+  my $aros_href = $acl_href->{aros_href};
+
+  return undef if !$aros_href->{$aro};
+
+  my $query = $rldbh->prepare("call rlapi_aros_delete(?)");
+  $query->execute($aros_href->{$aro});
+
+  $self->get_tree();
+  return 1;
+
+}
+
+#####
+
+sub remove_aco {
+  my $self = shift;
+  my $aco = shift;
+
+  $aco =~ s/\///;
+
+  my $rldbh = ${$self->{dbh_ref}};
+
+  my $acl_href = $self->{acl_href};
+  my $acos_href = $acl_href->{acos_href};
+
+  return undef if !$acos_href->{$aco};
+
+  my $query = $rldbh->prepare("call rlapi_acos_delete(?)");
+  $query->execute($acos_href->{$aco});
+
+  $self->get_tree();
+  return 1;
+
+}
+
+#####
+
+sub remove_aros_acos {
+  my $self = shift;
+  my $aro = shift;
+  my $aco = shift;
+
+  $aro =~ s/^\///;
+  $aco =~ s/^\///;
+
+  my $rldbh = ${$self->{dbh_ref}};
+
+  my $acl_href = $self->{acl_href};
+  my $aros_href = $acl_href->{aros_href};
+  my $acos_href = $acl_href->{acos_href};
+  my $aros_acos_href = $acl_href->{aros_acos_href};
+
+  my $aro_id = $aros_href->{$aro};
+  my $aco_id = $acos_href->{$aco};
+
+  return undef if !$aro_id or !$aco_id;
+
+  my $query = $rldbh->prepare("delete from rlapi_aros_acos where aros_id = ? and acos_id = ?");
+  $query->execute($aro_id, $aco_id);
+
+  return 1;
+
+}
+
+return 1;

文件差異過大導致無法顯示
+ 5197 - 0
server/scripts/RideLogicAPIQueryWrapper.pm


+ 303 - 0
server/scripts/RideLogicDBI.pm

@@ -0,0 +1,303 @@
+package RideLogicDBI;
+
+use strict;
+use DBI;
+use POSIX;
+
+our $debug = 0;
+our $PACKAGE = 'RideLogicDBI';
+
+require Exporter;
+#use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
+use vars qw( @ISA );
+
+#$VERSION     = "0.01";
+@ISA         = qw(Exporter);
+#@EXPORT      = qw( debug_print );
+#@EXPORT_OK   = qw( debug_print );
+#%EXPORT_TAGS = ( DEFAULT => [ qw( &debug_print ) ] );
+#our @ISA = qw(Exporter);
+
+our %CON ;
+
+our $data_source;
+our $username;
+our $password;
+our $attr_href;
+
+my $PACKAGE_NAME = "RideLogicDBI";
+
+my %session;
+my %session_active;
+my %session_inactive;
+my %session_ref_count;
+
+my %query;
+my %query_usage;
+
+sub debug_print {
+  print "SESSION:\n";
+  foreach my $k (keys(%session)) {
+    print "$k ";
+    foreach my $t (keys(%{$session{$k}})) {
+      print " ($t ", $session{$k}->{$t}, ")";
+    }
+    print "\n";
+  }
+  print "\n";
+  print "SESSION_ACTIVE:\n";
+  foreach my $k (keys(%session_active)) {
+    print "$k ";
+    foreach my $t (keys(%{$session_active{$k}})) {
+      print " ($t ", $session_active{$k}->{$t}, ")";
+    }
+    print "\n";
+  }
+  print "\n";
+  print "SESSION_INACTIVE:\n";
+  foreach my $k (keys(%session_inactive)) {
+    print "$k ";
+    foreach my $t (keys(%{$session_inactive{$k}})) {
+      print " ($t ", $session_inactive{$k}->{$t}, ")";
+    }
+    print "\n";
+  }
+  print "\n";
+  print "SESSION_REF_COUNT:\n";
+  foreach my $k (keys(%session_ref_count)) {
+    print "$k ", $session_ref_count{$k}, "\n";
+  }
+
+
+}
+
+
+sub construct_key {
+  my ($a, $b, $c, $h) = @_;
+  my $h_k = "";
+
+  if (defined($h))
+  {
+    foreach my $t (sort(keys(%$h)))
+    {
+      my ($x, $y) = ($t, $h->{$t});
+      $x =~ s/([\\:])/\\$1/g;
+      $y =~ s/([\\:])/\\$1/g;
+      $h_k .= ":$x:$y";
+    }
+  }
+
+  $a =~ s/([\\:])/\\$1/g;
+  $b =~ s/([\\:])/\\$1/g;
+  $c =~ s/([\\:])/\\$1/g;
+
+  return "$a:$b:$c$h_k";
+
+}
+
+# remove from inactive hash
+# add to active hash
+# return session key
+sub activate_from_inactive_pool {
+  my $k = shift;
+
+  my $href = $session_inactive{$k};
+  my ($ref, $v) = each %$href;
+
+  delete $href->{$ref};
+  delete $session_inactive{$k} if (scalar(keys(%{$href}))==0);
+
+  my $session_key = $k . ":" . $ref;
+  $session_active{$k}->{$ref} = 1;
+  $session{$session_key}->{'active_timestamp'} = strftime "%a %b %e %H:%M:%S %Y", localtime;
+
+  return $session_key;
+
+}
+
+
+sub deactivate_from_active_pool {
+  my $session_key = shift;
+
+  my $orig_sess = $session_key;
+
+  $session_key =~ m/(.*):(\d+)$/;
+  my ($k, $ref) = ($1, $2);
+
+  delete $session_active{$k}->{$ref};
+  delete $session_active{$k} if !scalar(keys(%{$session_active{$k}}));
+
+  $session_inactive{$k} = {} if !exists($session_inactive{$k});
+  $session_inactive{$k}->{$ref} = 1;
+
+}
+
+sub create_new_db_session {
+  my ($k, $dsn, $user, $pass, $attr) = @_;
+  my $ref = "0";
+
+  my $dbh = DBI->connect($dsn, $user, $pass, $attr);
+  return undef if (!$dbh);
+
+  my $dbh_ref = \$dbh;
+
+  if (!exists($session_ref_count{$k}))
+  {
+    $session_ref_count{$k} = 1;
+  }
+  else
+  {
+    $ref = $session_ref_count{$k}++;
+  }
+
+  my $session_key = $k . ":" . $ref;
+  $session{$session_key} = {};
+  $session{$session_key}->{'dbh_ref'}           = $dbh_ref;
+  $session{$session_key}->{'active_timestamp'}  = strftime "%a %b %e %H:%M:%S %Y", localtime;
+  $session{$session_key}->{'lock_active'}       = 0;
+
+  $query{$session_key} = {};
+  $query_usage{$session_key} = {};
+
+  $session_active{$k}->{$ref} = 1;
+
+  return $session_key;
+}
+
+
+sub connect {
+  my ($class_name, $dsn, $user, $pass, $attr) = @_;
+
+  my $k = construct_key($dsn, $user, $pass, $attr);
+
+  my $session_key = ( exists($session_inactive{$k}) ?
+                      activate_from_inactive_pool($k) :
+                      create_new_db_session($k, $dsn, $user, $pass, $attr) );
+  return undef if !$session_key;
+
+  my $class = {};
+  $class->{'key'}               = $session_key;
+  $class->{'dbh_ref'}           = $session{$session_key}->{'dbh_ref'};
+  $class->{'active_timestamp'}  = $session{$session_key}->{'active_timestamp'};
+  bless($class, $class_name);
+
+  my $dbh_ref = $session{$session_key}->{'dbh_ref'};
+  if ( !( defined($$dbh_ref) && $$dbh_ref->ping) ) 
+  {
+    $$dbh_ref = DBI->connect($dsn, $user, $pass, $attr);
+    $query{$session_key} = {};
+    $query_usage{$session_key} = {};
+    return undef if !$$dbh_ref;
+  }
+  return $class;
+
+}
+
+sub DESTROY {
+  my $self = shift;
+  deactivate_from_active_pool($self->{'key'});
+}
+
+sub prepare {
+  my $self = shift;
+  my $query = shift;
+
+  my $session_key = $self->{'key'};
+  my $dbh         = ${$self->{'dbh_ref'}};
+
+
+  if ( !defined($query{$session_key}->{$query}) )
+  {
+    $query{$session_key}->{$query} = $dbh->prepare($query);
+    $query_usage{$session_key}->{$query} = 0;
+  }
+
+  $query_usage{$session_key}->{$query}++;
+
+  return $query{$session_key}->{$query};
+
+}
+
+sub begin_work {
+  my $self = shift;
+  my $dbh = ${$self->{'dbh_ref'}};
+  $dbh->begin_work();
+}
+
+
+sub finish {
+  my $self = shift;
+  my $r = shift;
+  my $dbh = ${$self->{'dbh_ref'}};
+
+  if ($r) {
+    $dbh->commit();
+  } else {
+    $dbh->rollback();
+  }
+
+}
+
+sub rollback {
+  my $self = shift;
+  my $dbh = ${$self->{'dbh_ref'}};
+
+  $dbh->rollback();
+}
+
+sub commit {
+  my $self = shift;
+  my $dbh = ${$self->{'dbh_ref'}};
+  $dbh->commit();
+}
+
+sub last_insert_id {
+  my $self = shift;
+  my $dbh = ${$self->{'dbh_ref'}};
+  return $dbh->last_insert_id(undef, undef, undef, undef);
+}
+
+sub raise_error {
+  my $self = shift;
+  my $val = shift;
+  my $dbh = ${$self->{'dbh_ref'}};
+  return $dbh->{RaiseError} = $val;
+}
+
+sub rows {
+  my $self = shift;
+  my $dbh = ${$self->{'dbh_ref'}};
+  return $dbh->rows;
+}
+
+sub errstr {
+  my $self = shift;
+  my $dbh = ${$self->{'dbh_ref'}};
+  return $dbh->errstr;
+}
+
+sub lock_active {
+  my $self = shift;
+  my $v = shift;
+  $self->{'lock_active'} = $v if defined($v);
+  return $self->{'lock_active'};
+}
+
+sub disable_locking {
+  my $self = shift;
+  return $self->lock_active(1);
+}
+
+sub enable_locking {
+  my $self = shift;
+  return $self->lock_active(0);
+}
+
+sub get_query_usage_ref {
+  my $self = shift;
+  my $s = $self->{'key'};
+  return $query_usage{$s};
+}
+
+
+return 1;

+ 722 - 0
server/sql_schema/create_tables.sql

@@ -0,0 +1,722 @@
+--
+-- Copyright (c) 2019 Clementine Computing LLC.
+-- 
+-- This file is part of PopuFare.
+-- 
+-- PopuFare is free software: you can redistribute it and/or modify
+-- it under the terms of the GNU Affero General Public License as published by
+-- the Free Software Foundation, either version 3 of the License, or
+-- (at your option) any later version.
+-- 
+-- PopuFare 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 Affero General Public License for more details.
+-- 
+-- You should have received a copy of the GNU Affero General Public License
+-- along with PopuFare.  If not, see <https://www.gnu.org/licenses/>.
+--
+
+-- AVLS data table receives all AVLS chirps and stores them for later reference
+CREATE TABLE IF NOT EXISTS avls_data (
+
+  equip_num INT,
+  driver INT,
+  paddle INT,
+  route  INT,
+  trip  INT,
+  stop  INT,
+  chirp_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  latitude DOUBLE,
+  longitude DOUBLE,
+  heading DOUBLE,
+  velocity DOUBLE
+
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+-- Bus Pass Table holds all bus pass holders
+-- DROP TABLE IF EXISTS `active_rider_table`;
+CREATE TABLE IF NOT EXISTS active_rider_table (
+
+  -- The following entries are stored on the client as well and kept in sync
+  logical_card_id BIGINT NOT NULL,
+  seq_num BIGINT UNIQUE PRIMARY KEY AUTO_INCREMENT,
+  rfid_token VARCHAR(32),
+  mag_token VARCHAR(32),
+  rule_name VARCHAR(24),
+  rule_param VARCHAR(24),
+  
+  -- The rest are for server-side recordkeeping...
+  
+  deleted BOOLEAN DEFAULT '0',
+  parent_entity VARCHAR(32),
+  notes  VARCHAR(64),
+  
+  -- And some additional index data
+  
+  INDEX id_idx (logical_card_id),
+  INDEX id_and_seq (logical_card_id, seq_num)
+  
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+  
+-- Billing Log Table
+
+-- DROP TABLE IF EXISTS `billing_log`;
+CREATE TABLE IF NOT EXISTS billing_log (
+
+  -- MD5 sum of the orignal record as sent by the client, used to confirm storing the record
+  conf_checksum VARCHAR(32) UNIQUE,
+
+  -- Contents of the billing log entry coming from the client
+  equip_num     INT,
+  driver        INT,
+  paddle        INT,
+  route         INT,
+  trip          INT,
+  stop          INT,
+  ride_time TIMESTAMP,
+  latitude      DOUBLE,
+  longitude     DOUBLE,
+  action VARCHAR(16),
+  rule  VARCHAR(24),
+  ruleparam VARCHAR(24),
+  reason VARCHAR(64),
+  credential VARCHAR(32),
+  logical_card_id BIGINT NOT NULL,
+  cash_value INT,
+  stop_name VARCHAR(64)
+  
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+
+CREATE TABLE IF NOT EXISTS diagnostic_log (
+  servertime TIMESTAMP NOT NULL,
+  loglvl VARCHAR(8),
+  message VARCHAR(256),
+
+  -- And some additional index data
+  
+  INDEX servertime_idx (servertime),
+  UNIQUE INDEX lvl_msg_idx (servertime, loglvl)
+  
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+create table pass_option
+( id int not null auto_increment,
+  group_id    int,
+  param       int,
+  name        varchar(255),
+  rule        varchar(255),
+  db_rule     varchar(255),
+  type        varchar(255),
+  description varchar(255),
+  num_opt     int   default 0,
+  option0     varchar(255),
+  option1     varchar(255),
+  option2     varchar(255),
+  option3     varchar(255),
+  
+  active      tinyint default 0,
+  
+  primary key (id),
+  unique key id (id)
+) engine=InnoDB ;
+
+
+CREATE TABLE IF NOT EXISTS rule_class (
+  rulename VARCHAR(24),
+  ruleclass VARCHAR(24)
+  
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE IF NOT EXISTS update_level (
+  equip_num INT NOT NULL DEFAULT 0,
+  client_file VARCHAR(32) NOT NULL,
+  checksum VARCHAR(32) NOT NULL,
+  file_size INT NOT NULL,
+  file_path VARCHAR(512) NOT NULL,
+  fileversion VARCHAR(32),
+  
+  -- The following field is never manually set and is used only to pull the latest value
+  serial BIGINT NOT NULL AUTO_INCREMENT,
+
+  -- These indicies should make the operation of "give me the latest of each update for bus number X" fast  
+  
+  INDEX equip_num_idx (equip_num),
+  INDEX client_file_idx (client_file),
+  INDEX serial_idx (serial),
+  INDEX file_eqip_idx (client_file, equip_num)
+  
+)ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE IF NOT EXISTS bus_checkin_log (
+   checkin_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+   viper_num INT NOT NULL DEFAULT 0,
+   equip_num INT NOT NULL DEFAULT 0,
+   eth0_mac VARCHAR(17),
+   cell_imei VARCHAR(15),
+   cell_imsi VARCHAR(15),
+   version_data VARCHAR(256),
+
+   INDEX time_index(checkin_time),
+   INDEX equip_num_idx (equip_num),
+   INDEX viper_num_idx (viper_num)
+
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+DROP TABLE IF EXISTS `user_pass`;
+CREATE TABLE `user_pass` (
+  user_pass_id BIGINT UNIQUE NOT NULL auto_increment,
+  logical_card_id BIGINT default NULL,
+
+  issued datetime default NULL,
+  activated datetime default NULL,
+  deactivated datetime default NULL,
+
+  firstused datetime default NULL,
+  lastused datetime default NULL,
+
+  nrides_orig int(11) default NULL,
+  nrides_remain int(11) default NULL,
+
+  nday_orig int(11) default NULL,
+  nday_expiration datetime default NULL,
+
+  active tinyint(1) default 0,
+  expired tinyint(1) default 0,
+
+  rule varchar(255) default NULL,
+  queue_order int(11) default NULL,
+
+  comment varchar(255) default NULL,
+  paytype varchar(255) default NULL,
+
+  PRIMARY KEY  (user_pass_id),
+  UNIQUE KEY user_pass_idx (user_pass_id),
+  KEY user_pass_idx_logical_card_id (logical_card_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+
+DROP TABLE IF EXISTS `user_card`;
+CREATE TABLE `user_card` (
+  logical_card_id BIGINT UNIQUE NOT NULL auto_increment,
+
+  -- legacy, will remove in final phase2
+  -- card_number varchar(128) UNIQUE default NULL,
+  -- rfsite int(127) default NULL,
+  -- rfid int(127) default NULL,
+  --
+
+  mag_token varchar(255) default null,
+  rfid_token varchar(255) default null,
+
+  comment varchar(255) default NULL,
+  userid int(11) default NULL,
+
+  issued datetime default NULL,
+  active tinyint(1) default 1,
+  deactivated datetime default NULL,
+
+  lastused datetime default NULL,
+  firstused datetime default NULL,
+
+  group_id int(11) default NULL,
+  issuetype varchar(255) default NULL,
+
+
+  PRIMARY KEY  (logical_card_id),
+  UNIQUE KEY user_card_idx (logical_card_id),
+  KEY user_card_idx_mag_token (mag_token),
+  KEY user_card_idx_rfid_token (rfid_token),
+  KEY user_card_idx_mag_rfid_token (mag_token,rfid_token),
+  KEY user_card_idx_userid (userid)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+
+DROP TABLE IF EXISTS `groups`;
+CREATE TABLE `groups` (
+  group_id int(11) default NULL,
+  group_name varchar(255) default NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+
+DROP TABLE IF EXISTS `audit_user_pass`;
+CREATE TABLE `audit_user_pass` (
+  `audit_user_pass_id` int(11) NOT NULL auto_increment,
+  `old_user_pass_id` int(11) default NULL,
+  `old_logical_card_id` int(11) default NULL,
+  `old_issued` datetime default NULL,
+  `old_firstused` datetime default NULL,
+  `old_lastused` datetime default NULL,
+  `old_nrides_orig` int(11) default NULL,
+  `old_nrides_remain` int(11) default NULL,
+  `old_nday_orig` int(11) default NULL,
+  `old_nday_expiration` datetime default NULL,
+  `old_active` int(11) default NULL,
+  `old_rule` char(255) default NULL,
+  `old_queue_order` int(11) default NULL,
+  `old_log_id` int(11) default NULL,
+  `old_comment` varchar(255) default NULL,
+  `new_user_pass_id` int(11) default NULL,
+  `new_logical_card_id` int(11) default NULL,
+  `new_issued` datetime default NULL,
+  `new_firstused` datetime default NULL,
+  `new_lastused` datetime default NULL,
+  `new_nrides_orig` int(11) default NULL,
+  `new_nrides_remain` int(11) default NULL,
+  `new_nday_orig` int(11) default NULL,
+  `new_nday_expiration` datetime default NULL,
+  `new_active` int(11) default NULL,
+  `new_rule` char(255) default NULL,
+  `new_queue_order` int(11) default NULL,
+  `new_log_id` int(11) default NULL,
+  `new_comment` varchar(255) default NULL,
+  `comment` varchar(255) default NULL,
+  `owner_id` int(11) default NULL,
+  `timestamp` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
+  `old_expired` tinyint(10) default NULL,
+  `new_expired` tinyint(1) default NULL,
+  `old_paytype` varchar(255) default NULL,
+  `new_paytype` varchar(255) default NULL,
+  PRIMARY KEY  (`audit_user_pass_id`),
+  INDEX audit_user_pass_id_idx (audit_user_pass_id),
+  INDEX audit_user_pass_timestamp_idx (timestamp),
+  INDEX audit_user_pass_new_user_pass_id_idx (new_user_pass_id),
+  INDEX audit_user_pass_old_user_pass_id_idx (old_user_pass_id)
+
+
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+
+DROP TABLE IF EXISTS `audit_user_card`;
+CREATE TABLE `audit_user_card` (
+  `audit_user_card_id` int(11) NOT NULL auto_increment,
+  `old_logical_card_id` int(11) default NULL,
+--  `old_card_number` char(128) default NULL,
+--  `old_rfsite` int(11) default NULL,
+--  `old_rfid` int(11) default NULL,
+  `old_comment` varchar(255) default NULL,
+  `old_lastused` datetime default NULL,
+  `old_userid` int(11) default NULL,
+  `old_issued` datetime default NULL,
+  `old_firstused` datetime default NULL,
+  `old_group_id` int(11) default NULL,
+  `old_issuetype` varchar(255) default NULL,
+  `new_logical_card_id` int(11) default NULL,
+--  `new_card_number` char(128) default NULL,
+--  `new_rfsite` int(11) default NULL,
+--  `new_rfid` int(11) default NULL,
+  `new_comment` varchar(255) default NULL,
+  `new_lastused` datetime default NULL,
+  `new_userid` int(11) default NULL,
+  `new_issued` datetime default NULL,
+  `new_firstused` datetime default NULL,
+  `new_group_id` int(11) default NULL,
+  `new_issuetype` varchar(255) default NULL,
+  `timestamp` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
+  `comment` varchar(255) default NULL,
+  `old_mag_token` varchar(31) default NULL,
+  `new_mag_token` varchar(31) default NULL,
+  `old_rfid_token` varchar(31) default NULL,
+  `new_rfid_token` varchar(31) default NULL,
+  `old_active` tinyint(1) default NULL,
+  `new_active` tinyint(1) default NULL,
+  `old_deactivated` datetime default null,
+  `new_deactivated` datetime default null,
+  PRIMARY KEY  (`audit_user_card_id`),
+  INDEX audit_user_card_id_idx (audit_user_card_id),
+  INDEX audit_user_card_timestampe_idx (timestamp),
+
+  INDEX audit_user_card_new_logical_card_id_idx (new_logical_card_id),
+  INDEX audit_user_card_old_logical_card_id_idx (old_logical_card_id),
+
+  INDEX audit_user_card_new_mag_token_idx (new_mag_token),
+  INDEX audit_user_card_old_mag_token_idx (old_mag_token),
+
+  INDEX audit_user_card_new_rfid_token_idx (new_rfid_token),
+  INDEX audit_user_card_old_rfid_token_idx (old_rfid_token)
+
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+DROP TABLE IF EXISTS `users`;
+CREATE TABLE `users` (
+          `username` char(255) default NULL,
+          `userid` int(127) NOT NULL auto_increment,
+          `comment` char(255) default NULL,
+          `first_name` char(128) default NULL,
+          `last_name` char(128) default NULL,
+          `phone` char(31) default NULL,
+          `email` char(128) default NULL,
+          `address` char(255) default NULL,
+          `city` char(127) default NULL,
+          `state` char(127) default NULL,
+          `zip` char(31) default NULL,
+          `created` datetime default NULL,
+          `active` tinyint(1) default NULL,
+          `passwordhash` varchar(255) default NULL,
+          `shipping_address` varchar(255) default NULL,
+          `shipping_city` varchar(255) default NULL,
+          `shipping_state` varchar(255) default NULL,
+          `shipping_zip` varchar(255) default NULL,
+          `shipping_name` varchar(255) default NULL,
+          `shipping_country_code` varchar(32) default NULL,
+          `shipping_country_name` varchar(255) default NULL,
+          `reset_attempts` int(11) default '0',
+          PRIMARY KEY  (`userid`)
+) ENGINE=InnoDB AUTO_INCREMENT=325 DEFAULT CHARSET=latin1;
+
+
+DROP TABLE IF EXISTS `audit_users`;
+CREATE TABLE `audit_users` (
+          `audit_users_id` int(11) NOT NULL auto_increment,
+          `timestamp` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
+          `comment` varchar(255) default NULL,
+          `old_username` char(255) default NULL,
+          `old_userid` int(127) default NULL,
+          `old_comment` char(255) default NULL,
+          `old_first_name` char(128) default NULL,
+          `old_last_name` char(128) default NULL,
+          `old_phone` char(31) default NULL,
+          `old_email` char(128) default NULL,
+          `old_address` char(255) default NULL,
+          `old_city` char(127) default NULL,
+          `old_state` char(127) default NULL,
+          `old_zip` char(31) default NULL,
+          `old_created` datetime default NULL,
+          `old_active` tinyint(1) default NULL,
+          `old_passwordhash` varchar(255) default NULL,
+          `new_username` char(255) default NULL,
+          `new_userid` int(127) default NULL,
+          `new_comment` char(255) default NULL,
+          `new_first_name` char(128) default NULL,
+          `new_last_name` char(128) default NULL,
+          `new_phone` char(31) default NULL,
+          `new_email` char(128) default NULL,
+          `new_address` char(255) default NULL,
+          `new_city` char(127) default NULL,
+          `new_state` char(127) default NULL,
+          `new_zip` char(31) default NULL,
+          `new_created` datetime default NULL,
+          `new_active` tinyint(1) default NULL,
+          `new_passwordhash` varchar(255) default NULL,
+          `old_shipping_address` varchar(255) default NULL,
+          `old_shipping_city` varchar(255) default NULL,
+          `old_shipping_state` varchar(255) default NULL,
+          `old_shipping_zip` varchar(255) default NULL,
+          `old_shipping_name` varchar(255) default NULL,
+          `old_shipping_country_code` varchar(255) default NULL,
+          `old_shipping_country_name` varchar(32) default NULL,
+          `old_reset_attempts` varchar(255) default NULL,
+          `new_shipping_address` varchar(255) default NULL,
+          `new_shipping_city` varchar(255) default NULL,
+          `new_shipping_state` varchar(255) default NULL,
+          `new_shipping_zip` varchar(255) default NULL,
+          `new_shipping_name` varchar(255) default NULL,
+          `new_shipping_country_code` varchar(255) default NULL,
+          `new_shipping_country_name` varchar(32) default NULL,
+          `new_reset_attempts` varchar(255) default NULL,
+          PRIMARY KEY  (`audit_users_id`)
+) ENGINE=InnoDB AUTO_INCREMENT=373 DEFAULT CHARSET=latin1;
+
+
+-- Admin Web UI tables
+
+DROP TABLE IF EXISTS `admins`;
+CREATE TABLE `admins` (
+          `userid` int(127) not NULL  auto_increment,
+          `group_id` int default NULL,
+          `username` char(255) default NULL,
+          `password` char(255) default NULL,
+          `active` tinyint default 1,
+          `comment` varchar(255) default null,
+          PRIMARY KEY (`userid`)
+) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+
+DROP TABLE IF EXISTS `audit_admins`;
+CREATE TABLE `audit_admins` (
+          `audit_admins_id` int(11) NOT NULL auto_increment,
+          `timestamp` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
+          `comment` varchar(255) default NULL,
+          `old_userid` int default NULL,
+          `old_username` varchar(255) default NULL,
+          `old_password` varchar(255) default NULL,
+          `new_userid` int default NULL,
+          `new_username` varchar(255) default NULL,
+          `new_password` varchar(255) default NULL,
+          PRIMARY KEY  (`audit_admins_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+
+
+DROP TABLE IF EXISTS `admins_session_info`;
+CREATE TABLE `admins_session_info` (
+          `userid` int(255) default NULL,
+          `sessionid` char(255) default NULL,
+          `lastactive` datetime default NULL
+) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+
+DROP TABLE IF EXISTS `admin_groups`;
+CREATE TABLE `admin_groups` (
+          `userid` int(127) default NULL,
+          `group_id` int(11) default NULL,
+          `permissions` int(127) default NULL
+) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+
+DROP TABLE IF EXISTS `rule_mappings`;
+CREATE TABLE `rule_mappings` (
+          `rule` char(255) NOT NULL default '',
+          `rule_text` char(255) default NULL,
+          `group_id` int(11) default NULL,
+          PRIMARY KEY  (`rule`)
+) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+
+DROP TABLE IF EXISTS `org_default_pass_value`;
+CREATE TABLE `org_default_pass_value` (
+          `id` int(11) NOT NULL auto_increment,
+          `name` varchar(255) default NULL,
+          `nday` int(11) default NULL,
+          `nride` int(11) default NULL,
+          `description` varchar(255) default NULL,
+          `start` datetime default NULL,
+          `end` datetime default NULL,
+          PRIMARY KEY  (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=latin1;
+
+DROP TABLE IF EXISTS `org_default_card_value`;
+CREATE TABLE `org_default_card_value` (
+          `id` int(11) NOT NULL auto_increment,
+          `group_id` int(11) default NULL,
+          `mag_track` int(11) default NULL,
+          `rf_length` int(11) default NULL,
+          `rf_site` int(11) default NULL,
+          PRIMARY KEY  (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+
+
+-- WEB API tables
+
+DROP TABLE IF EXISTS `authorization_log`;
+CREATE TABLE `authorization_log` (
+          `authorization_log_id` int(11) NOT NULL auto_increment,
+          `timestamp` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
+          `authorization_type` varchar(255) default NULL,
+          `authorization_code` varchar(255) default NULL,
+          `user_id` int(11) default NULL,
+          `logical_card_id` int(11) default NULL,
+          `user_pass_id` int(11) default NULL,
+          `comment` varchar(255) default NULL,
+          PRIMARY KEY  (`authorization_log_id`)
+) ENGINE=InnoDB AUTO_INCREMENT=1981 DEFAULT CHARSET=latin1;
+
+DROP TABLE IF EXISTS `org_card_order_queue`;
+CREATE TABLE `org_card_order_queue` (
+          `org_card_order_queue_id` int(11) NOT NULL auto_increment,
+          `userid` int(11) default NULL,
+          `logical_card_id` int(11) default NULL,
+          `created` timestamp NOT NULL default CURRENT_TIMESTAMP,
+          `processed` timestamp NULL default NULL,
+          `comment` varchar(255) default NULL,
+          `pending` tinyint(1) default NULL,
+          PRIMARY KEY  (`org_card_order_queue_id`)
+) ENGINE=InnoDB AUTO_INCREMENT=54 DEFAULT CHARSET=latin1;
+
+
+DROP TABLE IF EXISTS `org_api_session`;
+CREATE TABLE `org_api_session` (
+          `timestamp` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
+          `ip` varchar(15) default NULL,
+          `active` tinyint(4) default NULL,
+          `server_token` varchar(255) default NULL,
+          `user_token` varchar(255) default NULL
+) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+
+DROP TABLE IF EXISTS `org_api_log`;
+CREATE TABLE `org_api_log` (
+          `log_id` int(11) NOT NULL auto_increment,
+          `timestamp` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
+          `log` blob,
+          PRIMARY KEY  (`log_id`)
+) ENGINE=InnoDB AUTO_INCREMENT=500 DEFAULT CHARSET=latin1;
+
+DROP TABLE IF EXISTS `org_api_password_reset`;
+CREATE TABLE `org_api_password_reset` (
+          `timestamp` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
+          `token` varchar(255) default NULL,
+          `userid` int(11) default NULL,
+          `email` varchar(255) default NULL,
+          `active` tinyint(1) default NULL
+) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+
+DROP TABLE IF EXISTS `org_api_register_email`;
+CREATE TABLE `org_api_register_email` (
+          `timestamp` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
+          `token` varchar(255) default NULL,
+          `active` tinyint(1) default NULL,
+          `email` varchar(255) default NULL
+) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+
+
+
+-- drivers, stops and paddles + misc
+
+DROP TABLE IF EXISTS `drivers`;
+SET @saved_cs_client     = @@character_set_client;
+SET character_set_client = utf8;
+CREATE TABLE `drivers` (
+  `id` int(11) NOT NULL default '0',
+  `pin` varchar(8) NOT NULL default '',
+  `name` varchar(32) default NULL,
+  UNIQUE KEY `id` (`id`)
+) ENGINE=MyISAM DEFAULT CHARSET=latin1;
+SET character_set_client = @saved_cs_client;
+
+--
+-- Table structure for table `stops`
+--
+
+DROP TABLE IF EXISTS `stops`;
+SET @saved_cs_client     = @@character_set_client;
+SET character_set_client = utf8;
+CREATE TABLE `stops` (
+  `id` int(11) NOT NULL default '0',
+  `latitude` double NOT NULL default '0',
+  `longitude` double NOT NULL default '0',
+  `name` varchar(32) default NULL,
+  PRIMARY KEY  (`id`),
+  UNIQUE KEY `id` (`id`)
+) ENGINE=MyISAM DEFAULT CHARSET=latin1;
+SET character_set_client = @saved_cs_client;
+
+--
+-- Table structure for table `paddles`
+--
+
+DROP TABLE IF EXISTS `paddles`;
+SET @saved_cs_client     = @@character_set_client;
+SET character_set_client = utf8;
+CREATE TABLE `paddles` (
+  `id` int(11) NOT NULL default '0',
+  `slot` int(11) NOT NULL default '0',
+  `arrival` time default NULL,
+  `route` int(11) default NULL,
+  `trip` int(11) default NULL,
+  `stage` int(11) default NULL,
+  `stop` int(11) default NULL,
+  `stopid` int(11) NOT NULL default '0',
+  KEY `ididx` (`id`)
+) ENGINE=MyISAM DEFAULT CHARSET=latin1;
+SET character_set_client = @saved_cs_client;
+
+--
+-- Table structure for table `old_stops`
+--
+
+DROP TABLE IF EXISTS `old_stops`;
+SET @saved_cs_client     = @@character_set_client;
+SET character_set_client = utf8;
+CREATE TABLE `old_stops` (
+  `verstring` text,
+  `id` int(11) NOT NULL default '0',
+  `latitude` double NOT NULL default '0',
+  `longitude` double NOT NULL default '0',
+  `name` varchar(32) default NULL
+) ENGINE=MyISAM DEFAULT CHARSET=latin1;
+SET character_set_client = @saved_cs_client;
+
+--
+-- Table structure for table `old_paddles`
+--
+
+DROP TABLE IF EXISTS `old_paddles`;
+SET @saved_cs_client     = @@character_set_client;
+SET character_set_client = utf8;
+CREATE TABLE `old_paddles` (
+  `verstring` text,
+  `id` int(11) NOT NULL default '0',
+  `slot` int(11) NOT NULL default '0',
+  `arrival` time default NULL,
+  `route` int(11) default NULL,
+  `trip` int(11) default NULL,
+  `stage` int(11) default NULL,
+  `stop` int(11) default NULL,
+  `stopid` int(11) NOT NULL default '0'
+) ENGINE=MyISAM DEFAULT CHARSET=latin1;
+SET character_set_client = @saved_cs_client;
+
+--
+-- Table structure for table `live_stops`
+--
+
+DROP TABLE IF EXISTS `live_stops`;
+SET @saved_cs_client     = @@character_set_client;
+SET character_set_client = utf8;
+CREATE TABLE `live_stops` (
+  `id` int(11) NOT NULL default '0',
+  `latitude` double NOT NULL default '0',
+  `longitude` double NOT NULL default '0',
+  `name` varchar(32) default NULL,
+  PRIMARY KEY  (`id`),
+  UNIQUE KEY `id` (`id`)
+) ENGINE=MyISAM DEFAULT CHARSET=latin1;
+SET character_set_client = @saved_cs_client;
+
+--
+-- Table structure for table `live_paddles`
+--
+
+DROP TABLE IF EXISTS `live_paddles`;
+SET @saved_cs_client     = @@character_set_client;
+SET character_set_client = utf8;
+CREATE TABLE `live_paddles` (
+  `id` int(11) NOT NULL default '0',
+  `slot` int(11) NOT NULL default '0',
+  `arrival` time default NULL,
+  `route` int(11) default NULL,
+  `trip` int(11) default NULL,
+  `stage` int(11) default NULL,
+  `stop` int(11) default NULL,
+  `stopid` int(11) NOT NULL default '0',
+  KEY `ididx` (`id`)
+) ENGINE=MyISAM DEFAULT CHARSET=latin1;
+SET character_set_client = @saved_cs_client;
+
+
+DROP TABLE IF EXISTS `price_point`;
+SET @saved_cs_client     = @@character_set_client;
+SET character_set_client = utf8;
+CREATE TABLE `price_point` (
+  `id` int(11) NOT NULL auto_increment,
+  `price` double default NULL,
+  `param` int(11) default NULL,
+  `name` varchar(255) default NULL,
+  `rule` varchar(32) default NULL,
+  `db_rule` varchar(32) default NULL,
+  `group_id` int(11) default NULL,
+  `type` varchar(255) default NULL,
+  `description` varchar(1024) default NULL,
+  `num_opt` int default 0,
+  `price_option0` varchar(255) default null,
+  `price_option1` varchar(255) default null,
+  `price_option2` varchar(255) default null,
+  `price_option3` varchar(255) default null,
+  `active` tinyint(1) default 0,
+  UNIQUE KEY `id` (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1;
+SET character_set_client = @saved_cs_client;
+
+drop table if exists billing_log_annotation;
+SET @saved_cs_client     = @@character_set_client;
+SET character_set_client = utf8;
+create table if not exists billing_log_annotation (
+  id int(11) unique not null auto_increment,
+  seq_num int not null,
+  rule varchar(24),
+  ruleparam varchar(24),
+  reason varchar(64),
+  credential varchar(32),
+  note varchar(512),
+  timestamp timestamp default current_timestamp,
+  primary key (id),
+  key billing_log_annotation_seq_num_key (seq_num)
+) ENGINE=InnoDB default CHARSET=utf8;
+SET character_set_client = @saved_cs_client;
+