From 336e3c1cabef46d5825bb0540a6715dff86450e7 Mon Sep 17 00:00:00 2001 From: Charlie Date: Sat, 13 Jun 2009 17:24:28 +0000 Subject: Port voucher login ability on CaptivePortal from M0n0Wall. Various locking fixes are done with the import and this means that as of now pfSense has a better performin/behaving CP than m0n0wall. --- etc/inc/voucher.inc | 406 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 406 insertions(+) create mode 100644 etc/inc/voucher.inc (limited to 'etc/inc/voucher.inc') diff --git a/etc/inc/voucher.inc b/etc/inc/voucher.inc new file mode 100644 index 0000000..34f4f77 --- /dev/null +++ b/etc/inc/voucher.inc @@ -0,0 +1,406 @@ +. + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + +*/ + +/* include all configuration functions */ +require_once("config.inc"); + +/* + *Authenticate a voucher and return the remaining time credit in minutes + * if $test is set, don't mark the voucher as used nor add it to the list + * of active vouchers + */ +function voucher_auth($voucher_received, $test = 0) { + + global $g, $config, $dirtyfile; + + // if $test is set, simply test the voucher. Don't change anything + // but return a more verbose error and result message back + + if (! $test) + $voucherlck = lock('voucher'); + + // read rolls into assoc array with rollid as key and minutes as value + $a_roll = &$config['voucher']['roll']; + foreach ($a_roll as $rollent) { + $tickets_per_roll[$rollent['number']] = $rollent['count']; + $minutes_per_roll[$rollent['number']] = $rollent['minutes']; + } + + // split into an array. Useful for multiple vouchers given + $a_vouchers_received = split("[\t\n\r ]+",$voucher_received); + $error = 0; + $test_result = array(); // used to display for voucher test option in GUI + $total_minutes = 0; + $first_voucher = ""; + $first_voucher_roll = 0; + + // go through all received vouchers, check their valid and extract + // Roll# and Ticket# using the external readvoucher binary + + foreach ($a_vouchers_received as $voucher) { + + $v = escapeshellarg($voucher); + if (strlen($voucher) < 3) + continue; // seems too short to be a voucher! + + $result = exec("/usr/local/bin/voucher -c {$g['varetc_path']}/voucher.cfg -k {$g['varetc_path']}/voucher.public -- $v"); + list($status, $roll, $nr) = explode(" ", $result); + if ($status == "OK") { + if (!$first_voucher) + { + $first_voucher = $voucher; // store first voucher. Thats the one we give the timecredit + $first_voucher_roll = $roll; + } + // check if we have this ticket on a registered roll for this ticket + if ($tickets_per_roll[$roll] && ($nr <= $tickets_per_roll[$roll])) { + // voucher is from a registered roll. + if (!isset($active_vouchers[$roll])) + $active_vouchers[$roll] = voucher_read_active_db($roll); + // valid voucher. Store roll# and ticket# + if ($line = $active_vouchers[$roll][$voucher]) { + list($timestamp,$minutes) = explode(",", $line); + // we have an already active voucher here. + $remaining = intval((($timestamp + 60*$minutes) - time())/60); + $test_result[] = "$voucher ($roll/$nr) active and good for $remaining Minutes"; + $total_minutes += $remaining; + } else { + // voucher not used. Check if ticket Id is on the roll (not too high) + // and if the ticket is marked used. + // check if voucher already marked as used + if (!isset($bitstring[$roll])) + $bitstring[$roll] = voucher_read_used_db($roll); + $pos = $nr >> 3; // divide by 8 -> octet + $mask = 1 << ($nr % 8); + if (ord($bitstring[$roll][$pos]) & $mask) { + $test_result[] = "$voucher ($roll/$nr) already used and expired"; + $total_minutes = -1; // voucher expired + $error++; + } else { + // mark bit for this voucher as used + $bitstring[$roll][$pos] = chr(ord($bitstring[$roll][$pos]) | $mask); + $test_result[] = "$voucher ($roll/$nr) good for {$minutes_per_roll[$roll]} Minutes"; + $total_minutes += $minutes_per_roll[$roll]; + } + } + } else { + $test_result[] = "$voucher ($roll/$nr): not found on any registererd Roll"; + } + } else { + // hmm, thats weired ... not what I expected + $test_result[] = "$voucher invalid: $result !!"; + $error++; + } + } + + // if this was a test call, we're done. Return the result. + if ($test) { + if ($error) { + $test_result[] = "Access denied!"; + } else { + $test_result[] = "Access granted for $total_minutes Minutes in total."; + } + unlock($voucherlck); + return $test_result; + } + + // if we had an error (one of the vouchers is invalid), return 0. + // Discussion: we could return the time remaining for good vouchers, but then + // the user wouldn't know that he used at least one invalid voucher. + + if ($error) { + unlock($voucherlck); + if ($total_minutes > 0) // probably not needed, but want to make sure + $total_minutes = 0; // we only report -1 (expired) or 0 (no access) + return $total_minutes; // well, at least one voucher had errors. Say NO ACCESS + } + + // All given vouchers were valid and this isn't simply a test. + // Write back the used DB's + + if (is_array($bitstring)) + foreach ($bitstring as $roll => $used) + voucher_write_used_db($roll, base64_encode($used)); + + // Active DB: we only add the first voucher if multiple given + // and give that one all the time credit. This allows the user to logout and + // log in later using just the first voucher. It also keeps username limited + // to one voucher and that voucher shows the correct time credit in 'active vouchers' + + if ($line = $active_vouchers[$first_voucher_roll][$first_voucher]) { + list($timestamp, $minutes) = explode(",", $line); + } else { + $timestamp = time(); // new voucher + $minutes = $total_minutes; + } + + $active_vouchers[$first_voucher_roll][$first_voucher] = "$timestamp,$minutes"; + voucher_write_active_db($roll, $active_vouchers[$first_voucher_roll]); + + // mark the DB's as dirty. + if ($fd = fopen($dirtyfile, "w")) + fclose($fd); + + unlock($voucherlck); + + return $total_minutes; +} + +function voucher_configure() { + global $config, $g; + + /* kill any running minicron */ + killbypid("{$g['varrun_path']}/vouchercron.pid"); + + if (isset($config['voucher']['enable'])) { + + if ($g['booting']) { + echo "Enabling voucher support... "; + } + + // start cron if we're asked to save runtime DB periodically + // to XML config if it changed + $croninterval = $config['voucher']['saveinterval'] * 60; // need seconds. Config has minutes + if ($croninterval) { + /* start pruning process (interval defaults to 60 seconds) */ + mwexec("/usr/local/bin/minicron $croninterval {$g['varrun_path']}/vouchercron.pid " . + "/etc/rc.savevoucher"); + } + + $voucherlck = lock('voucher'); + /* write public key used to verify vouchers */ + $pubkey = base64_decode($config['voucher']['publickey']); + $fd = fopen("{$g['varetc_path']}/voucher.public", "w"); + if (!$fd) { + printf("Error: cannot write voucher.public\n"); + unlock($voucherlck); + return 1; + } + chmod("{$g['varetc_path']}/voucher.public", 0600); + fwrite($fd, $pubkey); + fclose($fd); + + /* write config file used by voucher binary to decode vouchers */ + $fd = fopen("{$g['varetc_path']}/voucher.cfg", "w"); + if (!$fd) { + printf("Error: cannot write voucher.cfg\n"); + unlock($voucherlck); + return 1; + } + chmod("{$g['varetc_path']}/voucher.cfg", 0600); + fwrite($fd, "{$config['voucher']['rollbits']},{$config['voucher']['ticketbits']},{$config['voucher']['checksumbits']},{$config['voucher']['magic']},{$config['voucher']['charset']}\n"); + fclose($fd); + unlock($voucherlck); + + if ($g['booting']) { + + // create active and used DB per roll on ramdisk from config + $a_roll = &$config['voucher']['roll']; + $voucherlck = lock('voucher'); + + foreach ($a_roll as $rollent) { + + $roll = $rollent['number']; + voucher_write_used_db($roll, $rollent['used']); + $minutes = $rollent['minutes']; + $active_vouchers = array(); + $a_active = &$rollent['active']; + if (is_array($a_active)) { + foreach ($a_active as $activent) { + $voucher = $activent['voucher']; + $timestamp = $activent['timestamp']; + $minutes = $activent['minutes']; + // its tempting to check for expired timestamps, but during + // bootup, we most likely don't have the correct time time. + $active_vouchers[$voucher] = "$timestamp,$minutes"; + } + } + voucher_write_active_db($roll, $active_vouchers); + } + + unlock($voucherlck); + echo "done\n"; + } + } + return 0; +} + +/* write bitstring of used vouchers to ramdisk. + * Bitstring must already be base64_encoded! + */ +function voucher_write_used_db($roll, $vdb) { + + global $g; + + $fd = fopen("{$g['vardb_path']}/voucher_used_$roll.db", "w"); + if ($fd) { + fwrite($fd, $vdb . "\n"); + fclose($fd); + } else { + voucher_log(LOG_ERR, "cant write {$g['vardb_path']}/voucher_used_$roll.db"); + } +} + +/* return assoc array of active vouchers with activation timestamp + * voucher is index. + */ +function voucher_read_active_db($roll) { + + global $g; + + $active = array(); + $dirty = 0; + $file = "{$g['vardb_path']}/voucher_active_$roll.db"; + if (file_exists($file)) { + $fd = fopen($file, "r"); + if ($fd) { + while (!feof($fd)) { + $line = trim(fgets($fd)); + if ($line) { + list($voucher,$timestamp,$minutes) = explode(",", $line); // voucher,timestamp + if ((($timestamp + 60*$minutes) - time()) > 0) { + $active[$voucher] = "$timestamp,$minutes"; + } else { + $dirty=1; + } + } + } + fclose($fd); + if ($dirty) // if we found expired entries, lets save our snapshot + voucher_write_active_db($roll, $active); + } + } + return $active; +} + +/* store array of active vouchers back to DB */ +function voucher_write_active_db($roll, $active) { + + global $g; + + $fd = fopen("{$g['vardb_path']}/voucher_active_$roll.db", "w"); + if ($fd) { + foreach($active as $voucher => $value) + fwrite($fd, "$voucher,$value\n"); + fclose($fd); + } +} + +/* return how many vouchers are marked used on a roll */ +function voucher_used_count($roll) { + + global $g; + + $bitstring = voucher_read_used_db($roll); + $max = strlen($bitstring) * 8; + $used = 0; + for ($i = 1; $i <= $max; $i++) { + // check if ticket already used or not. + $pos = $i >> 3; // divide by 8 -> octet + $mask = 1 << (($i % 8)-1); // mask to test bit in octet + if (ord($bitstring[$pos]) & $mask) + $used++; + } + return $used; +} + +function voucher_read_used_db($roll) { + + global $g; + + $vdb = ""; + $file = "{$g['vardb_path']}/voucher_used_$roll.db"; + if (file_exists($file)) { + $fd = fopen($file, "r"); + if ($fd) { + $vdb = trim(fgets($fd)); + fclose($fd); + } else { + voucher_log(LOG_ERR, "cant read {$g['vardb_path']}/voucher_used_$roll.db"); + } + } + return base64_decode($vdb); +} + +function voucher_unlink_db($roll) { + + global $g; + unlink("{$g['vardb_path']}/voucher_used_$roll.db"); + unlink("{$g['vardb_path']}/voucher_active_$roll.db"); +} + +/* we share the log with captiveportal for now */ +function voucher_log($priority, $message) { + + define_syslog_variables(); + $message = trim($message); + openlog("logportalauth", LOG_PID, LOG_LOCAL4); + syslog($priority, "Voucher: " . $message); + closelog(); +} + +/* Save active and used voucher DB into XML config and write it to flash + * Called during reboot -> system_reboot_cleanup() and minicron + */ +function voucher_save_db_to_config() { + + global $config, $g, $dirtyfile; + + if (!isset($config['voucher']['enable']) || $config['voucher']['saveinterval'] == 0) + return; // no vouchers or don't want to save DB's + + if (!file_exists($dirtyfile)) + return; // nothing changed. + + $voucherlck = lock('voucher'); + + // walk all active rolls and save runtime DB's to flash + $a_roll = &$config['voucher']['roll']; +// foreach ($a_roll as $rollent) { + while (list($key, $value) = each($a_roll)) { + $rollent = &$a_roll[$key]; + $roll = $rollent['number']; + $bitmask = voucher_read_used_db($roll); + $rollent['used'] = base64_encode($bitmask); + $active_vouchers = voucher_read_active_db($roll); + $db = array(); + foreach($active_vouchers as $voucher => $line) { + list($timestamp,$minutes) = explode(",", $line); + $activent['voucher'] = $voucher; + $activent['timestamp'] = $timestamp; + $activent['minutes'] = $minutes; + $db[] = $activent; + } + $rollent['active'] = $db; + } + unlock($voucherlck); + unlink($dirtyfile); + write_config(); + return; +} + +?> -- cgit v1.1