Project

General

Profile

Download (92.9 KB) Statistics
| Branch: | Tag: | Revision:
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 */
27
require_once("auth.inc");
28
require_once("PEAR.php"); // required for bcmath
29
require_once("Auth/RADIUS.php"); // required for radius accounting
30
require_once("config.inc");
31
require_once("functions.inc");
32
require_once("filter.inc");
33
require_once("voucher.inc");
34

    
35
/* Captiveportal Radius Accounting */
36
PEAR::loadExtension('bcmath');
37
// The RADIUS Package doesn't have these vars so we create them ourself
38
define("CUSTOM_RADIUS_ACCT_INPUT_GIGAWORDS", "52");
39
define("CUSTOM_RADIUS_ACCT_OUTPUT_GIGAWORDS", "53");
40
define("GIGAWORDS_RIGHT_OPERAND", '4294967296'); // 2^32
41

    
42
function 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\$">
107
EOD;
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

    
116
EOD;
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">
121
EOD;
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>
134
EOD;
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}">
143
EOD;
144
		}
145
	}
146

    
147
if ($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>
159
EOD;
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 &hearts; 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

    
176
EOD;
177

    
178
	return $htmltext;
179
}
180

    
181
function 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

    
213
function 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

    
225
function captiveportal_configure_zone($cpcfg) {
226
	global $config, $g, $cpzone, $cpzoneid;
227

    
228
	$captiveportallck = lock("captiveportal{$cpzone}", LOCK_EX);
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. Cause all users to disconnect */
239
		captiveportal_init_rules(true);
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[
324
LogoutWin = window.open('', 'Logout', 'toolbar=0,scrollbars=0,location=0,statusbar=0,menubar=0,resizable=0,width=256,height=64');
325
if (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

    
341
document.location.href="<?=\$my_redirurl;?>";
342
//]]>
343
</script>
344
</body>
345
</html>
346

    
347
EOD;
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
		/* delete outdated radius server database if exist */
377
		unlink_if_exists("{$g['vardb_path']}/captiveportal_radius_{$cpzone}.db");
378

    
379
		if (platform_booting()) {
380
			/* send Accounting-On to server */
381
			captiveportal_send_server_accounting('on');
382
			echo "done\n";
383
		}
384

    
385
	} else {
386
		killbypid("{$g['varrun_path']}/nginx-{$cpzone}-CaptivePortal.pid");
387
		killbypid("{$g['varrun_path']}/nginx-{$cpzone}-CaptivePortal-SSL.pid");
388
		killbypid("{$g['varrun_path']}/cp_prunedb_{$cpzone}.pid");
389
		@unlink("{$g['varetc_path']}/captiveportal_{$cpzone}.html");
390
		@unlink("{$g['varetc_path']}/captiveportal-{$cpzone}-error.html");
391
		@unlink("{$g['varetc_path']}/captiveportal-{$cpzone}-logout.html");
392

    
393
		captiveportal_radius_stop_all(10); // NAS-Request
394

    
395
		captiveportal_filterdns_configure();
396

    
397
		/* remove old information */
398
		unlink_if_exists("{$g['vardb_path']}/captiveportal{$cpzone}.db");
399
		unlink_if_exists("{$g['vardb_path']}/captiveportal_radius_{$cpzone}.db");
400
		unlink_if_exists("{$g['vardb_path']}/captiveportal_{$cpzone}.rules");
401
		/* Release allocated pipes for this zone */
402
		$pipes_to_remove = captiveportal_free_dnrules();
403

    
404
		captiveportal_delete_rules($pipes_to_remove);
405

    
406
		if (empty($config['captiveportal'])) {
407
			set_single_sysctl("net.link.ether.ipfw", "0");
408
		} else {
409
			/* Deactivate ipfw(4) if not needed */
410
			$cpactive = false;
411
			if (is_array($config['captiveportal'])) {
412
				foreach ($config['captiveportal'] as $cpkey => $cp) {
413
					if (isset($cp['enable'])) {
414
						$cpactive = true;
415
						break;
416
					}
417
				}
418
			}
419
			if ($cpactive === false) {
420
				set_single_sysctl("net.link.ether.ipfw", "0");
421
			}
422
		}
423
	}
424

    
425
	unlock($captiveportallck);
426

    
427
	return 0;
428
}
429

    
430
function captiveportal_init_webgui() {
431
	global $config, $cpzone;
432

    
433
	if (is_array($config['captiveportal'])) {
434
		foreach ($config['captiveportal'] as $cpkey => $cp) {
435
			$cpzone = $cpkey;
436
			captiveportal_init_webgui_zone($cp);
437
		}
438
	}
439
}
440

    
441
function captiveportal_init_webgui_zonename($zone) {
442
	global $config, $cpzone;
443

    
444
	if (isset($config['captiveportal'][$zone])) {
445
		$cpzone = $zone;
446
		captiveportal_init_webgui_zone($config['captiveportal'][$zone]);
447
	}
448
}
449

    
450
function captiveportal_init_webgui_zone($cpcfg) {
451
	global $g, $config, $cpzone;
452

    
453
	if (!isset($cpcfg['enable'])) {
454
		return;
455
	}
456

    
457
	if (isset($cpcfg['httpslogin'])) {
458
		$cert = lookup_cert($cpcfg['certref']);
459
		$crt = base64_decode($cert['crt']);
460
		$key = base64_decode($cert['prv']);
461
		$ca = ca_chain($cert);
462

    
463
		/* generate nginx configuration */
464
		if (!empty($cpcfg['listenporthttps'])) {
465
			$listenporthttps = $cpcfg['listenporthttps'];
466
		} else {
467
			$listenporthttps = 8001 + $cpcfg['zoneid'];
468
		}
469
		system_generate_nginx_config("{$g['varetc_path']}/nginx-{$cpzone}-CaptivePortal-SSL.conf",
470
			$crt, $key, $ca, "nginx-{$cpzone}-CaptivePortal-SSL.pid", $listenporthttps, "/usr/local/captiveportal",
471
			"cert-{$cpzone}-portal.pem", "ca-{$cpzone}-portal.pem", $cpzone, false);
472
	}
473

    
474
	/* generate nginx configuration */
475
	if (!empty($cpcfg['listenporthttp'])) {
476
		$listenporthttp = $cpcfg['listenporthttp'];
477
	} else {
478
		$listenporthttp = 8000 + $cpcfg['zoneid'];
479
	}
480
	system_generate_nginx_config("{$g['varetc_path']}/nginx-{$cpzone}-CaptivePortal.conf",
481
		"", "", "", "nginx-{$cpzone}-CaptivePortal.pid", $listenporthttp, "/usr/local/captiveportal",
482
		"", "", $cpzone, false);
483

    
484
	@unlink("{$g['varrun']}/nginx-{$cpzone}-CaptivePortal.pid");
485
	/* attempt to start nginx */
486
	$res = mwexec("/usr/local/sbin/nginx -c {$g['varetc_path']}/nginx-{$cpzone}-CaptivePortal.conf");
487

    
488
	/* fire up https instance */
489
	if (isset($cpcfg['httpslogin'])) {
490
		@unlink("{$g['varrun']}/nginx-{$cpzone}-CaptivePortal-SSL.pid");
491
		$res = mwexec("/usr/local/sbin/nginx -c {$g['varetc_path']}/nginx-{$cpzone}-CaptivePortal-SSL.conf");
492
	}
493
}
494

    
495
function captiveportal_init_rules_byinterface($interface) {
496
	global $cpzone, $cpzoneid, $config;
497

    
498
	if (!is_array($config['captiveportal'])) {
499
		return;
500
	}
501

    
502
	foreach ($config['captiveportal'] as $cpkey => $cp) {
503
		$cpzone = $cpkey;
504
		$cpzoneid = $cp['zoneid'];
505
		$cpinterfaces = explode(",", $cp['interface']);
506
		if (in_array($interface, $cpinterfaces)) {
507
			captiveportal_init_rules();
508
			break;
509
		}
510
	}
511
}
512

    
513
/* Create basic rules used by all zones */
514
function captiveportal_init_general_rules($flush = false) {
515
	global $g;
516

    
517
	$flush_rule = '';
518
	if ($flush) {
519
		$flush_rule = 'flush';
520
	}
521

    
522
	/* Already loaded */
523
	if (!$flush && (mwexec("/sbin/ipfw list 1000", true) == 0)) {
524
		return;
525
	}
526

    
527
	$cprules = <<<EOD
528
{$flush_rule}
529
# Table with interfaces that have CP enabled
530
table cp_ifaces create type iface valtype skipto
531

    
532
# Redirect each CP interface to its specific rule
533
add 1000 skipto tablearg all from any to any via table(cp_ifaces)
534

    
535
# This interface has no cp zone configured
536
add 1100 allow all from any to any
537

    
538
# block everything else
539
add 65534 deny all from any to any
540
EOD;
541

    
542
	/* load rules */
543
	file_put_contents("{$g['tmp_path']}/ipfw.cp.rules", $cprules);
544
	mwexec("/sbin/ipfw -q {$g['tmp_path']}/ipfw.cp.rules", true);
545
	@unlink("{$g['tmp_path']}/ipfw_{$cpzone}.cp.rules");
546
	unset($cprules);
547
}
548

    
549
/* Create a string with ipfw rule and increase rulenum */
550
function captiveportal_create_ipfw_rule($cmd, &$rulenum, $args) {
551
	$rule = "{$cmd} {$rulenum} {$args}\n";
552
	$rulenum++;
553

    
554
	return $rule;
555
}
556

    
557
/* Return first rule number for a cp zone */
558
function captiveportal_ipfw_ruleno($id) {
559
	global $g;
560

    
561
	return 2000 + $id * $g['captiveportal_rules_interval'];
562
}
563

    
564
/* reinit will disconnect all users, be careful! */
565
function captiveportal_init_rules($reinit = false) {
566
	global $config, $g, $cpzone, $cpzoneid;
567

    
568
	if (!isset($config['captiveportal'][$cpzone]['enable'])) {
569
		return;
570
	}
571

    
572
	captiveportal_load_modules();
573
	captiveportal_init_general_rules();
574

    
575
	/* Cleanup so nothing is leaked */
576
	captiveportal_free_dnrules();
577
	unlink_if_exists("{$g['vardb_path']}/captiveportal_{$cpzone}.rules");
578

    
579
	$skipto = captiveportal_ipfw_ruleno($cpzoneid);
580

    
581
	$cprules = '';
582

    
583
	$cpips = array();
584
	$ifaces = get_configured_interface_list();
585
	$cpinterfaces = explode(",", $config['captiveportal'][$cpzone]['interface']);
586
	$firsttime = 0;
587
	foreach ($cpinterfaces as $cpifgrp) {
588
		if (!isset($ifaces[$cpifgrp])) {
589
			continue;
590
		}
591
		$tmpif = get_real_interface($cpifgrp);
592
		if (empty($tmpif)) {
593
			continue;
594
		}
595

    
596
		$cpipm = get_interface_ip($cpifgrp);
597

    
598
		if (!is_ipaddr($cpipm)) {
599
			continue;
600
		}
601

    
602
		$cpips[] = $cpipm;
603
		if (is_array($config['virtualip']['vip'])) {
604
			foreach ($config['virtualip']['vip'] as $vip) {
605
				if (($vip['interface'] == $cpifgrp) &&
606
				    (($vip['mode'] == "carp") ||
607
				    ($vip['mode'] == "ipalias"))) {
608
					$cpips[] = $vip['subnet'];
609
				}
610
			}
611
		}
612

    
613
		$cprules .= "table cp_ifaces add {$tmpif} {$skipto}\n";
614
	}
615
	if (count($cpips) > 0) {
616
		$cpactive = true;
617
	} else {
618
		return false;
619
	}
620

    
621
	if ($reinit == false) {
622
		$captiveportallck = lock("captiveportal{$cpzone}");
623
	}
624

    
625
	$rulenum = $skipto;
626
	$cprules .= "table {$cpzone}_pipe_mac create type mac valtype pipe\n";
627
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
628
	    "pipe tablearg MAC table({$cpzone}_pipe_mac)");
629
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
630
	    "allow pfsync from any to any");
631
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
632
	    "allow carp from any to any\n");
633
	$cprules .= "# layer 2: pass ARP\n";
634
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
635
	    "pass layer2 mac-type arp,rarp");
636
	$cprules .= "# pfsense requires for WPA\n";
637
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
638
	    "pass layer2 mac-type 0x888e,0x88c7");
639
	$cprules .= "# PPP Over Ethernet Session Stage/Discovery Stage\n";
640
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
641
	    "pass layer2 mac-type 0x8863,0x8864\n");
642
	$cprules .= "# layer 2: block anything else non-IP(v4/v6)\n";
643
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
644
	    "deny layer2 not mac-type ip,ipv6");
645

    
646
	/* These tables contain host ips */
647
	$cprules .= "table {$cpzone}_host_ips create type addr\n";
648
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
649
	    "pass ip from any to table({$cpzone}_host_ips) in");
650
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
651
	    "pass ip from table({$cpzone}_host_ips) to any out");
652
	foreach ($cpips as $cpip) {
653
		$cprules .= "table {$cpzone}_host_ips add {$cpip}\n";
654
	}
655
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
656
	    "pass ip from any to 255.255.255.255 in");
657
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
658
	    "pass ip from 255.255.255.255 to any out");
659

    
660
	/* Allowed ips */
661
	$cprules .= "table {$cpzone}_allowed_up create type addr valtype pipe\n";
662
	$cprules .= "table {$cpzone}_allowed_down create type addr valtype pipe\n";
663
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
664
	    "pipe tablearg ip from table({$cpzone}_allowed_up) to any in");
665
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
666
	    "pipe tablearg ip from any to table({$cpzone}_allowed_down) in");
667
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
668
	    "pipe tablearg ip from table({$cpzone}_allowed_up) to any out");
669
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
670
	    "pipe tablearg ip from any to table({$cpzone}_allowed_down) out");
