· 6 years ago · Aug 19, 2019, 03:34 PM
1<?php
2/*
3 * captiveportal.inc
4 *
5 * part of pfSense (https://www.pfsense.org)
6 * Copyright (c) 2004-2019 Rubicon Communications, LLC (Netgate)
7 * All rights reserved.
8 *
9 * originally part of m0n0wall (http://m0n0.ch/wall)
10 * Copyright (c) 2003-2006 Manuel Kasper <mk@neon1.net>.
11 * All rights reserved.
12 *
13 * Licensed under the Apache License, Version 2.0 (the "License");
14 * you may not use this file except in compliance with the License.
15 * You may obtain a copy of the License at
16 *
17 * http://www.apache.org/licenses/LICENSE-2.0
18 *
19 * Unless required by applicable law or agreed to in writing, software
20 * distributed under the License is distributed on an "AS IS" BASIS,
21 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22 * See the License for the specific language governing permissions and
23 * limitations under the License.
24 */
25
26/* include all configuration functions */
27require_once("auth.inc");
28require_once("PEAR.php"); // required for bcmath
29require_once("Auth/RADIUS.php"); // required for radius accounting
30require_once("config.inc");
31require_once("functions.inc");
32require_once("filter.inc");
33require_once("voucher.inc");
34
35/* Captiveportal Radius Accounting */
36PEAR::loadExtension('bcmath');
37// The RADIUS Package doesn't have these vars so we create them ourself
38define("CUSTOM_RADIUS_ACCT_INPUT_GIGAWORDS", "52");
39define("CUSTOM_RADIUS_ACCT_OUTPUT_GIGAWORDS", "53");
40define("GIGAWORDS_RIGHT_OPERAND", '4294967296'); // 2^32
41
42function get_default_captive_portal_html() {
43 global $config, $g, $cpzone;
44
45 $translated_text1 = gettext("User");
46 $translated_text2 = gettext("Password");
47 $translated_text3 = gettext("First Authentication Method ");
48 $translated_text4 = gettext("Second Authentication Method ");
49 // default images to use.
50 $logo_src = "captiveportal-default-logo.png";
51 $bg_src = "linear-gradient(135deg, #1475CF, #2B40B5, #1C1275)";
52 // Check if customlogo is set and if the element exists
53 // Check if the image is in the directory
54 if (isset($config['captiveportal'][$cpzone]['customlogo'])) {
55 foreach ($config['captiveportal'][$cpzone]['element'] as $element) {
56 if (strpos($element['name'], "captiveportal-logo.") !== false) {
57 if (file_exists("{$g['captiveportal_path']}/{$element['name']}")) {
58 $logo_src = $element['name'];
59 break;
60 }
61 }
62 }
63 }
64 // check if custombg is set and if the element exists
65 if (isset($config['captiveportal'][$cpzone]['custombg'])) {
66 foreach ($config['captiveportal'][$cpzone]['element'] as $element) {
67 if (strpos($element['name'],"captiveportal-background.") !== false) {
68 if( file_exists("{$g['captiveportal_path']}/{$element['name']}")) {
69 $bg_src = "url(" . $element['name'] . ")" . "center center no-repeat fixed";
70 break;
71 }
72 }
73 }
74
75 }
76 // bring in terms and conditions
77 $termsconditions = base64_decode($config['captiveportal'][$cpzone]['termsconditions']);
78 // if there is no terms and conditions do not require the checkbox to be selected.
79 $disabled = "";
80 if ($termsconditions) {
81 $disabled = "disabled";
82 }
83 $htmltext = <<<EOD
84<!DOCTYPE html>
85<html>
86
87<head>
88
89 <meta charset="UTF-8">
90 <meta name="viewport" content="width=device-width, initial-scale=1.0">
91 <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
92 <title>Captive Portal Login Page</title>
93 <style>
94 #content,.login,.login-card a,.login-card h1,.login-help{text-align:center}body,html{margin:0;padding:0;width:100%;height:100%;display:table}#content{font-family:'Source Sans Pro',sans-serif;background-color:#1C1275;background:{$bg_src};-webkit-background-size:cover;-moz-background-size:cover;-o-background-size:cover;background-size:cover;display:table-cell;vertical-align:middle}.login-card{padding:40px;width:280px;background-color:#F7F7F7;margin:100px auto 10px;border-radius:2px;box-shadow:0 2px 2px rgba(0,0,0,.3);overflow:hidden}.login-card h1{font-weight:400;font-size:2.3em;color:#1383c6}.login-card h1 span{color:#f26721}.login-card img{width:70%;height:70%}.login-card input[type=submit]{width:100%;display:block;margin-bottom:10px;position:relative}.login-card input[type=text],input[type=password]{height:44px;font-size:16px;width:100%;margin-bottom:10px;-webkit-appearance:none;background:#fff;border:1px solid #d9d9d9;border-top:1px solid silver;padding:0 8px;box-sizing:border-box;-moz-box-sizing:border-box}.login-card input[type=text]:hover,input[type=password]:hover{border:1px solid #b9b9b9;border-top:1px solid #a0a0a0;-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.login{font-size:14px;font-family:Arial,sans-serif;font-weight:700;height:36px;padding:0 8px}.login-submit{-webkit-appearance:none;-moz-appearance:none;appearance:none;border:0;color:#fff;text-shadow:0 1px rgba(0,0,0,.1);background-color:#4d90fe}.login-submit:disabled{opacity:.6}.login-submit:hover{border:0;text-shadow:0 1px rgba(0,0,0,.3);background-color:#357ae8}.login-card a{text-decoration:none;color:#222;font-weight:400;display:inline-block;opacity:.6;transition:opacity ease .5s}.login-card a:hover{opacity:1}.login-help{width:100%;font-size:12px}.list{list-style-type:none;padding:0}.list__item{margin:0 0 .7rem;padding:0}label{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;text-align:left;font-size:14px;}input[type=checkbox]{-webkit-box-flex:0;-webkit-flex:none;-ms-flex:none;flex:none;margin-right:10px;float:left}@media screen and (max-width:450px){.login-card{width:70%!important}.login-card img{width:30%;height:30%}}textarea{width:66%;margin:auto;height:120px;max-height:120px;background-color:#f7f7f7;padding:20px}#terms{display:none;padding-top:100px;padding-bottom:300px;}.auth_source{border: 1px solid lightgray; padding:20px 8px 0px 8px; margin-top: -2em; border-radius: 2px; }.auth_head{background-color:#f7f7f7;display:inline-block;}.auth_head_div{text-align:left;}#error-message{text-align:left;color:#ff3e3e;font-style:italic;}
95 </style>
96</head>
97
98<body>
99<div id="content">
100 <div class="login-card">
101 <img src="{$logo_src}"/><br>
102 <h1></h1>
103 <div id="error-message">
104 \$PORTAL_MESSAGE\$
105 </div>
106 <form name="login_form" method="post" action="\$PORTAL_ACTION\$">
107EOD;
108 if ($config['captiveportal'][$cpzone]['auth_method'] != "none"){
109 if ($config['captiveportal'][$cpzone]['auth_method'] === 'authserver' && !empty($config['captiveportal'][$cpzone]['auth_server2'])) {
110 $htmltext .= <<<EOD
111 <div class="auth_head_div">
112 <h6 class="auth_head">{$translated_text3}</h6>
113 </div>
114 <div class="auth_source">
115
116EOD;
117 }
118 $htmltext .=<<<EOD
119 <input type="text" name="auth_user" placeholder="{$translated_text1}" id="auth_user">
120 <input type="password" name="auth_pass" placeholder="{$translated_text2}" id="auth_pass">
121EOD;
122
123 if ($config['captiveportal'][$cpzone]['auth_method'] === 'authserver' && !empty($config['captiveportal'][$cpzone]['auth_server2'])) {
124 $htmltext .= <<<EOD
125 </div>
126 <div class="auth_head_div">
127 <h6 class="auth_head">{$translated_text4}</h6>
128 </div>
129 <div class="auth_source">
130
131 <input type="text" name="auth_user2" placeholder="{$translated_text1}" id="auth_user2">
132 <input type="password" name="auth_pass2" placeholder="{$translated_text2}" id="auth_pass2">
133 </div>
134EOD;
135 }
136
137
138 if (isset($config['voucher'][$cpzone]['enable'])) {
139 $translated_text = gettext("Voucher Code");
140 $htmltext .= <<<EOD
141 <br /><br />
142 <input name="auth_voucher" type="text" placeholder="{$translated_text}">
143EOD;
144 }
145 }
146
147if ($termsconditions) {
148 $htmltext .= <<<EOD
149 <div class="login-help">
150 <ul class="list">
151 <li class="list__item">
152 <label class="label--checkbox">
153 <input type="checkbox" class="checkbox" onchange="document.getElementById('login').disabled = !this.checked;">
154 <span>I agree with the <a rel="noopener" href="#terms" onclick="document.getElementById('terms').style.display = 'block';">terms & conditions</a></span>
155 </label>
156 </li>
157 </ul>
158 </div>
159EOD;
160}
161 $htmltext .= <<<EOD
162
163 <input name="redirurl" type="hidden" value="\$PORTAL_REDIRURL\$">
164 <input type="submit" name="accept" class="login login-submit" value="Login" id="login" {$disabled}>
165 </form>
166 <br />
167 <span> <i>Made with ♥ by</i> <strong>Netgate</strong></span>
168 </div>
169 <div id="terms">
170 <textarea readonly>{$termsconditions}</textarea>
171 </div>
172</div>
173</body>
174</html>
175
176EOD;
177
178 return $htmltext;
179}
180
181function captiveportal_load_modules() {
182 global $config;
183
184 mute_kernel_msgs();
185 if (!is_module_loaded("ipfw.ko")) {
186 mwexec("/sbin/kldload ipfw");
187 /* make sure ipfw is not on pfil hooks */
188 set_sysctl(array(
189 "net.inet.ip.pfil.inbound" => "pf",
190 "net.inet6.ip6.pfil.inbound" => "pf",
191 "net.inet.ip.pfil.outbound" => "pf",
192 "net.inet6.ip6.pfil.outbound" => "pf"
193 ));
194 }
195 /* Activate layer2 filtering */
196 set_sysctl(array(
197 "net.link.ether.ipfw" => "1",
198 "net.inet.ip.fw.one_pass" => "1",
199 "net.inet.ip.fw.tables_max" => "65534"
200 ));
201
202 /* Always load dummynet now that even allowed ip and mac passthrough use it. */
203 if (!is_module_loaded("dummynet.ko")) {
204 mwexec("/sbin/kldload dummynet");
205 set_sysctl(array(
206 "net.inet.ip.dummynet.io_fast" => "1",
207 "net.inet.ip.dummynet.hash_size" => "256"
208 ));
209 }
210 unmute_kernel_msgs();
211}
212
213function captiveportal_configure() {
214 global $config, $cpzone, $cpzoneid;
215
216 if (is_array($config['captiveportal'])) {
217 foreach ($config['captiveportal'] as $cpkey => $cp) {
218 $cpzone = $cpkey;
219 $cpzoneid = $cp['zoneid'];
220 captiveportal_configure_zone($cp);
221 }
222 }
223}
224
225function captiveportal_configure_zone($cpcfg) {
226 global $config, $g, $cpzone, $cpzoneid;
227
228
229
230 if (isset($cpcfg['enable'])) {
231
232 if (platform_booting()) {
233 echo "Starting captive portal({$cpcfg['zone']})... ";
234 } else {
235 captiveportal_syslog("Reconfiguring captive portal({$cpcfg['zone']}).");
236 }
237
238 /* (re)init ipfw rules */
239 captiveportal_init_rules();
240
241 /* kill any running minicron */
242 killbypid("{$g['varrun_path']}/cp_prunedb_{$cpzone}.pid");
243
244 /* initialize minicron interval value */
245 $croninterval = $cpcfg['croninterval'] ? $cpcfg['croninterval'] : 60;
246
247 /* double check if the $croninterval is numeric and at least 10 seconds. If not we set it to 60 to avoid problems */
248 if ((!is_numeric($croninterval)) || ($croninterval < 10)) {
249 $croninterval = 60;
250 }
251
252 /* write portal page */
253 if (is_array($cpcfg['page']) && $cpcfg['page']['htmltext']) {
254 $htmltext = base64_decode($cpcfg['page']['htmltext']);
255 } else {
256 /* example/template page */
257 $htmltext = get_default_captive_portal_html();
258 }
259
260 $fd = @fopen("{$g['varetc_path']}/captiveportal_{$cpzone}.html", "w");
261 if ($fd) {
262 // Special case handling. Convert so that we can pass this page
263 // through the PHP interpreter later without clobbering the vars.
264 $htmltext = str_replace("\$PORTAL_ZONE\$", "#PORTAL_ZONE#", $htmltext);
265 $htmltext = str_replace("\$PORTAL_REDIRURL\$", "#PORTAL_REDIRURL#", $htmltext);
266 $htmltext = str_replace("\$PORTAL_MESSAGE\$", "#PORTAL_MESSAGE#", $htmltext);
267 $htmltext = str_replace("\$CLIENT_MAC\$", "#CLIENT_MAC#", $htmltext);
268 $htmltext = str_replace("\$CLIENT_IP\$", "#CLIENT_IP#", $htmltext);
269 $htmltext = str_replace("\$PORTAL_ACTION\$", "#PORTAL_ACTION#", $htmltext);
270 if ($cpcfg['preauthurl']) {
271 $htmltext = str_replace("\$PORTAL_REDIRURL\$", "{$cpcfg['preauthurl']}", $htmltext);
272 $htmltext = str_replace("#PORTAL_REDIRURL#", "{$cpcfg['preauthurl']}", $htmltext);
273 }
274 fwrite($fd, $htmltext);
275 fclose($fd);
276 }
277 unset($htmltext);
278
279 /* write error page */
280 if (is_array($cpcfg['page']) && $cpcfg['page']['errtext']) {
281 $errtext = base64_decode($cpcfg['page']['errtext']);
282 } else {
283 /* example page */
284 $errtext = get_default_captive_portal_html();
285 }
286
287 $fd = @fopen("{$g['varetc_path']}/captiveportal-{$cpzone}-error.html", "w");
288 if ($fd) {
289 // Special case handling. Convert so that we can pass this page
290 // through the PHP interpreter later without clobbering the vars.
291 $errtext = str_replace("\$PORTAL_ZONE\$", "#PORTAL_ZONE#", $errtext);
292 $errtext = str_replace("\$PORTAL_REDIRURL\$", "#PORTAL_REDIRURL#", $errtext);
293 $errtext = str_replace("\$PORTAL_MESSAGE\$", "#PORTAL_MESSAGE#", $errtext);
294 $errtext = str_replace("\$CLIENT_MAC\$", "#CLIENT_MAC#", $errtext);
295 $errtext = str_replace("\$CLIENT_IP\$", "#CLIENT_IP#", $errtext);
296 $errtext = str_replace("\$PORTAL_ACTION\$", "#PORTAL_ACTION#", $errtext);
297 if ($cpcfg['preauthurl']) {
298 $errtext = str_replace("\$PORTAL_REDIRURL\$", "{$cpcfg['preauthurl']}", $errtext);
299 $errtext = str_replace("#PORTAL_REDIRURL#", "{$cpcfg['preauthurl']}", $errtext);
300 }
301 fwrite($fd, $errtext);
302 fclose($fd);
303 }
304 unset($errtext);
305
306 /* write logout page */
307 if (is_array($cpcfg['page']) && $cpcfg['page']['logouttext']) {
308 $logouttext = base64_decode($cpcfg['page']['logouttext']);
309 } else {
310 /* example page */
311 $translated_text1 = gettext("Redirecting...");
312 $translated_text2 = gettext("Redirecting to");
313 $translated_text3 = gettext("Logout");
314 $translated_text4 = gettext("Click the button below to disconnect");
315 $logouttext = <<<EOD
316<html>
317<head><title>{$translated_text1}</title></head>
318<body>
319<span style="font-family: Tahoma, Verdana, Arial, Helvetica, sans-serif; font-size: 11px;">
320<b>{$translated_text2} <a href="<?=\$my_redirurl;?>"><?=\$my_redirurl;?></a>...</b>
321</span>
322<script type="text/javascript">
323//<![CDATA[
324LogoutWin = window.open('', 'Logout', 'toolbar=0,scrollbars=0,location=0,statusbar=0,menubar=0,resizable=0,width=256,height=64');
325if (LogoutWin) {
326 LogoutWin.document.write('<html>');
327 LogoutWin.document.write('<head><title>{$translated_text3}</title></head>') ;
328 LogoutWin.document.write('<body style="background-color:#435370">');
329 LogoutWin.document.write('<div class="text-center" style="color: #ffffff; font-family: Tahoma, Verdana, Arial, Helvetica, sans-serif; font-size: 11px;">') ;
330 LogoutWin.document.write('<b>{$translated_text4}</b><p />');
331 LogoutWin.document.write('<form method="POST" action="<?=\$logouturl;?>">');
332 LogoutWin.document.write('<input name="logout_id" type="hidden" value="<?=\$sessionid;?>" />');
333 LogoutWin.document.write('<input name="zone" type="hidden" value="<?=\$cpzone;?>" />');
334 LogoutWin.document.write('<input name="logout" type="submit" value="{$translated_text3}" />');
335 LogoutWin.document.write('</form>');
336 LogoutWin.document.write('</div></body>');
337 LogoutWin.document.write('</html>');
338 LogoutWin.document.close();
339}
340
341document.location.href="<?=\$my_redirurl;?>";
342//]]>
343</script>
344</body>
345</html>
346
347EOD;
348 }
349
350 $fd = @fopen("{$g['varetc_path']}/captiveportal-{$cpzone}-logout.html", "w");
351 if ($fd) {
352 fwrite($fd, $logouttext);
353 fclose($fd);
354 }
355 unset($logouttext);
356
357 /* write elements */
358 captiveportal_write_elements();
359
360 /* kill any running CP nginx instances */
361 killbypid("{$g['varrun_path']}/nginx-{$cpzone}-CaptivePortal.pid");
362 killbypid("{$g['varrun_path']}/nginx-{$cpzone}-CaptivePortal-SSL.pid");
363
364 /* start up the webserving daemon */
365 captiveportal_init_webgui_zone($cpcfg);
366
367 /* Kill any existing prunecaptiveportal processes */
368 if (file_exists("{$g['varrun_path']}/cp_prunedb_{$cpzone}.pid")) {
369 killbypid("{$g['varrun_path']}/cp_prunedb_{$cpzone}.pid");
370 }
371
372 /* start pruning process (interval defaults to 60 seconds) */
373 mwexec("/usr/local/bin/minicron $croninterval {$g['varrun_path']}/cp_prunedb_{$cpzone}.pid " .
374 "/etc/rc.prunecaptiveportal {$cpzone}");
375
376
377
378 if (platform_booting()) {
379 /* send Accounting-On to server */
380 captiveportal_send_server_accounting('on');
381 echo "done\n";
382
383 if (isset($cpcfg['preservedb'])) {
384 $connected_users = captiveportal_read_db();
385 if (!empty($connected_users)) {
386 echo "Reconnecting users to captive portal {$cpcfg['zone']}... ";
387 foreach ($connected_users as $user) {
388 captiveportal_reserve_ruleno($user['pipeno']);
389 $bw_up = intval($user['bw_up']);
390 $bw_down = intval($user['bw_down']);
391 $clientip = $user['ip'];
392 $clientmac = $user['mac'];
393 $bw_up_pipeno = $user['pipeno'];
394 $bw_down_pipeno = $user['pipeno'] + 1;
395
396 $rule_entry = "{$clientip}/" . (is_ipaddrv6($clientip) ? "128" : "32");
397 if (!isset($cpcfg['nomacfilter'])) {
398 $rule_entry .= ",{$clientmac}";
399 }
400
401 $_gb = @pfSense_ipfw_pipe("pipe {$bw_up_pipeno} config bw {$bw_up}Kbit/s queue 100 buckets 16");
402 $_gb = @pfSense_ipfw_pipe("pipe {$bw_down_pipeno} config bw {$bw_down}Kbit/s queue 100 buckets 16");
403 $_gb = @pfSense_ipfw_table("{$cpzone}_auth_up", IP_FW_TABLE_XADD, "{$rule_entry}", $bw_up_pipeno);
404 $_gb = @pfSense_ipfw_table("{$cpzone}_auth_down", IP_FW_TABLE_XADD, "{$rule_entry}", $bw_down_pipeno);
405 }
406 echo "done\n";
407 }
408 }
409 }
410
411 } else {
412 killbypid("{$g['varrun_path']}/nginx-{$cpzone}-CaptivePortal.pid");
413 killbypid("{$g['varrun_path']}/nginx-{$cpzone}-CaptivePortal-SSL.pid");
414 killbypid("{$g['varrun_path']}/cp_prunedb_{$cpzone}.pid");
415 @unlink("{$g['varetc_path']}/captiveportal_{$cpzone}.html");
416 @unlink("{$g['varetc_path']}/captiveportal-{$cpzone}-error.html");
417 @unlink("{$g['varetc_path']}/captiveportal-{$cpzone}-logout.html");
418
419 captiveportal_radius_stop_all(10); // NAS-Request
420
421 captiveportal_filterdns_configure();
422
423 /* remove old information */
424 unlink_if_exists("{$g['vardb_path']}/captiveportal{$cpzone}.db");
425
426
427 /* Release allocated pipes for this zone */
428 $pipes_to_remove = captiveportal_free_dnrules();
429
430 captiveportal_delete_rules($pipes_to_remove);
431
432 if (empty($config['captiveportal'])) {
433 set_single_sysctl("net.link.ether.ipfw", "0");
434 } else {
435 /* Deactivate ipfw(4) if not needed */
436 $cpactive = false;
437 if (is_array($config['captiveportal'])) {
438 foreach ($config['captiveportal'] as $cpkey => $cp) {
439 if (isset($cp['enable'])) {
440 $cpactive = true;
441 break;
442 }
443 }
444 }
445 if ($cpactive === false) {
446 set_single_sysctl("net.link.ether.ipfw", "0");
447 }
448 }
449 }
450
451
452
453 return 0;
454}
455
456function captiveportal_init_webgui() {
457 global $config, $cpzone;
458
459 if (is_array($config['captiveportal'])) {
460 foreach ($config['captiveportal'] as $cpkey => $cp) {
461 $cpzone = $cpkey;
462 captiveportal_init_webgui_zone($cp);
463 }
464 }
465}
466
467function captiveportal_init_webgui_zonename($zone) {
468 global $config, $cpzone;
469
470 if (isset($config['captiveportal'][$zone])) {
471 $cpzone = $zone;
472 captiveportal_init_webgui_zone($config['captiveportal'][$zone]);
473 }
474}
475
476function captiveportal_init_webgui_zone($cpcfg) {
477 global $g, $config, $cpzone;
478
479 if (!isset($cpcfg['enable'])) {
480 return;
481 }
482
483 if (isset($cpcfg['httpslogin'])) {
484 $cert = lookup_cert($cpcfg['certref']);
485 $crt = base64_decode($cert['crt']);
486 $key = base64_decode($cert['prv']);
487 $ca = ca_chain($cert);
488
489 /* generate nginx configuration */
490 if (!empty($cpcfg['listenporthttps'])) {
491 $listenporthttps = $cpcfg['listenporthttps'];
492 } else {
493 $listenporthttps = 8001 + $cpcfg['zoneid'];
494 }
495 system_generate_nginx_config("{$g['varetc_path']}/nginx-{$cpzone}-CaptivePortal-SSL.conf",
496 $crt, $key, $ca, "nginx-{$cpzone}-CaptivePortal-SSL.pid", $listenporthttps, "/usr/local/captiveportal",
497 "cert-{$cpzone}-portal.pem", "ca-{$cpzone}-portal.pem", $cpzone, false);
498 }
499
500 /* generate nginx configuration */
501 if (!empty($cpcfg['listenporthttp'])) {
502 $listenporthttp = $cpcfg['listenporthttp'];
503 } else {
504 $listenporthttp = 8000 + $cpcfg['zoneid'];
505 }
506 system_generate_nginx_config("{$g['varetc_path']}/nginx-{$cpzone}-CaptivePortal.conf",
507 "", "", "", "nginx-{$cpzone}-CaptivePortal.pid", $listenporthttp, "/usr/local/captiveportal",
508 "", "", $cpzone, false);
509
510 @unlink("{$g['varrun']}/nginx-{$cpzone}-CaptivePortal.pid");
511 /* attempt to start nginx */
512 $res = mwexec("/usr/local/sbin/nginx -c {$g['varetc_path']}/nginx-{$cpzone}-CaptivePortal.conf");
513
514 /* fire up https instance */
515 if (isset($cpcfg['httpslogin'])) {
516 @unlink("{$g['varrun']}/nginx-{$cpzone}-CaptivePortal-SSL.pid");
517 $res = mwexec("/usr/local/sbin/nginx -c {$g['varetc_path']}/nginx-{$cpzone}-CaptivePortal-SSL.conf");
518 }
519}
520
521function captiveportal_init_rules_byinterface($interface) {
522 global $cpzone, $cpzoneid, $config;
523
524 if (!is_array($config['captiveportal'])) {
525 return;
526 }
527
528 foreach ($config['captiveportal'] as $cpkey => $cp) {
529 $cpzone = $cpkey;
530 $cpzoneid = $cp['zoneid'];
531 $cpinterfaces = explode(",", $cp['interface']);
532 if (in_array($interface, $cpinterfaces)) {
533 captiveportal_init_rules();
534 break;
535 }
536 }
537}
538
539/* Create basic rules used by all zones */
540function captiveportal_init_general_rules($flush = false) {
541 global $g;
542
543 $flush_rule = '';
544 if ($flush) {
545 $flush_rule = 'flush';
546 }
547
548 /* Already loaded */
549 if (!$flush && (mwexec("/sbin/ipfw list 1000", true) == 0)) {
550 return;
551 }
552
553 $cprules = <<<EOD
554{$flush_rule}
555# Table with interfaces that have CP enabled
556table cp_ifaces create type iface valtype skipto
557
558# Redirect each CP interface to its specific rule
559add 1000 skipto tablearg all from any to any via table(cp_ifaces)
560
561# This interface has no cp zone configured
562add 1100 allow all from any to any
563
564# block everything else
565add 65534 deny all from any to any
566EOD;
567
568 /* load rules */
569 file_put_contents("{$g['tmp_path']}/ipfw.cp.rules", $cprules);
570 mwexec("/sbin/ipfw -q {$g['tmp_path']}/ipfw.cp.rules", true);
571 @unlink("{$g['tmp_path']}/ipfw_{$cpzone}.cp.rules");
572 unset($cprules);
573}
574
575/* Create a string with ipfw rule and increase rulenum */
576function captiveportal_create_ipfw_rule($cmd, &$rulenum, $args) {
577 $rule = "{$cmd} {$rulenum} {$args}\n";
578 $rulenum++;
579
580 return $rule;
581}
582
583/* Return first rule number for a cp zone */
584function captiveportal_ipfw_ruleno($id) {
585 global $g;
586
587 return 2000 + $id * $g['captiveportal_rules_interval'];
588}
589
590/* reinit will disconnect all users, be careful! */
591function captiveportal_init_rules($reinit = false) {
592 global $config, $g, $cpzone, $cpzoneid;
593
594 if (!isset($config['captiveportal'][$cpzone]['enable'])) {
595 return;
596 }
597
598 captiveportal_load_modules();
599 captiveportal_init_general_rules();
600
601
602
603 $skipto = captiveportal_ipfw_ruleno($cpzoneid);
604
605 $cprules = '';
606
607 $cpips = array();
608 $ifaces = get_configured_interface_list();
609 $cpinterfaces = explode(",", $config['captiveportal'][$cpzone]['interface']);
610 $firsttime = 0;
611 foreach ($cpinterfaces as $cpifgrp) {
612 if (!isset($ifaces[$cpifgrp])) {
613 continue;
614 }
615 $tmpif = get_real_interface($cpifgrp);
616 if (empty($tmpif)) {
617 continue;
618 }
619
620 $cpipm = get_interface_ip($cpifgrp);
621
622 if (!is_ipaddr($cpipm)) {
623 continue;
624 }
625
626 $cpips[] = $cpipm;
627 if (is_array($config['virtualip']['vip'])) {
628 foreach ($config['virtualip']['vip'] as $vip) {
629 if (($vip['interface'] == $cpifgrp) &&
630 (($vip['mode'] == "carp") ||
631 ($vip['mode'] == "ipalias"))) {
632 $cpips[] = $vip['subnet'];
633 }
634 }
635 }
636
637 $cprules .= "table cp_ifaces add {$tmpif} {$skipto}\n";
638 }
639 if (count($cpips) > 0) {
640 $cpactive = true;
641 } else {
642 return false;
643 }
644
645 $captiveportallck = try_lock("captiveportal{$cpzone}", 0);
646
647 $tables = captiveportal_get_ipfw_table_names();
648
649 $rulenum = $skipto;
650 $cprules .= "table {$cpzone}_pipe_mac create type mac valtype pipe\n";
651 $cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
652 "pipe tablearg MAC table({$cpzone}_pipe_mac)");
653 $cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
654 "allow pfsync from any to any");
655 $cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
656 "allow carp from any to any\n");
657 $cprules .= "# layer 2: pass ARP\n";
658 $cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
659 "pass layer2 mac-type arp,rarp");
660 $cprules .= "# pfsense requires for WPA\n";
661 $cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
662 "pass layer2 mac-type 0x888e,0x88c7");
663 $cprules .= "# PPP Over Ethernet Session Stage/Discovery Stage\n";
664 $cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
665 "pass layer2 mac-type 0x8863,0x8864\n");
666 $cprules .= "# layer 2: block anything else non-IP(v4/v6)\n";
667 $cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
668 "deny layer2 not mac-type ip,ipv6");
669
670 /* These tables contain host ips */
671 $cprules .= "table {$cpzone}_host_ips create type addr\n";
672 $cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
673 "pass ip from any to table({$cpzone}_host_ips) in");
674 $cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
675 "pass ip from table({$cpzone}_host_ips) to any out");
676 foreach ($cpips as $cpip) {
677 $cprules .= "table {$cpzone}_host_ips add {$cpip}\n";
678 }
679 $cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
680 "pass ip from any to 255.255.255.255 in");
681 $cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
682 "pass ip from 255.255.255.255 to any out");
683
684 /* Allowed ips */
685 $cprules .= "table {$cpzone}_allowed_up create type addr valtype pipe\n";
686 $cprules .= "table {$cpzone}_allowed_down create type addr valtype pipe\n";
687 $cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
688 "pipe tablearg ip from table({$cpzone}_allowed_up) to any in");
689 $cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
690 "pipe tablearg ip from any to table({$cpzone}_allowed_down) in");
691 $cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
692 "pipe tablearg ip from table({$cpzone}_allowed_up) to any out");
693 $cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
694 "pipe tablearg ip from any to table({$cpzone}_allowed_down) out");
695
696 /* Authenticated users rules. */
697 if (!in_array("{$cpzone}_auth_up", $tables)) {
698 $cprules .= "table {$cpzone}_auth_up create type addr valtype pipe\n";
699 }
700 if (!in_array("{$cpzone}_auth_down", $tables)) {
701 $cprules .= "table {$cpzone}_auth_down create type addr valtype pipe\n";
702 }
703 $cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
704 "pipe tablearg ip from table({$cpzone}_auth_up) to any layer2 in");
705 $cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
706 "pipe tablearg ip from any to table({$cpzone}_auth_down) layer2 out");
707
708 if (!empty($config['captiveportal'][$cpzone]['listenporthttp'])) {
709 $listenporthttp = $config['captiveportal'][$cpzone]['listenporthttp'];
710 } else {
711 $listenporthttp = 8000 + $cpzoneid;
712 }
713
714 if (isset($config['captiveportal'][$cpzone]['httpslogin'])) {
715 if (!empty($config['captiveportal'][$cpzone]['listenporthttps'])) {
716 $listenporthttps = $config['captiveportal'][$cpzone]['listenporthttps'];
717 } else {
718 $listenporthttps = 8001 + $cpzoneid;
719 }
720 if (!isset($config['captiveportal'][$cpzone]['nohttpsforwards'])) {
721 $cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
722 "fwd 127.0.0.1,{$listenporthttps} tcp from any to any dst-port 443 in");
723 }
724 }
725
726 $cprules .= "# redirect non-authenticated clients to captive portal\n";
727 $cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
728 "fwd 127.0.0.1,{$listenporthttp} tcp from any to any dst-port 80 in");
729 $cprules .= "# let the responses from the captive portal web server back out\n";
730 $cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
731 "pass tcp from any to any out");
732 $cprules .= "# This CP zone is over, skip to last rule\n";
733 $cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
734 "skipto 65534 all from any to any");
735
736 /* generate passthru mac database */
737 $cprules .= captiveportal_passthrumac_configure(true);
738 $cprules .= "\n";
739
740 /* allowed ipfw rules to make allowed ip work */
741 $cprules .= captiveportal_allowedip_configure();
742
743 /* allowed ipfw rules to make allowed hostnames work */
744 $cprules .= captiveportal_allowedhostname_configure();
745
746 /* load rules */
747 /* if reinit : flush pipes, so nothing is leaked. if not reinit, just destroy tables without flushing pipes */
748 if ($reinit) {
749 $pipes_to_remove = captiveportal_free_dnrules();
750 captiveportal_delete_rules($pipes_to_remove);
751 } else {
752 captiveportal_delete_rules(array(), $reinit);
753 }
754
755 /* load rules */
756 $captiveportallck = lock("captiveportal{$cpzone}");
757 file_put_contents("{$g['tmp_path']}/ipfw_{$cpzone}.cp.rules", $cprules);
758 mwexec("/sbin/ipfw -q {$g['tmp_path']}/ipfw_{$cpzone}.cp.rules", true);
759 @unlink("{$g['tmp_path']}/ipfw_{$cpzone}.cp.rules");
760 unset($cprules);
761
762 captiveportal_filterdns_configure();
763
764
765 unlock($captiveportallck);
766 if ($reinit) {
767 unlink_if_exists("{$g['vardb_path']}/captiveportal{$cpzone}.db");
768 } else {
769 foreach (captiveportal_read_db() as $user) {
770 $bw_up = intval($user['bw_up']);
771 $bw_down = intval($user['bw_down']);
772 $clientip = $user['ip'];
773 $clientmac = $user['mac'];
774 $bw_up_pipeno = $user['pipeno'];
775 $bw_down_pipeno = $user['pipeno'] + 1;
776
777 $rule_entry = "{$clientip}/" . (is_ipaddrv6($clientip) ? "128" : "32");
778 if (!isset($config['captiveportal'][$cpzone]['nomacfilter'])) {
779 $rule_entry .= ",{$clientmac}";
780 }
781 $_gb = @pfSense_ipfw_pipe("pipe {$bw_up_pipeno} config bw {$bw_up}Kbit/s queue 100 buckets 16");
782 $_gb = @pfSense_ipfw_pipe("pipe {$bw_down_pipeno} config bw {$bw_down}Kbit/s queue 100 buckets 16");
783
784 $_gb = @pfSense_ipfw_table("{$cpzone}_auth_up", IP_FW_TABLE_XADD, "{$rule_entry}", $bw_up_pipeno);
785 $_gb = @pfSense_ipfw_table("{$cpzone}_auth_down", IP_FW_TABLE_XADD, "{$rule_entry}", $bw_down_pipeno);
786 }
787 }
788}
789
790/* Delete all rules related to specific cpzone */
791function captiveportal_delete_rules($pipes_to_remove = array(), $clear_auth_rules = true) {
792 global $g, $cpzoneid, $cpzone;
793
794 $skipto1 = captiveportal_ipfw_ruleno($cpzoneid);
795 $skipto2 = $skipto1 + $g['captiveportal_rules_interval'];
796
797 $cp_ifaces = pfSense_ipfw_table_list("cp_ifaces");
798 if (is_array($cp_ifaces)) {
799 foreach ($cp_ifaces as $cp_iface) {
800 if (empty($cp_iface['skipto']) ||
801 $cp_iface['skipto'] != $skipto1) {
802 continue;
803 }
804
805 pfSense_ipfw_table("cp_ifaces", IP_FW_TABLE_XDEL,
806 $cp_iface['iface']);
807 }
808 }
809
810 mwexec("/sbin/ipfw delete {$skipto1}-{$skipto2}", true);
811
812 $tables = captiveportal_get_ipfw_table_names();
813
814 $delrules = "";
815 foreach ($tables as $table) {
816 if (!$clear_auth_rules && substr($table, 0, strlen($cpzone . '_auth')) == $cpzone . '_auth') {
817 continue;
818 } else {
819 $delrules .= "table {$table} destroy\n";
820 }
821 }
822
823 foreach ($pipes_to_remove as $pipeno) {
824 $delrules .= "pipe delete {$pipeno}\n";
825 }
826
827 if (empty($delrules)) {
828 return;
829 }
830
831 file_put_contents("{$g['tmp_path']}/ipfw_{$cpzone}.deltable.rules", $delrules);
832 mwexec("/sbin/ipfw -q {$g['tmp_path']}/ipfw_{$cpzone}.deltable.rules", true);
833 @unlink("{$g['tmp_path']}/ipfw_{$cpzone}.deltable.rules");
834}
835
836/*
837 * Remove clients that have been around for longer than the specified amount of time
838 * db file structure:
839 * timestamp,ipfw_rule_no,clientip,clientmac,username,sessionid,password,session_timeout,idle_timeout,session_terminate_time,interim_interval,traffic_quota,auth_method,context
840 * (password is in Base64 and only saved when reauthentication is enabled)
841 */
842function captiveportal_prune_old() {
843 global $g, $config, $cpzone, $cpzoneid;
844
845 if (empty($cpzone)) {
846 return;
847 }
848
849 $cpcfg = $config['captiveportal'][$cpzone];
850 $vcpcfg = $config['voucher'][$cpzone];
851
852 /* check for expired entries */
853 $idletimeout = 0;
854 $timeout = 0;
855 if (!empty($cpcfg['timeout']) && is_numeric($cpcfg['timeout'])) {
856 $timeout = $cpcfg['timeout'] * 60;
857 }
858
859 if (!empty($cpcfg['idletimeout']) && is_numeric($cpcfg['idletimeout'])) {
860 $idletimeout = $cpcfg['idletimeout'] * 60;
861 }
862
863 /* check for entries exceeding their traffic quota */
864 $trafficquota = 0;
865 if (!empty($cpcfg['trafficquota']) && is_numeric($cpcfg['trafficquota'])) {
866 $trafficquota = $cpcfg['trafficquota'] * 1048576;
867 }
868
869 /* Is there any job to do? */
870 if (!$timeout && !$idletimeout && !$trafficquota && !isset($cpcfg['reauthenticate']) &&
871 !isset($cpcfg['radiussession_timeout']) && !isset($cpcfg['radiustraffic_quota']) &&
872 !isset($vcpcfg['enable']) && !isset($cpcfg['radacct_enable'])) {
873 return;
874 }
875
876
877 /* Read database */
878 /* NOTE: while this can be simplified in non radius case keep as is for now */
879 $cpdb = captiveportal_read_db();
880
881 $unsetindexes = array();
882 $voucher_needs_sync = false;
883 /*
884 * Snapshot the time here to use for calculation to speed up the process.
885 * If something is missed next run will catch it!
886 */
887 $pruning_time = time();
888 foreach ($cpdb as $cpentry) {
889 $stop_time = $pruning_time;
890
891 $timedout = false;
892 $term_cause = 1;
893 /* hard timeout or session_timeout from radius if enabled */
894 if (isset($cpcfg['radiussession_timeout'])) {
895 $utimeout = (is_numeric($cpentry[7])) ? $cpentry[7] : $timeout;
896 } else {
897 $utimeout = $timeout;
898 }
899 if ($utimeout) {
900 if (($pruning_time - $cpentry[0]) >= $utimeout) {
901 $timedout = true;
902 $term_cause = 5; // Session-Timeout
903 $logout_cause = 'SESSION TIMEOUT';
904 }
905 }
906
907 /* Session-Terminate-Time */
908 if (!$timedout && !empty($cpentry[9])) {
909 if ($pruning_time >= $cpentry[9]) {
910 $timedout = true;
911 $term_cause = 5; // Session-Timeout
912 $logout_cause = 'SESSION TIMEOUT';
913 }
914 }
915
916 /* check if an idle_timeout has been set and if its set change the idletimeout to this value */
917 $uidletimeout = (is_numeric($cpentry[8])) ? $cpentry[8] : $idletimeout;
918 /* if an idle timeout is specified, get last activity timestamp from ipfw */
919 if (!$timedout && $uidletimeout > 0) {
920 $lastact = captiveportal_get_last_activity($cpentry[2]);
921 /* If the user has logged on but not sent any traffic they will never be logged out.
922 * We "fix" this by setting lastact to the login timestamp.
923 */
924 $lastact = $lastact ? $lastact : $cpentry[0];
925 if ($lastact && (($pruning_time - $lastact) >= $uidletimeout)) {
926 $timedout = true;
927 $term_cause = 4; // Idle-Timeout
928 $logout_cause = 'IDLE TIMEOUT';
929 if (!isset($config['captiveportal'][$cpzone]['includeidletime'])) {
930 $stop_time = $lastact;
931 }
932 }
933 }
934
935 /* if vouchers are configured, activate session timeouts */
936 if (!$timedout && isset($vcpcfg['enable']) && !empty($cpentry[7])) {
937 if ($pruning_time >= ($cpentry[0] + $cpentry[7])) {
938 $timedout = true;
939 $term_cause = 5; // Session-Timeout
940 $logout_cause = 'SESSION TIMEOUT';
941 $voucher_needs_sync = true;
942 }
943 }
944
945 /* traffic quota, value retrieved from the radius attribute if the option is enabled */
946 if (isset($cpcfg['radiustraffic_quota'])) {
947 $utrafficquota = (is_numeric($cpentry[11])) ? $cpentry[11] : $trafficquota;
948 } else {
949 $utrafficquota = $trafficquota;
950 }
951 if (!$timedout && $utrafficquota > 0) {
952 $volume = getVolume($cpentry[2], $cpentry[3]);
953 if (($volume['input_bytes'] + $volume['output_bytes']) > $utrafficquota ) {
954 $timedout = true;
955 $term_cause = 10; // NAS-Request
956 $logout_cause = 'QUOTA EXCEEDED';
957 }
958 }
959
960 if ($timedout) {
961 captiveportal_disconnect($cpentry, $term_cause, $stop_time);
962 captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], $logout_cause);
963 $unsetindexes[] = $cpentry[5];
964 }
965
966 /* do periodic reauthentication? For Radius servers, send accounting updates? */
967 if (!$timedout) {
968 //Radius servers : send accounting
969 if (isset($cpcfg['radacct_enable']) && $cpentry['authmethod'] === 'radius') {
970 if (substr($cpcfg['reauthenticateacct'], 0, 9) === "stopstart") {
971 /* stop and restart accounting */
972 if ($cpcfg['reauthenticateacct'] === "stopstartfreeradius") {
973 $rastart_time = 0;
974 $rastop_time = 60;
975 } else {
976 $rastart_time = $cpentry[0];
977 $rastop_time = time();
978 }
979 captiveportal_send_server_accounting('stop',
980 $cpentry[1], // ruleno
981 $cpentry[4], // username
982 $cpentry[2], // clientip
983 $cpentry[3], // clientmac
984 $cpentry[5], // sessionid
985 $rastart_time, // start time
986 $rastop_time, // Stop Time
987 10); // NAS Request
988 $clientsn = (is_ipaddrv6($cpentry[2])) ? 128 : 32;
989 pfSense_ipfw_table_zerocnt("{$cpzone}_auth_up", "{$cpentry[2]}/{$clientsn}");
990 pfSense_ipfw_table_zerocnt("{$cpzone}_auth_down", "{$cpentry[2]}/{$clientsn}");
991 if ($cpcfg['reauthenticateacct'] == "stopstartfreeradius") {
992 /* Need to pause here or the FreeRADIUS server gets confused about packet ordering. */
993 sleep(1);
994 }
995 captiveportal_send_server_accounting('start',
996 $cpentry[1], // ruleno
997 $cpentry[4], // username
998 $cpentry[2], // clientip
999 $cpentry[3], // clientmac
1000 $cpentry[5]); // sessionid
1001 } else if ($cpcfg['reauthenticateacct'] == "interimupdate") {
1002 $session_time = $pruning_time - $cpentry[0];
1003 if (!empty($cpentry[10]) && $cpentry[10] > 60) {
1004 $interval = $cpentry[10];
1005 } else {
1006 $interval = 0;
1007 }
1008 $past_interval_min = ($session_time > $interval);
1009 if ($interval != 0) {
1010 $within_interval = ($session_time % $interval >= 0 && $session_time % $interval <= 59);
1011 }
1012 if ($interval === 0 || ($interval > 0 && $past_interval_min && $within_interval)) {
1013 captiveportal_send_server_accounting('update',
1014 $cpentry[1], // ruleno
1015 $cpentry[4], // username
1016 $cpentry[2], // clientip
1017 $cpentry[3], // clientmac
1018 $cpentry[5], // sessionid
1019 $cpentry[0]); // start time
1020 }
1021 }
1022 }
1023
1024 /* check this user again */
1025 if (isset($cpcfg['reauthenticate']) && $cpentry['context'] !== 'voucher') {
1026 $auth_result = captiveportal_authenticate_user(
1027 $cpentry[4], // username
1028 base64_decode($cpentry[6]), // password
1029 $cpentry[3], // clientmac
1030 $cpentry[2], // clientip
1031 $cpentry[1], // ruleno
1032 $cpentry['context']); // context
1033 if ($auth_result['result'] === false) {
1034 captiveportal_disconnect($cpentry, 17);
1035 captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "DISCONNECT - REAUTHENTICATION FAILED", $auth_list['reply_message']);
1036 $unsetindexes[] = $cpentry[5];
1037 } else if ($auth_result['result'] === true) {
1038 if ($cpentry['authmethod'] !== $auth_result['auth_method']) {
1039 // if the user got authenticated against another server type: we update the database
1040 if (!empty($cpentry[5])) {
1041 captiveportal_update_entry($cpentry['sessionid'], $auth_result['auth_method'], 'authmethod');
1042 captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CHANGED AUTHENTICATION SERVER", $auth_list['reply_message']);
1043 }
1044 // User was logged on a RADIUS server, but is now logged in by another server type : we send an accounting Stop
1045 if (isset($config['captiveportal'][$cpzone]['radacct_enable']) && $cpentry['authmethod'] == 'radius') {
1046 if ($cpcfg['reauthenticateacct'] === "stopstartfreeradius") {
1047 $rastart_time = 0;
1048 $rastop_time = 60;
1049 } else {
1050 $rastart_time = $cpentry[0];
1051 $rastop_time = time();
1052 }
1053 captiveportal_send_server_accounting('stop',
1054 $cpentry[1], // ruleno
1055 $cpentry[4], // username
1056 $cpentry[2], // clientip
1057 $cpentry[3], // clientmac
1058 $cpentry[5], // sessionid
1059 $rastart_time, // start time
1060 $rastop_time, // Stop Time
1061 3); // Lost Service
1062 // User was logged on a non-RADIUS Server but is now logged in by a RADIUS server : we send an accounting Start
1063 } else if(isset($config['captiveportal'][$cpzone]['radacct_enable']) && $auth_result['auth_method'] === 'radius') {
1064 captiveportal_send_server_accounting('start',
1065 $cpentry[1], // ruleno
1066 $cpentry[4], // username
1067 $cpentry[2], // clientip
1068 $cpentry[3], // clientmac
1069 $cpentry[5], // sessionid
1070 $cpentry[0]); // start_time
1071 }
1072 }
1073 captiveportal_reapply_attributes($cpentry, $auth_result['attributes']);
1074 }
1075 }
1076 }
1077 }
1078 unset($cpdb);
1079
1080 captiveportal_prune_old_automac();
1081
1082 if ($voucher_needs_sync == true) {
1083 /* Trigger a sync of the vouchers on config */
1084 send_event("service sync vouchers");
1085 }
1086
1087 /* write database */
1088 if (!empty($unsetindexes)) {
1089 captiveportal_remove_entries($unsetindexes);
1090 }
1091}
1092
1093function captiveportal_prune_old_automac() {
1094 global $g, $config, $cpzone, $cpzoneid;
1095
1096 if (is_array($config['captiveportal'][$cpzone]['passthrumac']) &&
1097 isset($config['captiveportal'][$cpzone]['passthrumacadd'])) {
1098 $tmpvoucherdb = array();
1099 $macrules = "";
1100 $writecfg = false;
1101 foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $eid => $emac) {
1102 if ($emac['logintype'] != "voucher") {
1103 continue;
1104 }
1105 if (isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) {
1106 if (isset($tmpvoucherdb[$emac['username']])) {
1107 $temac = $config['captiveportal'][$cpzone]['passthrumac'][$tmpvoucherdb[$emac['username']]];
1108 $pipeno = captiveportal_get_dn_passthru_ruleno($temac['mac']);
1109 if ($pipeno) {
1110 captiveportal_free_dn_ruleno($pipeno);
1111 $macrules .= "table {$cpzone}_pipe_mac delete any,{$temac['mac']}\n";
1112 $macrules .= "table {$cpzone}_pipe_mac delete {$temac['mac']},any\n";
1113 $macrules .= "pipe delete {$pipeno}\n";
1114 ++$pipeno;
1115 $macrules .= "pipe delete {$pipeno}\n";
1116 }
1117 $writecfg = true;
1118 captiveportal_logportalauth($temac['username'], $temac['mac'],
1119 $temac['ip'], "DUPLICATE {$temac['username']} LOGIN - TERMINATING OLD SESSION");
1120 unset($config['captiveportal'][$cpzone]['passthrumac'][$tmpvoucherdb[$emac['username']]]);
1121 }
1122 $tmpvoucherdb[$emac['username']] = $eid;
1123 }
1124 if (voucher_auth($emac['username']) <= 0) {
1125 $pipeno = captiveportal_get_dn_passthru_ruleno($emac['mac']);
1126 if ($pipeno) {
1127 captiveportal_free_dn_ruleno($pipeno);
1128 $macrules .= "table {$cpzone}_pipe_mac delete any,{$emac['mac']}\n";
1129 $macrules .= "table {$cpzone}_pipe_mac delete {$emac['mac']},any\n";
1130 $macrules .= "pipe delete {$pipeno}\n";
1131 ++$pipeno;
1132 $macrules .= "pipe delete {$pipeno}\n";
1133 }
1134 $writecfg = true;
1135 captiveportal_logportalauth($emac['username'], $emac['mac'],
1136 $emac['ip'], "EXPIRED {$emac['username']} LOGIN - TERMINATING SESSION");
1137 unset($config['captiveportal'][$cpzone]['passthrumac'][$eid]);
1138 }
1139 }
1140 unset($tmpvoucherdb);
1141 if (!empty($macrules)) {
1142 @file_put_contents("{$g['tmp_path']}/macentry.prunerules.tmp", $macrules);
1143 unset($macrules);
1144 mwexec("/sbin/ipfw -q {$g['tmp_path']}/macentry.prunerules.tmp");
1145 }
1146 if ($writecfg === true) {
1147 write_config("Prune session for auto-added macs");
1148 }
1149 }
1150}
1151
1152/* remove a single client according to the DB entry */
1153function captiveportal_disconnect($dbent, $term_cause = 1, $stop_time = null) {
1154 global $g, $config, $cpzone, $cpzoneid;
1155
1156 $stop_time = (empty($stop_time)) ? time() : $stop_time;
1157
1158 /* this client needs to be deleted - remove ipfw rules */
1159 if (isset($config['captiveportal'][$cpzone]['radacct_enable']) && $dbent['authmethod'] == 'radius') {
1160 if ($config['captiveportal'][$cpzone]['reauthenticateacct'] == "stopstartfreeradius") {
1161 /*
1162 * Interim updates are on so the session time must be
1163 * reported as the elapsed time since the previous
1164 * interim update.
1165 */
1166 $session_time = ($stop_time - $dbent[0]) % 60;
1167 $start_time = $stop_time - $session_time;
1168 } else {
1169 $start_time = $dbent[0];
1170 }
1171 captiveportal_send_server_accounting('stop',
1172 $dbent[1], // ruleno
1173 $dbent[4], // username
1174 $dbent[2], // clientip
1175 $dbent[3], // clientmac
1176 $dbent[5], // sessionid
1177 $start_time, // start time
1178 $stop_time, // stop time
1179 $term_cause); // Acct-Terminate-Cause
1180 }
1181
1182 if (is_ipaddr($dbent[2])) {
1183 /*
1184 * Delete client's ip entry from tables auth_up and auth_down.
1185 * It's not necessary to explicit specify mac address here
1186 */
1187 $cpsession = captiveportal_isip_logged($dbent[2]);
1188 if (!empty($cpsession)) {
1189 $clientsn = (is_ipaddrv6($dbent[2])) ? 128 : 32;
1190 pfSense_ipfw_table("{$cpzone}_auth_up",
1191 IP_FW_TABLE_XDEL, "{$dbent[2]}/{$clientsn}");
1192 pfSense_ipfw_table("{$cpzone}_auth_down",
1193 IP_FW_TABLE_XDEL, "{$dbent[2]}/{$clientsn}");
1194 }
1195 /* XXX: Redundant?! Ensure all pf(4) states are killed. */
1196 $_gb = @pfSense_kill_states($dbent[2]);
1197 $_gb = @pfSense_kill_srcstates($dbent[2]);
1198 }
1199
1200 /*
1201 * These are the pipe numbers we use to control traffic shaping for
1202 * each logged in user via captive portal
1203 * We could get an error if the pipe doesn't exist but everything
1204 * should still be fine
1205 */
1206 if (!empty($dbent[1])) {
1207 /*
1208 * Call captiveportal_free_dnrules() in dry_run mode to verify
1209 * if there are pipes to be removed and prevent the attempt to
1210 * delete invalid pipes
1211 */
1212 $removed_pipes = captiveportal_free_dnrules($dbent[1],
1213 $dbent[1]+1, true);
1214
1215 if (!empty($removed_pipes)) {
1216 $_gb = @pfSense_ipfw_pipe("pipe delete {$dbent[1]}");
1217 $_gb = @pfSense_ipfw_pipe("pipe delete " .
1218 ($dbent[1]+1));
1219
1220 /*
1221 * Release the ruleno so it can be reallocated to new
1222 * clients
1223 */
1224 captiveportal_free_dn_ruleno($dbent[1]);
1225 }
1226 }
1227
1228 // XMLRPC Call over to the master Voucher node
1229 if (xmlrpc_sync_voucher_details($syncip, $syncport,
1230 $vouchersyncusername, $syncpass)) {
1231 $remote_status = xmlrpc_sync_voucher_disconnect($dbent, $syncip,
1232 $syncport, $syncpass, $vouchersyncusername, $term_cause,
1233 $stop_time);
1234 }
1235
1236}
1237
1238/* remove a single client by sessionid */
1239function captiveportal_disconnect_client($sessionid, $term_cause = 1, $logoutReason = "LOGOUT") {
1240 global $g, $config;
1241
1242 $sessionid = SQLite3::escapeString($sessionid);
1243 /* read database */
1244 $result = captiveportal_read_db("WHERE sessionid = '{$sessionid}'");
1245
1246 /* find entry */
1247 if (!empty($result)) {
1248
1249 foreach ($result as $cpentry) {
1250 captiveportal_disconnect($cpentry, $term_cause);
1251 captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "DISCONNECT");
1252 }
1253 captiveportal_remove_entries(array($sessionid));
1254 unset($result);
1255 }
1256}
1257
1258/* remove all clients */
1259function captiveportal_disconnect_all($term_cause = 6, $logoutReason = "DISCONNECT") {
1260 global $g, $config, $cpzone, $cpzoneid;
1261
1262 /* check if we're pruning old entries and eventually wait */
1263 $rcprunelock = try_lock("rcprunecaptiveportal{$cpzone}", 15);
1264
1265 /* if we still don't have the lock, unlock forcefully and take it */
1266 if (!$rcprunelock) {
1267 log_error("CP zone ${cpzone}: could not obtain the lock for more than 15 seconds, lock taken forcefully to disconnect all users");
1268 unlock_force("rcprunecaptiveportal{$cpzone}");
1269 $rcprunelock = lock("rcprunecaptiveportal{$cpzone}", LOCK_EX);
1270 }
1271
1272 /* take a lock so new users won't be able to log in */
1273 $cpdblck = lock("captiveportaldb{$cpzone}", LOCK_EX);
1274
1275 captiveportal_radius_stop_all($term_cause, $logoutReason);
1276
1277 /* reinit ipfw rules, flush user database */
1278
1279 captiveportal_init_rules(true);
1280
1281 unlock($cpdblck);
1282 unlock($rcprunelock);
1283}
1284
1285/* send RADIUS acct stop for all current clients connected with RADIUS servers */
1286function captiveportal_radius_stop_all($term_cause = 6, $logoutReason = "DISCONNECT") {
1287 global $g, $config, $cpzone, $cpzoneid;
1288
1289 $cpdb = captiveportal_read_db();
1290
1291 $radacct = isset($config['captiveportal'][$cpzone]['radacct_enable']) ? true : false;
1292 foreach ($cpdb as $cpentry) {
1293 if ($cpentry['authmethod'] === 'radius' && $radacct) {
1294 if ($config['captiveportal'][$cpzone]['reauthenticateacct'] == "stopstartfreeradius") {
1295 $session_time = (time() - $cpentry[0]) % 60;
1296 $start_time = time() - $session_time;
1297 } else {
1298 $start_time = $cpentry[0];
1299 }
1300 captiveportal_send_server_accounting('stop',
1301 $cpentry[1], // ruleno
1302 $cpentry[4], // username
1303 $cpentry[2], // clientip
1304 $cpentry[3], // clientmac
1305 $cpentry[5], // sessionid
1306 $start_time, // start time
1307 $stop_time, // stop time
1308 $term_cause); // Acct-Terminate-Cause
1309 }
1310 captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], $logoutReason);
1311 }
1312 unset($cpdb);
1313}
1314
1315function captiveportal_passthrumac_configure_entry($macent, $pipeinrule = false) {
1316 global $config, $g, $cpzone;
1317
1318 $bwUp = 0;
1319 if (!empty($macent['bw_up'])) {
1320 $bwUp = $macent['bw_up'];
1321 } else if (!empty($config['captiveportal'][$cpzone]['bwdefaultup'])) {
1322 $bwUp = $config['captiveportal'][$cpzone]['bwdefaultup'];
1323 }
1324 $bwDown = 0;
1325 if (!empty($macent['bw_down'])) {
1326 $bwDown = $macent['bw_down'];
1327 } else if (!empty($config['captiveportal'][$cpzone]['bwdefaultdn'])) {
1328 $bwDown = $config['captiveportal'][$cpzone]['bwdefaultdn'];
1329 }
1330
1331 if ($macent['action'] == 'pass') {
1332 $rules = "";
1333
1334 $pipeno = captiveportal_get_next_dn_ruleno('pipe_mac');
1335
1336 $pipeup = $pipeno;
1337 if ($pipeinrule == true) {
1338 $_gb = @pfSense_ipfw_pipe("pipe {$pipeno} config bw {$bwUp}Kbit/s queue 100 buckets 16");
1339 } else {
1340 $rules .= "pipe {$pipeno} config bw {$bwUp}Kbit/s queue 100 buckets 16\n";
1341 }
1342
1343 $pipedown = $pipeno + 1;
1344 if ($pipeinrule == true) {
1345 $_gb = @pfSense_ipfw_pipe("pipe {$pipedown} config bw {$bwDown}Kbit/s queue 100 buckets 16");
1346 } else {
1347 $rules .= "pipe {$pipedown} config bw {$bwDown}Kbit/s queue 100 buckets 16\n";
1348 }
1349
1350 $rules .= "table {$cpzone}_pipe_mac add any,{$macent['mac']} {$pipeup}\n";
1351 $rules .= "table {$cpzone}_pipe_mac add {$macent['mac']},any {$pipedown}\n";
1352 }
1353
1354 return $rules;
1355}
1356
1357function captiveportal_passthrumac_delete_entry($macent) {
1358 global $cpzone;
1359 $rules = "";
1360
1361 if ($macent['action'] == 'pass') {
1362 $pipeno = captiveportal_get_dn_passthru_ruleno($macent['mac']);
1363
1364 if (!empty($pipeno)) {
1365 captiveportal_free_dn_ruleno($pipeno);
1366 $rules .= "table {$cpzone}_pipe_mac delete any,{$macent['mac']}\n";
1367 $rules .= "table {$cpzone}_pipe_mac delete {$macent['mac']},any\n";
1368 $rules .= "pipe delete " . $pipeno . "\n";
1369 $rules .= "pipe delete " . ++$pipeno . "\n";
1370 }
1371 }
1372
1373 return $rules;
1374}
1375
1376function captiveportal_passthrumac_configure($filename = false, $startindex = 0, $stopindex = 0) {
1377 global $config, $g, $cpzone;
1378
1379 $rules = "";
1380
1381 if (is_array($config['captiveportal'][$cpzone]['passthrumac'])) {
1382 if ($stopindex > 0) {
1383 $fd = fopen($filename, "w");
1384 for ($idx = $startindex; $idx <= $stopindex; $idx++) {
1385 if (isset($config['captiveportal'][$cpzone]['passthrumac'][$idx])) {
1386 $rules = captiveportal_passthrumac_configure_entry($config['captiveportal'][$cpzone]['passthrumac'][$idx]);
1387 fwrite($fd, $rules);
1388 }
1389 }
1390 fclose($fd);
1391
1392 return;
1393 } else {
1394 $nentries = count($config['captiveportal'][$cpzone]['passthrumac']);
1395 if ($nentries > 2000) {
1396 $nloops = $nentries / 1000;
1397 $remainder= $nentries % 1000;
1398 for ($i = 0; $i < $nloops; $i++) {
1399 mwexec_bg("/usr/local/sbin/fcgicli -f /etc/rc.captiveportal_configure_mac -d \"cpzone={$cpzone}&startidx=" . ($i * 1000) . "&stopidx=" . ((($i+1) * 1000) - 1) . "\"");
1400 }
1401 if ($remainder > 0) {
1402 mwexec_bg("/usr/local/sbin/fcgicli -f /etc/rc.captiveportal_configure_mac -d \"cpzone={$cpzone}&startidx=" . ($i * 1000) . "&stopidx=" . (($i* 1000) + $remainder) ."\"");
1403 }
1404 } else {
1405 foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $macent) {
1406 $rules .= captiveportal_passthrumac_configure_entry($macent, true);
1407 }
1408 }
1409 }
1410 }
1411
1412 return $rules;
1413}
1414
1415function captiveportal_passthrumac_findbyname($username) {
1416 global $config, $cpzone;
1417
1418 if (is_array($config['captiveportal'][$cpzone]['passthrumac'])) {
1419 foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $macent) {
1420 if ($macent['username'] == $username) {
1421 return $macent;
1422 }
1423 }
1424 }
1425 return NULL;
1426}
1427
1428/*
1429 * table (3=IN)/(4=OUT) hold allowed ip's without bw limits
1430 */
1431function captiveportal_allowedip_configure_entry($ipent, $ishostname = false) {
1432 global $g, $config, $cpzone;
1433
1434 /* Instead of copying this entire function for something
1435 * easy such as hostname vs ip address add this check
1436 */
1437 if ($ishostname === true) {
1438 if (!platform_booting()) {
1439 $ipaddress = gethostbyname($ipent['hostname']);
1440 if (!is_ipaddr($ipaddress)) {
1441 return;
1442 }
1443 } else {
1444 $ipaddress = "";
1445 }
1446 } else {
1447 $ipaddress = $ipent['ip'];
1448 }
1449
1450 $rules = "";
1451 $cp_filterdns_conf = "";
1452 $enBwup = 0;
1453 if (!empty($ipent['bw_up'])) {
1454 $enBwup = intval($ipent['bw_up']);
1455 } else if (!empty($config['captiveportal'][$cpzone]['bwdefaultup'])) {
1456 $enBwup = $config['captiveportal'][$cpzone]['bwdefaultup'];
1457 }
1458 $enBwdown = 0;
1459 if (!empty($ipent['bw_down'])) {
1460 $enBwdown = intval($ipent['bw_down']);
1461 } else if (!empty($config['captiveportal'][$cpzone]['bwdefaultdn'])) {
1462 $enBwdown = $config['captiveportal'][$cpzone]['bwdefaultdn'];
1463 }
1464
1465 $pipeup = captiveportal_get_next_dn_ruleno('allowed');
1466 $_gb = @pfSense_ipfw_pipe("pipe {$pipeup} config bw {$enBwup}Kbit/s queue 100 buckets 16");
1467 $pipedown = $pipeup + 1;
1468 $_gb = @pfSense_ipfw_pipe("pipe {$pipedown} config bw {$enBwdown}Kbit/s queue 100 buckets 16");
1469
1470 if ($ishostname === true) {
1471 $cp_filterdns_conf .= "ipfw {$ipent['hostname']} {$cpzone}_allowed_up pipe {$pipeup}\n";
1472 $cp_filterdns_conf .= "ipfw {$ipent['hostname']} {$cpzone}_allowed_down pipe {$pipedown}\n";
1473 if (!is_ipaddr($ipaddress)) {
1474 return array("", $cp_filterdns_conf);
1475 }
1476 }
1477
1478 $subnet = "";
1479 if (!empty($ipent['sn'])) {
1480 $subnet = "/{$ipent['sn']}";
1481 }
1482 $rules .= "table {$cpzone}_allowed_up add {$ipaddress}{$subnet} {$pipeup}\n";
1483 $rules .= "table {$cpzone}_allowed_down add {$ipaddress}{$subnet} {$pipedown}\n";
1484
1485 if ($ishostname === true) {
1486 return array($rules, $cp_filterdns_conf);
1487 } else {
1488 return $rules;
1489 }
1490}
1491
1492function captiveportal_allowedhostname_configure() {
1493 global $config, $g, $cpzone, $cpzoneid;
1494
1495 $rules = "";
1496 if (!is_array($config['captiveportal'][$cpzone]['allowedhostname'])) {
1497 return $rules;
1498 }
1499
1500 $rules = "\n# captiveportal_allowedhostname_configure()\n";
1501 $cp_filterdns_conf = "";
1502 foreach ($config['captiveportal'][$cpzone]['allowedhostname'] as $hostnameent) {
1503 $tmprules = captiveportal_allowedip_configure_entry($hostnameent, true);
1504 $rules .= $tmprules[0];
1505 $cp_filterdns_conf .= $tmprules[1];
1506 }
1507 $cp_filterdns_filename = "{$g['varetc_path']}/filterdns-{$cpzone}-captiveportal.conf";
1508 @file_put_contents($cp_filterdns_filename, $cp_filterdns_conf);
1509 unset($cp_filterdns_conf);
1510
1511 return $rules;
1512}
1513
1514function captiveportal_filterdns_configure() {
1515 global $config, $g, $cpzone, $cpzoneid;
1516
1517 $cp_filterdns_filename = $g['varetc_path'] .
1518 "/filterdns-{$cpzone}-captiveportal.conf";
1519
1520 if (isset($config['captiveportal'][$cpzone]['enable']) &&
1521 is_array($config['captiveportal'][$cpzone]['allowedhostname']) &&
1522 file_exists($cp_filterdns_filename)) {
1523 if (isvalidpid($g['varrun_path'] .
1524 "/filterdns-{$cpzone}-cpah.pid")) {
1525 sigkillbypid($g['varrun_path'] .
1526 "/filterdns-{$cpzone}-cpah.pid", "HUP");
1527 } else {
1528 mwexec("/usr/local/sbin/filterdns -p " .
1529 "{$g['varrun_path']}/filterdns-{$cpzone}-cpah.pid" .
1530 " -i 300 -c {$cp_filterdns_filename} -d 1");
1531 }
1532 } else {
1533 killbypid("{$g['varrun_path']}/filterdns-{$cpzone}-cpah.pid");
1534 @unlink("{$g['varrun_path']}/filterdns-{$cpzone}-cpah.pid");
1535 }
1536
1537 return $rules;
1538}
1539
1540function captiveportal_allowedip_configure() {
1541 global $config, $g, $cpzone;
1542
1543 $rules = "";
1544 if (is_array($config['captiveportal'][$cpzone]['allowedip'])) {
1545 foreach ($config['captiveportal'][$cpzone]['allowedip'] as $ipent) {
1546 $rules .= captiveportal_allowedip_configure_entry($ipent);
1547 }
1548 }
1549
1550 return $rules;
1551}
1552
1553/* get last activity timestamp given client IP address */
1554function captiveportal_get_last_activity($ip) {
1555 global $cpzone;
1556
1557 /* Reading only from one of the tables is enough of approximation. */
1558 $tables = array("{$cpzone}_allowed_up", "{$cpzone}_auth_up");
1559
1560 foreach ($tables as $table) {
1561 $ipfw = pfSense_ipfw_table_lookup($table, $ip);
1562 if (is_array($ipfw)) {
1563 /* Workaround for #46652 */
1564 if ($ipfw['packets'] > 0) {
1565 return $ipfw['timestamp'];
1566 } else {
1567 return 0;
1568 }
1569 }
1570 }
1571
1572 return 0;
1573}
1574
1575
1576/* log successful captive portal authentication to syslog */
1577/* part of this code from php.net */
1578function captiveportal_logportalauth($user, $mac, $ip, $status, $message = null) {
1579 // Log it
1580 if (!$message) {
1581 $message = "{$status}: {$user}, {$mac}, {$ip}";
1582 } else {
1583 $message = trim($message);
1584 $message = "{$status}: {$user}, {$mac}, {$ip}, {$message}";
1585 }
1586 captiveportal_syslog($message);
1587}
1588
1589/* log simple messages to syslog */
1590function captiveportal_syslog($message) {
1591 global $cpzone;
1592
1593 $message = trim($message);
1594 $message = "Zone: {$cpzone} - {$message}";
1595 openlog("logportalauth", LOG_PID, LOG_LOCAL4);
1596 // Log it
1597 syslog(LOG_INFO, $message);
1598 closelog();
1599}
1600
1601/* Authenticate users using Authentication Backend */
1602function captiveportal_authenticate_user(&$login = '', &$password = '', $clientmac = '', $clientip = '', $pipeno = 'null', $context = 'first') {
1603 global $g, $config, $cpzone;
1604 $cpcfg = $config['captiveportal'][$cpzone];
1605
1606 $login_status = 'FAILURE';
1607 $login_msg = gettext('Invalid credentials specified');
1608 $reply_attributes = array();
1609 $auth_method = '';
1610 $auth_result = null;
1611
1612 /*
1613 Management of the reply Message (reason why the authentication failed) :
1614 multiple authentication servers can be used, so multiple reply messages could theoretically be returned.
1615 But only one message is returned (the most important one).
1616 The return value of authenticate_user() define how important messages are :
1617 - Reply message of a successful auth is more important than reply message of
1618 a user failed auth(invalid credentials/authorization)
1619
1620 - Reply message of a user failed auth is more important than reply message of
1621 a server failed auth (unable to contact server)
1622
1623 - When multiple user failed auth are encountered, messages returned by remote servers
1624 (eg. reply in RADIUS Access-Reject) are more important than pfSense error messages.
1625
1626 The $authlevel variable is a flag indicating the status of authentication
1627 0 = failed server auth
1628 1 = failed user auth
1629 2 = failed user auth with custom server reply recieved
1630 3 = successful auth
1631 */
1632 $authlevel = 0;
1633
1634 /* Getting authentication servers from captiveportal configuration */
1635 $auth_servers = array();
1636
1637 if ($cpcfg['auth_method'] === 'none') {
1638 $auth_servers[] = array('type' => 'none');
1639 } else {
1640 if ($context === 'second') {
1641 $fullauthservers = explode(",", $cpcfg['auth_server2']);
1642 } else {
1643 $fullauthservers = explode(",", $cpcfg['auth_server']);
1644 }
1645
1646 foreach ($fullauthservers as $authserver) {
1647 if (strpos($authserver, ' - ') !== false) {
1648 $authserver = explode(' - ', $authserver);
1649 array_shift($authserver);
1650 $authserver = implode(' - ', $authserver);
1651
1652 if (auth_get_authserver($authserver) !== null) {
1653 $auth_servers[] = auth_get_authserver($authserver);
1654 } else {
1655 log_error("Zone: {$cpzone} - Captive portal was unable to find the settings of the server '{$authserver}' used for authentication !");
1656 }
1657 }
1658 }
1659 }
1660
1661 /* Unable to find the any authentication server config - shouldn't happen! - bail out */
1662 if (count($auth_servers) === 0) {
1663 log_error("Zone: {$cpzone} - No valid server could be used for authentication.");
1664 $login_msg = gettext("Internal Error");
1665 } else {
1666 foreach ($auth_servers as $authcfg) {
1667 if ($authlevel < 3) {
1668 $radmac_error = false;
1669 $attributes = array("nas_identifier" => empty($cpcfg["radiusnasid"]) ? "CaptivePortal-{$cpzone}" : $cpcfg["radiusnasid"],
1670 "nas_port_type" => RADIUS_ETHERNET,
1671 "nas_port" => $pipeno,
1672 "framed_ip" => $clientip);
1673 if (mac_format($clientmac) !== null) {
1674 $attributes["calling_station_id"] = mac_format($clientmac);
1675 }
1676
1677 $result = null;
1678 $status = null;
1679 $msg = null;
1680
1681 /* Radius MAC authentication */
1682 if ($context === 'radmac' && $clientmac) {
1683 if ($authcfg['type'] === 'radius') {
1684 $login = mac_format($clientmac);
1685 $status = "MACHINE LOGIN";
1686 } else {
1687 /* Trying to perform a Radius MAC authentication on a non-radius server - shouldn't happen! - bail out */
1688 $msg = gettext("Internal Error");
1689 log_error("Zone: {$cpzone} - Trying to perform RADIUS MAC authentication on a non-RADIUS server !");
1690 $radmac_error = true;
1691 $result = null;
1692 }
1693 }
1694
1695 if (!$radmac_error) {
1696 if ($authcfg['type'] === 'none') {
1697 $result = true;
1698 } else {
1699 $result = authenticate_user($login, $password, $authcfg, $attributes);
1700 }
1701
1702 if (!empty($attributes['error_message'])) {
1703 $msg = $attributes['error_message'];
1704 }
1705
1706 if ($authcfg['type'] == 'Local Auth' && $result && isset($cpcfg['localauth_priv'])) {
1707 if (!userHasPrivilege(getUserEntry($login), "user-services-captiveportal-login")) {
1708 $result = false;
1709 $msg = gettext("Access Denied");
1710 }
1711 }
1712 if ($context === 'radmac' && $result === null && empty($attributes['reply_message'])) {
1713 $msg = gettext("RADIUS MAC Authentication Failed.");
1714 }
1715
1716 if (empty($status)) {
1717 if ($result === true) {
1718 $status = "ACCEPT";
1719 } elseif ($result === null) {
1720 $status = "ERROR";
1721 } else {
1722 $status = "FAILURE";
1723 }
1724 }
1725
1726 if ($context === 'radmac' && $login == mac_format($clientmac) || $authcfg['type'] === 'none' && empty($login)) {
1727 $login = "unauthenticated";
1728 }
1729 }
1730 // We determine a flag
1731 if ($result === true) {
1732 $val = 3;
1733 } elseif ($result === false && !empty($attributes['reply_message'])) {
1734 $val = 2;
1735 $msg = $attributes['reply_message'];
1736 } elseif ($result === false) {
1737 $val = 1;
1738 } elseif ($result === null) {
1739 $val = 0;
1740 }
1741
1742 if ($val >= $authlevel) {
1743 $authlevel = $val;
1744 $auth_method = $authcfg['type'];
1745 $login_status = $status;
1746 $login_msg = $msg;
1747 $reply_attributes = $attributes;
1748 $auth_result = $result;
1749 }
1750 }
1751 }
1752 }
1753
1754 return array('result'=>$auth_result, 'attributes'=>$reply_attributes, 'auth_method' =>$auth_method, 'login_status'=> $login_status, 'login_message' => $login_msg);
1755}
1756
1757function captiveportal_opendb() {
1758 global $g, $config, $cpzone, $cpzoneid;
1759
1760 $db_path = "{$g['vardb_path']}/captiveportal{$cpzone}.db";
1761 $createquery = "CREATE TABLE IF NOT EXISTS captiveportal (" .
1762 "allow_time INTEGER, pipeno INTEGER, ip TEXT, mac TEXT, username TEXT, " .
1763 "sessionid TEXT, bpassword TEXT, session_timeout INTEGER, idle_timeout INTEGER, " .
1764 "session_terminate_time INTEGER, interim_interval INTEGER, traffic_quota INTEGER, " .
1765 "bw_up INTEGER, bw_down INTEGER, authmethod TEXT, context TEXT); " .
1766 "CREATE UNIQUE INDEX IF NOT EXISTS idx_active ON captiveportal (sessionid, username); " .
1767 "CREATE INDEX IF NOT EXISTS user ON captiveportal (username); " .
1768 "CREATE INDEX IF NOT EXISTS ip ON captiveportal (ip); " .
1769 "CREATE INDEX IF NOT EXISTS starttime ON captiveportal (allow_time)";
1770
1771 try {
1772 $DB = new SQLite3($db_path);
1773 $DB->busyTimeout(60000);
1774 } catch (Exception $e) {
1775 captiveportal_syslog("Could not open {$db_path} as an sqlite database for {$cpzone}. Error message: " . $e->getMessage() . " -- Trying again.");
1776 unlink_if_exists($db_path);
1777 try {
1778 $DB = new SQLite3($db_path);
1779 $DB->busyTimeout(60000);
1780 } catch (Exception $e) {
1781 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.");
1782 return;
1783 }
1784 }
1785
1786 if (!$DB) {
1787 captiveportal_syslog("Could not open {$db_path} as an sqlite database for {$cpzone}. Error message: {$DB->lastErrorMsg()}. Trying again.");
1788 unlink_if_exists($db_path);
1789 $DB = new SQLite3($db_path);
1790 $DB->busyTimeout(60000);
1791 if (!$DB) {
1792 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.");
1793 return;
1794 }
1795 }
1796
1797 if (! $DB->exec($createquery)) {
1798 captiveportal_syslog("Error during table {$cpzone} creation. Error message: {$DB->lastErrorMsg()}. Resetting and trying again.");
1799
1800 /* If unable to initialize the database, reset and try again. */
1801 $DB->close();
1802 unset($DB);
1803 unlink_if_exists($db_path);
1804 $DB = new SQLite3($db_path);
1805 $DB->busyTimeout(60000);
1806 if ($DB->exec($createquery)) {
1807 captiveportal_syslog("Successfully reinitialized tables for {$cpzone} -- database has been reset.");
1808 if (!is_numericint($cpzoneid)) {
1809 if (is_array($config['captiveportal'])) {
1810 foreach ($config['captiveportal'] as $cpkey => $cp) {
1811 if ($cpzone == $cpkey) {
1812 $cpzoneid = $cp['zoneid'];
1813 }
1814 }
1815 }
1816 }
1817 if (is_numericint($cpzoneid)) {
1818 $table_names = captiveportal_get_ipfw_table_names();
1819 foreach ($table_names as $table_name) {
1820 mwexec("/sbin/ipfw table {$table_name} flush");
1821 }
1822 captiveportal_syslog("Flushed tables for {$cpzone} after database reset.");
1823 }
1824 } else {
1825 captiveportal_syslog("Still unable to create tables for {$cpzone}. Error message: {$DB->lastErrorMsg()}. Remove the database file manually and try again.");
1826 }
1827 }
1828
1829 return $DB;
1830}
1831
1832/* Get all tables for specific cpzone */
1833function captiveportal_get_ipfw_table_names() {
1834 global $cpzone;
1835
1836 $result = array();
1837 $tables = pfSense_ipfw_tables_list();
1838
1839 if (!is_array($tables)) {
1840 return $result;
1841 }
1842
1843 $len = strlen($cpzone) + 1;
1844 foreach ($tables as $table) {
1845 if (substr($table['name'], 0, $len) != $cpzone . '_') {
1846 continue;
1847 }
1848
1849 $result[] = $table['name'];
1850 }
1851
1852 return $result;
1853}
1854
1855/* read captive portal DB into array */
1856function captiveportal_read_db($query = "") {
1857 $cpdb = array();
1858
1859 $DB = captiveportal_opendb();
1860 if ($DB) {
1861 $response = $DB->query("SELECT * FROM captiveportal {$query}");
1862 if ($response != FALSE) {
1863 while ($row = $response->fetchArray()) {
1864 $cpdb[] = $row;
1865 }
1866 }
1867 $DB->close();
1868 }
1869
1870 return $cpdb;
1871}
1872
1873function captiveportal_remove_entries($remove) {
1874
1875 if (!is_array($remove) || empty($remove)) {
1876 return;
1877 }
1878
1879 $query = "DELETE FROM captiveportal WHERE sessionid in (";
1880 foreach ($remove as $idx => $unindex) {
1881 $query .= "'{$unindex}'";
1882 if ($idx < (count($remove) - 1)) {
1883 $query .= ",";
1884 }
1885 }
1886 $query .= ")";
1887 captiveportal_write_db($query);
1888}
1889
1890/* write captive portal DB */
1891function captiveportal_write_db($queries) {
1892 global $g;
1893
1894 if (is_array($queries)) {
1895 $query = implode(";", $queries);
1896 } else {
1897 $query = $queries;
1898 }
1899
1900 $DB = captiveportal_opendb();
1901 if ($DB) {
1902 $DB->exec("BEGIN TRANSACTION");
1903 $result = $DB->exec($query);
1904 if (!$result) {
1905 captiveportal_syslog("Trying to modify DB returned error: {$DB->lastErrorMsg()}");
1906 } else {
1907 $DB->exec("END TRANSACTION");
1908 }
1909 $DB->close();
1910 return $result;
1911 } else {
1912 return true;
1913 }
1914}
1915
1916function captiveportal_write_elements() {
1917 global $g, $config, $cpzone;
1918
1919 $cpcfg = $config['captiveportal'][$cpzone];
1920
1921 if (!is_dir($g['captiveportal_element_path'])) {
1922 @mkdir($g['captiveportal_element_path']);
1923 }
1924
1925 if (is_array($cpcfg['element'])) {
1926 foreach ($cpcfg['element'] as $data) {
1927 /* Do not attempt to decode or write out empty files. */
1928 if (isset($data['nocontent'])) {
1929 continue;
1930 }
1931 if (empty($data['content']) || empty(base64_decode($data['content']))) {
1932 unlink_if_exists("{$g['captiveportal_element_path']}/{$data['name']}");
1933 touch("{$g['captiveportal_element_path']}/{$data['name']}");
1934 } elseif (!@file_put_contents("{$g['captiveportal_element_path']}/{$data['name']}", base64_decode($data['content']))) {
1935 printf(gettext('Error: cannot open \'%1$s\' in captiveportal_write_elements()%2$s'), $data['name'], "\n");
1936 return 1;
1937 }
1938 if (!file_exists("{$g['captiveportal_path']}/{$data['name']}")) {
1939 @symlink("{$g['captiveportal_element_path']}/{$data['name']}", "{$g['captiveportal_path']}/{$data['name']}");
1940 }
1941 }
1942 }
1943
1944 return 0;
1945}
1946
1947function captiveportal_free_dnrules($rulenos_start = 2000,
1948 $rulenos_range_max = 64500, $dry_run = false, $clear_auth_pipes = true) {
1949 global $g, $cpzone;
1950
1951 $removed_pipes = array();
1952
1953 if (!file_exists("{$g['vardb_path']}/captiveportaldn.rules")) {
1954 return $removed_pipes;
1955 }
1956
1957 if (!$dry_run) {
1958 $cpruleslck = lock("captiveportalrulesdn", LOCK_EX);
1959 }
1960
1961 $rules = unserialize(file_get_contents(
1962 "{$g['vardb_path']}/captiveportaldn.rules"));
1963 $ridx = $rulenos_start;
1964 while ($ridx < $rulenos_range_max) {
1965 if (substr($rules[$ridx], 0, strlen($cpzone . '_')) == $cpzone . '_') {
1966 if (!$clear_auth_pipes && substr($rules[$ridx], 0, strlen($cpzone . '_auth')) == $cpzone . '_auth') {
1967 $ridx += 2;
1968 } else {
1969 if (!$dry_run) {
1970 $rules[$ridx] = false;
1971 }
1972 $removed_pipes[] = $ridx;
1973 $ridx++;
1974 if (!$dry_run) {
1975 $rules[$ridx] = false;
1976 }
1977 $removed_pipes[] = $ridx;
1978 $ridx++;
1979 }
1980 } else {
1981 $ridx += 2;
1982 }
1983 }
1984
1985 if (!$dry_run) {
1986 file_put_contents("{$g['vardb_path']}/captiveportaldn.rules",
1987 serialize($rules));
1988 unlock($cpruleslck);
1989 }
1990
1991 unset($rules);
1992
1993 return $removed_pipes;
1994}
1995
1996function captiveportal_reserve_ruleno($ruleno) {
1997 global $g, $cpzone;
1998
1999 $cpruleslck = lock("captiveportalrulesdn", LOCK_EX);
2000 if (file_exists("{$g['vardb_path']}/captiveportaldn.rules")) {
2001 $rules = unserialize(file_get_contents("{$g['vardb_path']}/captiveportaldn.rules"));
2002 } else {
2003 $rules = array_pad(array(), 64500, false);
2004 }
2005 $rules[$ruleno] = $cpzone . '_auth';
2006 $ruleno++;
2007 $rules[$ruleno] = $cpzone . '_auth';
2008
2009 file_put_contents("{$g['vardb_path']}/captiveportaldn.rules", serialize($rules));
2010 unlock($cpruleslck);
2011 unset($rules);
2012
2013 return $ruleno;
2014}
2015
2016function captiveportal_get_next_dn_ruleno($rule_type = 'default', $rulenos_start = 2000, $rulenos_range_max = 64500) {
2017 global $config, $g, $cpzone;
2018
2019 $cpruleslck = lock("captiveportalrulesdn", LOCK_EX);
2020 $ruleno = 0;
2021 if (file_exists("{$g['vardb_path']}/captiveportaldn.rules")) {
2022 $rules = unserialize(file_get_contents("{$g['vardb_path']}/captiveportaldn.rules"));
2023 $ridx = $rulenos_start;
2024 while ($ridx < $rulenos_range_max) {
2025 if (empty($rules[$ridx])) {
2026 $ruleno = $ridx;
2027 $rules[$ridx] = $cpzone . '_' . $rule_type;
2028 $ridx++;
2029 $rules[$ridx] = $cpzone . '_' . $rule_type;
2030 break;
2031 } else {
2032 $ridx += 2;
2033 }
2034 }
2035 } else {
2036 $rules = array_pad(array(), $rulenos_range_max, false);
2037 $ruleno = $rulenos_start;
2038 $rules[$rulenos_start] = $cpzone . '_' . $rule_type;
2039 $rulenos_start++;
2040 $rules[$rulenos_start] = $cpzone . '_' . $rule_type;
2041 }
2042 file_put_contents("{$g['vardb_path']}/captiveportaldn.rules", serialize($rules));
2043 unlock($cpruleslck);
2044 unset($rules);
2045
2046 return $ruleno;
2047}
2048
2049function captiveportal_free_dn_ruleno($ruleno) {
2050 global $config, $g;
2051
2052 $cpruleslck = lock("captiveportalrulesdn", LOCK_EX);
2053 if (file_exists("{$g['vardb_path']}/captiveportaldn.rules")) {
2054 $rules = unserialize(file_get_contents("{$g['vardb_path']}/captiveportaldn.rules"));
2055 $rules[$ruleno] = false;
2056 $ruleno++;
2057 $rules[$ruleno] = false;
2058 file_put_contents("{$g['vardb_path']}/captiveportaldn.rules", serialize($rules));
2059 unset($rules);
2060 }
2061 unlock($cpruleslck);
2062}
2063
2064function captiveportal_get_dn_passthru_ruleno($value) {
2065 global $config, $g, $cpzone, $cpzoneid;
2066
2067 $cpcfg = $config['captiveportal'][$cpzone];
2068 if (!isset($cpcfg['enable'])) {
2069 return NULL;
2070 }
2071
2072 $cpruleslck = lock("captiveportalrulesdn", LOCK_EX);
2073 $ruleno = NULL;
2074 if (file_exists("{$g['vardb_path']}/captiveportaldn.rules")) {
2075 unset($output);
2076 $item = pfSense_ipfw_table_lookup("{$cpzone}_pipe_mac",
2077 "any,{$value}");
2078 if (!is_array($item) || empty($item['pipe'])) {
2079 unlock($cpruleslck);
2080 return NULL;
2081 }
2082
2083 $ruleno = intval($item['pipe']);
2084 $rules = unserialize(file_get_contents("{$g['vardb_path']}/captiveportaldn.rules"));
2085 if (!$rules[$ruleno]) {
2086 $ruleno = NULL;
2087 }
2088 unset($rules);
2089 }
2090 unlock($cpruleslck);
2091
2092 return $ruleno;
2093}
2094
2095/**
2096 * This function will calculate the traffic produced by a client
2097 * based on its firewall rule
2098 *
2099 * Point of view: NAS
2100 *
2101 * Input means: from the client
2102 * Output means: to the client
2103 *
2104 */
2105
2106function getVolume($ip) {
2107 global $config, $cpzone;
2108
2109 $reverse = isset($config['captiveportal'][$cpzone]['reverseacct'])
2110 ? true : false;
2111 $volume = array();
2112 // Initialize vars properly, since we don't want NULL vars
2113 $volume['input_pkts'] = $volume['input_bytes'] = 0;
2114 $volume['output_pkts'] = $volume['output_bytes'] = 0;
2115
2116 $tables = array("allowed", "auth");
2117
2118 foreach($tables as $table) {
2119 $ipfw = pfSense_ipfw_table_lookup("{$cpzone}_{$table}_up", $ip);
2120 if (!is_array($ipfw)) {
2121 continue;
2122 }
2123 if ($reverse) {
2124 $volume['output_pkts'] = $ipfw['packets'];
2125 $volume['output_bytes'] = $ipfw['bytes'];
2126 } else {
2127 $volume['input_pkts'] = $ipfw['packets'];
2128 $volume['input_bytes'] = $ipfw['bytes'];
2129 }
2130 }
2131
2132 foreach($tables as $table) {
2133 $ipfw = pfSense_ipfw_table_lookup("{$cpzone}_{$table}_down",
2134 $ip);
2135 if (!is_array($ipfw)) {
2136 continue;
2137 }
2138 if ($reverse) {
2139 $volume['input_pkts'] = $ipfw['packets'];
2140 $volume['input_bytes'] = $ipfw['bytes'];
2141 } else {
2142 $volume['output_pkts'] = $ipfw['packets'];
2143 $volume['output_bytes'] = $ipfw['bytes'];
2144 }
2145 }
2146
2147 return $volume;
2148}
2149
2150function portal_ip_from_client_ip($cliip) {
2151 global $config, $cpzone;
2152
2153 $isipv6 = is_ipaddrv6($cliip);
2154 $interfaces = explode(",", $config['captiveportal'][$cpzone]['interface']);
2155 foreach ($interfaces as $cpif) {
2156 if ($isipv6) {
2157 $ip = get_interface_ipv6($cpif);
2158 $sn = get_interface_subnetv6($cpif);
2159 } else {
2160 $ip = get_interface_ip($cpif);
2161 $sn = get_interface_subnet($cpif);
2162 }
2163 if (ip_in_subnet($cliip, "{$ip}/{$sn}")) {
2164 return $ip;
2165 }
2166 }
2167
2168 $inet = ($isipv6) ? '-inet6' : '-inet';
2169 $iface = exec_command("/sbin/route -n get {$inet} {$cliip} | /usr/bin/awk '/interface/ { print \$2; };'");
2170 $iface = trim($iface, "\n");
2171 if (!empty($iface)) {
2172 $ip = ($isipv6) ? find_interface_ipv6($iface) : find_interface_ip($iface);
2173 if (is_ipaddr($ip)) {
2174 return $ip;
2175 }
2176 }
2177
2178 // doesn't match up to any particular interface
2179 // so let's set the portal IP to what PHP says
2180 // the server IP issuing the request is.
2181 // allows same behavior as 1.2.x where IP isn't
2182 // in the subnet of any CP interface (static routes, etc.)
2183 // rather than forcing to DNS hostname resolution
2184 $ip = $_SERVER['SERVER_ADDR'];
2185 if (is_ipaddr($ip)) {
2186 return $ip;
2187 }
2188
2189 return false;
2190}
2191
2192function portal_hostname_from_client_ip($cliip) {
2193 global $config, $cpzone;
2194
2195 $cpcfg = $config['captiveportal'][$cpzone];
2196
2197 if (isset($cpcfg['httpslogin'])) {
2198 $listenporthttps = $cpcfg['listenporthttps'] ? $cpcfg['listenporthttps'] : ($cpcfg['zoneid'] + 8001);
2199 $ourhostname = $cpcfg['httpsname'];
2200
2201 if ($listenporthttps != 443) {
2202 $ourhostname .= ":" . $listenporthttps;
2203 }
2204 } else {
2205 $listenporthttp = $cpcfg['listenporthttp'] ? $cpcfg['listenporthttp'] : ($cpcfg['zoneid'] + 8000);
2206 $ifip = portal_ip_from_client_ip($cliip);
2207 if (!$ifip) {
2208 $ourhostname = "{$config['system']['hostname']}.{$config['system']['domain']}";
2209 } else {
2210 $ourhostname = (is_ipaddrv6($ifip)) ? "[{$ifip}]" : "{$ifip}";
2211 }
2212
2213 if ($listenporthttp != 80) {
2214 $ourhostname .= ":" . $listenporthttp;
2215 }
2216 }
2217
2218 return $ourhostname;
2219}
2220
2221/* functions move from index.php */
2222
2223function portal_reply_page($redirurl, $type = null, $message = null, $clientmac = null, $clientip = null, $username = null, $password = null) {
2224 global $g, $config, $cpzone;
2225
2226 /* Get captive portal layout */
2227 if ($type == "redir") {
2228 header("Location: {$redirurl}");
2229 return;
2230 } else if ($type == "login") {
2231 $htmltext = get_include_contents("{$g['varetc_path']}/captiveportal_{$cpzone}.html");
2232 } else {
2233 $htmltext = get_include_contents("{$g['varetc_path']}/captiveportal-{$cpzone}-error.html");
2234 }
2235
2236 $cpcfg = $config['captiveportal'][$cpzone];
2237
2238 /* substitute the PORTAL_REDIRURL variable */
2239 if ($cpcfg['preauthurl']) {
2240 $htmltext = str_replace("\$PORTAL_REDIRURL\$", "{$cpcfg['preauthurl']}", $htmltext);
2241 $htmltext = str_replace("#PORTAL_REDIRURL#", "{$cpcfg['preauthurl']}", $htmltext);
2242 }
2243
2244 /* substitute other variables */
2245 $ourhostname = portal_hostname_from_client_ip($clientip);
2246 $protocol = (isset($cpcfg['httpslogin'])) ? 'https://' : 'http://';
2247 $htmltext = str_replace("\$PORTAL_ACTION\$", "{$protocol}{$ourhostname}/index.php?zone={$cpzone}", $htmltext);
2248 $htmltext = str_replace("#PORTAL_ACTION#", "{$protocol}{$ourhostname}/index.php?zone={$cpzone}", $htmltext);
2249
2250 $htmltext = str_replace("\$PORTAL_ZONE\$", htmlspecialchars($cpzone), $htmltext);
2251 $htmltext = str_replace("\$PORTAL_REDIRURL\$", htmlspecialchars($redirurl), $htmltext);
2252 $htmltext = str_replace("\$PORTAL_MESSAGE\$", htmlspecialchars($message), $htmltext);
2253 $htmltext = str_replace("\$CLIENT_MAC\$", htmlspecialchars($clientmac), $htmltext);
2254 $htmltext = str_replace("\$CLIENT_IP\$", htmlspecialchars($clientip), $htmltext);
2255
2256 // Special handling case for captive portal master page so that it can be ran
2257 // through the PHP interpreter using the include method above. We convert the
2258 // $VARIABLE$ case to #VARIABLE# in /etc/inc/captiveportal.inc before writing out.
2259 $htmltext = str_replace("#PORTAL_ZONE#", htmlspecialchars($cpzone), $htmltext);
2260 $htmltext = str_replace("#PORTAL_REDIRURL#", htmlspecialchars($redirurl), $htmltext);
2261 $htmltext = str_replace("#PORTAL_MESSAGE#", htmlspecialchars($message), $htmltext);
2262 $htmltext = str_replace("#CLIENT_MAC#", htmlspecialchars($clientmac), $htmltext);
2263 $htmltext = str_replace("#CLIENT_IP#", htmlspecialchars($clientip), $htmltext);
2264 $htmltext = str_replace("#USERNAME#", htmlspecialchars($username), $htmltext);
2265 $htmltext = str_replace("#PASSWORD#", htmlspecialchars($password), $htmltext);
2266
2267 echo $htmltext;
2268}
2269
2270function captiveportal_reapply_attributes($cpentry, $attributes) {
2271 global $config, $cpzone, $g;
2272
2273 if (isset($config['captiveportal'][$cpzone]['peruserbw'])) {
2274 $dwfaultbw_up = !empty($config['captiveportal'][$cpzone]['bwdefaultup']) ? $config['captiveportal'][$cpzone]['bwdefaultup'] : 0;
2275 $dwfaultbw_down = !empty($config['captiveportal'][$cpzone]['bwdefaultdn']) ? $config['captiveportal'][$cpzone]['bwdefaultdn'] : 0;
2276 } else {
2277 $dwfaultbw_up = $dwfaultbw_down = 0;
2278 }
2279 /* pipe throughputs must always be an integer, enforce that restriction again here. */
2280 if (isset($config['captiveportal'][$cpzone]['radiusperuserbw'])) {
2281 $bw_up = round(!empty($attributes['bw_up']) ? intval($attributes['bw_up'])/1000 : $dwfaultbw_up, 0);
2282 $bw_down = round(!empty($attributes['bw_down']) ? intval($attributes['bw_down'])/1000 : $dwfaultbw_down, 0);
2283 } else {
2284 $bw_up = round($dwfaultbw_up,0);
2285 $bw_down = round($dwfaultbw_down,0);
2286 }
2287
2288 $bw_up_pipeno = $cpentry[1];
2289 $bw_down_pipeno = $cpentry[1]+1;
2290
2291 if ($cpentry['bw_up'] !== $bw_up) {
2292 $_gb = @pfSense_ipfw_pipe("pipe {$bw_up_pipeno} config bw {$bw_up}Kbit/s queue 100 buckets 16");
2293 captiveportal_update_entry($cpentry['sessionid'], $bw_up, 'bw_up');
2294 }
2295 if ($cpentry['bw_down'] !== $bw_down) {
2296 $_gb = @pfSense_ipfw_pipe("pipe {$bw_down_pipeno} config bw {$bw_down}Kbit/s queue 100 buckets 16");
2297 captiveportal_update_entry($cpentry['sessionid'], $bw_down, 'bw_down');
2298 }
2299 unset($bw_up_pipeno, $bw_down_pipeno, $bw_up, $bw_down);
2300}
2301
2302function captiveportal_update_entry($sessionid, $new_value, $field_to_update) {
2303 global $cpzone;
2304
2305 if (!intval($new_value)) {
2306 $new_value = "'{$new_value}'";
2307 }
2308 captiveportal_write_db("UPDATE captiveportal SET {$field_to_update} = {$new_value} WHERE sessionid = '{$sessionid}'");
2309}
2310
2311function portal_allow($clientip, $clientmac, $username, $password = null, $attributes = null, $pipeno = null, $authmethod = null, $context = 'first') {
2312 global $redirurl, $g, $config, $type, $_POST, $cpzone, $cpzoneid;
2313
2314 // Ensure we create an array if we are missing attributes
2315 if (!is_array($attributes)) {
2316 $attributes = array();
2317 }
2318
2319 unset($sessionid);
2320
2321 /* Do not allow concurrent login execution. */
2322 $cpdblck = lock("captiveportaldb{$cpzone}", LOCK_EX);
2323
2324 if ($attributes['voucher']) {
2325 $remaining_time = $attributes['session_timeout'];
2326 $authmethod = "voucher"; // Set RADIUS-Attribute to Voucher to prevent ReAuth-Reqeuest for Vouchers Bug: #2155
2327 $context = "voucher";
2328 }
2329
2330 $writecfg = false;
2331 /* If both "Add MAC addresses of connected users as pass-through MAC" and "Disable concurrent logins" are checked,
2332 then we need to check if the user was already authenticated using another MAC Address, and if so remove the previous Pass-Through MAC. */
2333 if ((isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) && ($username != 'unauthenticated') && isset($config['captiveportal'][$cpzone]['passthrumacadd'])) {
2334 $mac = captiveportal_passthrumac_findbyname($username);
2335 if (!empty($mac)) {
2336 foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $idx => $macent) {
2337 if ($macent['mac'] != $mac['mac']) {
2338 continue;
2339 }
2340
2341 $pipeno = captiveportal_get_dn_passthru_ruleno($mac['mac']);
2342 if ($pipeno) {
2343 captiveportal_free_dn_ruleno($pipeno);
2344 @pfSense_ipfw_table("{$cpzone}_pipe_mac", IP_FW_TABLE_XDEL, "any,{$mac['mac']}");
2345 @pfSense_ipfw_table("{$cpzone}_pipe_mac", IP_FW_TABLE_XDEL, "{$mac['mac']},any");
2346 @pfSense_ipfw_pipe("pipe delete " . $pipeno+1);
2347 @pfSense_ipfw_pipe("pipe delete " . $pipeno);
2348 }
2349 unset($config['captiveportal'][$cpzone]['passthrumac'][$idx]);
2350 }
2351 }
2352 }
2353
2354 /* read in client database */
2355 $query = "WHERE ip = '{$clientip}'";
2356 $tmpusername = SQLite3::escapeString(strtolower($username));
2357 if (isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) {
2358 $query .= " OR (username != 'unauthenticated' AND lower(username) = '{$tmpusername}')";
2359 }
2360 $cpdb = captiveportal_read_db($query);
2361
2362 /* Snapshot the timestamp */
2363 $allow_time = time();
2364 $unsetindexes = array();
2365
2366 foreach ($cpdb as $cpentry) {
2367 /* on the same ip */
2368 if ($cpentry[2] == $clientip) {
2369 if (isset($config['captiveportal'][$cpzone]['nomacfilter']) || $cpentry[3] == $clientmac) {
2370 captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT LOGIN - REUSING OLD SESSION");
2371 } else {
2372 captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT LOGIN - REUSING IP {$cpentry[2]} WITH DIFFERENT MAC ADDRESS {$cpentry[3]}");
2373 }
2374 $sessionid = $cpentry[5];
2375 break;
2376 } elseif (($attributes['voucher']) && ($username != 'unauthenticated') && ($cpentry[4] == $username)) {
2377 // user logged in with an active voucher. Check for how long and calculate
2378 // how much time we can give him (voucher credit - used time)
2379 $remaining_time = $cpentry[0] + $cpentry[7] - $allow_time;
2380 if ($remaining_time < 0) { // just in case.
2381 $remaining_time = 0;
2382 }
2383
2384 /* This user was already logged in so we disconnect the old one */
2385 captiveportal_disconnect($cpentry, 13);
2386 captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT LOGIN - TERMINATING OLD SESSION");
2387 $unsetindexes[] = $cpentry[5];
2388 break;
2389 } elseif ((isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) && ($username != 'unauthenticated')) {
2390 /* on the same username */
2391 if (strcasecmp($cpentry[4], $username) == 0) {
2392 /* This user was already logged in so we disconnect the old one */
2393 captiveportal_disconnect($cpentry, 13);
2394 captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT LOGIN - TERMINATING OLD SESSION");
2395 $unsetindexes[] = $cpentry[5];
2396 break;
2397 }
2398 }
2399 }
2400 unset($cpdb);
2401
2402 if (!empty($unsetindexes)) {
2403 captiveportal_remove_entries($unsetindexes);
2404 }
2405
2406 if ($attributes['voucher'] && $remaining_time <= 0) {
2407 return 0; // voucher already used and no time left
2408 }
2409
2410 if (!isset($sessionid)) {
2411 /* generate unique session ID */
2412 $tod = gettimeofday();
2413 $sessionid = substr(md5(mt_rand() . $tod['sec'] . $tod['usec'] . $clientip . $clientmac), 0, 16);
2414
2415 if (isset($config['captiveportal'][$cpzone]['peruserbw'])) {
2416 $dwfaultbw_up = !empty($config['captiveportal'][$cpzone]['bwdefaultup']) ? $config['captiveportal'][$cpzone]['bwdefaultup'] : 0;
2417 $dwfaultbw_down = !empty($config['captiveportal'][$cpzone]['bwdefaultdn']) ? $config['captiveportal'][$cpzone]['bwdefaultdn'] : 0;
2418 } else {
2419 $dwfaultbw_up = $dwfaultbw_down = 0;
2420 }
2421 /* pipe throughputs must always be an integer, enforce that restriction again here. */
2422 if (isset($config['captiveportal'][$cpzone]['radiusperuserbw'])) {
2423 $bw_up = round(!empty($attributes['bw_up']) ? intval($attributes['bw_up'])/1000 : $dwfaultbw_up, 0);
2424 $bw_down = round(!empty($attributes['bw_down']) ? intval($attributes['bw_down'])/1000 : $dwfaultbw_down, 0);
2425 } else {
2426 $bw_up = round($dwfaultbw_up,0);
2427 $bw_down = round($dwfaultbw_down,0);
2428 }
2429
2430 if (isset($config['captiveportal'][$cpzone]['passthrumacadd'])) {
2431
2432 $mac = array();
2433 $mac['action'] = 'pass';
2434 $mac['mac'] = $clientmac;
2435 $mac['ip'] = $clientip; /* Used only for logging */
2436 $mac['username'] = $username;
2437 if ($attributes['voucher']) {
2438 $mac['logintype'] = "voucher";
2439 }
2440 if ($username == "unauthenticated") {
2441 $mac['descr'] = "Auto-added";
2442 } else if ($authmethod == "voucher") {
2443 $mac['descr'] = "Auto-added for voucher {$username}";
2444 } else {
2445 $mac['descr'] = "Auto-added for user {$username}";
2446 }
2447 if (!empty($bw_up)) {
2448 $mac['bw_up'] = $bw_up;
2449 }
2450 if (!empty($bw_down)) {
2451 $mac['bw_down'] = $bw_down;
2452 }
2453 if (!is_array($config['captiveportal'][$cpzone]['passthrumac'])) {
2454 $config['captiveportal'][$cpzone]['passthrumac'] = array();
2455 }
2456 //check for mac duplicates before adding it to config.
2457 $mac_duplicate = false;
2458 foreach($config['captiveportal'][$cpzone]['passthrumac'] as $mac_check){
2459 if($mac_check['mac'] == $mac['mac']){
2460 $mac_duplicate = true;
2461 }
2462 }
2463 if(!$mac_duplicate){
2464 $config['captiveportal'][$cpzone]['passthrumac'][] = $mac;
2465 }
2466 unlock($cpdblck);
2467 $macrules = captiveportal_passthrumac_configure_entry($mac);
2468 file_put_contents("{$g['tmp_path']}/macentry_{$cpzone}.rules.tmp", $macrules);
2469 mwexec("/sbin/ipfw -q {$g['tmp_path']}/macentry_{$cpzone}.rules.tmp");
2470 $writecfg = true;
2471 } else {
2472 /* See if a pipeno is passed, if not start sessions because this means there isn't one atm */
2473 if (is_null($pipeno)) {
2474 $pipeno = captiveportal_get_next_dn_ruleno('auth');
2475 }
2476
2477 /* if the pool is empty, return appropriate message and exit */
2478 if (is_null($pipeno)) {
2479 portal_reply_page($redirurl, "error", "System reached maximum login capacity");
2480 log_error("Zone: {$cpzone} - WARNING! Captive portal has reached maximum login capacity");
2481 unlock($cpdblck);
2482 return;
2483 }
2484
2485 $bw_up_pipeno = $pipeno;
2486 $bw_down_pipeno = $pipeno + 1;
2487 //$bw_up /= 1000; // Scale to Kbit/s
2488 $_gb = @pfSense_ipfw_pipe("pipe {$bw_up_pipeno} config bw {$bw_up}Kbit/s queue 100 buckets 16");
2489 $_gb = @pfSense_ipfw_pipe("pipe {$bw_down_pipeno} config bw {$bw_down}Kbit/s queue 100 buckets 16");
2490
2491 $rule_entry = "{$clientip}/" . (is_ipaddrv6($clientip) ? "128" : "32");
2492 if (!isset($config['captiveportal'][$cpzone]['nomacfilter'])) {
2493 $rule_entry .= ",{$clientmac}";
2494 }
2495 $_gb = @pfSense_ipfw_table("{$cpzone}_auth_up", IP_FW_TABLE_XADD, "{$rule_entry}", $bw_up_pipeno);
2496 $_gb = @pfSense_ipfw_table("{$cpzone}_auth_down", IP_FW_TABLE_XADD, "{$rule_entry}", $bw_down_pipeno);
2497
2498 if ($attributes['voucher']) {
2499 $attributes['session_timeout'] = $remaining_time;
2500 }
2501
2502 /* handle empty attributes */
2503 $session_timeout = (!empty($attributes['session_timeout'])) ? $attributes['session_timeout'] : 'NULL';
2504 $idle_timeout = (!empty($attributes['idle_timeout'])) ? $attributes['idle_timeout'] : 'NULL';
2505 $session_terminate_time = (!empty($attributes['session_terminate_time'])) ? $attributes['session_terminate_time'] : 'NULL';
2506 $interim_interval = (!empty($attributes['interim_interval'])) ? $attributes['interim_interval'] : 'NULL';
2507 $traffic_quota = (!empty($attributes['maxbytes'])) ? $attributes['maxbytes'] : 'NULL';
2508
2509 /* escape username */
2510 $safe_username = SQLite3::escapeString($username);
2511
2512 /* encode password in Base64 just in case it contains commas */
2513 $bpassword = (isset($config['captiveportal'][$cpzone]['reauthenticate'])) ? base64_encode($password) : '';
2514 $insertquery = "INSERT INTO captiveportal (allow_time, pipeno, ip, mac, username, sessionid, bpassword, session_timeout, idle_timeout, session_terminate_time, interim_interval, traffic_quota, bw_up, bw_down, authmethod, context) ";
2515 $insertquery .= "VALUES ({$allow_time}, {$pipeno}, '{$clientip}', '{$clientmac}', '{$safe_username}', '{$sessionid}', '{$bpassword}', ";
2516 $insertquery .= "{$session_timeout}, {$idle_timeout}, {$session_terminate_time}, {$interim_interval}, {$traffic_quota}, {$bw_up}, {$bw_down}, '{$authmethod}', '{$context}')";
2517
2518 /* store information to database */
2519 captiveportal_write_db($insertquery);
2520 unlock($cpdblck);
2521 unset($insertquery, $bpassword);
2522
2523 $radacct = isset($config['captiveportal'][$cpzone]['radacct_enable']) ? true : false;
2524 if ($authmethod === 'radius' && $radacct) {
2525 captiveportal_send_server_accounting('start',
2526 $pipeno, // ruleno
2527 $username, // username
2528 $clientip, // clientip
2529 $clientmac, // clientmac
2530 $sessionid, // sessionid
2531 time()); // start time
2532 }
2533 }
2534 } else {
2535 /* NOTE: #3062-11 If the pipeno has been allocated free it to not DoS the CP */
2536 if (!is_null($pipeno)) {
2537 captiveportal_free_dn_ruleno($pipeno);
2538 }
2539
2540 unlock($cpdblck);
2541 }
2542
2543 if ($writecfg == true) {
2544 write_config(gettext("Captive Portal allowed users configuration changed"));
2545 }
2546
2547 /* redirect user to desired destination */
2548 if (!empty($attributes['url_redirection'])) {
2549 $my_redirurl = $attributes['url_redirection'];
2550 } else if (!empty($redirurl)) {
2551 $my_redirurl = $redirurl;
2552 } else if (!empty($config['captiveportal'][$cpzone]['redirurl'])) {
2553 $my_redirurl = $config['captiveportal'][$cpzone]['redirurl'];
2554 }
2555
2556 if (isset($config['captiveportal'][$cpzone]['logoutwin_enable']) && !isset($config['captiveportal'][$cpzone]['passthrumacadd'])) {
2557 $ourhostname = portal_hostname_from_client_ip($clientip);
2558 $protocol = (isset($config['captiveportal'][$cpzone]['httpslogin'])) ? 'https://' : 'http://';
2559 $logouturl = "{$protocol}{$ourhostname}/";
2560
2561 if (isset($attributes['reply_message'])) {
2562 $message = $attributes['reply_message'];
2563 } else {
2564 $message = 0;
2565 }
2566
2567 include_once("{$g['varetc_path']}/captiveportal-{$cpzone}-logout.html");
2568
2569 } else {
2570 portal_reply_page($my_redirurl, "redir", "Just redirect the user.");
2571 }
2572
2573 return $sessionid;
2574}
2575
2576
2577/*
2578 * Used for when pass-through credits are enabled.
2579 * Returns true when there was at least one free login to deduct for the MAC.
2580 * Expired entries are removed as they are seen.
2581 * Active entries are updated according to the configuration.
2582 */
2583function portal_consume_passthrough_credit($clientmac) {
2584 global $config, $cpzone;
2585
2586 if (!empty($config['captiveportal'][$cpzone]['freelogins_count']) && is_numeric($config['captiveportal'][$cpzone]['freelogins_count'])) {
2587 $freeloginscount = $config['captiveportal'][$cpzone]['freelogins_count'];
2588 } else {
2589 return false;
2590 }
2591
2592 if (!empty($config['captiveportal'][$cpzone]['freelogins_resettimeout']) && is_numeric($config['captiveportal'][$cpzone]['freelogins_resettimeout'])) {
2593 $resettimeout = $config['captiveportal'][$cpzone]['freelogins_resettimeout'];
2594 } else {
2595 return false;
2596 }
2597
2598 if ($freeloginscount < 1 || $resettimeout <= 0 || !$clientmac) {
2599 return false;
2600 }
2601
2602 $updatetimeouts = isset($config['captiveportal'][$cpzone]['freelogins_updatetimeouts']);
2603
2604 /*
2605 * Read database of used MACs. Lines are a comma-separated list
2606 * of the time, MAC, then the count of pass-through credits remaining.
2607 */
2608 $usedmacs = captiveportal_read_usedmacs_db();
2609
2610 $currenttime = time();
2611 $found = false;
2612 foreach ($usedmacs as $key => $usedmac) {
2613 $usedmac = explode(",", $usedmac);
2614
2615 if ($usedmac[1] == $clientmac) {
2616 if ($usedmac[0] + ($resettimeout * 3600) > $currenttime) {
2617 if ($usedmac[2] < 1) {
2618 if ($updatetimeouts) {
2619 $usedmac[0] = $currenttime;
2620 unset($usedmacs[$key]);
2621 $usedmacs[] = implode(",", $usedmac);
2622 captiveportal_write_usedmacs_db($usedmacs);
2623 }
2624
2625 return false;
2626 } else {
2627 $usedmac[2] -= 1;
2628 $usedmacs[$key] = implode(",", $usedmac);
2629 }
2630
2631 $found = true;
2632 } else {
2633 unset($usedmacs[$key]);
2634 }
2635
2636 break;
2637 } else if ($usedmac[0] + ($resettimeout * 3600) <= $currenttime) {
2638 unset($usedmacs[$key]);
2639 }
2640 }
2641
2642 if (!$found) {
2643 $usedmac = array($currenttime, $clientmac, $freeloginscount - 1);
2644 $usedmacs[] = implode(",", $usedmac);
2645 }
2646
2647 captiveportal_write_usedmacs_db($usedmacs);
2648 return true;
2649}
2650
2651function captiveportal_read_usedmacs_db() {
2652 global $g, $cpzone;
2653
2654 $cpumaclck = lock("captiveusedmacs{$cpzone}");
2655 if (file_exists("{$g['vardb_path']}/captiveportal_usedmacs_{$cpzone}.db")) {
2656 $usedmacs = file("{$g['vardb_path']}/captiveportal_usedmacs_{$cpzone}.db", FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
2657 if (!$usedmacs) {
2658 $usedmacs = array();
2659 }
2660 } else {
2661 $usedmacs = array();
2662 }
2663
2664 unlock($cpumaclck);
2665 return $usedmacs;
2666}
2667
2668function captiveportal_write_usedmacs_db($usedmacs) {
2669 global $g, $cpzone;
2670
2671 $cpumaclck = lock("captiveusedmacs{$cpzone}", LOCK_EX);
2672 @file_put_contents("{$g['vardb_path']}/captiveportal_usedmacs_{$cpzone}.db", implode("\n", $usedmacs));
2673 unlock($cpumaclck);
2674}
2675
2676function captiveportal_blocked_mac($mac) {
2677 global $config, $g, $cpzone;
2678
2679 if (empty($mac) || !is_macaddr($mac)) {
2680 return false;
2681 }
2682
2683 if (!is_array($config['captiveportal'][$cpzone]['passthrumac'])) {
2684 return false;
2685 }
2686
2687 foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $passthrumac) {
2688 if (($passthrumac['action'] == 'block') &&
2689 ($passthrumac['mac'] == strtolower($mac))) {
2690 return true;
2691 }
2692 }
2693
2694 return false;
2695
2696}
2697
2698/* Captiveportal Radius Accounting */
2699
2700function gigawords($bytes) {
2701
2702 /*
2703 * RFC2866 Specifies a 32bit unsigned integer, which is a max of 4294967295
2704 * Currently there is a fault in the PECL radius_put_int function which can handle only 32bit signed integer.
2705 */
2706
2707 // We use BCMath functions since normal integers don't work with so large numbers
2708 $gigawords = bcdiv( bcsub( $bytes, remainder($bytes) ) , GIGAWORDS_RIGHT_OPERAND) ;
2709
2710 // We need to manually set this to a zero instead of NULL for put_int() safety
2711 if (is_null($gigawords)) {
2712 $gigawords = 0;
2713 }
2714
2715 return $gigawords;
2716}
2717
2718function remainder($bytes) {
2719 // Calculate the bytes we are going to send to the radius
2720 $bytes = bcmod($bytes, GIGAWORDS_RIGHT_OPERAND);
2721
2722 if (is_null($bytes)) {
2723 $bytes = 0;
2724 }
2725
2726 return $bytes;
2727}
2728
2729function captiveportal_send_server_accounting($type = 'on', $ruleno = null, $username = null, $clientip = null, $clientmac = null, $sessionid = null, $start_time = null, $stop_time = null, $term_cause = null) {
2730 global $cpzone, $config;
2731
2732 $cpcfg = $config['captiveportal'][$cpzone];
2733 $acctcfg = auth_get_authserver($cpcfg['radacct_server']);
2734
2735 if (!isset($cpcfg['radacct_enable']) || empty($acctcfg)) {
2736 return null;
2737 }
2738
2739 if ($type === 'on') {
2740 $racct = new Auth_RADIUS_Acct_On;
2741 } elseif ($type === 'off') {
2742 $racct = new Auth_RADIUS_Acct_Off;
2743 } elseif ($type === 'start') {
2744 $racct = new Auth_RADIUS_Acct_Start;
2745 if (!is_int($start_time)) {
2746 $start_time = time();
2747 }
2748 } elseif ($type === 'stop') {
2749 $racct = new Auth_RADIUS_Acct_Stop;
2750 if (!is_int($stop_time)) {
2751 $stop_time = time();
2752 }
2753 } elseif ($type === 'update') {
2754 $racct = new Auth_RADIUS_Acct_Update;
2755 if (!is_int($stop_time)) {
2756 $stop_time = time(); // "top time" here will be used only for calculating session time.
2757 }
2758 } else {
2759 return null;
2760 }
2761
2762 $racct->addServer($acctcfg['host'], $acctcfg['radius_acct_port'],
2763 $acctcfg['radius_secret'], $acctcfg['radius_timeout']);
2764
2765 $racct->authentic = RADIUS_AUTH_RADIUS;
2766 if ($cpcfg['auth_method'] === 'radmac' && $username === "unauthenticated" && !empty($clientmac)) {
2767 $racct->username = mac_format($clientmac);
2768 } elseif (!empty($username)) {
2769 $racct->username = $username;
2770 }
2771
2772 if (PEAR::isError($racct->start())) {
2773 captiveportal_syslog('RADIUS ACCOUNTING FAILED : '.$racct->getError());
2774 $racct->close();
2775 return null;
2776 }
2777
2778 $nasip = $acctcfg['radius_nasip_attribute'];
2779 if (!is_ipaddr($nasip)) {
2780 $nasip = get_interface_ip($nasip);
2781 if (!is_ipaddr($nasip)) {
2782 $nasip = get_interface_ip();//We use WAN interface IP as fallback for NAS-IP-Address
2783 }
2784 }
2785 $nasmac = get_interface_mac(find_ip_interface($nasip));
2786 $racct->putAttribute(RADIUS_NAS_IP_ADDRESS, $nasip, "addr");
2787
2788 $racct->putAttribute(RADIUS_NAS_IDENTIFIER, empty($cpcfg["radiusnasid"]) ? "CaptivePortal-{$cpzone}" : $cpcfg["radiusnasid"] );
2789
2790 if (is_int($ruleno)) {
2791 $racct->putAttribute(RADIUS_NAS_PORT_TYPE, RADIUS_ETHERNET);
2792 $racct->putAttribute(RADIUS_NAS_PORT, intval($ruleno), 'integer');
2793 }
2794
2795 if (!empty($sessionid)) {
2796 $racct->putAttribute(RADIUS_ACCT_SESSION_ID, $sessionid);
2797 }
2798
2799 if (!empty($clientip) && is_ipaddr($clientip)) {
2800 $racct->putAttribute(RADIUS_FRAMED_IP_ADDRESS, $clientip, "addr");
2801 }
2802 if (!empty($clientmac)) {
2803 $racct->putAttribute(RADIUS_CALLING_STATION_ID, mac_format($clientmac));
2804 }
2805 if (!empty($nasmac)) {
2806 $racct->putAttribute(RADIUS_CALLED_STATION_ID, mac_format($nasmac).':'.gethostname());
2807 }
2808
2809 // Accounting request Stop and Update : send the current data volume
2810 if (($type === 'stop' || $type === 'update') && is_int($start_time)) {
2811 $volume = getVolume($clientip);
2812 $session_time = $stop_time - $start_time;
2813 $volume['input_bytes_radius'] = remainder($volume['input_bytes']);
2814 $volume['input_gigawords'] = gigawords($volume['input_bytes']);
2815 $volume['output_bytes_radius'] = remainder($volume['output_bytes']);
2816 $volume['output_gigawords'] = gigawords($volume['output_bytes']);
2817
2818 // Volume stuff: Ingress
2819 $racct->putAttribute(RADIUS_ACCT_INPUT_PACKETS, intval($volume['input_pkts']), "integer");
2820 $racct->putAttribute(RADIUS_ACCT_INPUT_OCTETS, intval($volume['input_bytes_radius']), "integer");
2821 // Volume stuff: Outgress
2822 $racct->putAttribute(RADIUS_ACCT_OUTPUT_PACKETS, intval($volume['output_pkts']), "integer");
2823 $racct->putAttribute(RADIUS_ACCT_OUTPUT_OCTETS, intval($volume['output_bytes_radius']), "integer");
2824 $racct->putAttribute(RADIUS_ACCT_SESSION_TIME, intval($session_time), "integer");
2825
2826 $racct->putAttribute(CUSTOM_RADIUS_ACCT_OUTPUT_GIGAWORDS, intval($volume['output_gigawords']), "integer");
2827 $racct->putAttribute(CUSTOM_RADIUS_ACCT_INPUT_GIGAWORDS, intval($volume['input_gigawords']), "integer");
2828 // Set session_time
2829 $racct->session_time = $session_time;
2830 }
2831
2832 if ($type === 'stop') {
2833 if (empty($term_cause)) {
2834 $term_cause = 1;
2835 }
2836 $racct->putAttribute(RADIUS_ACCT_TERMINATE_CAUSE, $term_cause);
2837 }
2838
2839 // Send request
2840 $result = $racct->send();
2841
2842 if (PEAR::isError($result)) {
2843 captiveportal_syslog('RADIUS ACCOUNTING FAILED : '.$racct->getError());
2844 $result = null;
2845 } elseif ($result !== true) {
2846 $result = false;
2847 }
2848
2849 $racct->close();
2850 return $result;
2851}
2852
2853function captiveportal_isip_logged($clientip) {
2854 global $g, $cpzone;
2855
2856 /* read in client database */
2857 $query = "WHERE ip = '{$clientip}'";
2858 $cpdb = captiveportal_read_db($query);
2859 foreach ($cpdb as $cpentry) {
2860 return $cpentry;
2861 }
2862}
2863?>