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-2018 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
			$utimeout = (is_numeric($cpentry[7])) ? $cpentry[7] : $timeout;
834
		} else {
835
			$utimeout = $timeout;
836
		}
837
		if ($utimeout) {
838
			if (($pruning_time - $cpentry[0]) >= $utimeout) {
839
				$timedout = true;
840
				$term_cause = 5; // Session-Timeout
841
				$logout_cause = 'SESSION TIMEOUT';
842
			}
843
		}
844

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

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

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

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

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

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

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

    
1016
	captiveportal_prune_old_automac();
1017

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

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

    
1029
function captiveportal_prune_old_automac() {
1030
	global $g, $config, $cpzone, $cpzoneid;
1031

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

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

    
1092
	$stop_time = (empty($stop_time)) ? time() : $stop_time;
1093

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

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

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

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

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

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

    
1172
}
1173

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

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

    
1182
	/* find entry */
1183
	if (!empty($result)) {
1184

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

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

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

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

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

    
1211
	captiveportal_radius_stop_all($term_cause, $logoutReason);
1212

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

    
1220
	/* reinit ipfw rules */
1221
	captiveportal_init_rules(true);
1222

    
1223
	unlock($cpdblck);
1224
	unlock($rcprunelock);
1225
}
1226

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

    
1231
	$cpdb = captiveportal_read_db();
1232

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

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

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

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

    
1276
		$pipeno = captiveportal_get_next_dn_ruleno();
1277

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

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

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

    
1296
	return $rules;
1297
}
1298

    
1299
function captiveportal_passthrumac_delete_entry($macent) {
1300
	global $cpzone;
1301
	$rules = "";
1302

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

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

    
1315
	return $rules;
1316
}
1317

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

    
1321
	$rules = "";
1322

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

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

    
1354
	return $rules;
1355
}
1356

    
1357
function captiveportal_passthrumac_findbyname($username) {
1358
	global $config, $cpzone;
1359

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

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

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

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

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

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

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

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

    
1434
function captiveportal_allowedhostname_configure() {
1435
	global $config, $g, $cpzone, $cpzoneid;
1436

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

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

    
1453
	return $rules;
1454
}
1455

    
1456
function captiveportal_filterdns_configure() {
1457
	global $config, $g, $cpzone, $cpzoneid;
1458

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

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

    
1479
	return $rules;
1480
}
1481

    
1482
function captiveportal_allowedip_configure() {
1483
	global $config, $g, $cpzone;
1484

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

    
1492
	return $rules;
1493
}
1494

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

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

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

    
1514
	return 0;
1515
}
1516

    
1517

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1619
				$result = null;
1620
				$status = null;
1621
				$msg = null;
1622

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

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

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

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

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

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

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

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

    
1699
function captiveportal_opendb() {
1700
	global $g, $config, $cpzone, $cpzoneid;
1701

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

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

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

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

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

    
1771
	return $DB;
1772
}
1773

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

    
1778
	$result = array();
1779
	$tables = pfSense_ipfw_tables_list();
1780

    
1781
	if (!is_array($tables)) {
1782
		return $result;
1783
	}
1784

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

    
1791
		$result[] = $table['name'];
1792
	}
1793

    
1794
	return $result;
1795
}
1796

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

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

    
1812
	return $cpdb;
1813
}
1814

    
1815
function captiveportal_remove_entries($remove) {
1816

    
1817
	if (!is_array($remove) || empty($remove)) {
1818
		return;
1819
	}
1820

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

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

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

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

    
1858
function captiveportal_write_elements() {
1859
	global $g, $config, $cpzone;
1860

    
1861
	$cpcfg = $config['captiveportal'][$cpzone];
1862

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

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

    
1886
	return 0;
1887
}
1888

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

    
1893
	$removed_pipes = array();
1894

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

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

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

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

    
1929
	unset($rules);
1930

    
1931
	return $removed_pipes;
1932
}
1933

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

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

    
1964
	return $ruleno;