671

    
672
	/* Authenticated users rules. */
673
	$cprules .= "table {$cpzone}_auth_up create type addr valtype pipe\n";
674
	$cprules .= "table {$cpzone}_auth_down create type addr valtype pipe\n";
675
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
676
	    "pipe tablearg ip from table({$cpzone}_auth_up) to any layer2 in");
677
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
678
	    "pipe tablearg ip from any to table({$cpzone}_auth_down) layer2 out");
679

    
680
	if (!empty($config['captiveportal'][$cpzone]['listenporthttp'])) {
681
		$listenporthttp = $config['captiveportal'][$cpzone]['listenporthttp'];
682
	} else {
683
		$listenporthttp = 8000 + $cpzoneid;
684
	}
685

    
686
	if (isset($config['captiveportal'][$cpzone]['httpslogin'])) {
687
		if (!empty($config['captiveportal'][$cpzone]['listenporthttps'])) {
688
			$listenporthttps = $config['captiveportal'][$cpzone]['listenporthttps'];
689
		} else {
690
			$listenporthttps = 8001 + $cpzoneid;
691
		}
692
		if (!isset($config['captiveportal'][$cpzone]['nohttpsforwards'])) {
693
			$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
694
			    "fwd 127.0.0.1,{$listenporthttps} tcp from any to any dst-port 443 in");
695
		}
696
	}
697

    
698
	$cprules .= "# redirect non-authenticated clients to captive portal\n";
699
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
700
	    "fwd 127.0.0.1,{$listenporthttp} tcp from any to any dst-port 80 in");
701
	$cprules .= "# let the responses from the captive portal web server back out\n";
702
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
703
	    "pass tcp from any to any out");
704
	$cprules .= "# This CP zone is over, skip to last rule\n";
705
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
706
	    "skipto 65534 all from any to any");
707

    
708
	/* generate passthru mac database */
709
	$cprules .= captiveportal_passthrumac_configure(true);
710
	$cprules .= "\n";
711

    
712
	/* allowed ipfw rules to make allowed ip work */
713
	$cprules .= captiveportal_allowedip_configure();
714

    
715
	/* allowed ipfw rules to make allowed hostnames work */
716
	$cprules .= captiveportal_allowedhostname_configure();
717

    
718
	/* load rules */
719
	captiveportal_delete_rules();
720
	file_put_contents("{$g['tmp_path']}/ipfw_{$cpzone}.cp.rules", $cprules);
721
	mwexec("/sbin/ipfw -q {$g['tmp_path']}/ipfw_{$cpzone}.cp.rules", true);
722
	@unlink("{$g['tmp_path']}/ipfw_{$cpzone}.cp.rules");
723
	unset($cprules);
724

    
725
	captiveportal_filterdns_configure();
726

    
727
	if ($reinit == false) {
728
		unlock($captiveportallck);
729
	}
730
}
731

    
732
/* Delete all rules related to specific cpzone */
733
function captiveportal_delete_rules($pipes_to_remove = array()) {
734
	global $g, $cpzoneid, $cpzone;
735

    
736
	$skipto1 = captiveportal_ipfw_ruleno($cpzoneid);
737
	$skipto2 = $skipto1 + $g['captiveportal_rules_interval'];
738

    
739
	$cp_ifaces = pfSense_ipfw_table_list("cp_ifaces");
740
	if (is_array($cp_ifaces)) {
741
		foreach ($cp_ifaces as $cp_iface) {
742
			if (empty($cp_iface['skipto']) ||
743
			    $cp_iface['skipto'] != $skipto1) {
744
				continue;
745
			}
746

    
747
			pfSense_ipfw_table("cp_ifaces", IP_FW_TABLE_XDEL,
748
			    $cp_iface['iface']);
749
		}
750
	}
751

    
752
	mwexec("/sbin/ipfw delete {$skipto1}-{$skipto2}", true);
753

    
754
	$tables = captiveportal_get_ipfw_table_names();
755

    
756
	$delrules = "";
757
	foreach ($tables as $table) {
758
		$delrules .= "table {$table} destroy\n";
759
	}
760

    
761
	foreach ($pipes_to_remove as $pipeno) {
762
		$delrules .= "pipe delete {$pipeno}\n";
763
	}
764

    
765
	if (empty($delrules)) {
766
		return;
767
	}
768

    
769
	file_put_contents("{$g['tmp_path']}/ipfw_{$cpzone}.deltable.rules", $delrules);
770
	mwexec("/sbin/ipfw -q {$g['tmp_path']}/ipfw_{$cpzone}.deltable.rules", true);
771
	@unlink("{$g['tmp_path']}/ipfw_{$cpzone}.deltable.rules");
772
}
773

    
774
/*
775
 * Remove clients that have been around for longer than the specified amount of time
776
 * db file structure:
777
 * timestamp,ipfw_rule_no,clientip,clientmac,username,sessionid,password,session_timeout,idle_timeout,session_terminate_time,interim_interval,traffic_quota,auth_method,context
778
 * (password is in Base64 and only saved when reauthentication is enabled)
779
 */
780
function captiveportal_prune_old() {
781
	global $g, $config, $cpzone, $cpzoneid;
782

    
783
	if (empty($cpzone)) {
784
		return;
785
	}
786

    
787
	$cpcfg = $config['captiveportal'][$cpzone];
788
	$vcpcfg = $config['voucher'][$cpzone];
789

    
790
	/* check for expired entries */
791
	$idletimeout = 0;
792
	$timeout = 0;
793
	if (!empty($cpcfg['timeout']) && is_numeric($cpcfg['timeout'])) {
794
		$timeout = $cpcfg['timeout'] * 60;
795
	}
796

    
797
	if (!empty($cpcfg['idletimeout']) && is_numeric($cpcfg['idletimeout'])) {
798
		$idletimeout = $cpcfg['idletimeout'] * 60;
799
	}
800

    
801
	/* check for entries exceeding their traffic quota */
802
	$trafficquota = 0;
803
	if (!empty($cpcfg['trafficquota']) && is_numeric($cpcfg['trafficquota'])) {
804
		$trafficquota = $cpcfg['trafficquota'] * 1048576;
805
	}
806

    
807
	/* Is there any job to do? */
808
	if (!$timeout && !$idletimeout && !$trafficquota && !isset($cpcfg['reauthenticate']) &&
809
	    !isset($cpcfg['radiussession_timeout']) && !isset($cpcfg['radiustraffic_quota']) &&
810
	    !isset($vcpcfg['enable']) && !isset($cpcfg['radacct_enable'])) {
811
		return;
812
	}
813

    
814

    
815
	/* Read database */
816
	/* NOTE: while this can be simplified in non radius case keep as is for now */
817
	$cpdb = captiveportal_read_db();
818

    
819
	$unsetindexes = array();
820
	$voucher_needs_sync = false;
821
	/*
822
	 * Snapshot the time here to use for calculation to speed up the process.
823
	 * If something is missed next run will catch it!
824
	 */
825
	$pruning_time = time();
826
	foreach ($cpdb as $cpentry) {
827
		$stop_time = $pruning_time;
828

    
829
		$timedout = false;
830
		$term_cause = 1;
831
		/* hard timeout or session_timeout from radius if enabled */
832
		if (isset($cpcfg['radiussession_timeout'])) {
833
			$timeout = (is_numeric($cpentry[7])) ? $cpentry[7] : $timeout;
834
		}
835
		if ($timeout) {
836
			if (($pruning_time - $cpentry[0]) >= $timeout) {
837
				$timedout = true;
838
				$term_cause = 5; // Session-Timeout
839
				$logout_cause = 'SESSION TIMEOUT';
840
			}
841
		}
842

    
843
		/* Session-Terminate-Time */
844
		if (!$timedout && !empty($cpentry[9])) {
845
			if ($pruning_time >= $cpentry[9]) {
846
				$timedout = true;
847
				$term_cause = 5; // Session-Timeout
848
				$logout_cause = 'SESSION TIMEOUT';
849
			}
850
		}
851

    
852
		/* check if an idle_timeout has been set and if its set change the idletimeout to this value */
853
		$uidletimeout = (is_numeric($cpentry[8])) ? $cpentry[8] : $idletimeout;
854
		/* if an idle timeout is specified, get last activity timestamp from ipfw */
855
		if (!$timedout && $uidletimeout > 0) {
856
			$lastact = captiveportal_get_last_activity($cpentry[2]);
857
			/*	If the user has logged on but not sent any traffic they will never be logged out.
858
			 *	We "fix" this by setting lastact to the login timestamp.
859
			 */
860
			$lastact = $lastact ? $lastact : $cpentry[0];
861
			if ($lastact && (($pruning_time - $lastact) >= $uidletimeout)) {
862
				$timedout = true;
863
				$term_cause = 4; // Idle-Timeout
864
				$logout_cause = 'IDLE TIMEOUT';
865
				if (!isset($config['captiveportal'][$cpzone]['includeidletime'])) {
866
					$stop_time = $lastact;
867
				}
868
			}
869
		}
870

    
871
		/* if vouchers are configured, activate session timeouts */
872
		if (!$timedout && isset($vcpcfg['enable']) && !empty($cpentry[7])) {
873
			if ($pruning_time >= ($cpentry[0] + $cpentry[7])) {
874
				$timedout = true;
875
				$term_cause = 5; // Session-Timeout
876
				$logout_cause = 'SESSION TIMEOUT';
877
				$voucher_needs_sync = true;
878
			}
879
		}
880

    
881
		/* traffic quota, value retrieved from the radius attribute if the option is enabled */
882
		if (isset($cpcfg['radiustraffic_quota'])) {
883
			$trafficquota = (is_numeric($cpentry[11])) ? $cpentry[11] : $trafficquota;
884
		}
885
		if (!$timedout && $trafficquota > 0) {
886
			$volume = getVolume($cpentry[2], $cpentry[3]);
887
			if (($volume['input_bytes'] + $volume['output_bytes']) > $trafficquota) {
888
				$timedout = true;
889
				$term_cause = 10; // NAS-Request
890
				$logout_cause = 'QUOTA EXCEEDED';
891
			}
892
		}
893

    
894
		if ($timedout) {
895
			captiveportal_disconnect($cpentry, $term_cause, $stop_time);
896
			captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], $logout_cause);
897
			$unsetindexes[] = $cpentry[5];
898
		}
899

    
900
		/* do periodic reauthentication? For Radius servers, send accounting updates? */
901
		if (!$timedout) {
902
			//Radius servers : send accounting
903
			if (isset($cpcfg['radacct_enable']) && $cpentry[12] === 'radius') {
904
				if (substr($cpcfg['reauthenticateacct'], 0, 9) === "stopstart") {
905
					/* stop and restart accounting */
906
					if ($cpcfg['reauthenticateacct'] === "stopstartfreeradius") {
907
						$rastart_time = 0;
908
						$rastop_time = 60;
909
					} else {
910
						$rastart_time = $cpentry[0];
911
						$rastop_time = time();
912
					}
913
					captiveportal_send_server_accounting('stop',
914
						$cpentry[1], // ruleno
915
						$cpentry[4], // username
916
						$cpentry[2], // clientip
917
						$cpentry[3], // clientmac
918
						$cpentry[5], // sessionid
919
						$rastart_time, // start time
920
						$rastop_time, // Stop Time
921
						10); // NAS Request
922
					$clientsn = (is_ipaddrv6($cpentry[2])) ? 128 : 32;
923
					pfSense_ipfw_table_zerocnt("{$cpzone}_auth_up", "{$cpentry[2]}/{$clientsn}");
924
					pfSense_ipfw_table_zerocnt("{$cpzone}_auth_down", "{$cpentry[2]}/{$clientsn}");
925
					if ($cpcfg['reauthenticateacct'] == "stopstartfreeradius") {
926
						/* Need to pause here or the FreeRADIUS server gets confused about packet ordering. */
927
						sleep(1);
928
					}
929
					captiveportal_send_server_accounting('start',
930
						$cpentry[1], // ruleno
931
						$cpentry[4], // username
932
						$cpentry[2], // clientip
933
						$cpentry[3], // clientmac
934
						$cpentry[5]); // sessionid
935
				} else if ($cpcfg['reauthenticateacct'] == "interimupdate") {
936
					$session_time = $pruning_time - $cpentry[0];
937
					if (!empty($cpentry[10]) && $cpentry[10] > 60) {
938
						$interval = $cpentry[10];
939
					} else {
940
						$interval = 0;
941
					}
942
					$past_interval_min = ($session_time > $interval);
943
					if ($interval != 0) {
944
						$within_interval = ($session_time % $interval >= 0 && $session_time % $interval <= 59);
945
					}
946
					if ($interval === 0 || ($interval > 0 && $past_interval_min && $within_interval)) {
947
					captiveportal_send_server_accounting('update',
948
						$cpentry[1], // ruleno
949
						$cpentry[4], // username
950
						$cpentry[2], // clientip
951
						$cpentry[3], // clientmac
952
						$cpentry[5], // sessionid
953
						$cpentry[0]); // start time
954
					}
955
				}
956
			}
957

    
958
			/* check this user again */
959
			if (isset($cpcfg['reauthenticate']) && $cpentry[13] !== 'voucher') {
960
				$auth_result = captiveportal_authenticate_user(
961
					$cpentry[4], // username
962
					base64_decode($cpentry[6]), // password
963
					$cpentry[3], // clientmac
964
					$cpentry[2], // clientip
965
					$cpentry[1], // ruleno
966
					$cpentry[13]); // context
967
				if ($auth_result['result'] === false) {
968
					captiveportal_disconnect($cpentry, 17);
969
					captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "DISCONNECT - REAUTHENTICATION FAILED", $auth_list['reply_message']);
970
					$unsetindexes[] = $cpentry[5];
971
				} else if ($auth_result['result'] === true) {
972
					if ($cpentry[12] !== $auth_result['auth_method']) {
973
						// if the user got authenticated against another server type:  we update the database
974
						if (!empty($cpentry[5])) {
975
							captiveportal_write_db("UPDATE captiveportal SET authmethod = '{$auth_result['auth_method']}' WHERE sessionid = '{$cpentry[5]}'");
976
							captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CHANGED AUTHENTICATION SERVER", $auth_list['reply_message']);
977
						}
978
						// User was logged on a RADIUS server, but is now logged in by another server type : we send an accounting Stop
979
						if (isset($config['captiveportal'][$cpzone]['radacct_enable']) && $cpentry[12] =='radius') {
980
							if ($cpcfg['reauthenticateacct'] === "stopstartfreeradius") {
981
								$rastart_time = 0;
982
								$rastop_time = 60;
983
							} else {
984
								$rastart_time = $cpentry[0];
985
								$rastop_time = time();
986
							}
987
							captiveportal_send_server_accounting('stop',
988
								$cpentry[1], // ruleno
989
								$cpentry[4], // username
990
								$cpentry[2], // clientip
991
								$cpentry[3], // clientmac
992
								$cpentry[5], // sessionid
993
								$rastart_time, // start time
994
								$rastop_time, // Stop Time
995
								3); // Lost Service
996
						// User was logged on a non-RADIUS Server but is now logged in by a RADIUS server : we send an accounting Start
997
						} else if(isset($config['captiveportal'][$cpzone]['radacct_enable']) && $auth_result['auth_method'] === 'radius') {
998
							captiveportal_send_server_accounting('start',
999
								$cpentry[1], // ruleno
1000
								$cpentry[4], // username
1001
								$cpentry[2], // clientip
1002
								$cpentry[3], // clientmac
1003
								$cpentry[5], // sessionid
1004
								$cpentry[0]); // start_time
1005
						}
1006
					}
1007
					captiveportal_reapply_attributes($cpentry, $auth_result['attributes']);
1008
				}
1009
			}
