buspass_server.pl 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
  1. #!/usr/bin/perl -Tw
  2. #
  3. # Copyright (c) 2019 Clementine Computing LLC.
  4. #
  5. # This file is part of PopuFare.
  6. #
  7. # PopuFare is free software: you can redistribute it and/or modify
  8. # it under the terms of the GNU Affero General Public License as published by
  9. # the Free Software Foundation, either version 3 of the License, or
  10. # (at your option) any later version.
  11. #
  12. # PopuFare is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU Affero General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU Affero General Public License
  18. # along with PopuFare. If not, see <https://www.gnu.org/licenses/>.
  19. #
  20. require 5.002;
  21. use strict;
  22. use Socket;
  23. use Switch;
  24. use Carp;
  25. use DBI;
  26. use FileHandle;
  27. use Fcntl;
  28. use Compress::Zlib;
  29. use POSIX;
  30. my $DATADIR = $ENV{'HOME'} . "/data";
  31. #my $database_path = 'DBI:mysql:busdb';
  32. #my $database_path = 'DBI:SQLite:dbname=../bus.sqlite';
  33. my $database_path = 'DBI:SQLite:dbname=' . $DATADIR . '/bus.sqlite';
  34. my $database_user = '';
  35. my $database_pass = '';
  36. my $bind_ip = '127.0.0.1';
  37. my $bind_port = 7277;
  38. #----------------------------------------------Ugly exception handling logic using closures and anonymous functions----
  39. #-------------------------------------------This is in there to deal with the fact that CreditCall uses the die("error")
  40. #-------------------------------------------function instead of returning an error message in many cases...
  41. # This utility function returns the passed string sans any leading or trailing whitespace.
  42. #
  43. sub strip_whitespace
  44. {
  45. my $str = shift; #grab our first parameter
  46. $str =~ s/^\s+//; #strip leading whitespace
  47. $str =~ s/\s+$//; #strip trailing whitespace
  48. return $str; #return the improved string
  49. }
  50. # This function takes two coderef parameters, the second of which is usually an explicit call to the
  51. # 'catch' function which itself takes a coderef parameter. This allows the code employing this suite of
  52. # functions to look somewhat like a conventional exception handling mechanism:
  53. #
  54. # try
  55. # {
  56. # do_something_that_might_die();
  57. # }
  58. # catch
  59. # {
  60. # my $errmsg = $_;
  61. # log_the_error_message($errmsg);
  62. # perform_some_cleanup();
  63. # };
  64. #
  65. # DO NOT FORGET THAT LAST SEMICOLON, EVERYTHING GOES TO HELL IF YOU DO!
  66. #
  67. sub try(&$)
  68. {
  69. my ($attempt, $handler) = @_;
  70. eval
  71. {
  72. &$attempt;
  73. };
  74. if($@)
  75. {
  76. do_catch($handler);
  77. }
  78. }
  79. # This function strips off the whitespace from the exception message reported by die()
  80. # and places the result into the default variable such that the code in the catch block can
  81. # just examine $_ to figure out what the cause of the error is, or to display or log
  82. # the error message.
  83. #
  84. sub do_catch(&$)
  85. {
  86. my ($handler) = @_;
  87. local $_ = strip_whitespace($@);
  88. &$handler;
  89. }
  90. # This just takes an explicit coderef and returns it unharmed. The only
  91. # purpose of this is so the try/catch structure looks pretty and familiar.
  92. #
  93. sub catch(&) {$_[0]}
  94. #--------------------------------------------------------------------------------------------------------------------
  95. #my $DebugMode = 1;
  96. my $DebugMode = 0;
  97. # This function only executes the passed code reference if the global variable $DebugMode is non-zero.
  98. # The reason for this is that any calculation (like a FooBar::ComplexObject->toString call) will not be
  99. # performed if we are not in debug mode, sort of like a very limited form of lazy evaluation.
  100. #
  101. sub ifdebug(&@)
  102. {
  103. my ($cmd) = @_;
  104. &$cmd() if($DebugMode);
  105. }
  106. sub ServerReply
  107. {
  108. my $client_query = $_[0];
  109. chomp($client_query);
  110. my $response = "";
  111. my $hangup_flag=0;
  112. #Turning this on will use FLUSH instead of ZFLUSH, which is much slower
  113. my $do_legacy_flush = 0;
  114. switch ($client_query)
  115. {
  116. case /^QUERY\t[0-9][0-9]*$/
  117. {
  118. my $sequence_number = $client_query;
  119. $sequence_number =~ s/^QUERY\t//;
  120. my $dbh = DBI->connect($database_path, $database_user, $database_pass)
  121. or die "Couldn't connect to database: " . DBI->errstr;
  122. #A query to check for the validity of the queried sequence number
  123. my $seqcheck = $dbh->prepare('SELECT seq_num FROM active_rider_table WHERE seq_num = ?') or die "Couldn't prepare statement: " . $dbh->errstr;
  124. #Prepare to send records
  125. 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 = ' .
  126. '(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')
  127. or die "Couldn't prepare statement: " . $dbh->errstr;
  128. $seqcheck->execute($sequence_number);
  129. #Check if the client is on the same page as us
  130. #if not, tell them to flush everything and send it all again
  131. my $flushdata = 0;
  132. if ($sequence_number == 0)
  133. {
  134. $flushdata = 1;
  135. $sth->execute($sequence_number) # Execute the query
  136. or die "Couldn't execute statement: " . $sth->errstr;
  137. }
  138. elsif (!$seqcheck->fetchrow_array())
  139. {
  140. $sth->execute(0) # Get everything
  141. or die "Couldn't execute statement: " . $sth->errstr;
  142. $flushdata = 1;
  143. }
  144. else
  145. {
  146. $sth->execute($sequence_number) # Execute the query
  147. or die "Couldn't execute statement: " . $sth->errstr;
  148. }
  149. # Read the matching records and print them out
  150. # $data[0] = deleted
  151. # $data[1] = seq_num
  152. # $data[2] = logical_card_id
  153. # $data[3] = mag_token
  154. # $data[4] = rfid_token
  155. # $data[5] = rule_name
  156. # $data[6] = rule_param
  157. my @data ;
  158. #If we are doing a flush
  159. if($flushdata)
  160. {
  161. if($do_legacy_flush)
  162. {
  163. $response .= "FLUSH\n" if $flushdata;
  164. while (@data = $sth->fetchrow_array())
  165. {
  166. if (!$data[0])
  167. {
  168. $data[3] = "" unless defined $data[3]; #populate any NULL mag_token with ""
  169. $data[4] = "" unless defined $data[4]; #populate any NULL rfid_token with ""
  170. $data[6] = "" unless defined $data[6]; #populate any NULL rule_param with ""
  171. $response .= "UPDATE\t$data[1]\t$data[2]\t$data[3]\t$data[4]\t$data[5]\t$data[6]\n";
  172. }
  173. }
  174. $response .= "FLUSHDONE\n" if $flushdata;
  175. }
  176. else
  177. {
  178. my $z = deflateInit( -Level => Z_BEST_COMPRESSION ) or die "Cannot create a deflation stream\n";
  179. my $size = 0;
  180. my $dat = "";
  181. my ($zout, $stat);
  182. my $cmpdat;
  183. while (@data = $sth->fetchrow_array())
  184. {
  185. if (!$data[0])
  186. {
  187. $data[3] = "" unless defined $data[3]; #populate any NULL mag_token with ""
  188. $data[4] = "" unless defined $data[4]; #populate any NULL rfid_token with ""
  189. $data[6] = "" unless defined $data[6]; #populate any NULL rule_param with ""
  190. $dat .= "UPDATE\t$data[1]\t$data[2]\t$data[3]\t$data[4]\t$data[5]\t$data[6]\n";
  191. }
  192. }
  193. ($zout, $stat) = $z->deflate($dat);
  194. $stat == Z_OK or die "deflation failed...";
  195. $cmpdat = $zout;
  196. ($zout, $stat) = $z->flush();
  197. $stat == Z_OK or die "deflation failed...";
  198. $cmpdat .= $zout;
  199. $size = $z->total_out();
  200. $response .= "ZFLUSH\t$size\n";
  201. $response .= $cmpdat;
  202. $response .= "ZFLUSHDONE\n";
  203. #Set the "HANG-UP" flag to make the server hang up on a client who has just done a ZFLUSH
  204. #so that the client will start a fresh server session with its shiny new database
  205. $hangup_flag = 1;
  206. }
  207. }
  208. else
  209. {
  210. while (@data = $sth->fetchrow_array())
  211. {
  212. if ($data[0])
  213. {
  214. $response .= "DELETE\t$data[1]\t$data[2]\n";
  215. } else
  216. {
  217. $data[3] = "" unless defined $data[3]; #populate any NULL mag_token with ""
  218. $data[4] = "" unless defined $data[4]; #populate any NULL rfid_token with ""
  219. $data[6] = "" unless defined $data[6]; #populate any NULL rule_param with ""
  220. $response .= "UPDATE\t$data[1]\t$data[2]\t$data[3]\t$data[4]\t$data[5]\t$data[6]\n";
  221. }
  222. }
  223. }
  224. $seqcheck->finish;
  225. $sth->finish;
  226. $dbh->disconnect;
  227. }
  228. else
  229. {
  230. $response = "ERROR\n" . $client_query;
  231. }
  232. }
  233. if($response eq "")
  234. {
  235. $response .= "NOP\n";
  236. }
  237. return ($response, $hangup_flag);
  238. }
  239. sub handle_client()
  240. {
  241. close SERVER;
  242. CLIENT->autoflush(1);
  243. my $linebuffer;
  244. while($linebuffer = <CLIENT>)
  245. {
  246. ## DEBUG
  247. print "## buspass: $linebuffer\n";
  248. my ($reply, $hangup_flag) = ServerReply($linebuffer);
  249. print CLIENT $reply;
  250. if($hangup_flag)
  251. {
  252. sleep(60);
  253. shutdown(CLIENT, 2);
  254. close CLIENT;
  255. return 0;
  256. }
  257. } #while data from client
  258. close CLIENT;
  259. }
  260. my $waitedpid = 0;
  261. my $sigreceived = 0;
  262. sub REAPER
  263. {
  264. while (($waitedpid = waitpid(-1, WNOHANG))>0) { }
  265. $SIG{CHLD} = \&REAPER; # loathe sysV
  266. $sigreceived = 1;
  267. }
  268. sub spawn
  269. {
  270. my $coderef = shift; #grab the first parameter
  271. unless (@_ == 0 && $coderef && ref($coderef) eq 'CODE') #verify that it consists of a non-null block of executable perl code
  272. {
  273. confess "usage: spawn CODEREF"; #complain if this is not the case
  274. }
  275. my $pid;
  276. if (!defined($pid = fork)) #attempt a fork, remembering the returned PID value
  277. {
  278. close CLIENT;
  279. return; #failed to fork, we'd better close the client
  280. }
  281. elsif ($pid) #If the returned process ID is non-zero, that indicates that we are the parent process
  282. {
  283. return; # i'm the parent
  284. }
  285. else #otherwise, if the returned process ID is 0, that means we're the child process
  286. {
  287. exit &$coderef(); #in which case, we want to execute the child handler that was passed in, and then
  288. #exit this (child) process when we've finished our conversation(s) with the
  289. #other (client) end of the socket.
  290. }
  291. }
  292. #----------------------------------------------------------------------
  293. # Local network settings for Inter-Process communication.
  294. #----------------------------------------------------------------------
  295. my $proto = getprotobyname('tcp');
  296. my $addr = sockaddr_in( $bind_port ,inet_aton($bind_ip));;
  297. #----------------------------------------------------------------------
  298. my $max_retries = 10; #Maximum number of address-binding retries before we give up.
  299. my $retry_count = $max_retries; #number of retries left...
  300. my $retry_delay = 3; #number of seconds to wait between retries at binding to our designated IPC address
  301. my $got_network = 0; #flag to let us know that we can quit retrying once we have gotten a valid listening socket
  302. while( ($retry_count > 0) && (!$got_network) )
  303. {
  304. try #Try and allocate a socket, bind it to our IPC address, and set it to listen for connections
  305. {
  306. socket(SERVER,PF_INET,SOCK_STREAM,$proto) || die "socket: $!";
  307. setsockopt(SERVER, SOL_SOCKET, SO_REUSEADDR, 1);
  308. bind (SERVER, $addr) || die "bind: $!";
  309. listen(SERVER,5) || die "listen: $!";
  310. $got_network = 1;
  311. }
  312. catch #If that didn't work for some reason, log the error, clean up, and prepair to retry
  313. {
  314. my $errmsg = $_; #Remember the error message
  315. close(SERVER); #Clean up the server socket if it needs it
  316. #Decrement our remaining retry counter
  317. $retry_count = $retry_count - 1;
  318. #Log the message to our debug log
  319. print "Failed to allocate socket, will retry $retry_count times: $errmsg\n";
  320. #Wait a reasonable period before trying again
  321. sleep $retry_delay;
  322. };
  323. }
  324. if($got_network) #If we met with success binding to the network, report it
  325. {
  326. my $logmsg = "Socket setup successful. Listening for clients at $bind_ip:$bind_port\n";
  327. print $logmsg;
  328. }
  329. else #If we ran out of patience and gave up, report that as well and exit
  330. {
  331. my $errmsg = "Could not allocate and bind listening socket at $bind_ip:$bind_port after $max_retries attempts.\n";
  332. die $errmsg;
  333. }
  334. # Set up our signal handler which will clean up defunct child processes and let the main
  335. # accept() loop know that the reason accept returned was due to a signal, not a legit connection.
  336. $SIG{CHLD} = \&REAPER;
  337. #This for loop is efficient, but confusting, so I'll break it down by clause
  338. #
  339. # The first clause ($sigreceived = 0) clears the signal received flag that will be set if the
  340. # accept() call was interrupted by a signal. This clause runs once before the first run of the loop
  341. #
  342. # The second clause is the test clause, it will process the contents of the loop if EITHER
  343. # accept() has returned (presumably generating a valid file handle for the CLIENT end of the
  344. # socket, OR the signal received flag is set (thus accept would have returned early without
  345. # having actually accepted a connection.
  346. #
  347. # The third clause (the 'incrementer') is run after each time the body is executed, before the
  348. # test clause is executed again (deciding whether to run the body or drop out... This test
  349. # clause will close the parent process' copy of the CLIENT file handle since (see body below)
  350. # after the body executes, all communication with the socket referred to by that file handle
  351. # will be carried out by the spawned child process. This frees the parent's copy of the CLIENT
  352. # file handle to be used again in the parent process for the next accepted incoming connection.
  353. for ( $sigreceived = 0; accept(CLIENT,SERVER) || $sigreceived; $sigreceived = 0, close CLIENT)
  354. {
  355. 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
  356. print "connection received.\n"; #Print a diagnostic message confirming that we have made a connection
  357. spawn sub {handle_client();}; #fork() off a child process that will handle communication with the socket pointed to by the CLIENT file handle
  358. }