diff options
Diffstat (limited to 'src/etc/inc/captiveportal.inc')
-rw-r--r-- | src/etc/inc/captiveportal.inc | 2409 |
1 files changed, 2409 insertions, 0 deletions
diff --git a/src/etc/inc/captiveportal.inc b/src/etc/inc/captiveportal.inc new file mode 100644 index 0000000..bd294e4 --- /dev/null +++ b/src/etc/inc/captiveportal.inc @@ -0,0 +1,2409 @@ +<?php +/* + captiveportal.inc + part of pfSense (https://www.pfsense.org) + Copyright (C) 2004-2011 Scott Ullrich <sullrich@gmail.com> + Copyright (C) 2009-2012 Ermal Luçi <eri@pfsense.org> + Copyright (C) 2003-2006 Manuel Kasper <mk@neon1.net>. + + originally part of m0n0wall (http://m0n0.ch/wall) + 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. + + This version of captiveportal.inc has been modified by Rob Parker + <rob.parker@keycom.co.uk> to include changes for per-user bandwidth management + via returned RADIUS attributes. This page has been modified to delete any + added rules which may have been created by other per-user code (index.php, etc). + These changes are (c) 2004 Keycom PLC. + + pfSense_BUILDER_BINARIES: /sbin/ipfw /sbin/route + pfSense_BUILDER_BINARIES: /usr/local/sbin/lighttpd /usr/local/bin/minicron /sbin/pfctl + pfSense_BUILDER_BINARIES: /bin/hostname /bin/cp + pfSense_MODULE: captiveportal +*/ + +/* include all configuration functions */ +require_once("config.inc"); +require_once("functions.inc"); +require_once("filter.inc"); +require_once("radius.inc"); +require_once("voucher.inc"); + +function get_default_captive_portal_html() { + global $config, $g, $cpzone; + + $htmltext = <<<EOD +<html> +<body> +<form method="post" action="\$PORTAL_ACTION\$"> + <input name="redirurl" type="hidden" value="\$PORTAL_REDIRURL\$"> + <input name="zone" type="hidden" value="\$PORTAL_ZONE\$"> + <center> + <table cellpadding="6" cellspacing="0" width="550" height="380" style="border:1px solid #000000"> + <tr height="10" bgcolor="#990000"> + <td style="border-bottom:1px solid #000000"> + <font color='white'> + <b> + {$g['product_name']} captive portal + </b> + </font> + </td> + </tr> + <tr> + <td> + <div id="mainlevel"> + <center> + <table width="100%" border="0" cellpadding="5" cellspacing="0"> + <tr> + <td> + <center> + <div id="mainarea"> + <center> + <table width="100%" border="0" cellpadding="5" cellspacing="5"> + <tr> + <td> + <div id="maindivarea"> + <center> + <div id='statusbox'> + <font color='red' face='arial' size='+1'> + <b> + \$PORTAL_MESSAGE\$ + </b> + </font> + </div> + <br /> + <div id='loginbox'> + <table> + <tr><td colspan="2"><center>Welcome to the {$g['product_name']} Captive Portal!</td></tr> + <tr><td> </td></tr> + <tr><td align="right">Username:</td><td><input name="auth_user" type="text" style="border: 1px dashed;"></td></tr> + <tr><td align="right">Password:</td><td><input name="auth_pass" type="password" style="border: 1px dashed;"></td></tr> + <tr><td> </td></tr> + +EOD; + + if (isset($config['voucher'][$cpzone]['enable'])) { + $htmltext .= <<<EOD + <tr> + <td align="right">Enter Voucher Code: </td> + <td><input name="auth_voucher" type="text" style="border:1px dashed;" size="22"></td> + </tr> + +EOD; + } + + $htmltext .= <<<EOD + <tr> + <td colspan="2"><center><input name="accept" type="submit" value="Continue"></center></td> + </tr> + </table> + </div> + </center> + </div> + </td> + </tr> + </table> + </center> + </div> + </center> + </td> + </tr> + </table> + </center> + </div> + </td> + </tr> + </table> + </center> +</form> +</body> +</html> + +EOD; + + return $htmltext; +} + +function captiveportal_load_modules() { + global $config; + + mute_kernel_msgs(); + if (!is_module_loaded("ipfw.ko")) { + mwexec("/sbin/kldload ipfw"); + /* make sure ipfw is not on pfil hooks */ + set_sysctl(array( + "net.inet.ip.pfil.inbound" => "pf", "net.inet6.ip6.pfil.inbound" => "pf", + "net.inet.ip.pfil.outbound" => "pf", "net.inet6.ip6.pfil.outbound" => "pf") + ); + } + /* Activate layer2 filtering */ + set_sysctl(array("net.link.ether.ipfw" => "1", "net.inet.ip.fw.one_pass" => "1")); + + /* Always load dummynet now that even allowed ip and mac passthrough use it. */ + if (!is_module_loaded("dummynet.ko")) { + mwexec("/sbin/kldload dummynet"); + set_sysctl(array("net.inet.ip.dummynet.io_fast" => "1", "net.inet.ip.dummynet.hash_size" => "256")); + } + unmute_kernel_msgs(); +} + +function captiveportal_configure() { + global $config, $cpzone, $cpzoneid; + + if (is_array($config['captiveportal'])) { + foreach ($config['captiveportal'] as $cpkey => $cp) { + $cpzone = $cpkey; + $cpzoneid = $cp['zoneid']; + captiveportal_configure_zone($cp); + } + } +} + +function captiveportal_configure_zone($cpcfg) { + global $config, $g, $cpzone, $cpzoneid; + + $captiveportallck = lock("captiveportal{$cpzone}", LOCK_EX); + + if (isset($cpcfg['enable'])) { + + if (platform_booting()) { + echo "Starting captive portal({$cpcfg['zone']})... "; + + /* remove old information */ + unlink_if_exists("{$g['vardb_path']}/captiveportal{$cpzone}.db"); + } else { + captiveportal_syslog("Reconfiguring captive portal({$cpcfg['zone']})."); + } + + /* init ipfw rules */ + captiveportal_init_rules(true); + + /* kill any running minicron */ + killbypid("{$g['varrun_path']}/cp_prunedb_{$cpzone}.pid"); + + /* initialize minicron interval value */ + $croninterval = $cpcfg['croninterval'] ? $cpcfg['croninterval'] : 60; + + /* double check if the $croninterval is numeric and at least 10 seconds. If not we set it to 60 to avoid problems */ + if ((!is_numeric($croninterval)) || ($croninterval < 10)) { + $croninterval = 60; + } + + /* write portal page */ + if (is_array($cpcfg['page']) && $cpcfg['page']['htmltext']) { + $htmltext = base64_decode($cpcfg['page']['htmltext']); + } else { + /* example/template page */ + $htmltext = get_default_captive_portal_html(); + } + + $fd = @fopen("{$g['varetc_path']}/captiveportal_{$cpzone}.html", "w"); + if ($fd) { + // Special case handling. Convert so that we can pass this page + // through the PHP interpreter later without clobbering the vars. + $htmltext = str_replace("\$PORTAL_ZONE\$", "#PORTAL_ZONE#", $htmltext); + $htmltext = str_replace("\$PORTAL_REDIRURL\$", "#PORTAL_REDIRURL#", $htmltext); + $htmltext = str_replace("\$PORTAL_MESSAGE\$", "#PORTAL_MESSAGE#", $htmltext); + $htmltext = str_replace("\$CLIENT_MAC\$", "#CLIENT_MAC#", $htmltext); + $htmltext = str_replace("\$CLIENT_IP\$", "#CLIENT_IP#", $htmltext); + $htmltext = str_replace("\$ORIGINAL_PORTAL_IP\$", "#ORIGINAL_PORTAL_IP#", $htmltext); + $htmltext = str_replace("\$PORTAL_ACTION\$", "#PORTAL_ACTION#", $htmltext); + if ($cpcfg['preauthurl']) { + $htmltext = str_replace("\$PORTAL_REDIRURL\$", "{$cpcfg['preauthurl']}", $htmltext); + $htmltext = str_replace("#PORTAL_REDIRURL#", "{$cpcfg['preauthurl']}", $htmltext); + } + fwrite($fd, $htmltext); + fclose($fd); + } + unset($htmltext); + + /* write error page */ + if (is_array($cpcfg['page']) && $cpcfg['page']['errtext']) { + $errtext = base64_decode($cpcfg['page']['errtext']); + } else { + /* example page */ + $errtext = get_default_captive_portal_html(); + } + + $fd = @fopen("{$g['varetc_path']}/captiveportal-{$cpzone}-error.html", "w"); + if ($fd) { + // Special case handling. Convert so that we can pass this page + // through the PHP interpreter later without clobbering the vars. + $errtext = str_replace("\$PORTAL_ZONE\$", "#PORTAL_ZONE#", $errtext); + $errtext = str_replace("\$PORTAL_REDIRURL\$", "#PORTAL_REDIRURL#", $errtext); + $errtext = str_replace("\$PORTAL_MESSAGE\$", "#PORTAL_MESSAGE#", $errtext); + $errtext = str_replace("\$CLIENT_MAC\$", "#CLIENT_MAC#", $errtext); + $errtext = str_replace("\$CLIENT_IP\$", "#CLIENT_IP#", $errtext); + $errtext = str_replace("\$ORIGINAL_PORTAL_IP\$", "#ORIGINAL_PORTAL_IP#", $errtext); + $errtext = str_replace("\$PORTAL_ACTION\$", "#PORTAL_ACTION#", $errtext); + if ($cpcfg['preauthurl']) { + $errtext = str_replace("\$PORTAL_REDIRURL\$", "{$cpcfg['preauthurl']}", $errtext); + $errtext = str_replace("#PORTAL_REDIRURL#", "{$cpcfg['preauthurl']}", $errtext); + } + fwrite($fd, $errtext); + fclose($fd); + } + unset($errtext); + + /* write logout page */ + if (is_array($cpcfg['page']) && $cpcfg['page']['logouttext']) { + $logouttext = base64_decode($cpcfg['page']['logouttext']); + } else { + /* example page */ + $logouttext = <<<EOD +<html> +<head><title>Redirecting...</title></head> +<body> +<span style="font-family: Tahoma, Verdana, Arial, Helvetica, sans-serif; font-size: 11px;"> +<b>Redirecting to <a href="<?=\$my_redirurl;?>"><?=\$my_redirurl;?></a>...</b> +</span> +<script type="text/javascript"> +//<![CDATA[ +LogoutWin = window.open('', 'Logout', 'toolbar=0,scrollbars=0,location=0,statusbar=0,menubar=0,resizable=0,width=256,height=64'); +if (LogoutWin) { + LogoutWin.document.write('<html>'); + LogoutWin.document.write('<head><title>Logout</title></head>') ; + LogoutWin.document.write('<body bgcolor="#435370">'); + LogoutWin.document.write('<div align="center" style="color: #ffffff; font-family: Tahoma, Verdana, Arial, Helvetica, sans-serif; font-size: 11px;">') ; + LogoutWin.document.write('<b>Click the button below to disconnect</b><p />'); + LogoutWin.document.write('<form method="POST" action="<?=\$logouturl;?>">'); + LogoutWin.document.write('<input name="logout_id" type="hidden" value="<?=\$sessionid;?>" />'); + LogoutWin.document.write('<input name="zone" type="hidden" value="<?=\$cpzone;?>" />'); + LogoutWin.document.write('<input name="logout" type="submit" value="Logout" />'); + LogoutWin.document.write('</form>'); + LogoutWin.document.write('</div></body>'); + LogoutWin.document.write('</html>'); + LogoutWin.document.close(); +} + +document.location.href="<?=\$my_redirurl;?>"; +//]]> +</script> +</body> +</html> + +EOD; + } + + $fd = @fopen("{$g['varetc_path']}/captiveportal-{$cpzone}-logout.html", "w"); + if ($fd) { + fwrite($fd, $logouttext); + fclose($fd); + } + unset($logouttext); + + /* write elements */ + captiveportal_write_elements(); + + /* kill any running mini_httpd */ + killbypid("{$g['varrun_path']}/lighty-{$cpzone}-CaptivePortal.pid"); + killbypid("{$g['varrun_path']}/lighty-{$cpzone}-CaptivePortal-SSL.pid"); + + /* start up the webserving daemon */ + captiveportal_init_webgui_zone($cpcfg); + + /* Kill any existing prunecaptiveportal processes */ + if (file_exists("{$g['varrun_path']}/cp_prunedb_{$cpzone}.pid")) { + killbypid("{$g['varrun_path']}/cp_prunedb_{$cpzone}.pid"); + } + + /* start pruning process (interval defaults to 60 seconds) */ + mwexec("/usr/local/bin/minicron $croninterval {$g['varrun_path']}/cp_prunedb_{$cpzone}.pid " . + "/etc/rc.prunecaptiveportal {$cpzone}"); + + /* generate radius server database */ + unlink_if_exists("{$g['vardb_path']}/captiveportal_radius_{$cpzone}.db"); + captiveportal_init_radius_servers(); + + if (platform_booting()) { + /* send Accounting-On to server */ + captiveportal_send_server_accounting(); + echo "done\n"; + } + + } else { + killbypid("{$g['varrun_path']}/lighty-{$cpzone}-CaptivePortal.pid"); + killbypid("{$g['varrun_path']}/lighty-{$cpzone}-CaptivePortal-SSL.pid"); + killbypid("{$g['varrun_path']}/cp_prunedb_{$cpzone}.pid"); + @unlink("{$g['varetc_path']}/captiveportal_{$cpzone}.html"); + @unlink("{$g['varetc_path']}/captiveportal-{$cpzone}-error.html"); + @unlink("{$g['varetc_path']}/captiveportal-{$cpzone}-logout.html"); + + captiveportal_radius_stop_all(); + + /* send Accounting-Off to server */ + if (!platform_booting()) { + captiveportal_send_server_accounting(true); + } + + /* remove old information */ + unlink_if_exists("{$g['vardb_path']}/captiveportal{$cpzone}.db"); + unlink_if_exists("{$g['vardb_path']}/captiveportal_radius_{$cpzone}.db"); + unlink_if_exists("{$g['vardb_path']}/captiveportal_{$cpzone}.rules"); + /* Release allocated pipes for this zone */ + captiveportal_free_dnrules(); + + mwexec("/sbin/ipfw zone {$cpzoneid} destroy", true); + + if (empty($config['captiveportal'])) { + set_single_sysctl("net.link.ether.ipfw", "0"); + } else { + /* Deactivate ipfw(4) if not needed */ + $cpactive = false; + if (is_array($config['captiveportal'])) { + foreach ($config['captiveportal'] as $cpkey => $cp) { + if (isset($cp['enable'])) { + $cpactive = true; + break; + } + } + } + if ($cpactive === false) { + set_single_sysctl("net.link.ether.ipfw", "0"); + } + } + } + + unlock($captiveportallck); + + return 0; +} + +function captiveportal_init_webgui() { + global $config, $cpzone; + + if (is_array($config['captiveportal'])) { + foreach ($config['captiveportal'] as $cpkey => $cp) { + $cpzone = $cpkey; + captiveportal_init_webgui_zone($cp); + } + } +} + +function captiveportal_init_webgui_zonename($zone) { + global $config, $cpzone; + + if (isset($config['captiveportal'][$zone])) { + $cpzone = $zone; + captiveportal_init_webgui_zone($config['captiveportal'][$zone]); + } +} + +function captiveportal_init_webgui_zone($cpcfg) { + global $g, $config, $cpzone; + + if (!isset($cpcfg['enable'])) { + return; + } + + if (isset($cpcfg['httpslogin'])) { + $cert = lookup_cert($cpcfg['certref']); + $crt = base64_decode($cert['crt']); + $key = base64_decode($cert['prv']); + $ca = ca_chain($cert); + + /* generate lighttpd configuration */ + if (!empty($cpcfg['listenporthttps'])) { + $listenporthttps = $cpcfg['listenporthttps']; + } else { + $listenporthttps = 8001 + $cpcfg['zoneid']; + } + system_generate_lighty_config("{$g['varetc_path']}/lighty-{$cpzone}-CaptivePortal-SSL.conf", + $crt, $key, $ca, "lighty-{$cpzone}-CaptivePortal-SSL.pid", $listenporthttps, "/usr/local/captiveportal", + "cert-{$cpzone}-portal.pem", "ca-{$cpzone}-portal.pem", $cpzone); + } + + /* generate lighttpd configuration */ + if (!empty($cpcfg['listenporthttp'])) { + $listenporthttp = $cpcfg['listenporthttp']; + } else { + $listenporthttp = 8000 + $cpcfg['zoneid']; + } + system_generate_lighty_config("{$g['varetc_path']}/lighty-{$cpzone}-CaptivePortal.conf", + "", "", "", "lighty-{$cpzone}-CaptivePortal.pid", $listenporthttp, "/usr/local/captiveportal", + "", "", $cpzone); + + @unlink("{$g['varrun']}/lighty-{$cpzone}-CaptivePortal.pid"); + /* attempt to start lighttpd */ + $res = mwexec("/usr/local/sbin/lighttpd -f {$g['varetc_path']}/lighty-{$cpzone}-CaptivePortal.conf"); + + /* fire up https instance */ + if (isset($cpcfg['httpslogin'])) { + @unlink("{$g['varrun']}/lighty-{$cpzone}-CaptivePortal-SSL.pid"); + $res = mwexec("/usr/local/sbin/lighttpd -f {$g['varetc_path']}/lighty-{$cpzone}-CaptivePortal-SSL.conf"); + } +} + +function captiveportal_init_rules_byinterface($interface) { + global $cpzone, $cpzoneid, $config; + + if (!is_array($config['captiveportal'])) { + return; + } + + foreach ($config['captiveportal'] as $cpkey => $cp) { + $cpzone = $cpkey; + $cpzoneid = $cp['zoneid']; + $cpinterfaces = explode(",", $cp['interface']); + if (in_array($interface, $cpinterfaces)) { + captiveportal_init_rules(); + break; + } + } +} + +/* reinit will disconnect all users, be careful! */ +function captiveportal_init_rules($reinit = false) { + global $config, $g, $cpzone, $cpzoneid; + + if (!isset($config['captiveportal'][$cpzone]['enable'])) { + return; + } + + captiveportal_load_modules(); + mwexec("/sbin/ipfw zone {$cpzoneid} create", true); + + /* Cleanup so nothing is leaked */ + captiveportal_free_dnrules(); + unlink_if_exists("{$g['vardb_path']}/captiveportal_{$cpzone}.rules"); + + $cpips = array(); + $ifaces = get_configured_interface_list(); + $cpinterfaces = explode(",", $config['captiveportal'][$cpzone]['interface']); + $firsttime = 0; + foreach ($cpinterfaces as $cpifgrp) { + if (!isset($ifaces[$cpifgrp])) { + continue; + } + $tmpif = get_real_interface($cpifgrp); + if (!empty($tmpif)) { + $cpipm = get_interface_ip($cpifgrp); + if (is_ipaddr($cpipm)) { + $cpips[] = $cpipm; + if (!is_array($config['virtualip']) || !is_array($config['virtualip']['vip'])) { + continue; + } + foreach ($config['virtualip']['vip'] as $vip) { + if (($vip['interface'] == $cpifgrp) && (($vip['mode'] == "carp") || ($vip['mode'] == "ipalias"))) { + $cpips[] = $vip['subnet']; + } + } + } + mwexec("/sbin/ipfw zone {$cpzoneid} madd {$tmpif}", true); + } + } + if (count($cpips) > 0) { + $cpactive = true; + } else { + return false; + } + + if ($reinit == false) { + $captiveportallck = lock("captiveportal{$cpzone}"); + } + + $cprules = <<<EOD + +flush +add 65291 allow pfsync from any to any +add 65292 allow carp from any to any + +# layer 2: pass ARP +add 65301 pass layer2 mac-type arp,rarp +# pfsense requires for WPA +add 65302 pass layer2 mac-type 0x888e,0x88c7 +# PPP Over Ethernet Session Stage/Discovery Stage +add 65303 pass layer2 mac-type 0x8863,0x8864 + +# layer 2: block anything else non-IP(v4/v6) +add 65307 deny layer2 not mac-type ip,ipv6 + +EOD; + + $rulenum = 65310; + /* These tables contain host ips */ + $cprules .= "add {$rulenum} pass ip from any to table(100) in\n"; + $rulenum++; + $cprules .= "add {$rulenum} pass ip from table(100) to any out\n"; + $rulenum++; + $ips = ""; + foreach ($cpips as $cpip) { + $cprules .= "table 100 add {$cpip}\n"; + } + $cprules .= "table 100 add 255.255.255.255\n"; + $cprules .= "add {$rulenum} pass ip from any to {$ips} in\n"; + $rulenum++; + $cprules .= "add {$rulenum} pass ip from {$ips} to any out\n"; + $rulenum++; + $cprules .= "add {$rulenum} pass icmp from {$ips} to any out icmptype 0\n"; + $rulenum++; + $cprules .= "add {$rulenum} pass icmp from any to {$ips} in icmptype 8 \n"; + $rulenum++; + /* Allowed ips */ + $cprules .= "add {$rulenum} pipe tablearg ip from table(3) to any in\n"; + $rulenum++; + $cprules .= "add {$rulenum} pipe tablearg ip from any to table(4) in\n"; + $rulenum++; + $cprules .= "add {$rulenum} pipe tablearg ip from table(3) to any out\n"; + $rulenum++; + $cprules .= "add {$rulenum} pipe tablearg ip from any to table(4) out\n"; + $rulenum++; + + /* Authenticated users rules. */ + $cprules .= "add {$rulenum} pipe tablearg ip from table(1) to any in\n"; + $rulenum++; + $cprules .= "add {$rulenum} pipe tablearg ip from any to table(2) out\n"; + $rulenum++; + + if (!empty($config['captiveportal'][$cpzone]['listenporthttp'])) { + $listenporthttp = $config['captiveportal'][$cpzone]['listenporthttp']; + } else { + $listenporthttp = 8000 + $cpzoneid; + } + + if (isset($config['captiveportal'][$cpzone]['httpslogin'])) { + if (!empty($config['captiveportal'][$cpzone]['listenporthttps'])) { + $listenporthttps = $config['captiveportal'][$cpzone]['listenporthttps']; + } else { + $listenporthttps = 8001 + $cpzoneid; + } + if (!isset($config['captiveportal'][$cpzone]['nohttpsforwards'])) { + $cprules .= "add 65531 fwd 127.0.0.1,{$listenporthttps} tcp from any to any dst-port 443 in\n"; + } + } + + $cprules .= <<<EOD + +# redirect non-authenticated clients to captive portal +add 65532 fwd 127.0.0.1,{$listenporthttp} tcp from any to any dst-port 80 in +# let the responses from the captive portal web server back out +add 65533 pass tcp from any to any out +# block everything else +add 65534 deny all from any to any + +EOD; + + /* generate passthru mac database */ + $cprules .= captiveportal_passthrumac_configure(true); + $cprules .= "\n"; + + /* allowed ipfw rules to make allowed ip work */ + $cprules .= captiveportal_allowedip_configure(); + + /* allowed ipfw rules to make allowed hostnames work */ + $cprules .= captiveportal_allowedhostname_configure(); + + /* load rules */ + file_put_contents("{$g['tmp_path']}/ipfw_{$cpzone}.cp.rules", $cprules); + mwexec("/sbin/ipfw -x {$cpzoneid} -q {$g['tmp_path']}/ipfw_{$cpzone}.cp.rules", true); + //@unlink("{$g['tmp_path']}/ipfw_{$cpzone}.cp.rules"); + unset($cprules); + + if ($reinit == false) { + unlock($captiveportallck); + } +} + +/* + * Remove clients that have been around for longer than the specified amount of time + * db file structure: + * timestamp,ipfw_rule_no,clientip,clientmac,username,sessionid,password,session_timeout,idle_timeout,session_terminate_time,interim_interval + * (password is in Base64 and only saved when reauthentication is enabled) + */ +function captiveportal_prune_old() { + global $g, $config, $cpzone, $cpzoneid; + + if (empty($cpzone)) { + return; + } + + $cpcfg = $config['captiveportal'][$cpzone]; + $vcpcfg = $config['voucher'][$cpzone]; + + /* check for expired entries */ + $idletimeout = 0; + $timeout = 0; + if (!empty($cpcfg['timeout']) && is_numeric($cpcfg['timeout'])) { + $timeout = $cpcfg['timeout'] * 60; + } + + if (!empty($cpcfg['idletimeout']) && is_numeric($cpcfg['idletimeout'])) { + $idletimeout = $cpcfg['idletimeout'] * 60; + } + + /* Is there any job to do? */ + if (!$timeout && !$idletimeout && !isset($cpcfg['reauthenticate']) && + !isset($cpcfg['radiussession_timeout']) && !isset($vcpcfg['enable'])) { + return; + } + + $radiussrvs = captiveportal_get_radius_servers(); + + /* Read database */ + /* NOTE: while this can be simplified in non radius case keep as is for now */ + $cpdb = captiveportal_read_db(); + + $unsetindexes = array(); + $voucher_needs_sync = false; + /* + * Snapshot the time here to use for calculation to speed up the process. + * If something is missed next run will catch it! + */ + $pruning_time = time(); + $stop_time = $pruning_time; + foreach ($cpdb as $cpentry) { + + $timedout = false; + $term_cause = 1; + if (empty($cpentry[11])) { + $cpentry[11] = 'first'; + } + $radiusservers = $radiussrvs[$cpentry[11]]; + + /* hard timeout? */ + if ($timeout) { + if (($pruning_time - $cpentry[0]) >= $timeout) { + $timedout = true; + $term_cause = 5; // Session-Timeout + } + } + + /* Session-Terminate-Time */ + if (!$timedout && !empty($cpentry[9])) { + if ($pruning_time >= $cpentry[9]) { + $timedout = true; + $term_cause = 5; // Session-Timeout + } + } + + /* check if the radius idle_timeout attribute has been set and if its set change the idletimeout to this value */ + $uidletimeout = (is_numeric($cpentry[8])) ? $cpentry[8] : $idletimeout; + /* if an idle timeout is specified, get last activity timestamp from ipfw */ + if (!$timedout && $uidletimeout > 0) { + $lastact = captiveportal_get_last_activity($cpentry[2], $cpentry[3]); + /* If the user has logged on but not sent any traffic they will never be logged out. + * We "fix" this by setting lastact to the login timestamp. + */ + $lastact = $lastact ? $lastact : $cpentry[0]; + if ($lastact && (($pruning_time - $lastact) >= $uidletimeout)) { + $timedout = true; + $term_cause = 4; // Idle-Timeout + $stop_time = $lastact; // Entry added to comply with WISPr + } + } + + /* if vouchers are configured, activate session timeouts */ + if (!$timedout && isset($vcpcfg['enable']) && !empty($cpentry[7])) { + if ($pruning_time >= ($cpentry[0] + $cpentry[7])) { + $timedout = true; + $term_cause = 5; // Session-Timeout + $voucher_needs_sync = true; + } + } + + /* if radius session_timeout is enabled and the session_timeout is not null, then check if the user should be logged out */ + if (!$timedout && isset($cpcfg['radiussession_timeout']) && !empty($cpentry[7])) { + if ($pruning_time >= ($cpentry[0] + $cpentry[7])) { + $timedout = true; + $term_cause = 5; // Session-Timeout + } + } + + if ($timedout) { + captiveportal_disconnect($cpentry, $radiusservers, $term_cause, $stop_time); + captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "TIMEOUT"); + $unsetindexes[] = $cpentry[5]; + } + + /* do periodic RADIUS reauthentication? */ + if (!$timedout && !empty($radiusservers)) { + if (isset($cpcfg['radacct_enable'])) { + if ($cpcfg['reauthenticateacct'] == "stopstart") { + /* stop and restart accounting */ + RADIUS_ACCOUNTING_STOP($cpentry[1], // ruleno + $cpentry[4], // username + $cpentry[5], // sessionid + $cpentry[0], // start time + $radiusservers, + $cpentry[2], // clientip + $cpentry[3], // clientmac + 10); // NAS Request + $clientsn = (is_ipaddrv6($cpentry[2])) ? 128 : 32; + $_gb = @pfSense_ipfw_Tableaction($cpzoneid, IP_FW_TABLE_XZEROENTRY, 1, $cpentry[2], $clientsn, $cpentry[3]); + $_gb = @pfSense_ipfw_Tableaction($cpzoneid, IP_FW_TABLE_XZEROENTRY, 2, $cpentry[2], $clientsn, $cpentry[3]); + RADIUS_ACCOUNTING_START($cpentry[1], // ruleno + $cpentry[4], // username + $cpentry[5], // sessionid + $radiusservers, + $cpentry[2], // clientip + $cpentry[3]); // clientmac + } else if ($cpcfg['reauthenticateacct'] == "interimupdate") { + $session_time = $pruning_time - $cpentry[0]; + if (!empty($cpentry[10]) && $cpentry[10] > 60) { + $interval = $cpentry[10]; + } else { + $interval = 0; + } + $past_interval_min = ($session_time > $interval); + if ($interval != 0) { + $within_interval = ($session_time % $interval >= 0 && $session_time % $interval <= 59); + } + if ($interval === 0 || ($interval > 0 && $past_interval_min && $within_interval)) { + RADIUS_ACCOUNTING_STOP($cpentry[1], // ruleno + $cpentry[4], // username + $cpentry[5], // sessionid + $cpentry[0], // start time + $radiusservers, + $cpentry[2], // clientip + $cpentry[3], // clientmac + 10, // NAS Request + true); // Interim Updates + } + } + } + + /* check this user against RADIUS again */ + if (isset($cpcfg['reauthenticate'])) { + $auth_list = RADIUS_AUTHENTICATION($cpentry[4], // username + base64_decode($cpentry[6]), // password + $radiusservers, + $cpentry[2], // clientip + $cpentry[3], // clientmac + $cpentry[1]); // ruleno + if ($auth_list['auth_val'] == 3) { + captiveportal_disconnect($cpentry, $radiusservers, 17); + captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "RADIUS_DISCONNECT", $auth_list['reply_message']); + $unsetindexes[] = $cpentry[5]; + } else if ($auth_list['auth_val'] == 2) { + captiveportal_reapply_attributes($cpentry, $auth_list); + } + } + } + } + unset($cpdb); + + captiveportal_prune_old_automac(); + + if ($voucher_needs_sync == true) { + /* Trigger a sync of the vouchers on config */ + send_event("service sync vouchers"); + } + + /* write database */ + if (!empty($unsetindexes)) { + captiveportal_remove_entries($unsetindexes); + } +} + +function captiveportal_prune_old_automac() { + global $g, $config, $cpzone, $cpzoneid; + + if (is_array($config['captiveportal'][$cpzone]['passthrumac']) && isset($config['captiveportal'][$cpzone]['passthrumacaddusername'])) { + $tmpvoucherdb = array(); + $macrules = ""; + $writecfg = false; + foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $eid => $emac) { + if ($emac['logintype'] == "voucher") { + if (isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) { + if (isset($tmpvoucherdb[$emac['username']])) { + $temac = $config['captiveportal'][$cpzone]['passthrumac'][$tmpvoucherdb[$emac['username']]]; + $ruleno = captiveportal_get_ipfw_passthru_ruleno($temac['mac']); + $pipeno = captiveportal_get_dn_passthru_ruleno($temac['mac']); + if ($ruleno) { + captiveportal_free_ipfw_ruleno($ruleno); + $macrules .= "delete {$ruleno}"; + ++$ruleno; + $macrules .= "delete {$ruleno}"; + } + if ($pipeno) { + captiveportal_free_dn_ruleno($pipeno); + $macrules .= "pipe delete {$pipeno}\n"; + ++$pipeno; + $macrules .= "pipe delete {$pipeno}\n"; + } + $writecfg = true; + captiveportal_logportalauth($temac['username'], $temac['mac'], $temac['ip'], "DUPLICATE {$temac['username']} LOGIN - TERMINATING OLD SESSION"); + unset($config['captiveportal'][$cpzone]['passthrumac'][$tmpvoucherdb[$emac['username']]]); + } + $tmpvoucherdb[$emac['username']] = $eid; + } + if (voucher_auth($emac['username']) <= 0) { + $ruleno = captiveportal_get_ipfw_passthru_ruleno($emac['mac']); + $pipeno = captiveportal_get_dn_passthru_ruleno($emac['mac']); + if ($ruleno) { + captiveportal_free_ipfw_ruleno($ruleno); + $macrules .= "delete {$ruleno}"; + ++$ruleno; + $macrules .= "delete {$ruleno}"; + } + if ($pipeno) { + captiveportal_free_dn_ruleno($pipeno); + $macrules .= "pipe delete {$pipeno}\n"; + ++$pipeno; + $macrules .= "pipe delete {$pipeno}\n"; + } + $writecfg = true; + captiveportal_logportalauth($emac['username'], $emac['mac'], $emac['ip'], "EXPIRED {$emac['username']} LOGIN - TERMINATING SESSION"); + unset($config['captiveportal'][$cpzone]['passthrumac'][$eid]); + } + } + } + unset($tmpvoucherdb); + if (!empty($macrules)) { + @file_put_contents("{$g['tmp_path']}/macentry.prunerules.tmp", $macrules); + unset($macrules); + mwexec("/sbin/ipfw -x {$cpzoneid} -q {$g['tmp_path']}/macentry.prunerules.tmp"); + } + if ($writecfg === true) { + write_config("Prune session for auto-added macs"); + } + } +} + +/* remove a single client according to the DB entry */ +function captiveportal_disconnect($dbent, $radiusservers, $term_cause = 1, $stop_time = null) { + global $g, $config, $cpzone, $cpzoneid; + + $stop_time = (empty($stop_time)) ? time() : $stop_time; + + /* this client needs to be deleted - remove ipfw rules */ + if (isset($config['captiveportal'][$cpzone]['radacct_enable']) && !empty($radiusservers)) { + RADIUS_ACCOUNTING_STOP($dbent[1], // ruleno + $dbent[4], // username + $dbent[5], // sessionid + $dbent[0], // start time + $radiusservers, + $dbent[2], // clientip + $dbent[3], // clientmac + $term_cause, // Acct-Terminate-Cause + false, + $stop_time); + } + + if (is_ipaddr($dbent[2])) { + /* Delete client's ip entry from tables 1 and 2. */ + $clientsn = (is_ipaddrv6($dbent[2])) ? 128 : 32; + pfSense_ipfw_Tableaction($cpzoneid, IP_FW_TABLE_XDEL, 1, $dbent[2], $clientsn, $dbent[3]); + pfSense_ipfw_Tableaction($cpzoneid, IP_FW_TABLE_XDEL, 2, $dbent[2], $clientsn, $dbent[3]); + /* XXX: Redundant?! Ensure all pf(4) states are killed. */ + $_gb = @pfSense_kill_states($dbent[2]); + $_gb = @pfSense_kill_srcstates($dbent[2]); + } + + /* + * These are the pipe numbers we use to control traffic shaping for each logged in user via captive portal + * We could get an error if the pipe doesn't exist but everything should still be fine + */ + if (!empty($dbent[1])) { + $_gb = @pfSense_pipe_action("pipe delete {$dbent[1]}"); + $_gb = @pfSense_pipe_action("pipe delete " . ($dbent[1]+1)); + + /* Release the ruleno so it can be reallocated to new clients. */ + captiveportal_free_dn_ruleno($dbent[1]); + } + + // XMLRPC Call over to the master Voucher node + if (!empty($config['voucher'][$cpzone]['vouchersyncdbip'])) { + $syncip = $config['voucher'][$cpzone]['vouchersyncdbip']; + $syncport = $config['voucher'][$cpzone]['vouchersyncport']; + $syncpass = $config['voucher'][$cpzone]['vouchersyncpass']; + $vouchersyncusername = $config['voucher'][$cpzone]['vouchersyncusername']; + $remote_status = xmlrpc_sync_voucher_disconnect($dbent, $syncip, $syncport, $syncpass, $vouchersyncusername, $term_cause, $stop_time); + } + +} + +/* remove a single client by sessionid */ +function captiveportal_disconnect_client($sessionid, $term_cause = 1, $logoutReason = "LOGOUT") { + global $g, $config; + + $radiusservers = captiveportal_get_radius_servers(); + + /* read database */ + $result = captiveportal_read_db("WHERE sessionid = '{$sessionid}'"); + + /* find entry */ + if (!empty($result)) { + captiveportal_write_db("DELETE FROM captiveportal WHERE sessionid = '{$sessionid}'"); + + foreach ($result as $cpentry) { + if (empty($cpentry[11])) { + $cpentry[11] = 'first'; + } + captiveportal_disconnect($cpentry, $radiusservers[$cpentry[11]], $term_cause); + captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "DISCONNECT"); + } + unset($result); + } +} + +/* send RADIUS acct stop for all current clients */ +function captiveportal_radius_stop_all() { + global $config, $cpzone; + + if (!isset($config['captiveportal'][$cpzone]['radacct_enable'])) { + return; + } + + $radiusservers = captiveportal_get_radius_servers(); + if (!empty($radiusservers)) { + $cpdb = captiveportal_read_db(); + foreach ($cpdb as $cpentry) { + if (empty($cpentry[11])) { + $cpentry[11] = 'first'; + } + if (!empty($radiusservers[$cpentry[11]])) { + RADIUS_ACCOUNTING_STOP($cpentry[1], // ruleno + $cpentry[4], // username + $cpentry[5], // sessionid + $cpentry[0], // start time + $radiusservers[$cpentry[11]], + $cpentry[2], // clientip + $cpentry[3], // clientmac + 7); // Admin Reboot + } + } + } +} + +function captiveportal_passthrumac_configure_entry($macent, $pipeinrule = false) { + global $config, $g, $cpzone; + + $bwUp = 0; + if (!empty($macent['bw_up'])) { + $bwUp = $macent['bw_up']; + } else if (!empty($config['captiveportal'][$cpzone]['bwdefaultup'])) { + $bwUp = $config['captiveportal'][$cpzone]['bwdefaultup']; + } + $bwDown = 0; + if (!empty($macent['bw_down'])) { + $bwDown = $macent['bw_down']; + } else if (!empty($config['captiveportal'][$cpzone]['bwdefaultdn'])) { + $bwDown = $config['captiveportal'][$cpzone]['bwdefaultdn']; + } + + $ruleno = captiveportal_get_next_ipfw_ruleno(); + + if ($macent['action'] == 'pass') { + $rules = ""; + $pipeno = captiveportal_get_next_dn_ruleno(); + + $pipeup = $pipeno; + if ($pipeinrule == true) { + $_gb = @pfSense_pipe_action("pipe {$pipeno} config bw {$bwUp}Kbit/s queue 100 buckets 16"); + } else { + $rules .= "pipe {$pipeno} config bw {$bwUp}Kbit/s queue 100 buckets 16\n"; + } + + $pipedown = $pipeno + 1; + if ($pipeinrule == true) { + $_gb = @pfSense_pipe_action("pipe {$pipedown} config bw {$bwDown}Kbit/s queue 100 buckets 16"); + } else { + $rules .= "pipe {$pipedown} config bw {$bwDown}Kbit/s queue 100 buckets 16\n"; + } + + $rules .= "add {$ruleno} pipe {$pipeup} ip from any to any MAC any {$macent['mac']}\n"; + $ruleno++; + $rules .= "add {$ruleno} pipe {$pipedown} ip from any to any MAC {$macent['mac']} any\n"; + } + + return $rules; +} + +function captiveportal_passthrumac_delete_entry($macent) { + $rules = ""; + + if ($macent['action'] == 'pass') { + $ruleno = captiveportal_get_ipfw_passthru_ruleno($macent['mac']); + + if (!$ruleno) { + return $rules; + } + + captiveportal_free_ipfw_ruleno($ruleno); + + $rules .= "delete {$ruleno}\n"; + $rules .= "delete " . ++$ruleno . "\n"; + + $pipeno = captiveportal_get_dn_passthru_ruleno($macent['mac']); + + if (!empty($pipeno)) { + captiveportal_free_dn_ruleno($pipeno); + $rules .= "pipe delete " . $pipeno . "\n"; + $rules .= "pipe delete " . ++$pipeno . "\n"; + } + } + + return $rules; +} + +function captiveportal_passthrumac_configure($filename = false, $startindex = 0, $stopindex = 0) { + global $config, $g, $cpzone; + + $rules = ""; + + if (is_array($config['captiveportal'][$cpzone]['passthrumac'])) { + if ($stopindex > 0) { + $fd = fopen($filename, "w"); + for ($idx = $startindex; $idx <= $stopindex; $idx++) { + if (isset($config['captiveportal'][$cpzone]['passthrumac'][$idx])) { + $rules = captiveportal_passthrumac_configure_entry($config['captiveportal'][$cpzone]['passthrumac'][$idx]); + fwrite($fd, $rules); + } + } + fclose($fd); + + return; + } else { + $nentries = count($config['captiveportal'][$cpzone]['passthrumac']); + if ($nentries > 2000) { + $nloops = $nentries / 1000; + $remainder= $nentries % 1000; + for ($i = 0; $i < $nloops; $i++) { + mwexec_bg("/usr/local/sbin/fcgicli -f /etc/rc.captiveportal_configure_mac -d \"cpzone={$cpzone}&startidx=" . ($i * 1000) . "&stopidx=" . ((($i+1) * 1000) - 1) . "\""); + } + if ($remainder > 0) { + mwexec_bg("/usr/local/sbin/fcgicli -f /etc/rc.captiveportal_configure_mac -d \"cpzone={$cpzone}&startidx=" . ($i * 1000) . "&stopidx=" . (($i* 1000) + $remainder) ."\""); + } + } else { + foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $macent) { + $rules .= captiveportal_passthrumac_configure_entry($macent, true); + } + } + } + } + + return $rules; +} + +function captiveportal_passthrumac_findbyname($username) { + global $config, $cpzone; + + if (is_array($config['captiveportal'][$cpzone]['passthrumac'])) { + foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $macent) { + if ($macent['username'] == $username) { + return $macent; + } + } + } + return NULL; +} + +/* + * table (3=IN)/(4=OUT) hold allowed ip's without bw limits + */ +function captiveportal_allowedip_configure_entry($ipent, $ishostname = false) { + global $g; + + /* Instead of copying this entire function for something + * easy such as hostname vs ip address add this check + */ + if ($ishostname === true) { + if (!platform_booting()) { + $ipaddress = gethostbyname($ipent['hostname']); + if (!is_ipaddr($ipaddress)) { + return; + } + } else { + $ipaddress = ""; + } + } else { + $ipaddress = $ipent['ip']; + } + + $rules = ""; + $cp_filterdns_conf = ""; + $enBwup = 0; + if (!empty($ipent['bw_up'])) { + $enBwup = intval($ipent['bw_up']); + } else if (!empty($config['captiveportal'][$cpzone]['bwdefaultup'])) { + $enBwup = $config['captiveportal'][$cpzone]['bwdefaultup']; + } + $enBwdown = 0; + if (!empty($ipent['bw_down'])) { + $enBwdown = intval($ipent['bw_down']); + } else if (!empty($config['captiveportal'][$cpzone]['bwdefaultdn'])) { + $enBwdown = $config['captiveportal'][$cpzone]['bwdefaultdn']; + } + + $pipeno = captiveportal_get_next_dn_ruleno(); + $_gb = @pfSense_pipe_action("pipe {$pipeno} config bw {$enBwup}Kbit/s queue 100 buckets 16"); + $pipedown = $pipeno + 1; + $_gb = @pfSense_pipe_action("pipe {$pipedown} config bw {$enBwdown}Kbit/s queue 100 buckets 16"); + if ($ishostname === true) { + $cp_filterdns_conf .= "ipfw {$ipent['hostname']} 3 pipe {$pipeno}\n"; + $cp_filterdns_conf .= "ipfw {$ipent['hostname']} 4 pipe {$pipedown}\n"; + if (!is_ipaddr($ipaddress)) { + return array("", $cp_filterdns_conf); + } + } + $subnet = ""; + if (!empty($ipent['sn'])) { + $subnet = "/{$ipent['sn']}"; + } + $rules .= "table 3 add {$ipaddress}{$subnet} {$pipeno}\n"; + $rules .= "table 4 add {$ipaddress}{$subnet} {$pipedown}\n"; + + if ($ishostname === true) { + return array($rules, $cp_filterdns_conf); + } else { + return $rules; + } +} + +function captiveportal_allowedhostname_configure() { + global $config, $g, $cpzone, $cpzoneid; + + $rules = ""; + if (is_array($config['captiveportal'][$cpzone]['allowedhostname'])) { + $rules = "\n# captiveportal_allowedhostname_configure()\n"; + $cp_filterdns_conf = ""; + foreach ($config['captiveportal'][$cpzone]['allowedhostname'] as $hostnameent) { + $tmprules = captiveportal_allowedip_configure_entry($hostnameent, true); + $rules .= $tmprules[0]; + $cp_filterdns_conf .= $tmprules[1]; + } + $cp_filterdns_filename = "{$g['varetc_path']}/filterdns-{$cpzone}-captiveportal.conf"; + @file_put_contents($cp_filterdns_filename, $cp_filterdns_conf); + unset($cp_filterdns_conf); + if (isvalidpid("{$g['varrun_path']}/filterdns-{$cpzone}-cpah.pid")) { + sigkillbypid("{$g['varrun_path']}/filterdns-{$cpzone}-cpah.pid", "HUP"); + } else { + mwexec("/usr/local/sbin/filterdns -p {$g['varrun_path']}/filterdns-{$cpzone}-cpah.pid -i 300 -c {$cp_filterdns_filename} -y {$cpzoneid} -d 1"); + } + } else { + killbypid("{$g['varrun_path']}/filterdns-{$cpzone}-cpah.pid"); + @unlink("{$g['varrun_path']}/filterdns-{$cpzone}-cpah.pid"); + } + + return $rules; +} + +function captiveportal_allowedip_configure() { + global $config, $g, $cpzone; + + $rules = ""; + if (is_array($config['captiveportal'][$cpzone]['allowedip'])) { + foreach ($config['captiveportal'][$cpzone]['allowedip'] as $ipent) { + $rules .= captiveportal_allowedip_configure_entry($ipent); + } + } + + return $rules; +} + +/* get last activity timestamp given client IP address */ +function captiveportal_get_last_activity($ip, $mac = NULL, $table = 1) { + global $cpzoneid; + + $ipfwoutput = pfSense_ipfw_getTablestats($cpzoneid, IP_FW_TABLE_XLISTENTRY, $table, $ip, $mac); + /* Reading only from one of the tables is enough of approximation. */ + if (is_array($ipfwoutput)) { + /* Workaround for #46652 */ + if ($ipfwoutput['packets'] > 0) { + return $ipfwoutput['timestamp']; + } else { + return 0; + } + } + + return 0; +} + +function captiveportal_init_radius_servers() { + global $config, $g, $cpzone; + + /* generate radius server database */ + if ($config['captiveportal'][$cpzone]['radiusip'] && + (!isset($config['captiveportal'][$cpzone]['auth_method']) || $config['captiveportal'][$cpzone]['auth_method'] == "radius")) { + $radiusip = $config['captiveportal'][$cpzone]['radiusip']; + $radiusip2 = ($config['captiveportal'][$cpzone]['radiusip2']) ? $config['captiveportal'][$cpzone]['radiusip2'] : null; + $radiusip3 = ($config['captiveportal'][$cpzone]['radiusip3']) ? $config['captiveportal'][$cpzone]['radiusip3'] : null; + $radiusip4 = ($config['captiveportal'][$cpzone]['radiusip4']) ? $config['captiveportal'][$cpzone]['radiusip4'] : null; + + if ($config['captiveportal'][$cpzone]['radiusport']) { + $radiusport = $config['captiveportal'][$cpzone]['radiusport']; + } else { + $radiusport = 1812; + } + if ($config['captiveportal'][$cpzone]['radiusacctport']) { + $radiusacctport = $config['captiveportal'][$cpzone]['radiusacctport']; + } else { + $radiusacctport = 1813; + } + if ($config['captiveportal'][$cpzone]['radiusport2']) { + $radiusport2 = $config['captiveportal'][$cpzone]['radiusport2']; + } else { + $radiusport2 = 1812; + } + if ($config['captiveportal'][$cpzone]['radiusport3']) { + $radiusport3 = $config['captiveportal'][$cpzone]['radiusport3']; + } else { + $radiusport3 = 1812; + } + if ($config['captiveportal'][$cpzone]['radiusport4']) { + $radiusport4 = $config['captiveportal'][$cpzone]['radiusport4']; + } else { + $radiusport4 = 1812; + } + + $radiuskey = $config['captiveportal'][$cpzone]['radiuskey']; + $radiuskey2 = $config['captiveportal'][$cpzone]['radiuskey2']; + $radiuskey3 = $config['captiveportal'][$cpzone]['radiuskey3']; + $radiuskey4 = $config['captiveportal'][$cpzone]['radiuskey4']; + + $cprdsrvlck = lock("captiveportalradius{$cpzone}", LOCK_EX); + $fd = @fopen("{$g['vardb_path']}/captiveportal_radius_{$cpzone}.db", "w"); + if (!$fd) { + captiveportal_syslog("Error: cannot open radius DB file in captiveportal_configure().\n"); + unlock($cprdsrvlck); + return 1; + } + if (isset($radiusip)) { + fwrite($fd, $radiusip . "," . $radiusport . "," . $radiusacctport . "," . $radiuskey . ",first"); + } + if (isset($radiusip2)) { + fwrite($fd, "\n" . $radiusip2 . "," . $radiusport2 . "," . $radiusacctport . "," . $radiuskey2 . ",first"); + } + if (isset($radiusip3)) { + fwrite($fd, "\n" . $radiusip3 . "," . $radiusport3 . "," . $radiusacctport . "," . $radiuskey3 . ",second"); + } + if (isset($radiusip4)) { + fwrite($fd, "\n" . $radiusip4 . "," . $radiusport4 . "," . $radiusacctport . "," . $radiuskey4 . ",second"); + } + + fclose($fd); + unlock($cprdsrvlck); + } +} + +/* read RADIUS servers into array */ +function captiveportal_get_radius_servers() { + global $g, $cpzone; + + $cprdsrvlck = lock("captiveportalradius{$cpzone}"); + if (file_exists("{$g['vardb_path']}/captiveportal_radius_{$cpzone}.db")) { + $radiusservers = array(); + $cpradiusdb = file("{$g['vardb_path']}/captiveportal_radius_{$cpzone}.db", + FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + if ($cpradiusdb) { + foreach ($cpradiusdb as $cpradiusentry) { + $line = trim($cpradiusentry); + if ($line) { + $radsrv = array(); + list($radsrv['ipaddr'], $radsrv['port'], $radsrv['acctport'], $radsrv['key'], $context) = explode(",", $line); + } + if (empty($context)) { + if (!is_array($radiusservers['first'])) { + $radiusservers['first'] = array(); + } + $radiusservers['first'] = $radsrv; + } else { + if (!is_array($radiusservers[$context])) { + $radiusservers[$context] = array(); + } + $radiusservers[$context][] = $radsrv; + } + } + } + unlock($cprdsrvlck); + return $radiusservers; + } + + unlock($cprdsrvlck); + return false; +} + +/* log successful captive portal authentication to syslog */ +/* part of this code from php.net */ +function captiveportal_logportalauth($user, $mac, $ip, $status, $message = null) { + // Log it + if (!$message) { + $message = "{$status}: {$user}, {$mac}, {$ip}"; + } else { + $message = trim($message); + $message = "{$status}: {$user}, {$mac}, {$ip}, {$message}"; + } + captiveportal_syslog($message); +} + +/* log simple messages to syslog */ +function captiveportal_syslog($message) { + global $cpzone; + + $message = trim($message); + $message = "Zone: {$cpzone} - {$message}"; + openlog("logportalauth", LOG_PID, LOG_LOCAL4); + // Log it + syslog(LOG_INFO, $message); + closelog(); +} + +function radius($username, $password, $clientip, $clientmac, $type, $radiusctx = null) { + global $g, $config, $cpzoneid; + + $pipeno = captiveportal_get_next_dn_ruleno(); + + /* If the pool is empty, return appropriate message and fail authentication */ + if (empty($pipeno)) { + $auth_list = array(); + $auth_list['auth_val'] = 1; + $auth_list['error'] = "System reached maximum login capacity"; + return $auth_list; + } + + $radiusservers = captiveportal_get_radius_servers(); + + if (is_null($radiusctx)) { + $radiusctx = 'first'; + } + + $auth_list = RADIUS_AUTHENTICATION($username, + $password, + $radiusservers[$radiusctx], + $clientip, + $clientmac, + $pipeno); + + if ($auth_list['auth_val'] == 2) { + captiveportal_logportalauth($username, $clientmac, $clientip, $type); + $sessionid = portal_allow($clientip, + $clientmac, + $username, + $password, + $auth_list, + $pipeno, + $radiusctx); + } else { + captiveportal_free_dn_ruleno($pipeno); + } + + return $auth_list; +} + +function captiveportal_opendb() { + global $g, $cpzone; + + $db_path = "{$g['vardb_path']}/captiveportal{$cpzone}.db"; + $createquery = "CREATE TABLE IF NOT EXISTS captiveportal (" . + "allow_time INTEGER, pipeno INTEGER, ip TEXT, mac TEXT, username TEXT, " . + "sessionid TEXT, bpassword TEXT, session_timeout INTEGER, idle_timeout INTEGER, " . + "session_terminate_time INTEGER, interim_interval INTEGER, radiusctx TEXT); " . + "CREATE UNIQUE INDEX IF NOT EXISTS idx_active ON captiveportal (sessionid, username); " . + "CREATE INDEX IF NOT EXISTS user ON captiveportal (username); " . + "CREATE INDEX IF NOT EXISTS ip ON captiveportal (ip); " . + "CREATE INDEX IF NOT EXISTS starttime ON captiveportal (allow_time)"; + + try { + $DB = new SQLite3($db_path); + } catch (Exception $e) { + captiveportal_syslog("Could not open {$db_path} as an sqlite database for {$cpzone}. Error message: " . $e->getMessage() . " -- Trying again."); + unlink_if_exists($db_path); + try { + $DB = new SQLite3($db_path); + } catch (Exception $e) { + captiveportal_syslog("Still could not open {$db_path} as an sqlite database for {$cpzone}. Error message: " . $e->getMessage() . " -- Remove the database file manually and ensure there is enough free space."); + return; + } + } + + if (!$DB) { + captiveportal_syslog("Could not open {$db_path} as an sqlite database for {$cpzone}. Error message: {$DB->lastErrorMsg()}. Trying again."); + unlink_if_exists($db_path); + $DB = new SQLite3($db_path); + if (!$DB) { + captiveportal_syslog("Still could not open {$db_path} as an sqlite database for {$cpzone}. Error message: {$DB->lastErrorMsg()}. Remove the database file manually and ensure there is enough free space."); + return; + } + } + + if (! $DB->exec($createquery)) { + captiveportal_syslog("Error during table {$cpzone} creation. Error message: {$DB->lastErrorMsg()}. Resetting and trying again."); + + /* If unable to initialize the database, reset and try again. */ + $DB->close(); + unset($DB); + unlink_if_exists($db_path); + $DB = new SQLite3($db_path); + if ($DB->exec($createquery)) { + captiveportal_syslog("Successfully reinitialized tables for {$cpzone} -- database has been reset."); + } else { + captiveportal_syslog("Still unable to create tables for {$cpzone}. Error message: {$DB->lastErrorMsg()}. Remove the database file manually and try again."); + } + } + + return $DB; +} + +/* read captive portal DB into array */ +function captiveportal_read_db($query = "") { + $cpdb = array(); + + $DB = captiveportal_opendb(); + if ($DB) { + $response = $DB->query("SELECT * FROM captiveportal {$query}"); + if ($response != FALSE) { + while ($row = $response->fetchArray()) { + $cpdb[] = $row; + } + } + $DB->close(); + } + + return $cpdb; +} + +function captiveportal_remove_entries($remove) { + + if (!is_array($remove) || empty($remove)) { + return; + } + + $query = "DELETE FROM captiveportal WHERE sessionid in ("; + foreach ($remove as $idx => $unindex) { + $query .= "'{$unindex}'"; + if ($idx < (count($remove) - 1)) { + $query .= ","; + } + } + $query .= ")"; + captiveportal_write_db($query); +} + +/* write captive portal DB */ +function captiveportal_write_db($queries) { + global $g; + + if (is_array($queries)) { + $query = implode(";", $queries); + } else { + $query = $queries; + } + + $DB = captiveportal_opendb(); + if ($DB) { + $DB->exec("BEGIN TRANSACTION"); + $result = $DB->exec($query); + if (!$result) { + captiveportal_syslog("Trying to modify DB returned error: {$DB->lastErrorMsg()}"); + } else { + $DB->exec("END TRANSACTION"); + } + $DB->close(); + return $result; + } else { + return true; + } +} + +function captiveportal_write_elements() { + global $g, $config, $cpzone; + + $cpcfg = $config['captiveportal'][$cpzone]; + + if (!is_dir($g['captiveportal_element_path'])) { + @mkdir($g['captiveportal_element_path']); + } + + if (is_array($cpcfg['element'])) { + conf_mount_rw(); + foreach ($cpcfg['element'] as $data) { + if (!@file_put_contents("{$g['captiveportal_element_path']}/{$data['name']}", base64_decode($data['content']))) { + printf(gettext("Error: cannot open '%s' in captiveportal_write_elements()%s"), $data['name'], "\n"); + return 1; + } + if (!file_exists("{$g['captiveportal_path']}/{$data['name']}")) { + @symlink("{$g['captiveportal_element_path']}/{$data['name']}", "{$g['captiveportal_path']}/{$data['name']}"); + } + } + conf_mount_ro(); + } + + return 0; +} + +function captiveportal_free_dnrules($rulenos_start = 2000, $rulenos_range_max = 64500) { + global $cpzone; + + $cpruleslck = lock("captiveportalrulesdn", LOCK_EX); + if (file_exists("{$g['vardb_path']}/captiveportaldn.rules")) { + $rules = unserialize(file_get_contents("{$g['vardb_path']}/captiveportaldn.rules")); + $ridx = $rulenos_start; + while ($ridx < $rulenos_range_max) { + if ($rules[$ridx] == $cpzone) { + $rules[$ridx] = false; + $ridx++; + $rules[$ridx] = false; + $ridx++; + } else { + $ridx += 2; + } + } + file_put_contents("{$g['vardb_path']}/captiveportaldn.rules", serialize($rules)); + unset($rules); + } + unlock($cpruleslck); +} + +function captiveportal_get_next_dn_ruleno($rulenos_start = 2000, $rulenos_range_max = 64500) { + global $config, $g, $cpzone; + + $cpruleslck = lock("captiveportalrulesdn", LOCK_EX); + $ruleno = 0; + if (file_exists("{$g['vardb_path']}/captiveportaldn.rules")) { + $rules = unserialize(file_get_contents("{$g['vardb_path']}/captiveportaldn.rules")); + $ridx = $rulenos_start; + while ($ridx < $rulenos_range_max) { + if (empty($rules[$ridx])) { + $ruleno = $ridx; + $rules[$ridx] = $cpzone; + $ridx++; + $rules[$ridx] = $cpzone; + break; + } else { + $ridx += 2; + } + } + } else { + $rules = array_pad(array(), $rulenos_range_max, false); + $ruleno = $rulenos_start; + $rules[$rulenos_start] = $cpzone; + $rulenos_start++; + $rules[$rulenos_start] = $cpzone; + } + file_put_contents("{$g['vardb_path']}/captiveportaldn.rules", serialize($rules)); + unlock($cpruleslck); + unset($rules); + + return $ruleno; +} + +function captiveportal_free_dn_ruleno($ruleno) { + global $config, $g; + + $cpruleslck = lock("captiveportalrulesdn", LOCK_EX); + if (file_exists("{$g['vardb_path']}/captiveportaldn.rules")) { + $rules = unserialize(file_get_contents("{$g['vardb_path']}/captiveportaldn.rules")); + $rules[$ruleno] = false; + $ruleno++; + $rules[$ruleno] = false; + file_put_contents("{$g['vardb_path']}/captiveportaldn.rules", serialize($rules)); + unset($rules); + } + unlock($cpruleslck); +} + +function captiveportal_get_dn_passthru_ruleno($value) { + global $config, $g, $cpzone, $cpzoneid; + + $cpcfg = $config['captiveportal'][$cpzone]; + if (!isset($cpcfg['enable'])) { + return NULL; + } + + $cpruleslck = lock("captiveportalrulesdn", LOCK_EX); + $ruleno = NULL; + if (file_exists("{$g['vardb_path']}/captiveportaldn.rules")) { + $rules = unserialize(file_get_contents("{$g['vardb_path']}/captiveportaldn.rules")); + unset($output); + $_gb = exec("/sbin/ipfw -x {$cpzoneid} show | /usr/bin/grep " . escapeshellarg($value) . " | /usr/bin/grep -v grep | /usr/bin/awk '{print $5}' | /usr/bin/head -n 1", $output); + $ruleno = intval($output[0]); + if (!$rules[$ruleno]) { + $ruleno = NULL; + } + unset($rules); + } + unlock($cpruleslck); + + return $ruleno; +} + +/* + * This function will calculate the lowest free firewall ruleno + * within the range specified based on the actual logged on users + * + */ +function captiveportal_get_next_ipfw_ruleno($rulenos_start = 2, $rulenos_range_max = 64500) { + global $config, $g, $cpzone; + + $cpcfg = $config['captiveportal'][$cpzone]; + if (!isset($cpcfg['enable'])) { + return NULL; + } + + $cpruleslck = lock("captiveportalrules{$cpzone}", LOCK_EX); + $ruleno = 0; + if (file_exists("{$g['vardb_path']}/captiveportal_{$cpzone}.rules")) { + $rules = unserialize(file_get_contents("{$g['vardb_path']}/captiveportal_{$cpzone}.rules")); + $ridx = $rulenos_start; + while ($ridx < $rulenos_range_max) { + if (empty($rules[$ridx])) { + $ruleno = $ridx; + $rules[$ridx] = $cpzone; + $ridx++; + $rules[$ridx] = $cpzone; + break; + } else { + /* + * This allows our traffic shaping pipes to be the in pipe the same as ruleno + * and the out pipe ruleno + 1. + */ + $ridx += 2; + } + } + } else { + $rules = array_pad(array(), $rulenos_range_max, false); + $ruleno = $rulenos_start; + $rules[$rulenos_start] = $cpzone; + $rulenos_start++; + $rules[$rulenos_start] = $cpzone; + } + file_put_contents("{$g['vardb_path']}/captiveportal_{$cpzone}.rules", serialize($rules)); + unlock($cpruleslck); + unset($rules); + + return $ruleno; +} + +function captiveportal_free_ipfw_ruleno($ruleno) { + global $config, $g, $cpzone; + + $cpcfg = $config['captiveportal'][$cpzone]; + if (!isset($cpcfg['enable'])) { + return NULL; + } + + $cpruleslck = lock("captiveportalrules{$cpzone}", LOCK_EX); + if (file_exists("{$g['vardb_path']}/captiveportal_{$cpzone}.rules")) { + $rules = unserialize(file_get_contents("{$g['vardb_path']}/captiveportal_{$cpzone}.rules")); + $rules[$ruleno] = false; + $ruleno++; + $rules[$ruleno] = false; + file_put_contents("{$g['vardb_path']}/captiveportal_{$cpzone}.rules", serialize($rules)); + unset($rules); + } + unlock($cpruleslck); +} + +function captiveportal_get_ipfw_passthru_ruleno($value) { + global $config, $g, $cpzone, $cpzoneid; + + $cpcfg = $config['captiveportal'][$cpzone]; + if (!isset($cpcfg['enable'])) { + return NULL; + } + + $cpruleslck = lock("captiveportalrules{$cpzone}", LOCK_EX); + $ruleno = NULL; + if (file_exists("{$g['vardb_path']}/captiveportal_{$cpzone}.rules")) { + $rules = unserialize(file_get_contents("{$g['vardb_path']}/captiveportal_{$cpzone}.rules")); + unset($output); + $_gb = exec("/sbin/ipfw -x {$cpzoneid} show | /usr/bin/grep " . escapeshellarg($value) . " | /usr/bin/grep -v grep | /usr/bin/awk '{print $1}' | /usr/bin/head -n 1", $output); + $ruleno = intval($output[0]); + if (!$rules[$ruleno]) { + $ruleno = NULL; + } + unset($rules); + } + unlock($cpruleslck); + + return $ruleno; +} + +/** + * This function will calculate the traffic produced by a client + * based on its firewall rule + * + * Point of view: NAS + * + * Input means: from the client + * Output means: to the client + * + */ + +function getVolume($ip, $mac = NULL) { + global $config, $cpzone, $cpzoneid; + + $reverse = empty($config['captiveportal'][$cpzone]['reverseacct']) ? false : true; + $volume = array(); + // Initialize vars properly, since we don't want NULL vars + $volume['input_pkts'] = $volume['input_bytes'] = $volume['output_pkts'] = $volume['output_bytes'] = 0 ; + + $ipfw = pfSense_ipfw_getTablestats($cpzoneid, IP_FW_TABLE_XLISTENTRY, 1, $ip, $mac); + if (is_array($ipfw)) { + if ($reverse) { + $volume['output_pkts'] = $ipfw['packets']; + $volume['output_bytes'] = $ipfw['bytes']; + } + else { + $volume['input_pkts'] = $ipfw['packets']; + $volume['input_bytes'] = $ipfw['bytes']; + } + } + + $ipfw = pfSense_ipfw_getTablestats($cpzoneid, IP_FW_TABLE_XLISTENTRY, 2, $ip, $mac); + if (is_array($ipfw)) { + if ($reverse) { + $volume['input_pkts'] = $ipfw['packets']; + $volume['input_bytes'] = $ipfw['bytes']; + } + else { + $volume['output_pkts'] = $ipfw['packets']; + $volume['output_bytes'] = $ipfw['bytes']; + } + } + + return $volume; +} + +/** + * Get the NAS-IP-Address based on the current wan address + * + * Use functions in interfaces.inc to find this out + * + */ + +function getNasIP() { + global $config, $cpzone; + + if (empty($config['captiveportal'][$cpzone]['radiussrcip_attribute'])) { + $nasIp = get_interface_ip(); + } else { + if (is_ipaddr($config['captiveportal'][$cpzone]['radiussrcip_attribute'])) { + $nasIp = $config['captiveportal'][$cpzone]['radiussrcip_attribute']; + } else { + $nasIp = get_interface_ip($config['captiveportal'][$cpzone]['radiussrcip_attribute']); + } + } + + if (!is_ipaddr($nasIp)) { + $nasIp = "0.0.0.0"; + } + + return $nasIp; +} + +function portal_ip_from_client_ip($cliip) { + global $config, $cpzone; + + $isipv6 = is_ipaddrv6($cliip); + $interfaces = explode(",", $config['captiveportal'][$cpzone]['interface']); + foreach ($interfaces as $cpif) { + if ($isipv6) { + $ip = get_interface_ipv6($cpif); + $sn = get_interface_subnetv6($cpif); + } else { + $ip = get_interface_ip($cpif); + $sn = get_interface_subnet($cpif); + } + if (ip_in_subnet($cliip, "{$ip}/{$sn}")) { + return $ip; + } + } + + $inet = ($isipv6) ? '-inet6' : '-inet'; + $iface = exec_command("/sbin/route -n get {$inet} {$cliip} | /usr/bin/awk '/interface/ { print \$2; };'"); + $iface = trim($iface, "\n"); + if (!empty($iface)) { + $ip = ($isipv6) ? find_interface_ipv6($iface) : find_interface_ip($iface); + if (is_ipaddr($ip)) { + return $ip; + } + } + + // doesn't match up to any particular interface + // so let's set the portal IP to what PHP says + // the server IP issuing the request is. + // allows same behavior as 1.2.x where IP isn't + // in the subnet of any CP interface (static routes, etc.) + // rather than forcing to DNS hostname resolution + $ip = $_SERVER['SERVER_ADDR']; + if (is_ipaddr($ip)) { + return $ip; + } + + return false; +} + +function portal_hostname_from_client_ip($cliip) { + global $config, $cpzone; + + $cpcfg = $config['captiveportal'][$cpzone]; + + if (isset($cpcfg['httpslogin'])) { + $listenporthttps = $cpcfg['listenporthttps'] ? $cpcfg['listenporthttps'] : ($cpcfg['zoneid'] + 8001); + $ourhostname = $cpcfg['httpsname']; + + if ($listenporthttps != 443) { + $ourhostname .= ":" . $listenporthttps; + } + } else { + $listenporthttp = $cpcfg['listenporthttp'] ? $cpcfg['listenporthttp'] : ($cpcfg['zoneid'] + 8000); + $ifip = portal_ip_from_client_ip($cliip); + if (!$ifip) { + $ourhostname = "{$config['system']['hostname']}.{$config['system']['domain']}"; + } else { + $ourhostname = (is_ipaddrv6($ifip)) ? "[{$ifip}]" : "{$ifip}"; + } + + if ($listenporthttp != 80) { + $ourhostname .= ":" . $listenporthttp; + } + } + + return $ourhostname; +} + +/* functions move from index.php */ + +function portal_reply_page($redirurl, $type = null, $message = null, $clientmac = null, $clientip = null, $username = null, $password = null) { + global $g, $config, $cpzone; + + /* Get captive portal layout */ + if ($type == "redir") { + header("Location: {$redirurl}"); + return; + } else if ($type == "login") { + $htmltext = get_include_contents("{$g['varetc_path']}/captiveportal_{$cpzone}.html"); + } else { + $htmltext = get_include_contents("{$g['varetc_path']}/captiveportal-{$cpzone}-error.html"); + } + + $cpcfg = $config['captiveportal'][$cpzone]; + + /* substitute the PORTAL_REDIRURL variable */ + if ($cpcfg['preauthurl']) { + $htmltext = str_replace("\$PORTAL_REDIRURL\$", "{$cpcfg['preauthurl']}", $htmltext); + $htmltext = str_replace("#PORTAL_REDIRURL#", "{$cpcfg['preauthurl']}", $htmltext); + } + + /* substitute other variables */ + $ourhostname = portal_hostname_from_client_ip($clientip); + $protocol = (isset($cpcfg['httpslogin'])) ? 'https://' : 'http://'; + $htmltext = str_replace("\$PORTAL_ACTION\$", "{$protocol}{$ourhostname}/", $htmltext); + $htmltext = str_replace("#PORTAL_ACTION#", "{$protocol}{$ourhostname}/", $htmltext); + + $htmltext = str_replace("\$PORTAL_ZONE\$", htmlspecialchars($cpzone), $htmltext); + $htmltext = str_replace("\$PORTAL_REDIRURL\$", htmlspecialchars($redirurl), $htmltext); + $htmltext = str_replace("\$PORTAL_MESSAGE\$", htmlspecialchars($message), $htmltext); + $htmltext = str_replace("\$CLIENT_MAC\$", htmlspecialchars($clientmac), $htmltext); + $htmltext = str_replace("\$CLIENT_IP\$", htmlspecialchars($clientip), $htmltext); + + // Special handling case for captive portal master page so that it can be ran + // through the PHP interpreter using the include method above. We convert the + // $VARIABLE$ case to #VARIABLE# in /etc/inc/captiveportal.inc before writing out. + $htmltext = str_replace("#PORTAL_ZONE#", htmlspecialchars($cpzone), $htmltext); + $htmltext = str_replace("#PORTAL_REDIRURL#", htmlspecialchars($redirurl), $htmltext); + $htmltext = str_replace("#PORTAL_MESSAGE#", htmlspecialchars($message), $htmltext); + $htmltext = str_replace("#CLIENT_MAC#", htmlspecialchars($clientmac), $htmltext); + $htmltext = str_replace("#CLIENT_IP#", htmlspecialchars($clientip), $htmltext); + $htmltext = str_replace("#USERNAME#", htmlspecialchars($username), $htmltext); + $htmltext = str_replace("#PASSWORD#", htmlspecialchars($password), $htmltext); + + echo $htmltext; +} + +function portal_mac_radius($clientmac, $clientip) { + global $config, $cpzone; + + $radmac_secret = $config['captiveportal'][$cpzone]['radmac_secret']; + + /* authentication against the radius server */ + $username = mac_format($clientmac); + $auth_list = radius($username, $radmac_secret, $clientip, $clientmac, "MACHINE LOGIN"); + if ($auth_list['auth_val'] == 2) { + return TRUE; + } + + if (!empty($auth_list['url_redirection'])) { + portal_reply_page($auth_list['url_redirection'], "redir"); + } + + return FALSE; +} + +function captiveportal_reapply_attributes($cpentry, $attributes) { + global $config, $cpzone, $g; + + if (isset($config['captiveportal'][$cpzone]['peruserbw'])) { + $dwfaultbw_up = !empty($config['captiveportal'][$cpzone]['bwdefaultup']) ? $config['captiveportal'][$cpzone]['bwdefaultup'] : 0; + $dwfaultbw_down = !empty($config['captiveportal'][$cpzone]['bwdefaultdn']) ? $config['captiveportal'][$cpzone]['bwdefaultdn'] : 0; + } else { + $dwfaultbw_up = $dwfaultbw_down = 0; + } + $bw_up = !empty($attributes['bw_up']) ? round(intval($attributes['bw_up'])/1000, 2) : $dwfaultbw_up; + $bw_down = !empty($attributes['bw_down']) ? round(intval($attributes['bw_down'])/1000, 2) : $dwfaultbw_down; + $bw_up_pipeno = $cpentry[1]; + $bw_down_pipeno = $cpentry[1]+1; + + $_gb = @pfSense_pipe_action("pipe {$bw_up_pipeno} config bw {$bw_up}Kbit/s queue 100 buckets 16"); + $_gb = @pfSense_pipe_action("pipe {$bw_down_pipeno} config bw {$bw_down}Kbit/s queue 100 buckets 16"); + //captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "RADIUS_BANDWIDTH_REAPPLY", "{$bw_up}/{$bw_down}"); + + unset($bw_up_pipeno, $bw_down_pipeno, $bw_up, $bw_down); +} + +function portal_allow($clientip, $clientmac, $username, $password = null, $attributes = null, $pipeno = null, $radiusctx = null) { + global $redirurl, $g, $config, $type, $passthrumac, $_POST, $cpzone, $cpzoneid; + + // Ensure we create an array if we are missing attributes + if (!is_array($attributes)) { + $attributes = array(); + } + + unset($sessionid); + + /* Do not allow concurrent login execution. */ + $cpdblck = lock("captiveportaldb{$cpzone}", LOCK_EX); + + if ($attributes['voucher']) { + $remaining_time = $attributes['session_timeout']; + } + + $writecfg = false; + /* Find an existing session */ + if ((isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) && $passthrumac) { + if (isset($config['captiveportal'][$cpzone]['passthrumacadd'])) { + $mac = captiveportal_passthrumac_findbyname($username); + if (!empty($mac)) { + if ($_POST['replacemacpassthru']) { + foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $idx => $macent) { + if ($macent['mac'] == $mac['mac']) { + $macrules = ""; + $ruleno = captiveportal_get_ipfw_passthru_ruleno($mac['mac']); + $pipeno = captiveportal_get_dn_passthru_ruleno($mac['mac']); + if ($ruleno) { + captiveportal_free_ipfw_ruleno($ruleno); + $macrules .= "delete {$ruleno}\n"; + ++$ruleno; + $macrules .= "delete {$ruleno}\n"; + } + if ($pipeno) { + captiveportal_free_dn_ruleno($pipeno); + $macrules .= "pipe delete {$pipeno}\n"; + ++$pipeno; + $macrules .= "pipe delete {$pipeno}\n"; + } + unset($config['captiveportal'][$cpzone]['passthrumac'][$idx]); + $mac['action'] = 'pass'; + $mac['mac'] = $clientmac; + $config['captiveportal'][$cpzone]['passthrumac'][] = $mac; + $macrules .= captiveportal_passthrumac_configure_entry($mac); + file_put_contents("{$g['tmp_path']}/macentry_{$cpzone}.rules.tmp", $macrules); + mwexec("/sbin/ipfw -x {$cpzoneid} -q {$g['tmp_path']}/macentry_{$cpzone}.rules.tmp"); + $writecfg = true; + $sessionid = true; + break; + } + } + } else { + portal_reply_page($redirurl, "error", "Username: {$username} is already authenticated using another MAC address.", + $clientmac, $clientip, $username, $password); + unlock($cpdblck); + return; + } + } + } + } + + /* read in client database */ + $query = "WHERE ip = '{$clientip}'"; + $tmpusername = strtolower($username); + if (isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) { + $query .= " OR (username != 'unauthenticated' AND lower(username) = '{$tmpusername}')"; + } + $cpdb = captiveportal_read_db($query); + + /* Snapshot the timestamp */ + $allow_time = time(); + $radiusservers = captiveportal_get_radius_servers(); + $unsetindexes = array(); + if (is_null($radiusctx)) { + $radiusctx = 'first'; + } + + foreach ($cpdb as $cpentry) { + if (empty($cpentry[11])) { + $cpentry[11] = 'first'; + } + /* on the same ip */ + if ($cpentry[2] == $clientip) { + if (isset($config['captiveportal'][$cpzone]['nomacfilter']) || $cpentry[3] == $clientmac) { + captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT LOGIN - REUSING OLD SESSION"); + } else { + captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT LOGIN - REUSING IP {$cpentry[2]} WITH DIFFERENT MAC ADDRESS {$cpentry[3]}"); + } + $sessionid = $cpentry[5]; + break; + } elseif (($attributes['voucher']) && ($username != 'unauthenticated') && ($cpentry[4] == $username)) { + // user logged in with an active voucher. Check for how long and calculate + // how much time we can give him (voucher credit - used time) + $remaining_time = $cpentry[0] + $cpentry[7] - $allow_time; + if ($remaining_time < 0) { // just in case. + $remaining_time = 0; + } + + /* This user was already logged in so we disconnect the old one */ + captiveportal_disconnect($cpentry, $radiusservers[$cpentry[11]], 13); + captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT LOGIN - TERMINATING OLD SESSION"); + $unsetindexes[] = $cpentry[5]; + break; + } elseif ((isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) && ($username != 'unauthenticated')) { + /* on the same username */ + if (strcasecmp($cpentry[4], $username) == 0) { + /* This user was already logged in so we disconnect the old one */ + captiveportal_disconnect($cpentry, $radiusservers[$cpentry[11]], 13); + captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT LOGIN - TERMINATING OLD SESSION"); + $unsetindexes[] = $cpentry[5]; + break; + } + } + } + unset($cpdb); + + if (!empty($unsetindexes)) { + captiveportal_remove_entries($unsetindexes); + } + + if ($attributes['voucher'] && $remaining_time <= 0) { + return 0; // voucher already used and no time left + } + + if (!isset($sessionid)) { + /* generate unique session ID */ + $tod = gettimeofday(); + $sessionid = substr(md5(mt_rand() . $tod['sec'] . $tod['usec'] . $clientip . $clientmac), 0, 16); + + if ($passthrumac) { + $mac = array(); + $mac['action'] = 'pass'; + $mac['mac'] = $clientmac; + $mac['ip'] = $clientip; /* Used only for logging */ + if (isset($config['captiveportal'][$cpzone]['passthrumacaddusername'])) { + $mac['username'] = $username; + if ($attributes['voucher']) { + $mac['logintype'] = "voucher"; + } + } + if ($username == "unauthenticated") { + $mac['descr'] = "Auto-added"; + } else { + $mac['descr'] = "Auto-added for user {$username}"; + } + if (!empty($bw_up)) { + $mac['bw_up'] = $bw_up; + } + if (!empty($bw_down)) { + $mac['bw_down'] = $bw_down; + } + if (!is_array($config['captiveportal'][$cpzone]['passthrumac'])) { + $config['captiveportal'][$cpzone]['passthrumac'] = array(); + } + $config['captiveportal'][$cpzone]['passthrumac'][] = $mac; + unlock($cpdblck); + $macrules = captiveportal_passthrumac_configure_entry($mac); + file_put_contents("{$g['tmp_path']}/macentry_{$cpzone}.rules.tmp", $macrules); + mwexec("/sbin/ipfw -x {$cpzoneid} -q {$g['tmp_path']}/macentry_{$cpzone}.rules.tmp"); + $writecfg = true; + } else { + /* See if a pipeno is passed, if not start sessions because this means there isn't one atm */ + if (is_null($pipeno)) { + $pipeno = captiveportal_get_next_dn_ruleno(); + } + + /* if the pool is empty, return appropriate message and exit */ + if (is_null($pipeno)) { + portal_reply_page($redirurl, "error", "System reached maximum login capacity"); + log_error("Zone: {$cpzone} - WARNING! Captive portal has reached maximum login capacity"); + unlock($cpdblck); + return; + } + + if (isset($config['captiveportal'][$cpzone]['peruserbw'])) { + $dwfaultbw_up = !empty($config['captiveportal'][$cpzone]['bwdefaultup']) ? $config['captiveportal'][$cpzone]['bwdefaultup'] : 0; + $dwfaultbw_down = !empty($config['captiveportal'][$cpzone]['bwdefaultdn']) ? $config['captiveportal'][$cpzone]['bwdefaultdn'] : 0; + } else { + $dwfaultbw_up = $dwfaultbw_down = 0; + } + $bw_up = !empty($attributes['bw_up']) ? round(intval($attributes['bw_up'])/1000, 2) : $dwfaultbw_up; + $bw_down = !empty($attributes['bw_down']) ? round(intval($attributes['bw_down'])/1000, 2) : $dwfaultbw_down; + + $bw_up_pipeno = $pipeno; + $bw_down_pipeno = $pipeno + 1; + //$bw_up /= 1000; // Scale to Kbit/s + $_gb = @pfSense_pipe_action("pipe {$bw_up_pipeno} config bw {$bw_up}Kbit/s queue 100 buckets 16"); + $_gb = @pfSense_pipe_action("pipe {$bw_down_pipeno} config bw {$bw_down}Kbit/s queue 100 buckets 16"); + + $clientsn = (is_ipaddrv6($clientip)) ? 128 : 32; + if (!isset($config['captiveportal'][$cpzone]['nomacfilter'])) { + $_gb = @pfSense_ipfw_Tableaction($cpzoneid, IP_FW_TABLE_XADD, 1, $clientip, $clientsn, $clientmac, $bw_up_pipeno); + } else { + $_gb = @pfSense_ipfw_Tableaction($cpzoneid, IP_FW_TABLE_XADD, 1, $clientip, $clientsn, NULL, $bw_up_pipeno); + } + + if (!isset($config['captiveportal'][$cpzone]['nomacfilter'])) { + $_gb = @pfSense_ipfw_Tableaction($cpzoneid, IP_FW_TABLE_XADD, 2, $clientip, $clientsn, $clientmac, $bw_down_pipeno); + } else { + $_gb = @pfSense_ipfw_Tableaction($cpzoneid, IP_FW_TABLE_XADD, 2, $clientip, $clientsn, NULL, $bw_down_pipeno); + } + + if ($attributes['voucher']) { + $attributes['session_timeout'] = $remaining_time; + } + + /* handle empty attributes */ + $session_timeout = (!empty($attributes['session_timeout'])) ? $attributes['session_timeout'] : 'NULL'; + $idle_timeout = (!empty($attributes['idle_timeout'])) ? $attributes['idle_timeout'] : 'NULL'; + $session_terminate_time = (!empty($attributes['session_terminate_time'])) ? $attributes['session_terminate_time'] : 'NULL'; + $interim_interval = (!empty($attributes['interim_interval'])) ? $attributes['interim_interval'] : 'NULL'; + + /* escape username */ + $safe_username = SQLite3::escapeString($username); + + /* encode password in Base64 just in case it contains commas */ + $bpassword = base64_encode($password); + $insertquery = "INSERT INTO captiveportal (allow_time, pipeno, ip, mac, username, sessionid, bpassword, session_timeout, idle_timeout, session_terminate_time, interim_interval, radiusctx) "; + $insertquery .= "VALUES ({$allow_time}, {$pipeno}, '{$clientip}', '{$clientmac}', '{$safe_username}', '{$sessionid}', '{$bpassword}', "; + $insertquery .= "{$session_timeout}, {$idle_timeout}, {$session_terminate_time}, {$interim_interval}, '{$radiusctx}')"; + + /* store information to database */ + captiveportal_write_db($insertquery); + unlock($cpdblck); + unset($insertquery, $bpassword); + + if (isset($config['captiveportal'][$cpzone]['radacct_enable']) && !empty($radiusservers[$radiusctx])) { + $acct_val = RADIUS_ACCOUNTING_START($pipeno, $username, $sessionid, $radiusservers[$radiusctx], $clientip, $clientmac); + if ($acct_val == 1) { + captiveportal_logportalauth($username, $clientmac, $clientip, $type, "RADIUS ACCOUNTING FAILED"); + } + } + } + } else { + /* NOTE: #3062-11 If the pipeno has been allocated free it to not DoS the CP and maintain proper operation as in radius() case */ + if (!is_null($pipeno)) { + captiveportal_free_dn_ruleno($pipeno); + } + + unlock($cpdblck); + } + + if ($writecfg == true) { + write_config(); + } + + /* redirect user to desired destination */ + if (!empty($attributes['url_redirection'])) { + $my_redirurl = $attributes['url_redirection']; + } else if (!empty($redirurl)) { + $my_redirurl = $redirurl; + } else if (!empty($config['captiveportal'][$cpzone]['redirurl'])) { + $my_redirurl = $config['captiveportal'][$cpzone]['redirurl']; + } + + if (isset($config['captiveportal'][$cpzone]['logoutwin_enable']) && !$passthrumac) { + $ourhostname = portal_hostname_from_client_ip($clientip); + $protocol = (isset($config['captiveportal'][$cpzone]['httpslogin'])) ? 'https://' : 'http://'; + $logouturl = "{$protocol}{$ourhostname}/"; + + if (isset($attributes['reply_message'])) { + $message = $attributes['reply_message']; + } else { + $message = 0; + } + + include("{$g['varetc_path']}/captiveportal-{$cpzone}-logout.html"); + + } else { + portal_reply_page($my_redirurl, "redir", "Just redirect the user."); + } + + return $sessionid; +} + + +/* + * Used for when pass-through credits are enabled. + * Returns true when there was at least one free login to deduct for the MAC. + * Expired entries are removed as they are seen. + * Active entries are updated according to the configuration. + */ +function portal_consume_passthrough_credit($clientmac) { + global $config, $cpzone; + + if (!empty($config['captiveportal'][$cpzone]['freelogins_count']) && is_numeric($config['captiveportal'][$cpzone]['freelogins_count'])) { + $freeloginscount = $config['captiveportal'][$cpzone]['freelogins_count']; + } else { + return false; + } + + if (!empty($config['captiveportal'][$cpzone]['freelogins_resettimeout']) && is_numeric($config['captiveportal'][$cpzone]['freelogins_resettimeout'])) { + $resettimeout = $config['captiveportal'][$cpzone]['freelogins_resettimeout']; + } else { + return false; + } + + if ($freeloginscount < 1 || $resettimeout <= 0 || !$clientmac) { + return false; + } + + $updatetimeouts = isset($config['captiveportal'][$cpzone]['freelogins_updatetimeouts']); + + /* + * Read database of used MACs. Lines are a comma-separated list + * of the time, MAC, then the count of pass-through credits remaining. + */ + $usedmacs = captiveportal_read_usedmacs_db(); + + $currenttime = time(); + $found = false; + foreach ($usedmacs as $key => $usedmac) { + $usedmac = explode(",", $usedmac); + + if ($usedmac[1] == $clientmac) { + if ($usedmac[0] + ($resettimeout * 3600) > $currenttime) { + if ($usedmac[2] < 1) { + if ($updatetimeouts) { + $usedmac[0] = $currenttime; + unset($usedmacs[$key]); + $usedmacs[] = implode(",", $usedmac); + captiveportal_write_usedmacs_db($usedmacs); + } + + return false; + } else { + $usedmac[2] -= 1; + $usedmacs[$key] = implode(",", $usedmac); + } + + $found = true; + } else { + unset($usedmacs[$key]); + } + + break; + } else if ($usedmac[0] + ($resettimeout * 3600) <= $currenttime) { + unset($usedmacs[$key]); + } + } + + if (!$found) { + $usedmac = array($currenttime, $clientmac, $freeloginscount - 1); + $usedmacs[] = implode(",", $usedmac); + } + + captiveportal_write_usedmacs_db($usedmacs); + return true; +} + +function captiveportal_read_usedmacs_db() { + global $g, $cpzone; + + $cpumaclck = lock("captiveusedmacs{$cpzone}"); + if (file_exists("{$g['vardb_path']}/captiveportal_usedmacs_{$cpzone}.db")) { + $usedmacs = file("{$g['vardb_path']}/captiveportal_usedmacs_{$cpzone}.db", FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + if (!$usedmacs) { + $usedmacs = array(); + } + } else { + $usedmacs = array(); + } + + unlock($cpumaclck); + return $usedmacs; +} + +function captiveportal_write_usedmacs_db($usedmacs) { + global $g, $cpzone; + + $cpumaclck = lock("captiveusedmacs{$cpzone}", LOCK_EX); + @file_put_contents("{$g['vardb_path']}/captiveportal_usedmacs_{$cpzone}.db", implode("\n", $usedmacs)); + unlock($cpumaclck); +} + +function captiveportal_blocked_mac($mac) { + global $config, $g, $cpzone; + + if (empty($mac) || !is_macaddr($mac)) { + return false; + } + + if (!is_array($config['captiveportal'][$cpzone]['passthrumac'])) { + return false; + } + + foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $passthrumac) { + if (($passthrumac['action'] == 'block') && + ($passthrumac['mac'] == strtolower($mac))) { + return true; + } + } + + return false; + +} + +function captiveportal_send_server_accounting($off = false) { + global $cpzone, $config; + + if (!isset($config['captiveportal'][$cpzone]['radacct_enable'])) { + return; + } + if ($off) { + $racct = new Auth_RADIUS_Acct_Off; + } else { + $racct = new Auth_RADIUS_Acct_On; + } + $radiusservers = captiveportal_get_radius_servers(); + if (empty($radiusservers)) { + return; + } + foreach ($radiusservers['first'] as $radsrv) { + // Add a new server to our instance + $racct->addServer($radsrv['ipaddr'], $radsrv['acctport'], $radsrv['key']); + } + if (PEAR::isError($racct->start())) { + $retvalue['acct_val'] = 1; + $retvalue['error'] = $racct->getMessage(); + + // If we encounter an error immediately stop this function and go back + $racct->close(); + return $retvalue; + } + // Send request + $result = $racct->send(); + // Evaluation of the response + // 5 -> Accounting-Response + // See RFC2866 for this. + if (PEAR::isError($result)) { + $retvalue['acct_val'] = 1; + $retvalue['error'] = $result->getMessage(); + } else if ($result === true) { + $retvalue['acct_val'] = 5 ; + } else { + $retvalue['acct_val'] = 1 ; + } + + $racct->close(); + return $retvalue; +} +?> |