1010
		}
1011
	}
1012
	unset($cpdb);
1013

    
1014
	captiveportal_prune_old_automac();
1015

    
1016
	if ($voucher_needs_sync == true) {
1017
		/* Trigger a sync of the vouchers on config */
1018
		send_event("service sync vouchers");
1019
	}
1020

    
1021
	/* write database */
1022
	if (!empty($unsetindexes)) {
1023
		captiveportal_remove_entries($unsetindexes);
1024
	}
1025
}
1026

    
1027
function captiveportal_prune_old_automac() {
1028
	global $g, $config, $cpzone, $cpzoneid;
1029

    
1030
	if (is_array($config['captiveportal'][$cpzone]['passthrumac']) &&
1031
	    isset($config['captiveportal'][$cpzone]['passthrumacadd'])) {
1032
		$tmpvoucherdb = array();
1033
		$macrules = "";
1034
		$writecfg = false;
1035
		foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $eid => $emac) {
1036
			if ($emac['logintype'] != "voucher") {
1037
				continue;
1038
			}
1039
			if (isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) {
1040
				if (isset($tmpvoucherdb[$emac['username']])) {
1041
					$temac = $config['captiveportal'][$cpzone]['passthrumac'][$tmpvoucherdb[$emac['username']]];
1042
					$pipeno = captiveportal_get_dn_passthru_ruleno($temac['mac']);
1043
					if ($pipeno) {
1044
						captiveportal_free_dn_ruleno($pipeno);
1045
						$macrules .= "table {$cpzone}_pipe_mac delete any,{$temac['mac']}\n";
1046
						$macrules .= "table {$cpzone}_pipe_mac delete {$temac['mac']},any\n";
1047
						$macrules .= "pipe delete {$pipeno}\n";
1048
						++$pipeno;
1049
						$macrules .= "pipe delete {$pipeno}\n";
1050
					}
1051
					$writecfg = true;
1052
					captiveportal_logportalauth($temac['username'], $temac['mac'],
1053
					    $temac['ip'], "DUPLICATE {$temac['username']} LOGIN - TERMINATING OLD SESSION");
1054
					unset($config['captiveportal'][$cpzone]['passthrumac'][$tmpvoucherdb[$emac['username']]]);
1055
				}
1056
				$tmpvoucherdb[$emac['username']] = $eid;
1057
			}
1058
			if (voucher_auth($emac['username']) <= 0) {
1059
				$pipeno = captiveportal_get_dn_passthru_ruleno($emac['mac']);
1060
				if ($pipeno) {
1061
					captiveportal_free_dn_ruleno($pipeno);
1062
					$macrules .= "table {$cpzone}_pipe_mac delete any,{$emac['mac']}\n";
1063
					$macrules .= "table {$cpzone}_pipe_mac delete {$emac['mac']},any\n";
1064
					$macrules .= "pipe delete {$pipeno}\n";
1065
					++$pipeno;
1066
					$macrules .= "pipe delete {$pipeno}\n";
1067
				}
1068
				$writecfg = true;
1069
				captiveportal_logportalauth($emac['username'], $emac['mac'],
1070
				    $emac['ip'], "EXPIRED {$emac['username']} LOGIN - TERMINATING SESSION");
1071
				unset($config['captiveportal'][$cpzone]['passthrumac'][$eid]);
1072
			}
1073
		}
1074
		unset($tmpvoucherdb);
1075
		if (!empty($macrules)) {
1076
			@file_put_contents("{$g['tmp_path']}/macentry.prunerules.tmp", $macrules);
1077
			unset($macrules);
1078
			mwexec("/sbin/ipfw -q {$g['tmp_path']}/macentry.prunerules.tmp");
1079
		}
1080
		if ($writecfg === true) {
1081
			write_config("Prune session for auto-added macs");
1082
		}
1083
	}
1084
}
1085

    
1086
/* remove a single client according to the DB entry */
1087
function captiveportal_disconnect($dbent, $term_cause = 1, $stop_time = null) {
1088
	global $g, $config, $cpzone, $cpzoneid;
1089

    
1090
	$stop_time = (empty($stop_time)) ? time() : $stop_time;
1091

    
1092
	/* this client needs to be deleted - remove ipfw rules */
1093
	if (isset($config['captiveportal'][$cpzone]['radacct_enable']) && $dbent[12] =='radius') {
1094
		if ($config['captiveportal'][$cpzone]['reauthenticateacct'] == "stopstartfreeradius") {
1095
			/*
1096
			 * Interim updates are on so the session time must be
1097
			 * reported as the elapsed time since the previous
1098
			 * interim update.
1099
			 */
1100
			$session_time = ($stop_time - $dbent[0]) % 60;
1101
			$start_time = $stop_time - $session_time;
1102
		} else {
1103
			$start_time = $dbent[0];
1104
		}
1105
		captiveportal_send_server_accounting('stop',
1106
			$dbent[1], // ruleno
1107
			$dbent[4], // username
1108
			$dbent[2], // clientip
1109
			$dbent[3], // clientmac
1110
			$dbent[5], // sessionid
1111
			$start_time, // start time
1112
			$stop_time, // stop time
1113
			$term_cause); // Acct-Terminate-Cause
1114
	}
1115

    
1116
	if (is_ipaddr($dbent[2])) {
1117
		/*
1118
		 * Delete client's ip entry from tables auth_up and auth_down.
1119
		 * It's not necessary to explicit specify mac address here
1120
		 */
1121
		$cpsession = captiveportal_isip_logged($dbent[2]);
1122
		if (!empty($cpsession)) {
1123
			$clientsn = (is_ipaddrv6($dbent[2])) ? 128 : 32;
1124
			pfSense_ipfw_table("{$cpzone}_auth_up",
1125
			    IP_FW_TABLE_XDEL, "{$dbent[2]}/{$clientsn}");
1126
			pfSense_ipfw_table("{$cpzone}_auth_down",
1127
			    IP_FW_TABLE_XDEL, "{$dbent[2]}/{$clientsn}");
1128
		}
1129
		/* XXX: Redundant?! Ensure all pf(4) states are killed. */
1130
		$_gb = @pfSense_kill_states($dbent[2]);
1131
		$_gb = @pfSense_kill_srcstates($dbent[2]);
1132
	}
1133

    
1134
	/*
1135
	 * These are the pipe numbers we use to control traffic shaping for
1136
	 * each logged in user via captive portal
1137
	 * We could get an error if the pipe doesn't exist but everything
1138
	 * should still be fine
1139
	 */
1140
	if (!empty($dbent[1])) {
1141
		/*
1142
		 * Call captiveportal_free_dnrules() in dry_run mode to verify
1143
		 * if there are pipes to be removed and prevent the attempt to
1144
		 * delete invalid pipes
1145
		 */
1146
		$removed_pipes = captiveportal_free_dnrules($dbent[1],
1147
		    $dbent[1]+1, true);
1148

    
1149
		if (!empty($removed_pipes)) {
1150
			$_gb = @pfSense_ipfw_pipe("pipe delete {$dbent[1]}");
1151
			$_gb = @pfSense_ipfw_pipe("pipe delete " .
1152
			    ($dbent[1]+1));
1153

    
1154
			/*
1155
			 * Release the ruleno so it can be reallocated to new
1156
			 * clients
1157
			 */
1158
			captiveportal_free_dn_ruleno($dbent[1]);
1159
		}
1160
	}
1161

    
1162
	// XMLRPC Call over to the master Voucher node
1163
	if (xmlrpc_sync_voucher_details($syncip, $syncport,
1164
	    $vouchersyncusername, $syncpass)) {
1165
		$remote_status = xmlrpc_sync_voucher_disconnect($dbent, $syncip,
1166
		    $syncport, $syncpass, $vouchersyncusername, $term_cause,
1167
		    $stop_time);
1168
	}
1169

    
1170
}
1171

    
1172
/* remove a single client by sessionid */
1173
function captiveportal_disconnect_client($sessionid, $term_cause = 1, $logoutReason = "LOGOUT") {
1174
	global $g, $config;
1175

    
1176
	$sessionid = SQLite3::escapeString($sessionid);
1177
	/* read database */
1178
	$result = captiveportal_read_db("WHERE sessionid = '{$sessionid}'");
1179

    
1180
	/* find entry */
1181
	if (!empty($result)) {
1182

    
1183
		foreach ($result as $cpentry) {
1184
			captiveportal_disconnect($cpentry, $term_cause);
1185
			captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "DISCONNECT");
1186
		}
1187
		captiveportal_remove_entries(array($sessionid));
1188
		unset($result);
1189
	}
1190
}
1191

    
1192
/* remove all clients */
1193
function captiveportal_disconnect_all($term_cause = 6, $logoutReason = "DISCONNECT") {
1194
	global $g, $config, $cpzone, $cpzoneid;
1195

    
1196
	/* check if we're pruning old entries and eventually wait */
1197
	$rcprunelock = try_lock("rcprunecaptiveportal{$cpzone}", 15);
1198

    
1199
	/* if we still don't have the lock, unlock forcefully and take it */
1200
	if (!$rcprunelock) {
1201
		log_error("CP zone ${cpzone}: could not obtain the lock for more than 15 seconds, lock taken forcefully to disconnect all users");
1202
		unlock_force("rcprunecaptiveportal{$cpzone}");
1203
		$rcprunelock = lock("rcprunecaptiveportal{$cpzone}", LOCK_EX);
1204
	}
1205

    
1206
	/* take a lock so new users won't be able to log in */
1207
	$cpdblck = lock("captiveportaldb{$cpzone}", LOCK_EX);
1208

    
1209
	captiveportal_radius_stop_all($term_cause, $logoutReason);
1210

    
1211
	/* remove users from the database */
1212
	$cpdb = captiveportal_read_db();
1213
	$unsetindexes = array_column($cpdb,5);
1214
	if (!empty($unsetindexes)) {
1215
		captiveportal_remove_entries($unsetindexes);
1216
	}
1217

    
1218
	/* reinit ipfw rules */
1219
	captiveportal_init_rules(true);
1220

    
1221
	unlock($cpdblck);
1222
	unlock($rcprunelock);
1223
}
1224

    
1225
/* send RADIUS acct stop for all current clients connected with RADIUS servers */
1226
function captiveportal_radius_stop_all($term_cause = 6, $logoutReason = "DISCONNECT") {
1227
	global $g, $config, $cpzone, $cpzoneid;
1228

    
1229
	$cpdb = captiveportal_read_db();
1230

    
1231
	$radacct = isset($config['captiveportal'][$cpzone]['radacct_enable']) ? true : false;
1232
	foreach ($cpdb as $cpentry) {
1233
		if ($cpentry[12] === 'radius' && $radacct) {
1234
			if ($config['captiveportal'][$cpzone]['reauthenticateacct'] == "stopstartfreeradius") {
1235
				$session_time = (time() - $cpentry[0]) % 60;
1236
				$start_time = time() - $session_time;
1237
			} else {
1238
				$start_time = $cpentry[0];
1239
			}
1240
			captiveportal_send_server_accounting('stop',
1241
				$cpentry[1], // ruleno
1242
				$cpentry[4], // username
1243
				$cpentry[2], // clientip
1244
				$cpentry[3], // clientmac
1245
				$cpentry[5], // sessionid
1246
				$start_time, // start time
1247
				$stop_time, // stop time
1248
				$term_cause); // Acct-Terminate-Cause
1249
		}
1250
		captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], $logoutReason);
1251
	}
1252
	unset($cpdb);