1965
}
1966

    
1967
function captiveportal_free_dn_ruleno($ruleno) {
1968
	global $config, $g;
1969

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

    
1982
function captiveportal_get_dn_passthru_ruleno($value) {
1983
	global $config, $g, $cpzone, $cpzoneid;
1984

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

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

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

    
2010
	return $ruleno;
2011
}
2012

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

    
2024
function getVolume($ip) {
2025
	global $config, $cpzone;
2026

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

    
2034
	$tables = array("allowed", "auth");
2035

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

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

    
2065
	return $volume;
2066
}
2067

    
2068
function portal_ip_from_client_ip($cliip) {
2069
	global $config, $cpzone;
2070

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

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

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

    
2107
	return false;
2108
}
2109

    
2110
function portal_hostname_from_client_ip($cliip) {
2111
	global $config, $cpzone;
2112

    
2113
	$cpcfg = $config['captiveportal'][$cpzone];
2114

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

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

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

    
2136
	return $ourhostname;
2137
}
2138

    
2139
/* functions move from index.php */
2140

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

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

    
2154
	$cpcfg = $config['captiveportal'][$cpzone];
2155

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

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

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

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

    
2185
	echo $htmltext;
2186
}
2187

    
2188
function captiveportal_reapply_attributes($cpentry, $attributes) {
2189
	global $config, $cpzone, $g;
2190

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

    
2206
	$bw_up_pipeno = $cpentry[1];
2207
	$bw_down_pipeno = $cpentry[1]+1;
2208

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

    
2213
	unset($bw_up_pipeno, $bw_down_pipeno, $bw_up, $bw_down);
2214
}
2215

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

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

    
2224
	unset($sessionid);
2225

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

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

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

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

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

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

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

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

    
2307
	if (!empty($unsetindexes)) {
2308
		captiveportal_remove_entries($unsetindexes);
2309
	}
2310

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

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

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

    
2335
		if (isset($config['captiveportal'][$cpzone]['passthrumacadd'])) {
2336

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

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

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

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

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

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

    
2414
			/* escape username */
2415
			$safe_username = SQLite3::escapeString($username);
2416

    
2417
			/* encode password in Base64 just in case it contains commas */
2418
			$bpassword = (isset($config['captiveportal'][$cpzone]['reauthenticate'])) ? base64_encode($password) : '';
2419
			$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) ";
2420
			$insertquery .= "VALUES ({$allow_time}, {$pipeno}, '{$clientip}', '{$clientmac}', '{$safe_username}', '{$sessionid}', '{$bpassword}', ";
2421
			$insertquery .= "{$session_timeout}, {$idle_timeout}, {$session_terminate_time}, {$interim_interval}, {$traffic_quota}, '{$authmethod}', '{$context}')";
2422

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

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

    
2445
		unlock($cpdblck);
2446
	}
2447

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

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

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

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

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

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

    
2478
	return $sessionid;
2479
}
2480

    
2481

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

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

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

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

    
2507
	$updatetimeouts = isset($config['captiveportal'][$cpzone]['freelogins_updatetimeouts']);
2508

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

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

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

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

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

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

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

    
2552
	captiveportal_write_usedmacs_db($usedmacs);
2553
	return true;
2554
}
2555

    
2556
function captiveportal_read_usedmacs_db() {
2557
	global $g, $cpzone;
2558

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

    
2569
	unlock($cpumaclck);
2570
	return $usedmacs;
2571
}
2572

    
2573
function captiveportal_write_usedmacs_db($usedmacs) {
2574
	global $g, $cpzone;
2575

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

    
2581
function captiveportal_blocked_mac($mac) {
2582
	global $config, $g, $cpzone;
2583

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

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

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

    
2599
	return false;
2600

    
2601
}
2602

    
2603
/* Captiveportal Radius Accounting */
2604

    
2605
function gigawords($bytes) {
2606

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

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

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

    
2620
	return $gigawords;
2621
}
2622

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

    
2627
	if (is_null($bytes)) {
2628
		$bytes = 0;
2629
	}
2630

    
2631
    return $bytes;
2632
}
2633

    
2634
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) {
2635
	global $cpzone, $config;
2636

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
2744
	// Send request
2745
	$result = $racct->send();
2746

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

    
2754
	$racct->close();
2755
	return $result;
2756
}
2757

    
2758
function captiveportal_isip_logged($clientip) {
2759
	global $g, $cpzone;
2760

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