1253
}
1254

    
1255
function captiveportal_passthrumac_configure_entry($macent, $pipeinrule = false) {
1256
	global $config, $g, $cpzone;
1257

    
1258
	$bwUp = 0;
1259
	if (!empty($macent['bw_up'])) {
1260
		$bwUp = $macent['bw_up'];
1261
	} else if (!empty($config['captiveportal'][$cpzone]['bwdefaultup'])) {
1262
		$bwUp = $config['captiveportal'][$cpzone]['bwdefaultup'];
1263
	}
1264
	$bwDown = 0;
1265
	if (!empty($macent['bw_down'])) {
1266
		$bwDown = $macent['bw_down'];
1267
	} else if (!empty($config['captiveportal'][$cpzone]['bwdefaultdn'])) {
1268
		$bwDown = $config['captiveportal'][$cpzone]['bwdefaultdn'];
1269
	}
1270

    
1271
	if ($macent['action'] == 'pass') {
1272
		$rules = "";
1273

    
1274
		$pipeno = captiveportal_get_next_dn_ruleno();
1275

    
1276
		$pipeup = $pipeno;
1277
		if ($pipeinrule == true) {
1278
			$_gb = @pfSense_ipfw_pipe("pipe {$pipeno} config bw {$bwUp}Kbit/s queue 100 buckets 16");
1279
		} else {
1280
			$rules .= "pipe {$pipeno} config bw {$bwUp}Kbit/s queue 100 buckets 16\n";
1281
		}
1282

    
1283
		$pipedown = $pipeno + 1;
1284
		if ($pipeinrule == true) {
1285
			$_gb = @pfSense_ipfw_pipe("pipe {$pipedown} config bw {$bwDown}Kbit/s queue 100 buckets 16");
1286
		} else {
1287
			$rules .= "pipe {$pipedown} config bw {$bwDown}Kbit/s queue 100 buckets 16\n";
1288
		}
1289

    
1290
		$rules .= "table {$cpzone}_pipe_mac add any,{$macent['mac']} {$pipeup}\n";
1291
		$rules .= "table {$cpzone}_pipe_mac add {$macent['mac']},any {$pipedown}\n";
1292
	}
1293

    
1294
	return $rules;
1295
}
1296

    
1297
function captiveportal_passthrumac_delete_entry($macent) {
1298
	global $cpzone;
1299
	$rules = "";
1300

    
1301
	if ($macent['action'] == 'pass') {
1302
		$pipeno = captiveportal_get_dn_passthru_ruleno($macent['mac']);
1303

    
1304
		if (!empty($pipeno)) {
1305
			captiveportal_free_dn_ruleno($pipeno);
1306
			$rules .= "table {$cpzone}_pipe_mac delete any,{$macent['mac']}\n";
1307
			$rules .= "table {$cpzone}_pipe_mac delete {$macent['mac']},any\n";
1308
			$rules .= "pipe delete " . $pipeno . "\n";
1309
			$rules .= "pipe delete " . ++$pipeno . "\n";
1310
		}
1311
	}
1312

    
1313
	return $rules;
1314
}
1315

    
1316
function captiveportal_passthrumac_configure($filename = false, $startindex = 0, $stopindex = 0) {
1317
	global $config, $g, $cpzone;
1318

    
1319
	$rules = "";
1320

    
1321
	if (is_array($config['captiveportal'][$cpzone]['passthrumac'])) {
1322
		if ($stopindex > 0) {
1323
			$fd = fopen($filename, "w");
1324
			for ($idx = $startindex; $idx <= $stopindex; $idx++) {
1325
				if (isset($config['captiveportal'][$cpzone]['passthrumac'][$idx])) {
1326
					$rules = captiveportal_passthrumac_configure_entry($config['captiveportal'][$cpzone]['passthrumac'][$idx]);
1327
					fwrite($fd, $rules);
1328
				}
1329
			}
1330
			fclose($fd);
1331

    
1332
			return;
1333
		} else {
1334
			$nentries = count($config['captiveportal'][$cpzone]['passthrumac']);
1335
			if ($nentries > 2000) {
1336
				$nloops = $nentries / 1000;
1337
				$remainder= $nentries % 1000;
1338
				for ($i = 0; $i < $nloops; $i++) {
1339
					mwexec_bg("/usr/local/sbin/fcgicli -f /etc/rc.captiveportal_configure_mac -d \"cpzone={$cpzone}&startidx=" . ($i * 1000) . "&stopidx=" . ((($i+1) * 1000) - 1) . "\"");
1340
				}
1341
				if ($remainder > 0) {
1342
					mwexec_bg("/usr/local/sbin/fcgicli -f /etc/rc.captiveportal_configure_mac -d \"cpzone={$cpzone}&startidx=" . ($i * 1000) . "&stopidx=" . (($i* 1000) + $remainder) ."\"");
1343
				}
1344
			} else {
1345
				foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $macent) {
1346
					$rules .= captiveportal_passthrumac_configure_entry($macent, true);
1347
				}
1348
			}
1349
		}
1350
	}
1351

    
1352
	return $rules;
1353
}
1354

    
1355
function captiveportal_passthrumac_findbyname($username) {
1356
	global $config, $cpzone;
1357

    
1358
	if (is_array($config['captiveportal'][$cpzone]['passthrumac'])) {
1359
		foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $macent) {
1360
			if ($macent['username'] == $username) {
1361
				return $macent;
1362
			}
1363
		}
1364
	}
1365
	return NULL;
1366
}
1367

    
1368
/*
1369
 * table (3=IN)/(4=OUT) hold allowed ip's without bw limits
1370
 */
1371
function captiveportal_allowedip_configure_entry($ipent, $ishostname = false) {
1372
	global $g, $config, $cpzone;
1373

    
1374
	/*  Instead of copying this entire function for something
1375
	 *  easy such as hostname vs ip address add this check
1376
	 */
1377
	if ($ishostname === true) {
1378
		if (!platform_booting()) {
1379
			$ipaddress = gethostbyname($ipent['hostname']);
1380
			if (!is_ipaddr($ipaddress)) {
1381
				return;
1382
			}
1383
		} else {
1384
			$ipaddress = "";
1385
		}
1386
	} else {
1387
		$ipaddress = $ipent['ip'];
1388
	}
1389

    
1390
	$rules = "";
1391
	$cp_filterdns_conf = "";
1392
	$enBwup = 0;
1393
	if (!empty($ipent['bw_up'])) {
1394
		$enBwup = intval($ipent['bw_up']);
1395
	} else if (!empty($config['captiveportal'][$cpzone]['bwdefaultup'])) {
1396
		$enBwup = $config['captiveportal'][$cpzone]['bwdefaultup'];
1397
	}
1398
	$enBwdown = 0;
1399
	if (!empty($ipent['bw_down'])) {
1400
		$enBwdown = intval($ipent['bw_down']);
1401
	} else if (!empty($config['captiveportal'][$cpzone]['bwdefaultdn'])) {
1402
		$enBwdown = $config['captiveportal'][$cpzone]['bwdefaultdn'];
1403
	}
1404

    
1405
	$pipeup = captiveportal_get_next_dn_ruleno();
1406
	$_gb = @pfSense_ipfw_pipe("pipe {$pipeup} config bw {$enBwup}Kbit/s queue 100 buckets 16");
1407
	$pipedown = $pipeup + 1;
1408
	$_gb = @pfSense_ipfw_pipe("pipe {$pipedown} config bw {$enBwdown}Kbit/s queue 100 buckets 16");
1409

    
1410
	if ($ishostname === true) {
1411
		$cp_filterdns_conf .= "ipfw {$ipent['hostname']} {$cpzone}_allowed_up pipe {$pipeup}\n";
1412
		$cp_filterdns_conf .= "ipfw {$ipent['hostname']} {$cpzone}_allowed_down pipe {$pipedown}\n";
1413
		if (!is_ipaddr($ipaddress)) {
1414
			return array("", $cp_filterdns_conf);
1415
		}
1416
	}
1417

    
1418
	$subnet = "";
1419
	if (!empty($ipent['sn'])) {
1420
		$subnet = "/{$ipent['sn']}";
1421
	}
1422
	$rules .= "table {$cpzone}_allowed_up add {$ipaddress}{$subnet} {$pipeup}\n";
1423
	$rules .= "table {$cpzone}_allowed_down add {$ipaddress}{$subnet} {$pipedown}\n";
1424

    
1425
	if ($ishostname === true) {
1426
		return array($rules, $cp_filterdns_conf);
1427
	} else {
1428
		return $rules;
1429
	}
1430
}
1431

    
1432
function captiveportal_allowedhostname_configure() {
1433
	global $config, $g, $cpzone, $cpzoneid;
1434

    
1435
	$rules = "";
1436
	if (!is_array($config['captiveportal'][$cpzone]['allowedhostname'])) {
1437
		return $rules;
1438
	}
1439

    
1440
	$rules = "\n# captiveportal_allowedhostname_configure()\n";
1441
	$cp_filterdns_conf = "";
1442
	foreach ($config['captiveportal'][$cpzone]['allowedhostname'] as $hostnameent) {
1443
		$tmprules = captiveportal_allowedip_configure_entry($hostnameent, true);
1444
		$rules .= $tmprules[0];
1445
		$cp_filterdns_conf .= $tmprules[1];
1446
	}
1447
	$cp_filterdns_filename = "{$g['varetc_path']}/filterdns-{$cpzone}-captiveportal.conf";
1448
	@file_put_contents($cp_filterdns_filename, $cp_filterdns_conf);
1449
	unset($cp_filterdns_conf);
1450

    
1451
	return $rules;
1452
}
1453

    
1454
function captiveportal_filterdns_configure() {
1455
	global $config, $g, $cpzone, $cpzoneid;
1456

    
1457
	$cp_filterdns_filename = $g['varetc_path'] .
1458
	    "/filterdns-{$cpzone}-captiveportal.conf";
1459

    
1460
	if (isset($config['captiveportal'][$cpzone]['enable']) &&
1461
	    is_array($config['captiveportal'][$cpzone]['allowedhostname']) &&
1462
	    file_exists($cp_filterdns_filename)) {
1463
		if (isvalidpid($g['varrun_path'] .
1464
		    "/filterdns-{$cpzone}-cpah.pid")) {
1465
			sigkillbypid($g['varrun_path'] .
1466
			    "/filterdns-{$cpzone}-cpah.pid", "HUP");
1467
		} else {
1468
			mwexec("/usr/local/sbin/filterdns -p " .
1469
			    "{$g['varrun_path']}/filterdns-{$cpzone}-cpah.pid" .
1470
			    " -i 300 -c {$cp_filterdns_filename} -d 1");
1471
		}
1472
	} else {
1473
		killbypid("{$g['varrun_path']}/filterdns-{$cpzone}-cpah.pid");
1474
		@unlink("{$g['varrun_path']}/filterdns-{$cpzone}-cpah.pid");
1475
	}
1476

    
1477
	return $rules;
1478
}
1479

    
1480
function captiveportal_allowedip_configure() {
1481
	global $config, $g, $cpzone;
1482

    
1483
	$rules = "";
1484
	if (is_array($config['captiveportal'][$cpzone]['allowedip'])) {
1485
		foreach ($config['captiveportal'][$cpzone]['allowedip'] as $ipent) {
1486
			$rules .= captiveportal_allowedip_configure_entry($ipent);
1487
		}
1488
	}
1489

    
1490
	return $rules;
1491
}
1492

    
1493
/* get last activity timestamp given client IP address */
1494
function captiveportal_get_last_activity($ip) {
1495
	global $cpzone;
1496

    
1497
	/* Reading only from one of the tables is enough of approximation. */
1498
	$tables = array("{$cpzone}_allowed_up", "{$cpzone}_auth_up");
1499

    
1500
	foreach ($tables as $table) {
1501
		$ipfw = pfSense_ipfw_table_lookup($table, $ip);
1502
		if (is_array($ipfw)) {
1503
			/* Workaround for #46652 */
1504
			if ($ipfw['packets'] > 0) {
1505
				return $ipfw['timestamp'];
1506
			} else {
1507
				return 0;
1508
			}
1509
		}
1510
	}
1511

    
1512
	return 0;
1513
}
1514

    
1515

    
1516
/* log successful captive portal authentication to syslog */
1517
/* part of this code from php.net */
1518
function captiveportal_logportalauth($user, $mac, $ip, $status, $message = null) {
1519
	// Log it
1520
	if (!$message) {
1521
		$message = "{$status}: {$user}, {$mac}, {$ip}";
1522
	} else {
1523
		$message = trim($message);
1524
		$message = "{$status}: {$user}, {$mac}, {$ip}, {$message}";
1525
	}
1526
	captiveportal_syslog($message);
1527
}
1528

    
1529
/* log simple messages to syslog */
1530
function captiveportal_syslog($message) {
1531
	global $cpzone;
1532

    
1533
	$message = trim($message);
1534
	$message = "Zone: {$cpzone} - {$message}";
1535
	openlog("logportalauth", LOG_PID, LOG_LOCAL4);
1536
	// Log it
1537
	syslog(LOG_INFO, $message);
1538
	closelog();
1539
}
1540

    
1541
/* Authenticate users using Authentication Backend */
1542
function captiveportal_authenticate_user(&$login = '', &$password = '', $clientmac = '', $clientip = '', $pipeno = 'null', $context = 'first') {
1543
	global $g, $config, $cpzone;
1544
	$cpcfg = $config['captiveportal'][$cpzone];
1545

    
1546
	$login_status = 'FAILURE';
1547
	$login_msg = gettext('Invalid credentials specified');
1548
	$reply_attributes = array();
1549
	$auth_method = '';
1550
	$auth_result = null;
1551

    
1552
	/*
1553
	Management of the reply Message (reason why the authentication failed) :
1554
	multiple authentication servers can be used, so multiple reply messages could theoretically be returned.
1555
	But only one message is returned (the most important one).
1556
	The return value of authenticate_user() define how important messages are :
1557
		- Reply message of a successful auth is more important than reply message of
1558
		a user failed auth(invalid credentials/authorization)
1559

    
1560
		- Reply message of a user failed auth is more important than reply message of
1561
		a server failed auth (unable to contact server)
1562

    
1563
		- When multiple user failed auth are encountered, messages returned by remote servers
1564
		(eg. reply in RADIUS Access-Reject) are more important than pfSense error messages.
1565

    
1566
	The $authlevel variable is a flag indicating the status of authentication
1567
	0 = failed server auth
1568
	1 = failed user auth
1569
	2 = failed user auth with custom server reply recieved
1570
	3 = successful auth
1571
	*/
1572
	$authlevel = 0;
1573

    
1574
	/* Getting authentication servers from captiveportal configuration */
1575
	$auth_servers = array();
1576

    
1577
	if ($cpcfg['auth_method'] === 'none') {
1578
		$auth_servers[] = array('type' => 'none');
1579
	} else {
1580
		if ($context === 'second') {
1581
			$fullauthservers = explode(",", $cpcfg['auth_server2']);
1582
		} else {
1583
			$fullauthservers = explode(",", $cpcfg['auth_server']);
1584
		}
1585

    
1586
		foreach ($fullauthservers as $authserver) {
1587
			if (strpos($authserver, ' - ') !== false) {
1588
				$authserver = explode(' - ', $authserver);
1589
				array_shift($authserver);
1590
				$authserver = implode(' - ', $authserver);
1591

    
1592
				if (auth_get_authserver($authserver) !== null) {
1593
					$auth_servers[] = auth_get_authserver($authserver);
1594
				} else {
1595
					log_error("Zone: {$cpzone} - Captive portal was unable to find the settings of the server '{$authserver}' used for authentication !");
1596
				}
1597
			}
1598
		}
1599
	}
1600

    
1601
	/* Unable to find the any authentication server config - shouldn't happen! - bail out */
1602
	if (count($auth_servers) === 0) {
1603
		log_error("Zone: {$cpzone} - No valid server could be used for authentication.");
1604
		$login_msg = gettext("Internal Error");
1605
	} else {
1606
		foreach ($auth_servers as $authcfg) {
1607
			if ($authlevel < 3) {
1608
				$radmac_error = false;
1609
				$attributes = array("nas_identifier" => empty($cpcfg["radiusnasid"]) ? "CaptivePortal-{$cpzone}" : $cpcfg["radiusnasid"],
1610
					"nas_port_type" => RADIUS_ETHERNET,
1611
					"nas_port" => $pipeno,
1612
					"framed_ip" => $clientip);
1613
				if (mac_format($clientmac) !== null) {
1614
					$attributes["calling_station_id"] = mac_format($clientmac);
1615
				}
1616

    
1617
				$result = null;
1618
				$status = null;
1619
				$msg = null;
1620

    
1621
				/* Radius MAC authentication */
1622
				if ($context === 'radmac' && $clientmac) {
1623
					if ($authcfg['type'] === 'radius') {
1624
						$login = mac_format($clientmac);
1625
						$status = "MACHINE LOGIN";
1626
					} else {
1627
						/* Trying to perform a Radius MAC authentication on a non-radius server - shouldn't happen! - bail out */
1628
						$msg = gettext("Internal Error");
1629
						log_error("Zone: {$cpzone} - Trying to perform RADIUS MAC authentication on a non-RADIUS server !");
1630
						$radmac_error = true;
1631
						$result = null;
1632
					}
1633
				}
1634

    
1635
				if (!$radmac_error) {
1636
					if ($authcfg['type'] === 'none') {
1637
						$result = true;
1638
					} else {
1639
						$result = authenticate_user($login, $password, $authcfg, $attributes);
1640
					}
1641

    
1642
					if (!empty($attributes['error_message'])) {
1643
						$msg = $attributes['error_message'];
1644
					}
1645

    
1646
					if ($authcfg['type'] == 'Local Auth' && $result && isset($cpcfg['localauth_priv'])) {
1647
						if (!userHasPrivilege(getUserEntry($login), "user-services-captiveportal-login")) {
1648
							$result = false;
1649
							$msg = gettext("Access Denied");
1650
						}
1651
					}
1652
					if ($context === 'radmac' && $result === null && empty($attributes['reply_message'])) {
1653
						$msg = gettext("RADIUS MAC Authentication Failed.");
1654
					}
1655

    
1656
					if (empty($status)) {
1657
						if ($result === true) {
1658
							$status = "ACCEPT";
1659
						} elseif ($result === null) {
1660
							$status = "ERROR";
1661
						} else {
1662
							$status = "FAILURE";
1663
						}
1664
					}
1665

    
1666
					if ($context === 'radmac' && $login == mac_format($clientmac) || $authcfg['type'] === 'none' && empty($login)) {
1667
						$login = "unauthenticated";
1668
					}
1669
				}
1670
				// We determine a flag
1671
				if ($result === true) {
1672
					$val = 3;
1673
				} elseif ($result === false && !empty($attributes['reply_message'])) {
1674
					$val = 2;
1675
					$msg = $attributes['reply_message'];
1676
				} elseif ($result === false) {
1677
					$val = 1;
1678
				} elseif ($result === null) {
1679
					$val = 0;
1680
				}
1681

    
1682
				if ($val >= $auth_val) {
1683
					$auth_val = $val;
1684
					$auth_method = $authcfg['type'];
1685
					$login_status = $status;
1686
					$login_msg = $msg;
1687
					$reply_attributes = $attributes;
1688
					$auth_result = $result;
1689
				}
1690
			}
1691
		}
1692
	}
1693

    
1694
	return array('result'=>$auth_result, 'attributes'=>$reply_attributes, 'auth_method' =>$auth_method, 'login_status'=> $login_status, 'login_message' => $login_msg);
1695
}
1696

    
1697
function captiveportal_opendb() {
1698
	global $g, $config, $cpzone, $cpzoneid;
1699

    
1700
	$db_path = "{$g['vardb_path']}/captiveportal{$cpzone}.db";
1701
	$createquery = "CREATE TABLE IF NOT EXISTS captiveportal (" .
1702
				"allow_time INTEGER, pipeno INTEGER, ip TEXT, mac TEXT, username TEXT, " .
1703
				"sessionid TEXT, bpassword TEXT, session_timeout INTEGER, idle_timeout INTEGER, " .
1704
				"session_terminate_time INTEGER, interim_interval INTEGER, traffic_quota INTEGER, " .
1705
				"authmethod TEXT, context TEXT); " .
1706
			"CREATE UNIQUE INDEX IF NOT EXISTS idx_active ON captiveportal (sessionid, username); " .
1707
			"CREATE INDEX IF NOT EXISTS user ON captiveportal (username); " .
1708
			"CREATE INDEX IF NOT EXISTS ip ON captiveportal (ip); " .
1709
			"CREATE INDEX IF NOT EXISTS starttime ON captiveportal (allow_time)";
1710

    
1711
	try {
1712
		$DB = new SQLite3($db_path);
1713
		$DB->busyTimeout(60000);
1714
	} catch (Exception $e) {
1715
		captiveportal_syslog("Could not open {$db_path} as an sqlite database for {$cpzone}. Error message: " . $e->getMessage() . " -- Trying again.");
1716
		unlink_if_exists($db_path);
1717
		try {
1718
			$DB = new SQLite3($db_path);
1719
			$DB->busyTimeout(60000);
1720
		} catch (Exception $e) {
1721
			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.");
1722
			return;
1723
		}
1724
	}
1725

    
1726
	if (!$DB) {
1727
		captiveportal_syslog("Could not open {$db_path} as an sqlite database for {$cpzone}. Error message: {$DB->lastErrorMsg()}. Trying again.");
1728
		unlink_if_exists($db_path);
1729
		$DB = new SQLite3($db_path);
1730
		$DB->busyTimeout(60000);
1731
		if (!$DB) {
1732
			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.");
1733
			return;
1734
		}
1735
	}
1736

    
1737
	if (! $DB->exec($createquery)) {
1738
		captiveportal_syslog("Error during table {$cpzone} creation. Error message: {$DB->lastErrorMsg()}. Resetting and trying again.");
1739

    
1740
		/* If unable to initialize the database, reset and try again. */
1741
		$DB->close();
1742
		unset($DB);
1743
		unlink_if_exists($db_path);
1744
		$DB = new SQLite3($db_path);
1745
		$DB->busyTimeout(60000);
1746
		if ($DB->exec($createquery)) {
1747
			captiveportal_syslog("Successfully reinitialized tables for {$cpzone} -- database has been reset.");
1748
			if (!is_numericint($cpzoneid)) {
1749
				if (is_array($config['captiveportal'])) {
1750
					foreach ($config['captiveportal'] as $cpkey => $cp) {
1751
						if ($cpzone == $cpkey) {
1752
							$cpzoneid = $cp['zoneid'];
1753
						}
1754
					}
1755
				}
1756
			}
1757
			if (is_numericint($cpzoneid)) {
1758
				$table_names = captiveportal_get_ipfw_table_names();
1759
				foreach ($table_names as $table_name) {
1760
					mwexec("/sbin/ipfw table {$table_name} flush");
1761
				}
1762
				captiveportal_syslog("Flushed tables for {$cpzone} after database reset.");
1763
			}
1764
		} else {
1765
			captiveportal_syslog("Still unable to create tables for {$cpzone}. Error message: {$DB->lastErrorMsg()}. Remove the database file manually and try again.");
1766
		}
1767
	}
1768

    
1769
	return $DB;
1770
}
1771

    
1772
/* Get all tables for specific cpzone */
1773
function captiveportal_get_ipfw_table_names() {
1774
	global $cpzone;
1775

    
1776
	$result = array();
1777
	$tables = pfSense_ipfw_tables_list();
1778

    
1779
	if (!is_array($tables)) {
1780
		return $result;
1781
	}
1782

    
1783
	$len = strlen($cpzone) + 1;
1784
	foreach ($tables as $table) {
1785
		if (substr($table['name'], 0, $len) != $cpzone . '_') {
1786
			continue;
1787
		}
1788

    
1789
		$result[] = $table['name'];
1790
	}
1791

    
1792
	return $result;
1793
}
1794

    
1795
/* read captive portal DB into array */
1796
function captiveportal_read_db($query = "") {
1797
	$cpdb = array();
1798

    
1799
	$DB = captiveportal_opendb();
1800
	if ($DB) {
1801
		$response = $DB->query("SELECT * FROM captiveportal {$query}");
1802
		if ($response != FALSE) {
1803
			while ($row = $response->fetchArray()) {
1804
				$cpdb[] = $row;
1805
			}
1806
		}
1807
		$DB->close();
1808
	}
1809

    
1810
	return $cpdb;
1811
}
1812

    
1813
function captiveportal_remove_entries($remove) {
1814

    
1815
	if (!is_array($remove) || empty($remove)) {
1816
		return;
1817
	}
1818

    
1819
	$query = "DELETE FROM captiveportal WHERE sessionid in (";
1820
	foreach ($remove as $idx => $unindex) {
1821
		$query .= "'{$unindex}'";
1822
		if ($idx < (count($remove) - 1)) {
1823
			$query .= ",";
1824
		}
1825
	}
1826
	$query .= ")";
1827
	captiveportal_write_db($query);
1828
}
1829

    
1830
/* write captive portal DB */
1831
function captiveportal_write_db($queries) {
1832
	global $g;
1833

    
1834
	if (is_array($queries)) {
1835
		$query = implode(";", $queries);
1836
	} else {
1837
		$query = $queries;
1838
	}
1839

    
1840
	$DB = captiveportal_opendb();
1841
	if ($DB) {
1842
		$DB->exec("BEGIN TRANSACTION");
1843
		$result = $DB->exec($query);
1844
		if (!$result) {
1845
			captiveportal_syslog("Trying to modify DB returned error: {$DB->lastErrorMsg()}");
1846
		} else {
1847
			$DB->exec("END TRANSACTION");
1848
		}
1849
		$DB->close();
1850
		return $result;
1851
	} else {
1852
		return true;
1853
	}
1854
}
1855

    
1856
function captiveportal_write_elements() {
1857
	global $g, $config, $cpzone;
1858

    
1859
	$cpcfg = $config['captiveportal'][$cpzone];
1860

    
1861
	if (!is_dir($g['captiveportal_element_path'])) {
1862
		@mkdir($g['captiveportal_element_path']);
1863
	}
1864

    
1865
	if (is_array($cpcfg['element'])) {
1866
		foreach ($cpcfg['element'] as $data) {
1867
			/* Do not attempt to decode or write out empty files. */
1868
			if (isset($data['nocontent'])) {
1869
					continue;
1870
			}
1871
			if (empty($data['content']) || empty(base64_decode($data['content']))) {
1872
				unlink_if_exists("{$g['captiveportal_element_path']}/{$data['name']}");
1873
				touch("{$g['captiveportal_element_path']}/{$data['name']}");
1874
			} elseif (!@file_put_contents("{$g['captiveportal_element_path']}/{$data['name']}", base64_decode($data['content']))) {
1875
				printf(gettext('Error: cannot open \'%1$s\' in captiveportal_write_elements()%2$s'), $data['name'], "\n");
1876
				return 1;
1877
			}
1878
			if (!file_exists("{$g['captiveportal_path']}/{$data['name']}")) {
1879
				@symlink("{$g['captiveportal_element_path']}/{$data['name']}", "{$g['captiveportal_path']}/{$data['name']}");
1880
			}
1881
		}
1882
	}
1883

    
1884
	return 0;
1885
}
1886

    
1887
function captiveportal_free_dnrules($rulenos_start = 2000,
1888
    $rulenos_range_max = 64500, $dry_run = false) {
1889
	global $g, $cpzone;
1890

    
1891
	$removed_pipes = array();
1892

    
1893
	if (!file_exists("{$g['vardb_path']}/captiveportaldn.rules")) {
1894
		return $removed_pipes;
1895
	}
1896

    
1897
	if (!$dry_run) {
1898
		$cpruleslck = lock("captiveportalrulesdn", LOCK_EX);
1899
	}
1900

    
1901
	$rules = unserialize(file_get_contents(
1902
	    "{$g['vardb_path']}/captiveportaldn.rules"));
1903
	$ridx = $rulenos_start;
1904
	while ($ridx < $rulenos_range_max) {
1905
		if ($rules[$ridx] == $cpzone) {
1906
			if (!$dry_run) {
1907
				$rules[$ridx] = false;
1908
			}
1909
			$removed_pipes[] = $ridx;
1910
			$ridx++;
1911
			if (!$dry_run) {
1912
				$rules[$ridx] = false;
1913
			}
1914
			$removed_pipes[] = $ridx;
1915
			$ridx++;
1916
		} else {
1917
			$ridx += 2;
1918
		}
1919
	}
1920

    
1921
	if (!$dry_run) {
1922
		file_put_contents("{$g['vardb_path']}/captiveportaldn.rules",
1923
		    serialize($rules));
1924
		unlock($cpruleslck);
1925
	}
1926

    
1927
	unset($rules);
1928

    
1929
	return $removed_pipes;
1930
}
1931

    
1932
function captiveportal_get_next_dn_ruleno($rulenos_start = 2000, $rulenos_range_max = 64500) {
1933
	global $config, $g, $cpzone;
1934

    
1935
	$cpruleslck = lock("captiveportalrulesdn", LOCK_EX);
1936
	$ruleno = 0;
1937
	if (file_exists("{$g['vardb_path']}/captiveportaldn.rules")) {
1938
		$rules = unserialize(file_get_contents("{$g['vardb_path']}/captiveportaldn.rules"));
1939
		$ridx = $rulenos_start;
1940
		while ($ridx < $rulenos_range_max) {
1941
			if (empty($rules[$ridx])) {
1942
				$ruleno = $ridx;
1943
				$rules[$ridx] = $cpzone;
1944
				$ridx++;
1945
				$rules[$ridx] = $cpzone;
1946
				break;
1947
			} else {
1948
				$ridx += 2;
1949
			}
1950
		}
1951
	} else {
1952
		$rules = array_pad(array(), $rulenos_range_max, false);
1953
		$ruleno = $rulenos_start;
1954
		$rules[$rulenos_start] = $cpzone;
1955
		$rulenos_start++;
1956
		$rules[$rulenos_start] = $cpzone;
1957
	}
1958
	file_put_contents("{$g['vardb_path']}/captiveportaldn.rules", serialize($rules));
1959
	unlock($cpruleslck);
1960
	unset($rules);
1961

    
1962
	return $ruleno;
1963
}
1964

    
1965
function captiveportal_free_dn_ruleno($ruleno) {
1966
	global $config, $g;
1967

    
1968
	$cpruleslck = lock("captiveportalrulesdn", LOCK_EX);
1969
	if (file_exists("{$g['vardb_path']}/captiveportaldn.rules")) {
1970
		$rules = unserialize(file_get_contents("{$g['vardb_path']}/captiveportaldn.rules"));
1971
		$rules[$ruleno] = false;
1972
		$ruleno++;
1973
		$rules[$ruleno] = false;
1974
		file_put_contents("{$g['vardb_path']}/captiveportaldn.rules", serialize($rules));
1975
		unset($rules);
1976
	}
1977
	unlock($cpruleslck);
1978
}
1979

    
1980
function captiveportal_get_dn_passthru_ruleno($value) {
1981
	global $config, $g, $cpzone, $cpzoneid;
1982

    
1983
	$cpcfg = $config['captiveportal'][$cpzone];
1984
	if (!isset($cpcfg['enable'])) {
1985
		return NULL;
1986
	}
1987

    
1988
	$cpruleslck = lock("captiveportalrulesdn", LOCK_EX);
1989
	$ruleno = NULL;
1990
	if (file_exists("{$g['vardb_path']}/captiveportaldn.rules")) {
1991
		unset($output);
1992
		$item = pfSense_ipfw_table_lookup("{$cpzone}_pipe_mac",
1993
		    "any,{$value}");
1994
		if (!is_array($item) || empty($item['pipe'])) {
1995
			unlock($cpruleslck);
1996
			return NULL;
1997
		}
1998

    
1999
		$ruleno = intval($item['pipe']);
2000
		$rules = unserialize(file_get_contents("{$g['vardb_path']}/captiveportaldn.rules"));
2001
		if (!$rules[$ruleno]) {
2002
			$ruleno = NULL;
2003
		}
2004
		unset($rules);
2005
	}
2006
	unlock($cpruleslck);
2007

    
2008
	return $ruleno;
2009
}
2010

    
2011
/**
2012
 * This function will calculate the traffic produced by a client
2013
 * based on its firewall rule
2014
 *
2015
 * Point of view: NAS
2016
 *
2017
 * Input means: from the client
2018
 * Output means: to the client
2019
 *
2020
 */
2021

    
2022
function getVolume($ip) {
2023
	global $config, $cpzone;
2024

    
2025
	$reverse = isset($config['captiveportal'][$cpzone]['reverseacct'])
2026
	    ? true : false;
2027
	$volume = array();
2028
	// Initialize vars properly, since we don't want NULL vars
2029
	$volume['input_pkts'] = $volume['input_bytes'] = 0;
2030
	$volume['output_pkts'] = $volume['output_bytes'] = 0;
2031

    
2032
	$tables = array("allowed", "auth");
2033

    
2034
	foreach($tables as $table) {
2035
		$ipfw = pfSense_ipfw_table_lookup("{$cpzone}_{$table}_up", $ip);
2036
		if (!is_array($ipfw)) {
2037
			continue;
2038
		}
2039
		if ($reverse) {
2040
			$volume['output_pkts'] = $ipfw['packets'];
2041
			$volume['output_bytes'] = $ipfw['bytes'];
2042
		} else {
2043
			$volume['input_pkts'] = $ipfw['packets'];
2044
			$volume['input_bytes'] = $ipfw['bytes'];
2045
		}
2046
	}
2047

    
2048
	foreach($tables as $table) {
2049
		$ipfw = pfSense_ipfw_table_lookup("{$cpzone}_{$table}_down",
2050
		    $ip);
2051
		if (!is_array($ipfw)) {
2052
			continue;
2053
		}
2054
		if ($reverse) {
2055
			$volume['input_pkts'] = $ipfw['packets'];
2056
			$volume['input_bytes'] = $ipfw['bytes'];
2057
		} else {
2058
			$volume['output_pkts'] = $ipfw['packets'];
2059
			$volume['output_bytes'] = $ipfw['bytes'];
2060
		}
2061
	}
2062

    
2063
	return $volume;
2064
}
2065

    
2066
function portal_ip_from_client_ip($cliip) {
2067
	global $config, $cpzone;
2068

    
2069
	$isipv6 = is_ipaddrv6($cliip);
2070
	$interfaces = explode(",", $config['captiveportal'][$cpzone]['interface']);
2071
	foreach ($interfaces as $cpif) {
2072
		if ($isipv6) {
2073
			$ip = get_interface_ipv6($cpif);
2074
			$sn = get_interface_subnetv6($cpif);
2075
		} else {
2076
			$ip = get_interface_ip($cpif);
2077
			$sn = get_interface_subnet($cpif);
2078
		}
2079
		if (ip_in_subnet($cliip, "{$ip}/{$sn}")) {
2080
			return $ip;
2081
		}
2082
	}
2083

    
2084
	$inet = ($isipv6) ? '-inet6' : '-inet';
2085
	$iface = exec_command("/sbin/route -n get {$inet} {$cliip} | /usr/bin/awk '/interface/ { print \$2; };'");
2086
	$iface = trim($iface, "\n");
2087
	if (!empty($iface)) {
2088
		$ip = ($isipv6) ? find_interface_ipv6($iface) : find_interface_ip($iface);
2089
		if (is_ipaddr($ip)) {
2090
			return $ip;
2091
		}
2092
	}
2093

    
2094
	// doesn't match up to any particular interface
2095
	// so let's set the portal IP to what PHP says
2096
	// the server IP issuing the request is.
2097
	// allows same behavior as 1.2.x where IP isn't
2098
	// in the subnet of any CP interface (static routes, etc.)
2099
	// rather than forcing to DNS hostname resolution
2100
	$ip = $_SERVER['SERVER_ADDR'];
2101
	if (is_ipaddr($ip)) {
2102
		return $ip;
2103
	}
2104

    
2105
	return false;
2106
}
2107

    
2108
function portal_hostname_from_client_ip($cliip) {
2109
	global $config, $cpzone;
2110

    
2111
	$cpcfg = $config['captiveportal'][$cpzone];
2112

    
2113
	if (isset($cpcfg['httpslogin'])) {
2114
		$listenporthttps = $cpcfg['listenporthttps'] ? $cpcfg['listenporthttps'] : ($cpcfg['zoneid'] + 8001);
2115
		$ourhostname = $cpcfg['httpsname'];
2116

    
2117
		if ($listenporthttps != 443) {
2118
			$ourhostname .= ":" . $listenporthttps;
2119
		}
2120
	} else {
2121
		$listenporthttp = $cpcfg['listenporthttp'] ? $cpcfg['listenporthttp'] : ($cpcfg['zoneid'] + 8000);
2122
		$ifip = portal_ip_from_client_ip($cliip);
2123
		if (!$ifip) {
2124
			$ourhostname = "{$config['system']['hostname']}.{$config['system']['domain']}";
2125
		} else {
2126
			$ourhostname = (is_ipaddrv6($ifip)) ? "[{$ifip}]" : "{$ifip}";
2127
		}
2128

    
2129
		if ($listenporthttp != 80) {
2130
			$ourhostname .= ":" . $listenporthttp;
2131
		}
2132
	}
2133

    
2134
	return $ourhostname;
2135
}
2136

    
2137
/* functions move from index.php */
2138

    
2139
function portal_reply_page($redirurl, $type = null, $message = null, $clientmac = null, $clientip = null, $username = null, $password = null) {
2140
	global $g, $config, $cpzone;
2141

    
2142
	/* Get captive portal layout */
2143
	if ($type == "redir") {
2144
		header("Location: {$redirurl}");
2145
		return;
2146
	} else if ($type == "login") {
2147
		$htmltext = get_include_contents("{$g['varetc_path']}/captiveportal_{$cpzone}.html");
2148
	} else {
2149
		$htmltext = get_include_contents("{$g['varetc_path']}/captiveportal-{$cpzone}-error.html");
2150
	}
2151

    
2152
	$cpcfg = $config['captiveportal'][$cpzone];
2153

    
2154
	/* substitute the PORTAL_REDIRURL variable */
2155
	if ($cpcfg['preauthurl']) {
2156
		$htmltext = str_replace("\$PORTAL_REDIRURL\$", "{$cpcfg['preauthurl']}", $htmltext);
2157
		$htmltext = str_replace("#PORTAL_REDIRURL#", "{$cpcfg['preauthurl']}", $htmltext);
2158
	}
2159

    
2160
	/* substitute other variables */
2161
	$ourhostname = portal_hostname_from_client_ip($clientip);
2162
	$protocol = (isset($cpcfg['httpslogin'])) ? 'https://' : 'http://';
2163
	$htmltext = str_replace("\$PORTAL_ACTION\$", "{$protocol}{$ourhostname}/index.php?zone={$cpzone}", $htmltext);
2164
	$htmltext = str_replace("#PORTAL_ACTION#", "{$protocol}{$ourhostname}/index.php?zone={$cpzone}", $htmltext);
2165

    
2166
	$htmltext = str_replace("\$PORTAL_ZONE\$", htmlspecialchars($cpzone), $htmltext);
2167
	$htmltext = str_replace("\$PORTAL_REDIRURL\$", htmlspecialchars($redirurl), $htmltext);
2168
	$htmltext = str_replace("\$PORTAL_MESSAGE\$", htmlspecialchars($message), $htmltext);
2169
	$htmltext = str_replace("\$CLIENT_MAC\$", htmlspecialchars($clientmac), $htmltext);
2170
	$htmltext = str_replace("\$CLIENT_IP\$", htmlspecialchars($clientip), $htmltext);
2171

    
2172
	// Special handling case for captive portal master page so that it can be ran
2173
	// through the PHP interpreter using the include method above.  We convert the
2174
	// $VARIABLE$ case to #VARIABLE# in /etc/inc/captiveportal.inc before writing out.
2175
	$htmltext = str_replace("#PORTAL_ZONE#", htmlspecialchars($cpzone), $htmltext);
2176
	$htmltext = str_replace("#PORTAL_REDIRURL#", htmlspecialchars($redirurl), $htmltext);
2177
	$htmltext = str_replace("#PORTAL_MESSAGE#", htmlspecialchars($message), $htmltext);
2178
	$htmltext = str_replace("#CLIENT_MAC#", htmlspecialchars($clientmac), $htmltext);
2179
	$htmltext = str_replace("#CLIENT_IP#", htmlspecialchars($clientip), $htmltext);
2180
	$htmltext = str_replace("#USERNAME#", htmlspecialchars($username), $htmltext);
2181
	$htmltext = str_replace("#PASSWORD#", htmlspecialchars($password), $htmltext);
2182

    
2183
	echo $htmltext;
2184
}
2185

    
2186
function captiveportal_reapply_attributes($cpentry, $attributes) {
2187
	global $config, $cpzone, $g;
2188

    
2189
	if (isset($config['captiveportal'][$cpzone]['peruserbw'])) {
2190
		$dwfaultbw_up = !empty($config['captiveportal'][$cpzone]['bwdefaultup']) ? $config['captiveportal'][$cpzone]['bwdefaultup'] : 0;
2191
		$dwfaultbw_down = !empty($config['captiveportal'][$cpzone]['bwdefaultdn']) ? $config['captiveportal'][$cpzone]['bwdefaultdn'] : 0;
2192
	} else {
2193
		$dwfaultbw_up = $dwfaultbw_down = 0;
2194
	}
2195
	/* pipe throughputs must always be an integer, enforce that restriction again here. */
2196
	if (isset($config['captiveportal'][$cpzone]['radiusperuserbw'])) {
2197
		$bw_up = round(!empty($attributes['bw_up']) ? intval($attributes['bw_up'])/1000 : $dwfaultbw_up, 0);
2198
		$bw_down = round(!empty($attributes['bw_down']) ? intval($attributes['bw_down'])/1000 : $dwfaultbw_down, 0);
2199
	} else {
2200
		$bw_up = round($dwfaultbw_up,0);
2201
		$bw_down = round($dwfaultbw_down,0);
2202
	}
2203

    
2204
	$bw_up_pipeno = $cpentry[1];
2205
	$bw_down_pipeno = $cpentry[1]+1;
2206

    
2207
	$_gb = @pfSense_ipfw_pipe("pipe {$bw_up_pipeno} config bw {$bw_up}Kbit/s queue 100 buckets 16");
2208
	$_gb = @pfSense_ipfw_pipe("pipe {$bw_down_pipeno} config bw {$bw_down}Kbit/s queue 100 buckets 16");
2209
	//captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "RADIUS_BANDWIDTH_REAPPLY", "{$bw_up}/{$bw_down}");
2210

    
2211
	unset($bw_up_pipeno, $bw_down_pipeno, $bw_up, $bw_down);
2212
}
2213

    
2214
function portal_allow($clientip, $clientmac, $username, $password = null, $attributes = null, $pipeno = null, $authmethod = null, $context = 'first') {
2215
	global $redirurl, $g, $config, $type, $_POST, $cpzone, $cpzoneid;
2216

    
2217
	// Ensure we create an array if we are missing attributes
2218
	if (!is_array($attributes)) {
2219
		$attributes = array();
2220
	}
2221

    
2222
	unset($sessionid);
2223

    
2224
	/* Do not allow concurrent login execution. */
2225
	$cpdblck = lock("captiveportaldb{$cpzone}", LOCK_EX);
2226

    
2227
	if ($attributes['voucher']) {
2228
		$remaining_time = $attributes['session_timeout'];
2229
		$authmethod = "voucher"; // Set RADIUS-Attribute to Voucher to prevent ReAuth-Reqeuest for Vouchers Bug: #2155
2230
		$context = "voucher";
2231
	}
2232

    
2233
	$writecfg = false;
2234
	/* If both "Add MAC addresses of connected users as pass-through MAC" and "Disable concurrent logins" are checked, 
2235
	then we need to check if the user was already authenticated using another MAC Address, and if so remove the previous Pass-Through MAC. */	
2236
	if ((isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) && ($username != 'unauthenticated') && isset($config['captiveportal'][$cpzone]['passthrumacadd'])) {
2237
		$mac = captiveportal_passthrumac_findbyname($username);
2238
		if (!empty($mac)) {
2239
			foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $idx => $macent) {
2240
				if ($macent['mac'] != $mac['mac']) {
2241
					continue;
2242
				}
2243

    
2244
				$pipeno = captiveportal_get_dn_passthru_ruleno($mac['mac']);
2245
				if ($pipeno) {
2246
					captiveportal_free_dn_ruleno($pipeno);
2247
					@pfSense_ipfw_table("{$cpzone}_pipe_mac", IP_FW_TABLE_XDEL, "any,{$mac['mac']}");
2248
					@pfSense_ipfw_table("{$cpzone}_pipe_mac", IP_FW_TABLE_XDEL, "{$mac['mac']},any");
2249
					@pfSense_ipfw_pipe("pipe delete " . $pipeno+1);
2250
					@pfSense_ipfw_pipe("pipe delete " . $pipeno);
2251
				}
2252
				unset($config['captiveportal'][$cpzone]['passthrumac'][$idx]);
2253
			}
2254
		}
2255
	}
2256

    
2257
	/* read in client database */
2258
	$query = "WHERE ip = '{$clientip}'";
2259
	$tmpusername = SQLite3::escapeString(strtolower($username));
2260
	if (isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) {
2261
		$query .= " OR (username != 'unauthenticated' AND lower(username) = '{$tmpusername}')";
2262
	}
2263
	$cpdb = captiveportal_read_db($query);
2264

    
2265
	/* Snapshot the timestamp */
2266
	$allow_time = time();
2267
	$unsetindexes = array();
2268

    
2269
	foreach ($cpdb as $cpentry) {
2270
		/* on the same ip */
2271
		if ($cpentry[2] == $clientip) {
2272
			if (isset($config['captiveportal'][$cpzone]['nomacfilter']) || $cpentry[3] == $clientmac) {
2273
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT LOGIN - REUSING OLD SESSION");
2274
			} else {
2275
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT LOGIN - REUSING IP {$cpentry[2]} WITH DIFFERENT MAC ADDRESS {$cpentry[3]}");
2276
			}
2277
			$sessionid = $cpentry[5];
2278
			break;
2279
		} elseif (($attributes['voucher']) && ($username != 'unauthenticated') && ($cpentry[4] == $username)) {
2280
			// user logged in with an active voucher. Check for how long and calculate
2281
			// how much time we can give him (voucher credit - used time)
2282
			$remaining_time = $cpentry[0] + $cpentry[7] - $allow_time;
2283
			if ($remaining_time < 0) { // just in case.
2284
				$remaining_time = 0;
2285
			}
2286

    
2287
			/* This user was already logged in so we disconnect the old one */
2288
			captiveportal_disconnect($cpentry, 13);
2289
			captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT LOGIN - TERMINATING OLD SESSION");
2290
			$unsetindexes[] = $cpentry[5];
2291
			break;
2292
		} elseif ((isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) && ($username != 'unauthenticated')) {
2293
			/* on the same username */
2294
			if (strcasecmp($cpentry[4], $username) == 0) {
2295
				/* This user was already logged in so we disconnect the old one */
2296
				captiveportal_disconnect($cpentry, 13);
2297
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT LOGIN - TERMINATING OLD SESSION");
2298
				$unsetindexes[] = $cpentry[5];
2299
				break;
2300
			}
2301
		}
2302
	}
2303
	unset($cpdb);
2304

    
2305
	if (!empty($unsetindexes)) {
2306
		captiveportal_remove_entries($unsetindexes);
2307
	}
2308

    
2309
	if ($attributes['voucher'] && $remaining_time <= 0) {
2310
		return 0;       // voucher already used and no time left
2311
	}
2312

    
2313
	if (!isset($sessionid)) {
2314
		/* generate unique session ID */
2315
		$tod = gettimeofday();
2316
		$sessionid = substr(md5(mt_rand() . $tod['sec'] . $tod['usec'] . $clientip . $clientmac), 0, 16);
2317

    
2318
		if (isset($config['captiveportal'][$cpzone]['peruserbw'])) {
2319
			$dwfaultbw_up = !empty($config['captiveportal'][$cpzone]['bwdefaultup']) ? $config['captiveportal'][$cpzone]['bwdefaultup'] : 0;
2320
			$dwfaultbw_down = !empty($config['captiveportal'][$cpzone]['bwdefaultdn']) ? $config['captiveportal'][$cpzone]['bwdefaultdn'] : 0;
2321
		} else {
2322
			$dwfaultbw_up = $dwfaultbw_down = 0;
2323
		}
2324
		/* pipe throughputs must always be an integer, enforce that restriction again here. */
2325
		if (isset($config['captiveportal'][$cpzone]['radiusperuserbw'])) {
2326
			$bw_up = round(!empty($attributes['bw_up']) ? intval($attributes['bw_up'])/1000 : $dwfaultbw_up, 0);
2327
			$bw_down = round(!empty($attributes['bw_down']) ? intval($attributes['bw_down'])/1000 : $dwfaultbw_down, 0);
2328
		} else {
2329
			$bw_up = round($dwfaultbw_up,0);
2330
			$bw_down = round($dwfaultbw_down,0);
2331
		}
2332

    
2333
		if (isset($config['captiveportal'][$cpzone]['passthrumacadd'])) {
2334

    
2335
			$mac = array();
2336
			$mac['action'] = 'pass';
2337
			$mac['mac'] = $clientmac;
2338
			$mac['ip'] = $clientip; /* Used only for logging */
2339
			$mac['username'] = $username;
2340
			if ($attributes['voucher']) {
2341
				$mac['logintype'] = "voucher";
2342
			}
2343
			if ($username == "unauthenticated") {
2344
				$mac['descr'] = "Auto-added";
2345
			} else if ($authmethod == "voucher") {
2346
				$mac['descr'] = "Auto-added for voucher {$username}";
2347
			} else {
2348
				$mac['descr'] = "Auto-added for user {$username}";
2349
			}
2350
			if (!empty($bw_up)) {
2351
				$mac['bw_up'] = $bw_up;
2352
			}
2353
			if (!empty($bw_down)) {
2354
				$mac['bw_down'] = $bw_down;
2355
			}
2356
			if (!is_array($config['captiveportal'][$cpzone]['passthrumac'])) {
2357
				$config['captiveportal'][$cpzone]['passthrumac'] = array();
2358
			}
2359
			//check for mac duplicates before adding it to config.
2360
			$mac_duplicate = false;
2361
			foreach($config['captiveportal'][$cpzone]['passthrumac'] as $mac_check){
2362
				if($mac_check['mac'] == $mac['mac']){
2363
					$mac_duplicate = true;
2364
				}
2365
			}
2366
			if(!$mac_duplicate){
2367
				$config['captiveportal'][$cpzone]['passthrumac'][] = $mac;
2368
			}
2369
			unlock($cpdblck);
2370
			$macrules = captiveportal_passthrumac_configure_entry($mac);
2371
			file_put_contents("{$g['tmp_path']}/macentry_{$cpzone}.rules.tmp", $macrules);
2372
			mwexec("/sbin/ipfw -q {$g['tmp_path']}/macentry_{$cpzone}.rules.tmp");
2373
			$writecfg = true;
2374
		} else {
2375
			/* See if a pipeno is passed, if not start sessions because this means there isn't one atm */
2376
			if (is_null($pipeno)) {
2377
				$pipeno = captiveportal_get_next_dn_ruleno();
2378
			}
2379

    
2380
			/* if the pool is empty, return appropriate message and exit */
2381
			if (is_null($pipeno)) {
2382
				portal_reply_page($redirurl, "error", "System reached maximum login capacity");
2383
				log_error("Zone: {$cpzone} - WARNING!  Captive portal has reached maximum login capacity");
2384
				unlock($cpdblck);
2385
				return;
2386
			}
2387

    
2388
			$bw_up_pipeno = $pipeno;
2389
			$bw_down_pipeno = $pipeno + 1;
2390
			//$bw_up /= 1000; // Scale to Kbit/s
2391
			$_gb = @pfSense_ipfw_pipe("pipe {$bw_up_pipeno} config bw {$bw_up}Kbit/s queue 100 buckets 16");
2392
			$_gb = @pfSense_ipfw_pipe("pipe {$bw_down_pipeno} config bw {$bw_down}Kbit/s queue 100 buckets 16");
2393

    
2394
			$rule_entry = "{$clientip}/" . (is_ipaddrv6($clientip) ? "128" : "32");
2395
			if (!isset($config['captiveportal'][$cpzone]['nomacfilter'])) {
2396
				$rule_entry .= ",{$clientmac}";
2397
			}
2398
			$_gb = @pfSense_ipfw_table("{$cpzone}_auth_up", IP_FW_TABLE_XADD, "{$rule_entry}", $bw_up_pipeno);
2399
			$_gb = @pfSense_ipfw_table("{$cpzone}_auth_down", IP_FW_TABLE_XADD, "{$rule_entry}", $bw_down_pipeno);
2400

    
2401
			if ($attributes['voucher']) {
2402
				$attributes['session_timeout'] = $remaining_time;
2403
			}
2404

    
2405
			/* handle empty attributes */
2406
			$session_timeout = (!empty($attributes['session_timeout'])) ? $attributes['session_timeout'] : 'NULL';
2407
			$idle_timeout = (!empty($attributes['idle_timeout'])) ? $attributes['idle_timeout'] : 'NULL';
2408
			$session_terminate_time = (!empty($attributes['session_terminate_time'])) ? $attributes['session_terminate_time'] : 'NULL';
2409
			$interim_interval = (!empty($attributes['interim_interval'])) ? $attributes['interim_interval'] : 'NULL';
2410
			$traffic_quota = (!empty($attributes['maxbytes'])) ? $attributes['maxbytes'] : 'NULL';
2411

    
2412
			/* escape username */
2413
			$safe_username = SQLite3::escapeString($username);
2414

    
2415
			/* encode password in Base64 just in case it contains commas */
2416
			$bpassword = (isset($config['captiveportal'][$cpzone]['reauthenticate'])) ? base64_encode($password) : '';
2417
			$insertquery = "INSERT INTO captiveportal (allow_time, pipeno, ip, mac, username, sessionid, bpassword, session_timeout, idle_timeout, session_terminate_time, interim_interval, traffic_quota, authmethod, context) ";
2418
			$insertquery .= "VALUES ({$allow_time}, {$pipeno}, '{$clientip}', '{$clientmac}', '{$safe_username}', '{$sessionid}', '{$bpassword}', ";
2419
			$insertquery .= "{$session_timeout}, {$idle_timeout}, {$session_terminate_time}, {$interim_interval}, {$traffic_quota}, '{$authmethod}', '{$context}')";
2420

    
2421
			/* store information to database */
2422
			captiveportal_write_db($insertquery);
2423
			unlock($cpdblck);
2424
			unset($insertquery, $bpassword);
2425

    
2426
			$radacct = isset($config['captiveportal'][$cpzone]['radacct_enable']) ? true : false;
2427
			if ($authmethod === 'radius' && $radacct) {
2428
				captiveportal_send_server_accounting('start',
2429
					$pipeno, // ruleno
2430
					$username, // username
2431
					$clientip, // clientip
2432
					$clientmac, // clientmac
2433
					$sessionid, // sessionid
2434
					time());  // start time
2435
			}
2436
		}
2437
	} else {
2438
		/* NOTE: #3062-11 If the pipeno has been allocated free it to not DoS the CP */
2439
		if (!is_null($pipeno)) {
2440
			captiveportal_free_dn_ruleno($pipeno);
2441
		}
2442

    
2443
		unlock($cpdblck);
2444
	}
2445

    
2446
	if ($writecfg == true) {
2447
		write_config(gettext("Captive Portal allowed users configuration changed"));
2448
	}
2449

    
2450
	/* redirect user to desired destination */
2451
	if (!empty($attributes['url_redirection'])) {
2452
		$my_redirurl = $attributes['url_redirection'];
2453
	} else if (!empty($redirurl)) {
2454
		$my_redirurl = $redirurl;
2455
	} else if (!empty($config['captiveportal'][$cpzone]['redirurl'])) {
2456
		$my_redirurl = $config['captiveportal'][$cpzone]['redirurl'];
2457
	}
2458

    
2459
	if (isset($config['captiveportal'][$cpzone]['logoutwin_enable']) && !isset($config['captiveportal'][$cpzone]['passthrumacadd'])) {
2460
		$ourhostname = portal_hostname_from_client_ip($clientip);
2461
		$protocol = (isset($config['captiveportal'][$cpzone]['httpslogin'])) ? 'https://' : 'http://';
2462
		$logouturl = "{$protocol}{$ourhostname}/";
2463

    
2464
		if (isset($attributes['reply_message'])) {
2465
			$message = $attributes['reply_message'];
2466
		} else {
2467
			$message = 0;
2468
		}
2469

    
2470
		include_once("{$g['varetc_path']}/captiveportal-{$cpzone}-logout.html");
2471

    
2472
	} else {
2473
		portal_reply_page($my_redirurl, "redir", "Just redirect the user.");
2474
	}
2475

    
2476
	return $sessionid;
2477
}
2478

    
2479

    
2480
/*
2481
 * Used for when pass-through credits are enabled.
2482
 * Returns true when there was at least one free login to deduct for the MAC.
2483
 * Expired entries are removed as they are seen.
2484
 * Active entries are updated according to the configuration.
2485
 */
2486
function portal_consume_passthrough_credit($clientmac) {
2487
	global $config, $cpzone;
2488

    
2489
	if (!empty($config['captiveportal'][$cpzone]['freelogins_count']) && is_numeric($config['captiveportal'][$cpzone]['freelogins_count'])) {
2490
		$freeloginscount = $config['captiveportal'][$cpzone]['freelogins_count'];
2491
	} else {
2492
		return false;
2493
	}
2494

    
2495
	if (!empty($config['captiveportal'][$cpzone]['freelogins_resettimeout']) && is_numeric($config['captiveportal'][$cpzone]['freelogins_resettimeout'])) {
2496
		$resettimeout = $config['captiveportal'][$cpzone]['freelogins_resettimeout'];
2497
	} else {
2498
		return false;
2499
	}
2500

    
2501
	if ($freeloginscount < 1 || $resettimeout <= 0 || !$clientmac) {
2502
		return false;
2503
	}
2504

    
2505
	$updatetimeouts = isset($config['captiveportal'][$cpzone]['freelogins_updatetimeouts']);
2506

    
2507
	/*
2508
	 * Read database of used MACs.  Lines are a comma-separated list
2509
	 * of the time, MAC, then the count of pass-through credits remaining.
2510
	 */
2511
	$usedmacs = captiveportal_read_usedmacs_db();
2512

    
2513
	$currenttime = time();
2514
	$found = false;
2515
	foreach ($usedmacs as $key => $usedmac) {
2516
		$usedmac = explode(",", $usedmac);
2517

    
2518
		if ($usedmac[1] == $clientmac) {
2519
			if ($usedmac[0] + ($resettimeout * 3600) > $currenttime) {
2520
				if ($usedmac[2] < 1) {
2521
					if ($updatetimeouts) {
2522
						$usedmac[0] = $currenttime;
2523
						unset($usedmacs[$key]);
2524
						$usedmacs[] = implode(",", $usedmac);
2525
						captiveportal_write_usedmacs_db($usedmacs);
2526
					}
2527

    
2528
					return false;
2529
				} else {
2530
					$usedmac[2] -= 1;
2531
					$usedmacs[$key] = implode(",", $usedmac);
2532
				}
2533

    
2534
				$found = true;
2535
			} else {
2536
				unset($usedmacs[$key]);
2537
			}
2538

    
2539
			break;
2540
		} else if ($usedmac[0] + ($resettimeout * 3600) <= $currenttime) {
2541
			unset($usedmacs[$key]);
2542
		}
2543
	}
2544

    
2545
	if (!$found) {
2546
		$usedmac = array($currenttime, $clientmac, $freeloginscount - 1);
2547
		$usedmacs[] = implode(",", $usedmac);
2548
	}
2549

    
2550
	captiveportal_write_usedmacs_db($usedmacs);
2551
	return true;
2552
}
2553

    
2554
function captiveportal_read_usedmacs_db() {
2555
	global $g, $cpzone;
2556

    
2557
	$cpumaclck = lock("captiveusedmacs{$cpzone}");
2558
	if (file_exists("{$g['vardb_path']}/captiveportal_usedmacs_{$cpzone}.db")) {
2559
		$usedmacs = file("{$g['vardb_path']}/captiveportal_usedmacs_{$cpzone}.db", FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
2560
		if (!$usedmacs) {
2561
			$usedmacs = array();
2562
		}
2563
	} else {
2564
		$usedmacs = array();
2565
	}
2566

    
2567
	unlock($cpumaclck);
2568
	return $usedmacs;
2569
}
2570

    
2571
function captiveportal_write_usedmacs_db($usedmacs) {
2572
	global $g, $cpzone;
2573

    
2574
	$cpumaclck = lock("captiveusedmacs{$cpzone}", LOCK_EX);
2575
	@file_put_contents("{$g['vardb_path']}/captiveportal_usedmacs_{$cpzone}.db", implode("\n", $usedmacs));
2576
	unlock($cpumaclck);
2577
}
2578

    
2579
function captiveportal_blocked_mac($mac) {
2580
	global $config, $g, $cpzone;
2581

    
2582
	if (empty($mac) || !is_macaddr($mac)) {
2583
		return false;
2584
	}
2585

    
2586
	if (!is_array($config['captiveportal'][$cpzone]['passthrumac'])) {
2587
		return false;
2588
	}
2589

    
2590
	foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $passthrumac) {
2591
		if (($passthrumac['action'] == 'block') &&
2592
		    ($passthrumac['mac'] == strtolower($mac))) {
2593
			return true;
2594
		}
2595
	}
2596

    
2597
	return false;
2598

    
2599
}
2600

    
2601
/* Captiveportal Radius Accounting */
2602

    
2603
function gigawords($bytes) {
2604

    
2605
	/*
2606
	 * RFC2866 Specifies a 32bit unsigned integer, which is a max of 4294967295
2607
	 * Currently there is a fault in the PECL radius_put_int function which can handle only 32bit signed integer.
2608
	 */
2609

    
2610
	// We use BCMath functions since normal integers don't work with so large numbers
2611
	$gigawords = bcdiv( bcsub( $bytes, remainder($bytes) ) , GIGAWORDS_RIGHT_OPERAND) ;
2612

    
2613
	// We need to manually set this to a zero instead of NULL for put_int() safety
2614
	if (is_null($gigawords)) {
2615
		$gigawords = 0;
2616
	}
2617

    
2618
	return $gigawords;
2619
}
2620

    
2621
function remainder($bytes) {
2622
	// Calculate the bytes we are going to send to the radius
2623
	$bytes = bcmod($bytes, GIGAWORDS_RIGHT_OPERAND);
2624

    
2625
	if (is_null($bytes)) {
2626
		$bytes = 0;
2627
	}
2628

    
2629
    return $bytes;
2630
}
2631

    
2632
function 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) {
2633
	global $cpzone, $config;
2634

    
2635
	$cpcfg = $config['captiveportal'][$cpzone];
2636
	$acctcfg = auth_get_authserver($cpcfg['radacct_server']);
2637

    
2638
	if (!isset($cpcfg['radacct_enable']) || empty($acctcfg)) {
2639
		return null;
2640
	}
2641

    
2642
	if ($type === 'on') {
2643
		$racct = new Auth_RADIUS_Acct_On;
2644
	} elseif ($type === 'off') {
2645
		$racct = new Auth_RADIUS_Acct_Off;
2646
	} elseif ($type === 'start') {
2647
		$racct = new Auth_RADIUS_Acct_Start;
2648
		if (!is_int($start_time)) {
2649
			$start_time = time();
2650
		}
2651
	} elseif ($type === 'stop') {
2652
		$racct = new Auth_RADIUS_Acct_Stop;
2653
		if (!is_int($stop_time)) {
2654
			$stop_time = time();
2655
		}
2656
	} elseif ($type === 'update') {
2657
        $racct = new Auth_RADIUS_Acct_Update;
2658
		if (!is_int($stop_time)) {
2659
			$stop_time = time(); // "top time" here will be used only for calculating session time.
2660
		}
2661
	} else {
2662
		return null;
2663
	}
2664

    
2665
	$racct->addServer($acctcfg['host'], $acctcfg['radius_acct_port'],
2666
		$acctcfg['radius_secret'], $acctcfg['radius_timeout']);
2667

    
2668
	$racct->authentic = RADIUS_AUTH_RADIUS;
2669
	if ($cpcfg['auth_method'] === 'radmac' && $username === "unauthenticated" && !empty($clientmac)) {
2670
		$racct->username = mac_format($clientmac);
2671
	} elseif (!empty($username)) {
2672
		$racct->username = $username;
2673
	}
2674

    
2675
	if (PEAR::isError($racct->start())) {
2676
		captiveportal_syslog('RADIUS ACCOUNTING FAILED : '.$racct->getError());
2677
		$racct->close();
2678
		return null;
2679
	}
2680

    
2681
	$nasip = $acctcfg['radius_nasip_attribute'];
2682
	if (!is_ipaddr($nasip)) {
2683
		$nasip = get_interface_ip($nasip);
2684
		if (!is_ipaddr($nasip)) {
2685
			$nasip = get_interface_ip();//We use WAN interface IP as fallback for NAS-IP-Address
2686
		}
2687
	}
2688
	$nasmac = get_interface_mac(find_ip_interface($nasip));
2689
	$racct->putAttribute(RADIUS_NAS_IP_ADDRESS, $nasip, "addr");
2690

    
2691
	$racct->putAttribute(RADIUS_NAS_IDENTIFIER, empty($cpcfg["radiusnasid"]) ? "CaptivePortal-{$cpzone}" : $cpcfg["radiusnasid"] );
2692

    
2693
	if (is_int($ruleno)) {
2694
		$racct->putAttribute(RADIUS_NAS_PORT_TYPE, RADIUS_ETHERNET);
2695
		$racct->putAttribute(RADIUS_NAS_PORT, intval($ruleno), 'integer');
2696
	}
2697

    
2698
	if (!empty($sessionid)) {
2699
		$racct->putAttribute(RADIUS_ACCT_SESSION_ID, $sessionid);
2700
	}
2701

    
2702
	if (!empty($clientip) && is_ipaddr($clientip)) {
2703
		$racct->putAttribute(RADIUS_FRAMED_IP_ADDRESS, $clientip, "addr");
2704
	}
2705
	if (!empty($clientmac)) {
2706
		$racct->putAttribute(RADIUS_CALLING_STATION_ID, mac_format($clientmac));
2707
	}
2708
	if (!empty($nasmac)) {
2709
		$racct->putAttribute(RADIUS_CALLED_STATION_ID, mac_format($nasmac).':'.gethostname());
2710
	}
2711

    
2712
	// Accounting request Stop and Update : send the current data volume
2713
	if (($type === 'stop' || $type === 'update') && is_int($start_time)) {
2714
		$volume = getVolume($clientip);
2715
		$session_time = $stop_time - $start_time;
2716
		$volume['input_bytes_radius'] = remainder($volume['input_bytes']);
2717
		$volume['input_gigawords'] = gigawords($volume['input_bytes']);
2718
		$volume['output_bytes_radius'] = remainder($volume['output_bytes']);
2719
		$volume['output_gigawords'] = gigawords($volume['output_bytes']);
2720

    
2721
		// Volume stuff: Ingress
2722
		$racct->putAttribute(RADIUS_ACCT_INPUT_PACKETS, intval($volume['input_pkts']), "integer");
2723
		$racct->putAttribute(RADIUS_ACCT_INPUT_OCTETS, intval($volume['input_bytes_radius']), "integer");
2724
		// Volume stuff: Outgress
2725
		$racct->putAttribute(RADIUS_ACCT_OUTPUT_PACKETS, intval($volume['output_pkts']), "integer");
2726
		$racct->putAttribute(RADIUS_ACCT_OUTPUT_OCTETS, intval($volume['output_bytes_radius']), "integer");
2727
		$racct->putAttribute(RADIUS_ACCT_SESSION_TIME, intval($session_time), "integer");
2728

    
2729
		$racct->putAttribute(CUSTOM_RADIUS_ACCT_OUTPUT_GIGAWORDS, intval($volume['output_gigawords']), "integer");
2730
		$racct->putAttribute(CUSTOM_RADIUS_ACCT_INPUT_GIGAWORDS, intval($volume['input_gigawords']), "integer");
2731
		// Set session_time
2732
		$racct->session_time = $session_time;
2733
	}
2734

    
2735
	if ($type === 'stop') {
2736
		if (empty($term_cause)) {
2737
			$term_cause = 1;
2738
		}
2739
		$racct->putAttribute(RADIUS_ACCT_TERMINATE_CAUSE, $term_cause);
2740
	}
2741

    
2742
	// Send request
2743
	$result = $racct->send();
2744

    
2745
	if (PEAR::isError($result)) {
2746
		 captiveportal_syslog('RADIUS ACCOUNTING FAILED : '.$racct->getError());
2747
		 $result = null;
2748
	} elseif ($result !== true) {
2749
		$result = false;
2750
	}
2751

    
2752
	$racct->close();
2753
	return $result;
2754
}
2755

    
2756
function captiveportal_isip_logged($clientip) {
2757
	global $g, $cpzone;
2758

    
2759
	/* read in client database */
2760
	$query = "WHERE ip = '{$clientip}'";
2761
	$cpdb = captiveportal_read_db($query);
2762
	foreach ($cpdb as $cpentry) {
2763
		return $cpentry;
2764
	}
2765
}
2766
?>
(6-6/60)