Project

General

Profile

Download (95.2 KB) Statistics
| Branch: | Tag: | Revision:
1
<?php
2
/*
3
 * captiveportal.inc
4
 *
5
 * part of pfSense (https://www.pfsense.org)
6
 * Copyright (c) 2004-2013 BSD Perimeter
7
 * Copyright (c) 2013-2016 Electric Sheep Fencing
8
 * Copyright (c) 2014-2019 Rubicon Communications, LLC (Netgate)
9
 * All rights reserved.
10
 *
11
 * originally part of m0n0wall (http://m0n0.ch/wall)
12
 * Copyright (c) 2003-2006 Manuel Kasper <mk@neon1.net>.
13
 * All rights reserved.
14
 *
15
 * Licensed under the Apache License, Version 2.0 (the "License");
16
 * you may not use this file except in compliance with the License.
17
 * You may obtain a copy of the License at
18
 *
19
 * http://www.apache.org/licenses/LICENSE-2.0
20
 *
21
 * Unless required by applicable law or agreed to in writing, software
22
 * distributed under the License is distributed on an "AS IS" BASIS,
23
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
24
 * See the License for the specific language governing permissions and
25
 * limitations under the License.
26
 */
27

    
28
/* include all configuration functions */
29
require_once("auth.inc");
30
require_once("PEAR.php"); // required for bcmath
31
require_once("Auth/RADIUS.php"); // required for radius accounting
32
require_once("config.inc");
33
require_once("functions.inc");
34
require_once("filter.inc");
35
require_once("voucher.inc");
36

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

    
44
function get_default_captive_portal_html() {
45
	global $config, $g, $cpzone;
46

    
47
	$translated_text1 = gettext("User");
48
	$translated_text2 = gettext("Password");
49
	$translated_text3 = gettext("First Authentication Method ");
50
	$translated_text4 = gettext("Second Authentication Method ");
51
	// default images to use.
52
	$logo_src = "captiveportal-default-logo.png";
53
	$bg_src = "linear-gradient(135deg, #1475CF, #2B40B5, #1C1275)";
54
	// Check if customlogo is set and if the element exists
55
	// Check if the image is in the directory
56
	if (isset($config['captiveportal'][$cpzone]['customlogo'])) {
57
		foreach ($config['captiveportal'][$cpzone]['element'] as $element) {
58
			if (strpos($element['name'], "captiveportal-logo.") !== false) {
59
				if (file_exists("{$g['captiveportal_path']}/{$element['name']}")) {
60
					$logo_src = $element['name'];
61
					break;
62
				}
63
			}
64
		}
65
	}
66
	// check if custombg is set and if the element exists
67
	if (isset($config['captiveportal'][$cpzone]['custombg'])) {
68
		foreach ($config['captiveportal'][$cpzone]['element'] as $element) {
69
			if (strpos($element['name'],"captiveportal-background.") !== false) {
70
				if( file_exists("{$g['captiveportal_path']}/{$element['name']}")) {
71
					$bg_src = "url(" . $element['name'] . ")" . "center center no-repeat fixed";
72
					break;
73
				}
74
			}
75
		}
76

    
77
	}
78
	// bring in terms and conditions
79
	$termsconditions = base64_decode($config['captiveportal'][$cpzone]['termsconditions']);
80
	// if there is no terms and conditions do not require the checkbox to be selected.
81
	$disabled = "";
82
	if ($termsconditions) {
83
		$disabled = "disabled";
84
	}
85
	$htmltext = <<<EOD
86
<!DOCTYPE html>
87
<html>
88

    
89
<head>
90

    
91
  <meta charset="UTF-8">
92
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
93
  <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
94
  <title>Captive Portal Login Page</title>
95
  <style>
96
	  #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;}
97
  </style>
98
</head>
99

    
100
<body>
101
<div id="content">
102
	<div class="login-card">
103
		<img src="{$logo_src}"/><br>
104
 		<h1></h1>
105
		<div id="error-message">
106
			\$PORTAL_MESSAGE\$
107
		</div>
108
	  <form name="login_form" method="post" action="\$PORTAL_ACTION\$">
109
EOD;
110
	if ($config['captiveportal'][$cpzone]['auth_method'] != "none"){
111
		if ($config['captiveportal'][$cpzone]['auth_method'] === 'authserver' && !empty($config['captiveportal'][$cpzone]['auth_server2'])) {
112
			$htmltext .= <<<EOD
113
			<div class="auth_head_div">
114
				<h6 class="auth_head">{$translated_text3}</h6>
115
			</div>
116
			<div class="auth_source">
117

    
118
EOD;
119
		}
120
		$htmltext .=<<<EOD
121
		<input type="text" name="auth_user" placeholder="{$translated_text1}" id="auth_user">
122
		<input type="password" name="auth_pass" placeholder="{$translated_text2}" id="auth_pass">
123
EOD;
124

    
125
		if ($config['captiveportal'][$cpzone]['auth_method'] === 'authserver' && !empty($config['captiveportal'][$cpzone]['auth_server2'])) {
126
			$htmltext .= <<<EOD
127
			</div>
128
			<div class="auth_head_div">
129
				<h6 class="auth_head">{$translated_text4}</h6>
130
			</div>
131
			<div class="auth_source">
132

    
133
			<input type="text" name="auth_user2" placeholder="{$translated_text1}" id="auth_user2">
134
			<input type="password" name="auth_pass2" placeholder="{$translated_text2}" id="auth_pass2">
135
			</div>
136
EOD;
137
		}
138

    
139

    
140
		if (isset($config['voucher'][$cpzone]['enable'])) {
141
			$translated_text = gettext("Voucher Code");
142
			$htmltext .= <<<EOD
143
				<br  /><br  />
144
				<input name="auth_voucher" type="text" placeholder="{$translated_text}">
145
EOD;
146
		}
147
	}
148

    
149
if ($termsconditions) {
150
	$htmltext .= <<<EOD
151
		  <div class="login-help">
152
			<ul class="list">
153
				<li class="list__item">
154
				  <label class="label--checkbox">
155
					<input type="checkbox" class="checkbox" onchange="document.getElementById('login').disabled = !this.checked;">
156
					<span>I agree with the <a  rel="noopener" href="#terms" onclick="document.getElementById('terms').style.display = 'block';">terms & conditions</a></span>
157
				  </label>
158
				</li>
159
			</ul>
160
		  </div>
161
EOD;
162
}
163
	$htmltext .= <<<EOD
164

    
165
		<input name="redirurl" type="hidden" value="\$PORTAL_REDIRURL\$">
166
		<input type="submit" name="accept" class="login login-submit" value="Login" id="login" {$disabled}>
167
	  </form>
168
	  <br  />
169
	  <span> <i>Made with &hearts; by</i> <strong>Netgate</strong></span>
170
	</div>
171
	<div id="terms">
172
		<textarea readonly>{$termsconditions}</textarea>
173
	</div>
174
</div>
175
</body>
176
</html>
177

    
178
EOD;
179

    
180
	return $htmltext;
181
}
182

    
183
function captiveportal_load_modules() {
184
	global $config;
185

    
186
	mute_kernel_msgs();
187
	if (!is_module_loaded("ipfw.ko")) {
188
		mwexec("/sbin/kldload ipfw");
189
		/* make sure ipfw is not on pfil hooks */
190
		set_sysctl(array(
191
		    "net.inet.ip.pfil.inbound" => "pf",
192
		    "net.inet6.ip6.pfil.inbound" => "pf",
193
		    "net.inet.ip.pfil.outbound" => "pf",
194
		    "net.inet6.ip6.pfil.outbound" => "pf"
195
		));
196
	}
197
	/* Activate layer2 filtering */
198
	set_sysctl(array(
199
	    "net.link.ether.ipfw" => "1",
200
	    "net.inet.ip.fw.one_pass" => "1",
201
	    "net.inet.ip.fw.tables_max" => "65534"
202
	));
203

    
204
	/* Always load dummynet now that even allowed ip and mac passthrough use it. */
205
	if (!is_module_loaded("dummynet.ko")) {
206
		mwexec("/sbin/kldload dummynet");
207
		set_sysctl(array(
208
		    "net.inet.ip.dummynet.io_fast" => "1",
209
		    "net.inet.ip.dummynet.hash_size" => "256"
210
		));
211
	}
212
	unmute_kernel_msgs();
213
}
214

    
215
function captiveportal_configure() {
216
	global $config, $cpzone, $cpzoneid;
217

    
218
	if (is_array($config['captiveportal'])) {
219
		foreach ($config['captiveportal'] as $cpkey => $cp) {
220
			$cpzone = $cpkey;
221
			$cpzoneid = $cp['zoneid'];
222
			captiveportal_configure_zone($cp);
223
		}
224
	}
225
}
226

    
227
function captiveportal_configure_zone($cpcfg) {
228
	global $config, $g, $cpzone, $cpzoneid;
229

    
230
	$captiveportallck = lock("captiveportal{$cpzone}", LOCK_EX);
231

    
232
	if (isset($cpcfg['enable'])) {
233

    
234
		if (platform_booting()) {
235
			echo "Starting captive portal({$cpcfg['zone']})... ";
236
		} else {
237
			captiveportal_syslog("Reconfiguring captive portal({$cpcfg['zone']}).");
238
		}
239

    
240
		/* (re)init ipfw rules. Cause all users to disconnect */
241
		captiveportal_init_rules(true);
242

    
243
		/* kill any running minicron */
244
		killbypid("{$g['varrun_path']}/cp_prunedb_{$cpzone}.pid");
245

    
246
		/* initialize minicron interval value */
247
		$croninterval = $cpcfg['croninterval'] ? $cpcfg['croninterval'] : 60;
248

    
249
		/* double check if the $croninterval is numeric and at least 10 seconds. If not we set it to 60 to avoid problems */
250
		if ((!is_numeric($croninterval)) || ($croninterval < 10)) {
251
			$croninterval = 60;
252
		}
253

    
254
		/* write portal page */
255
		if (is_array($cpcfg['page']) && $cpcfg['page']['htmltext']) {
256
			$htmltext = base64_decode($cpcfg['page']['htmltext']);
257
		} else {
258
			/* example/template page */
259
			$htmltext = get_default_captive_portal_html();
260
		}
261

    
262
		$fd = @fopen("{$g['varetc_path']}/captiveportal_{$cpzone}.html", "w");
263
		if ($fd) {
264
			// Special case handling.  Convert so that we can pass this page
265
			// through the PHP interpreter later without clobbering the vars.
266
			$htmltext = str_replace("\$PORTAL_ZONE\$", "#PORTAL_ZONE#", $htmltext);
267
			$htmltext = str_replace("\$PORTAL_REDIRURL\$", "#PORTAL_REDIRURL#", $htmltext);
268
			$htmltext = str_replace("\$PORTAL_MESSAGE\$", "#PORTAL_MESSAGE#", $htmltext);
269
			$htmltext = str_replace("\$CLIENT_MAC\$", "#CLIENT_MAC#", $htmltext);
270
			$htmltext = str_replace("\$CLIENT_IP\$", "#CLIENT_IP#", $htmltext);
271
			$htmltext = str_replace("\$PORTAL_ACTION\$", "#PORTAL_ACTION#", $htmltext);
272
			if ($cpcfg['preauthurl']) {
273
				$htmltext = str_replace("\$PORTAL_REDIRURL\$", "{$cpcfg['preauthurl']}", $htmltext);
274
				$htmltext = str_replace("#PORTAL_REDIRURL#", "{$cpcfg['preauthurl']}", $htmltext);
275
			}
276
			fwrite($fd, $htmltext);
277
			fclose($fd);
278
		}
279
		unset($htmltext);
280

    
281
		/* write error page */
282
		if (is_array($cpcfg['page']) && $cpcfg['page']['errtext']) {
283
			$errtext = base64_decode($cpcfg['page']['errtext']);
284
		} else {
285
			/* example page  */
286
			$errtext = get_default_captive_portal_html();
287
		}
288

    
289
		$fd = @fopen("{$g['varetc_path']}/captiveportal-{$cpzone}-error.html", "w");
290
		if ($fd) {
291
			// Special case handling.  Convert so that we can pass this page
292
			// through the PHP interpreter later without clobbering the vars.
293
			$errtext = str_replace("\$PORTAL_ZONE\$", "#PORTAL_ZONE#", $errtext);
294
			$errtext = str_replace("\$PORTAL_REDIRURL\$", "#PORTAL_REDIRURL#", $errtext);
295
			$errtext = str_replace("\$PORTAL_MESSAGE\$", "#PORTAL_MESSAGE#", $errtext);
296
			$errtext = str_replace("\$CLIENT_MAC\$", "#CLIENT_MAC#", $errtext);
297
			$errtext = str_replace("\$CLIENT_IP\$", "#CLIENT_IP#", $errtext);
298
			$errtext = str_replace("\$PORTAL_ACTION\$", "#PORTAL_ACTION#", $errtext);
299
			if ($cpcfg['preauthurl']) {
300
				$errtext = str_replace("\$PORTAL_REDIRURL\$", "{$cpcfg['preauthurl']}", $errtext);
301
				$errtext = str_replace("#PORTAL_REDIRURL#", "{$cpcfg['preauthurl']}", $errtext);
302
			}
303
			fwrite($fd, $errtext);
304
			fclose($fd);
305
		}
306
		unset($errtext);
307

    
308
		/* write logout page */
309
		if (is_array($cpcfg['page']) && $cpcfg['page']['logouttext']) {
310
			$logouttext = base64_decode($cpcfg['page']['logouttext']);
311
		} else {
312
			/* example page */
313
			$translated_text1 = gettext("Redirecting...");
314
			$translated_text2 = gettext("Redirecting to");
315
			$translated_text3 = gettext("Logout");
316
			$translated_text4 = gettext("Click the button below to disconnect");
317
			$logouttext = <<<EOD
318
<html>
319
<head><title>{$translated_text1}</title></head>
320
<body>
321
<span style="font-family: Tahoma, Verdana, Arial, Helvetica, sans-serif; font-size: 11px;">
322
<b>{$translated_text2} <a href="<?=\$my_redirurl;?>"><?=\$my_redirurl;?></a>...</b>
323
</span>
324
<script type="text/javascript">
325
//<![CDATA[
326
LogoutWin = window.open('', 'Logout', 'toolbar=0,scrollbars=0,location=0,statusbar=0,menubar=0,resizable=0,width=256,height=64');
327
if (LogoutWin) {
328
	LogoutWin.document.write('<html>');
329
	LogoutWin.document.write('<head><title>{$translated_text3}</title></head>') ;
330
	LogoutWin.document.write('<body style="background-color:#435370">');
331
	LogoutWin.document.write('<div class="text-center" style="color: #ffffff; font-family: Tahoma, Verdana, Arial, Helvetica, sans-serif; font-size: 11px;">') ;
332
	LogoutWin.document.write('<b>{$translated_text4}</b><p />');
333
	LogoutWin.document.write('<form method="POST" action="<?=\$logouturl;?>">');
334
	LogoutWin.document.write('<input name="logout_id" type="hidden" value="<?=\$sessionid;?>" />');
335
	LogoutWin.document.write('<input name="zone" type="hidden" value="<?=\$cpzone;?>" />');
336
	LogoutWin.document.write('<input name="logout" type="submit" value="{$translated_text3}" />');
337
	LogoutWin.document.write('</form>');
338
	LogoutWin.document.write('</div></body>');
339
	LogoutWin.document.write('</html>');
340
	LogoutWin.document.close();
341
}
342

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

    
349
EOD;
350
		}
351

    
352
		$fd = @fopen("{$g['varetc_path']}/captiveportal-{$cpzone}-logout.html", "w");
353
		if ($fd) {
354
			fwrite($fd, $logouttext);
355
			fclose($fd);
356
		}
357
		unset($logouttext);
358

    
359
		/* write elements */
360
		captiveportal_write_elements();
361

    
362
		/* kill any running CP nginx instances */
363
		killbypid("{$g['varrun_path']}/nginx-{$cpzone}-CaptivePortal.pid");
364
		killbypid("{$g['varrun_path']}/nginx-{$cpzone}-CaptivePortal-SSL.pid");
365

    
366
		/* start up the webserving daemon */
367
		captiveportal_init_webgui_zone($cpcfg);
368

    
369
		/* Kill any existing prunecaptiveportal processes */
370
		if (file_exists("{$g['varrun_path']}/cp_prunedb_{$cpzone}.pid")) {
371
			killbypid("{$g['varrun_path']}/cp_prunedb_{$cpzone}.pid");
372
		}
373

    
374
		/* start pruning process (interval defaults to 60 seconds) */
375
		mwexec("/usr/local/bin/minicron $croninterval {$g['varrun_path']}/cp_prunedb_{$cpzone}.pid " .
376
			"/etc/rc.prunecaptiveportal {$cpzone}");
377

    
378
		/* delete outdated radius server database if exist */
379
		unlink_if_exists("{$g['vardb_path']}/captiveportal_radius_{$cpzone}.db");
380

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

    
386
			if (isset($cpcfg['preservedb'])) {
387
				$connected_users = captiveportal_read_db();
388
				if (!empty($connected_users)) {
389
					echo "Reconnecting users to captive portal {$cpcfg['zone']}... ";
390
					foreach ($connected_users as $user) {
391
						captiveportal_reserve_ruleno($user['pipeno']);
392
						$bw_up = intval($user['bw_up']);
393
						$bw_down = intval($user['bw_down']);
394
						$clientip = $user['ip'];
395
						$clientmac = $user['mac'];
396
						$bw_up_pipeno = $user['pipeno'];
397
						$bw_down_pipeno = $user['pipeno'] + 1;
398

    
399
						$rule_entry = "{$clientip}/" . (is_ipaddrv6($clientip) ? "128" : "32");
400
						if (!isset($cpcfg['nomacfilter'])) {
401
							$rule_entry .= ",{$clientmac}";
402
						}
403

    
404
						$_gb = @pfSense_ipfw_pipe("pipe {$bw_up_pipeno} config bw {$bw_up}Kbit/s queue 100 buckets 16");
405
						$_gb = @pfSense_ipfw_pipe("pipe {$bw_down_pipeno} config bw {$bw_down}Kbit/s queue 100 buckets 16");
406
						$_gb = @pfSense_ipfw_table("{$cpzone}_auth_up", IP_FW_TABLE_XADD, "{$rule_entry}", $bw_up_pipeno);
407
						$_gb = @pfSense_ipfw_table("{$cpzone}_auth_down", IP_FW_TABLE_XADD, "{$rule_entry}", $bw_down_pipeno);
408
					}
409
					echo "done\n";
410
				}
411
			}
412
		}
413

    
414
	} else {
415
		killbypid("{$g['varrun_path']}/nginx-{$cpzone}-CaptivePortal.pid");
416
		killbypid("{$g['varrun_path']}/nginx-{$cpzone}-CaptivePortal-SSL.pid");
417
		killbypid("{$g['varrun_path']}/cp_prunedb_{$cpzone}.pid");
418
		@unlink("{$g['varetc_path']}/captiveportal_{$cpzone}.html");
419
		@unlink("{$g['varetc_path']}/captiveportal-{$cpzone}-error.html");
420
		@unlink("{$g['varetc_path']}/captiveportal-{$cpzone}-logout.html");
421

    
422
		captiveportal_radius_stop_all(10); // NAS-Request
423

    
424
		captiveportal_filterdns_configure();
425

    
426
		/* remove old information */
427
		unlink_if_exists("{$g['vardb_path']}/captiveportal{$cpzone}.db");
428
		unlink_if_exists("{$g['vardb_path']}/captiveportal_radius_{$cpzone}.db");
429
		unlink_if_exists("{$g['vardb_path']}/captiveportal_{$cpzone}.rules");
430
		/* Release allocated pipes for this zone */
431
		$pipes_to_remove = captiveportal_free_dnrules();
432

    
433
		captiveportal_delete_rules($pipes_to_remove);
434

    
435
		if (empty($config['captiveportal'])) {
436
			set_single_sysctl("net.link.ether.ipfw", "0");
437
		} else {
438
			/* Deactivate ipfw(4) if not needed */
439
			$cpactive = false;
440
			if (is_array($config['captiveportal'])) {
441
				foreach ($config['captiveportal'] as $cpkey => $cp) {
442
					if (isset($cp['enable'])) {
443
						$cpactive = true;
444
						break;
445
					}
446
				}
447
			}
448
			if ($cpactive === false) {
449
				set_single_sysctl("net.link.ether.ipfw", "0");
450
			}
451
		}
452
	}
453

    
454
	unlock($captiveportallck);
455

    
456
	return 0;
457
}
458

    
459
function captiveportal_init_webgui() {
460
	global $config, $cpzone;
461

    
462
	if (is_array($config['captiveportal'])) {
463
		foreach ($config['captiveportal'] as $cpkey => $cp) {
464
			$cpzone = $cpkey;
465
			captiveportal_init_webgui_zone($cp);
466
		}
467
	}
468
}
469

    
470
function captiveportal_init_webgui_zonename($zone) {
471
	global $config, $cpzone;
472

    
473
	if (isset($config['captiveportal'][$zone])) {
474
		$cpzone = $zone;
475
		captiveportal_init_webgui_zone($config['captiveportal'][$zone]);
476
	}
477
}
478

    
479
function captiveportal_init_webgui_zone($cpcfg) {
480
	global $g, $config, $cpzone;
481

    
482
	if (!isset($cpcfg['enable'])) {
483
		return;
484
	}
485

    
486
	if (isset($cpcfg['httpslogin'])) {
487
		$cert = lookup_cert($cpcfg['certref']);
488
		$crt = base64_decode($cert['crt']);
489
		$key = base64_decode($cert['prv']);
490
		$ca = ca_chain($cert);
491

    
492
		/* generate nginx configuration */
493
		if (!empty($cpcfg['listenporthttps'])) {
494
			$listenporthttps = $cpcfg['listenporthttps'];
495
		} else {
496
			$listenporthttps = 8001 + $cpcfg['zoneid'];
497
		}
498
		system_generate_nginx_config("{$g['varetc_path']}/nginx-{$cpzone}-CaptivePortal-SSL.conf",
499
			$crt, $key, $ca, "nginx-{$cpzone}-CaptivePortal-SSL.pid", $listenporthttps, "/usr/local/captiveportal",
500
			"cert-{$cpzone}-portal.pem", "ca-{$cpzone}-portal.pem", $cpzone, false);
501
	}
502

    
503
	/* generate nginx configuration */
504
	if (!empty($cpcfg['listenporthttp'])) {
505
		$listenporthttp = $cpcfg['listenporthttp'];
506
	} else {
507
		$listenporthttp = 8000 + $cpcfg['zoneid'];
508
	}
509
	system_generate_nginx_config("{$g['varetc_path']}/nginx-{$cpzone}-CaptivePortal.conf",
510
		"", "", "", "nginx-{$cpzone}-CaptivePortal.pid", $listenporthttp, "/usr/local/captiveportal",
511
		"", "", $cpzone, false);
512

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

    
517
	/* fire up https instance */
518
	if (isset($cpcfg['httpslogin'])) {
519
		@unlink("{$g['varrun']}/nginx-{$cpzone}-CaptivePortal-SSL.pid");
520
		$res = mwexec("/usr/local/sbin/nginx -c {$g['varetc_path']}/nginx-{$cpzone}-CaptivePortal-SSL.conf");
521
	}
522
}
523

    
524
function captiveportal_init_rules_byinterface($interface) {
525
	global $cpzone, $cpzoneid, $config;
526

    
527
	if (!is_array($config['captiveportal'])) {
528
		return;
529
	}
530

    
531
	foreach ($config['captiveportal'] as $cpkey => $cp) {
532
		$cpzone = $cpkey;
533
		$cpzoneid = $cp['zoneid'];
534
		$cpinterfaces = explode(",", $cp['interface']);
535
		if (in_array($interface, $cpinterfaces)) {
536
			captiveportal_init_rules();
537
			break;
538
		}
539
	}
540
}
541

    
542
/* Create basic rules used by all zones */
543
function captiveportal_init_general_rules($flush = false) {
544
	global $g;
545

    
546
	$flush_rule = '';
547
	if ($flush) {
548
		$flush_rule = 'flush';
549
	}
550

    
551
	/* Already loaded */
552
	if (!$flush && (mwexec("/sbin/ipfw list 1000", true) == 0)) {
553
		return;
554
	}
555

    
556
	$cprules = <<<EOD
557
{$flush_rule}
558
# Table with interfaces that have CP enabled
559
table cp_ifaces create type iface valtype skipto
560

    
561
# Redirect each CP interface to its specific rule
562
add 1000 skipto tablearg all from any to any via table(cp_ifaces)
563

    
564
# This interface has no cp zone configured
565
add 1100 allow all from any to any
566

    
567
# block everything else
568
add 65534 deny all from any to any
569
EOD;
570

    
571
	/* load rules */
572
	file_put_contents("{$g['tmp_path']}/ipfw.cp.rules", $cprules);
573
	mwexec("/sbin/ipfw -q {$g['tmp_path']}/ipfw.cp.rules", true);
574
	@unlink("{$g['tmp_path']}/ipfw_{$cpzone}.cp.rules");
575
	unset($cprules);
576
}
577

    
578
/* Create a string with ipfw rule and increase rulenum */
579
function captiveportal_create_ipfw_rule($cmd, &$rulenum, $args) {
580
	$rule = "{$cmd} {$rulenum} {$args}\n";
581
	$rulenum++;
582

    
583
	return $rule;
584
}
585

    
586
/* Return first rule number for a cp zone */
587
function captiveportal_ipfw_ruleno($id) {
588
	global $g;
589

    
590
	return 2000 + $id * $g['captiveportal_rules_interval'];
591
}
592

    
593
/* reinit will disconnect all users, be careful! */
594
function captiveportal_init_rules($reinit = false) {
595
	global $config, $g, $cpzone, $cpzoneid;
596

    
597
	if (!isset($config['captiveportal'][$cpzone]['enable'])) {
598
		return;
599
	}
600

    
601
	captiveportal_load_modules();
602
	captiveportal_init_general_rules();
603

    
604
	/* Cleanup so nothing is leaked */
605
	captiveportal_free_dnrules();
606
	unlink_if_exists("{$g['vardb_path']}/captiveportal_{$cpzone}.rules");
607

    
608
	$skipto = captiveportal_ipfw_ruleno($cpzoneid);
609

    
610
	$cprules = '';
611

    
612
	$cpips = array();
613
	$ifaces = get_configured_interface_list();
614
	$cpinterfaces = explode(",", $config['captiveportal'][$cpzone]['interface']);
615
	$firsttime = 0;
616
	foreach ($cpinterfaces as $cpifgrp) {
617
		if (!isset($ifaces[$cpifgrp])) {
618
			continue;
619
		}
620
		$tmpif = get_real_interface($cpifgrp);
621
		if (empty($tmpif)) {
622
			continue;
623
		}
624

    
625
		$cpipm = get_interface_ip($cpifgrp);
626

    
627
		if (!is_ipaddr($cpipm)) {
628
			continue;
629
		}
630

    
631
		$cpips[] = $cpipm;
632
		if (is_array($config['virtualip']['vip'])) {
633
			foreach ($config['virtualip']['vip'] as $vip) {
634
				if (($vip['interface'] == $cpifgrp) &&
635
				    (($vip['mode'] == "carp") ||
636
				    ($vip['mode'] == "ipalias"))) {
637
					$cpips[] = $vip['subnet'];
638
				}
639
			}
640
		}
641

    
642
		$cprules .= "table cp_ifaces add {$tmpif} {$skipto}\n";
643
	}
644
	if (count($cpips) > 0) {
645
		$cpactive = true;
646
	} else {
647
		return false;
648
	}
649

    
650
	if ($reinit == false) {
651
		$captiveportallck = lock("captiveportal{$cpzone}");
652
	}
653

    
654
	$rulenum = $skipto;
655
	$cprules .= "table {$cpzone}_pipe_mac create type mac valtype pipe\n";
656
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
657
	    "pipe tablearg MAC table({$cpzone}_pipe_mac)");
658
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
659
	    "allow pfsync from any to any");
660
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
661
	    "allow carp from any to any\n");
662
	$cprules .= "# layer 2: pass ARP\n";
663
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
664
	    "pass layer2 mac-type arp,rarp");
665
	$cprules .= "# pfsense requires for WPA\n";
666
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
667
	    "pass layer2 mac-type 0x888e,0x88c7");
668
	$cprules .= "# PPP Over Ethernet Session Stage/Discovery Stage\n";
669
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
670
	    "pass layer2 mac-type 0x8863,0x8864\n");
671
	$cprules .= "# layer 2: block anything else non-IP(v4/v6)\n";
672
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
673
	    "deny layer2 not mac-type ip,ipv6");
674

    
675
	/* These tables contain host ips */
676
	$cprules .= "table {$cpzone}_host_ips create type addr\n";
677
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
678
	    "pass ip from any to table({$cpzone}_host_ips) in");
679
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
680
	    "pass ip from table({$cpzone}_host_ips) to any out");
681
	foreach ($cpips as $cpip) {
682
		$cprules .= "table {$cpzone}_host_ips add {$cpip}\n";
683
	}
684
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
685
	    "pass ip from any to 255.255.255.255 in");
686
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
687
	    "pass ip from 255.255.255.255 to any out");
688

    
689
	/* Allowed ips */
690
	$cprules .= "table {$cpzone}_allowed_up create type addr valtype pipe\n";
691
	$cprules .= "table {$cpzone}_allowed_down create type addr valtype pipe\n";
692
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
693
	    "pipe tablearg ip from table({$cpzone}_allowed_up) to any in");
694
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
695
	    "pipe tablearg ip from any to table({$cpzone}_allowed_down) in");
696
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
697
	    "pipe tablearg ip from table({$cpzone}_allowed_up) to any out");
698
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
699
	    "pipe tablearg ip from any to table({$cpzone}_allowed_down) out");
700

    
701
	/* Authenticated users rules. */
702
	$cprules .= "table {$cpzone}_auth_up create type addr valtype pipe\n";
703
	$cprules .= "table {$cpzone}_auth_down create type addr valtype pipe\n";
704
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
705
	    "pipe tablearg ip from table({$cpzone}_auth_up) to any layer2 in");
706
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
707
	    "pipe tablearg ip from any to table({$cpzone}_auth_down) layer2 out");
708

    
709
	if (!empty($config['captiveportal'][$cpzone]['listenporthttp'])) {
710
		$listenporthttp = $config['captiveportal'][$cpzone]['listenporthttp'];
711
	} else {
712
		$listenporthttp = 8000 + $cpzoneid;
713
	}
714

    
715
	if (isset($config['captiveportal'][$cpzone]['httpslogin'])) {
716
		if (!empty($config['captiveportal'][$cpzone]['listenporthttps'])) {
717
			$listenporthttps = $config['captiveportal'][$cpzone]['listenporthttps'];
718
		} else {
719
			$listenporthttps = 8001 + $cpzoneid;
720
		}
721
		if (!isset($config['captiveportal'][$cpzone]['nohttpsforwards'])) {
722
			$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
723
			    "fwd 127.0.0.1,{$listenporthttps} tcp from any to any dst-port 443 in");
724
		}
725
	}
726

    
727
	$cprules .= "# redirect non-authenticated clients to captive portal\n";
728
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
729
	    "fwd 127.0.0.1,{$listenporthttp} tcp from any to any dst-port 80 in");
730
	$cprules .= "# let the responses from the captive portal web server back out\n";
731
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
732
	    "pass tcp from any to any out");
733
	$cprules .= "# This CP zone is over, skip to last rule\n";
734
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
735
	    "skipto 65534 all from any to any");
736

    
737
	/* generate passthru mac database */
738
	$cprules .= captiveportal_passthrumac_configure(true);
739
	$cprules .= "\n";
740

    
741
	/* allowed ipfw rules to make allowed ip work */
742
	$cprules .= captiveportal_allowedip_configure();
743

    
744
	/* allowed ipfw rules to make allowed hostnames work */
745
	$cprules .= captiveportal_allowedhostname_configure();
746

    
747
	/* load rules */
748
	captiveportal_delete_rules();
749
	file_put_contents("{$g['tmp_path']}/ipfw_{$cpzone}.cp.rules", $cprules);
750
	mwexec("/sbin/ipfw -q {$g['tmp_path']}/ipfw_{$cpzone}.cp.rules", true);
751
	@unlink("{$g['tmp_path']}/ipfw_{$cpzone}.cp.rules");
752
	unset($cprules);
753

    
754
	captiveportal_filterdns_configure();
755

    
756
	if ($reinit == false) {
757
		unlock($captiveportallck);
758
	}
759
}
760

    
761
/* Delete all rules related to specific cpzone */
762
function captiveportal_delete_rules($pipes_to_remove = array()) {
763
	global $g, $cpzoneid, $cpzone;
764

    
765
	$skipto1 = captiveportal_ipfw_ruleno($cpzoneid);
766
	$skipto2 = $skipto1 + $g['captiveportal_rules_interval'];
767

    
768
	$cp_ifaces = pfSense_ipfw_table_list("cp_ifaces");
769
	if (is_array($cp_ifaces)) {
770
		foreach ($cp_ifaces as $cp_iface) {
771
			if (empty($cp_iface['skipto']) ||
772
			    $cp_iface['skipto'] != $skipto1) {
773
				continue;
774
			}
775

    
776
			pfSense_ipfw_table("cp_ifaces", IP_FW_TABLE_XDEL,
777
			    $cp_iface['iface']);
778
		}
779
	}
780

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

    
783
	$tables = captiveportal_get_ipfw_table_names();
784

    
785
	$delrules = "";
786
	foreach ($tables as $table) {
787
		$delrules .= "table {$table} destroy\n";
788
	}
789

    
790
	foreach ($pipes_to_remove as $pipeno) {
791
		$delrules .= "pipe delete {$pipeno}\n";
792
	}
793

    
794
	if (empty($delrules)) {
795
		return;
796
	}
797

    
798
	file_put_contents("{$g['tmp_path']}/ipfw_{$cpzone}.deltable.rules", $delrules);
799
	mwexec("/sbin/ipfw -q {$g['tmp_path']}/ipfw_{$cpzone}.deltable.rules", true);
800
	@unlink("{$g['tmp_path']}/ipfw_{$cpzone}.deltable.rules");
801
}
802

    
803
/*
804
 * Remove clients that have been around for longer than the specified amount of time
805
 * db file structure:
806
 * timestamp,ipfw_rule_no,clientip,clientmac,username,sessionid,password,session_timeout,idle_timeout,session_terminate_time,interim_interval,traffic_quota,auth_method,context
807
 * (password is in Base64 and only saved when reauthentication is enabled)
808
 */
809
function captiveportal_prune_old() {
810
	global $g, $config, $cpzone, $cpzoneid;
811

    
812
	if (empty($cpzone)) {
813
		return;
814
	}
815

    
816
	$cpcfg = $config['captiveportal'][$cpzone];
817
	$vcpcfg = $config['voucher'][$cpzone];
818

    
819
	/* check for expired entries */
820
	$idletimeout = 0;
821
	$timeout = 0;
822
	if (!empty($cpcfg['timeout']) && is_numeric($cpcfg['timeout'])) {
823
		$timeout = $cpcfg['timeout'] * 60;
824
	}
825

    
826
	if (!empty($cpcfg['idletimeout']) && is_numeric($cpcfg['idletimeout'])) {
827
		$idletimeout = $cpcfg['idletimeout'] * 60;
828
	}
829

    
830
	/* check for entries exceeding their traffic quota */
831
	$trafficquota = 0;
832
	if (!empty($cpcfg['trafficquota']) && is_numeric($cpcfg['trafficquota'])) {
833
		$trafficquota = $cpcfg['trafficquota'] * 1048576;
834
	}
835

    
836
	/* Is there any job to do? */
837
	if (!$timeout && !$idletimeout && !$trafficquota && !isset($cpcfg['reauthenticate']) &&
838
	    !isset($cpcfg['radiussession_timeout']) && !isset($cpcfg['radiustraffic_quota']) &&
839
	    !isset($vcpcfg['enable']) && !isset($cpcfg['radacct_enable'])) {
840
		return;
841
	}
842

    
843

    
844
	/* Read database */
845
	/* NOTE: while this can be simplified in non radius case keep as is for now */
846
	$cpdb = captiveportal_read_db();
847

    
848
	$unsetindexes = array();
849
	$voucher_needs_sync = false;
850
	/*
851
	 * Snapshot the time here to use for calculation to speed up the process.
852
	 * If something is missed next run will catch it!
853
	 */
854
	$pruning_time = time();
855
	foreach ($cpdb as $cpentry) {
856
		$stop_time = $pruning_time;
857

    
858
		$timedout = false;
859
		$term_cause = 1;
860
		/* hard timeout or session_timeout from radius if enabled */
861
		if (isset($cpcfg['radiussession_timeout'])) {
862
			$utimeout = (is_numeric($cpentry[7])) ? $cpentry[7] : $timeout;
863
		} else {
864
			$utimeout = $timeout;
865
		}
866
		if ($utimeout) {
867
			if (($pruning_time - $cpentry[0]) >= $utimeout) {
868
				$timedout = true;
869
				$term_cause = 5; // Session-Timeout
870
				$logout_cause = 'SESSION TIMEOUT';
871
			}
872
		}
873

    
874
		/* Session-Terminate-Time */
875
		if (!$timedout && !empty($cpentry[9])) {
876
			if ($pruning_time >= $cpentry[9]) {
877
				$timedout = true;
878
				$term_cause = 5; // Session-Timeout
879
				$logout_cause = 'SESSION TIMEOUT';
880
			}
881
		}
882

    
883
		/* check if an idle_timeout has been set and if its set change the idletimeout to this value */
884
		$uidletimeout = (is_numeric($cpentry[8])) ? $cpentry[8] : $idletimeout;
885
		/* if an idle timeout is specified, get last activity timestamp from ipfw */
886
		if (!$timedout && $uidletimeout > 0) {
887
			$lastact = captiveportal_get_last_activity($cpentry[2]);
888
			/*	If the user has logged on but not sent any traffic they will never be logged out.
889
			 *	We "fix" this by setting lastact to the login timestamp.
890
			 */
891
			$lastact = $lastact ? $lastact : $cpentry[0];
892
			if ($lastact && (($pruning_time - $lastact) >= $uidletimeout)) {
893
				$timedout = true;
894
				$term_cause = 4; // Idle-Timeout
895
				$logout_cause = 'IDLE TIMEOUT';
896
				if (!isset($config['captiveportal'][$cpzone]['includeidletime'])) {
897
					$stop_time = $lastact;
898
				}
899
			}
900
		}
901

    
902
		/* if vouchers are configured, activate session timeouts */
903
		if (!$timedout && isset($vcpcfg['enable']) && !empty($cpentry[7])) {
904
			if ($pruning_time >= ($cpentry[0] + $cpentry[7])) {
905
				$timedout = true;
906
				$term_cause = 5; // Session-Timeout
907
				$logout_cause = 'SESSION TIMEOUT';
908
				$voucher_needs_sync = true;
909
			}
910
		}
911

    
912
		/* traffic quota, value retrieved from the radius attribute if the option is enabled */
913
		if (isset($cpcfg['radiustraffic_quota'])) {
914
			$utrafficquota = (is_numeric($cpentry[11])) ? $cpentry[11] : $trafficquota;
915
		} else {
916
			$utrafficquota = $trafficquota;
917
		}
918
		if (!$timedout && $utrafficquota > 0) {
919
			$volume = getVolume($cpentry[2], $cpentry[3]);
920
			if (($volume['input_bytes'] + $volume['output_bytes']) > $utrafficquota) {
921
				$timedout = true;
922
				$term_cause = 10; // NAS-Request
923
				$logout_cause = 'QUOTA EXCEEDED';
924
			}
925
		}
926

    
927
		if ($timedout) {
928
			captiveportal_disconnect($cpentry, $term_cause, $stop_time);
929
			captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], $logout_cause);
930
			$unsetindexes[] = $cpentry[5];
931
		}
932

    
933
		/* do periodic reauthentication? For Radius servers, send accounting updates? */
934
		if (!$timedout) {
935
			//Radius servers : send accounting
936
			if (isset($cpcfg['radacct_enable']) && $cpentry['authmethod'] === 'radius') {
937
				if (substr($cpcfg['reauthenticateacct'], 0, 9) === "stopstart") {
938
					/* stop and restart accounting */
939
					if ($cpcfg['reauthenticateacct'] === "stopstartfreeradius") {
940
						$rastart_time = 0;
941
						$rastop_time = 60;
942
					} else {
943
						$rastart_time = $cpentry[0];
944
						$rastop_time = time();
945
					}
946
					captiveportal_send_server_accounting('stop',
947
						$cpentry[1], // ruleno
948
						$cpentry[4], // username
949
						$cpentry[2], // clientip
950
						$cpentry[3], // clientmac
951
						$cpentry[5], // sessionid
952
						$rastart_time, // start time
953
						$rastop_time, // Stop Time
954
						10); // NAS Request
955
					$clientsn = (is_ipaddrv6($cpentry[2])) ? 128 : 32;
956
					pfSense_ipfw_table_zerocnt("{$cpzone}_auth_up", "{$cpentry[2]}/{$clientsn}");
957
					pfSense_ipfw_table_zerocnt("{$cpzone}_auth_down", "{$cpentry[2]}/{$clientsn}");
958
					if ($cpcfg['reauthenticateacct'] == "stopstartfreeradius") {
959
						/* Need to pause here or the FreeRADIUS server gets confused about packet ordering. */
960
						sleep(1);
961
					}
962
					captiveportal_send_server_accounting('start',
963
						$cpentry[1], // ruleno
964
						$cpentry[4], // username
965
						$cpentry[2], // clientip
966
						$cpentry[3], // clientmac
967
						$cpentry[5]); // sessionid
968
				} else if ($cpcfg['reauthenticateacct'] == "interimupdate") {
969
					$session_time = $pruning_time - $cpentry[0];
970
					if (!empty($cpentry[10]) && $cpentry[10] > 60) {
971
						$interval = $cpentry[10];
972
					} else {
973
						$interval = 0;
974
					}
975
					$past_interval_min = ($session_time > $interval);
976
					if ($interval != 0) {
977
						$within_interval = ($session_time % $interval >= 0 && $session_time % $interval <= 59);
978
					}
979
					if ($interval === 0 || ($interval > 0 && $past_interval_min && $within_interval)) {
980
					captiveportal_send_server_accounting('update',
981
						$cpentry[1], // ruleno
982
						$cpentry[4], // username
983
						$cpentry[2], // clientip
984
						$cpentry[3], // clientmac
985
						$cpentry[5], // sessionid
986
						$cpentry[0]); // start time
987
					}
988
				}
989
			}
990

    
991
			/* check this user again */
992
			if (isset($cpcfg['reauthenticate']) && $cpentry['context'] !== 'voucher') {
993
				$auth_result = captiveportal_authenticate_user(
994
					$cpentry[4], // username
995
					base64_decode($cpentry[6]), // password
996
					$cpentry[3], // clientmac
997
					$cpentry[2], // clientip
998
					$cpentry[1], // ruleno
999
					$cpentry['context']); // context
1000
				if ($auth_result['result'] === false) {
1001
					captiveportal_disconnect($cpentry, 17);
1002
					captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "DISCONNECT - REAUTHENTICATION FAILED", $auth_list['reply_message']);
1003
					$unsetindexes[] = $cpentry[5];
1004
				} else if ($auth_result['result'] === true) {
1005
					if ($cpentry['authmethod'] !== $auth_result['auth_method']) {
1006
						// if the user got authenticated against another server type:  we update the database
1007
						if (!empty($cpentry[5])) {
1008
							captiveportal_update_entry($cpentry['sessionid'], $auth_result['auth_method'], 'authmethod');
1009
							captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CHANGED AUTHENTICATION SERVER", $auth_list['reply_message']);
1010
						}
1011
						// User was logged on a RADIUS server, but is now logged in by another server type : we send an accounting Stop
1012
						if (isset($config['captiveportal'][$cpzone]['radacct_enable']) && $cpentry['authmethod'] == 'radius') {
1013
							if ($cpcfg['reauthenticateacct'] === "stopstartfreeradius") {
1014
								$rastart_time = 0;
1015
								$rastop_time = 60;
1016
							} else {
1017
								$rastart_time = $cpentry[0];
1018
								$rastop_time = time();
1019
							}
1020
							captiveportal_send_server_accounting('stop',
1021
								$cpentry[1], // ruleno
1022
								$cpentry[4], // username
1023
								$cpentry[2], // clientip
1024
								$cpentry[3], // clientmac
1025
								$cpentry[5], // sessionid
1026
								$rastart_time, // start time
1027
								$rastop_time, // Stop Time
1028
								3); // Lost Service
1029
						// User was logged on a non-RADIUS Server but is now logged in by a RADIUS server : we send an accounting Start
1030
						} else if(isset($config['captiveportal'][$cpzone]['radacct_enable']) && $auth_result['auth_method'] === 'radius') {
1031
							captiveportal_send_server_accounting('start',
1032
								$cpentry[1], // ruleno
1033
								$cpentry[4], // username
1034
								$cpentry[2], // clientip
1035
								$cpentry[3], // clientmac
1036
								$cpentry[5], // sessionid
1037
								$cpentry[0]); // start_time
1038
						}
1039
					}
1040
					captiveportal_reapply_attributes($cpentry, $auth_result['attributes']);
1041
				}
1042
			}
1043
		}
1044
	}
1045
	unset($cpdb);
1046

    
1047
	captiveportal_prune_old_automac();
1048

    
1049
	if ($voucher_needs_sync == true) {
1050
		/* Trigger a sync of the vouchers on config */
1051
		send_event("service sync vouchers");
1052
	}
1053

    
1054
	/* write database */
1055
	if (!empty($unsetindexes)) {
1056
		captiveportal_remove_entries($unsetindexes);
1057
	}
1058
}
1059

    
1060
function captiveportal_prune_old_automac() {
1061
	global $g, $config, $cpzone, $cpzoneid;
1062

    
1063
	if (is_array($config['captiveportal'][$cpzone]['passthrumac']) &&
1064
	    isset($config['captiveportal'][$cpzone]['passthrumacadd'])) {
1065
		$tmpvoucherdb = array();
1066
		$macrules = "";
1067
		$writecfg = false;
1068
		foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $eid => $emac) {
1069
			if ($emac['logintype'] != "voucher") {
1070
				continue;
1071
			}
1072
			if (isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) {
1073
				if (isset($tmpvoucherdb[$emac['username']])) {
1074
					$temac = $config['captiveportal'][$cpzone]['passthrumac'][$tmpvoucherdb[$emac['username']]];
1075
					$pipeno = captiveportal_get_dn_passthru_ruleno($temac['mac']);
1076
					if ($pipeno) {
1077
						captiveportal_free_dn_ruleno($pipeno);
1078
						$macrules .= "table {$cpzone}_pipe_mac delete any,{$temac['mac']}\n";
1079
						$macrules .= "table {$cpzone}_pipe_mac delete {$temac['mac']},any\n";
1080
						$macrules .= "pipe delete {$pipeno}\n";
1081
						++$pipeno;
1082
						$macrules .= "pipe delete {$pipeno}\n";
1083
					}
1084
					$writecfg = true;
1085
					captiveportal_logportalauth($temac['username'], $temac['mac'],
1086
					    $temac['ip'], "DUPLICATE {$temac['username']} LOGIN - TERMINATING OLD SESSION");
1087
					unset($config['captiveportal'][$cpzone]['passthrumac'][$tmpvoucherdb[$emac['username']]]);
1088
				}
1089
				$tmpvoucherdb[$emac['username']] = $eid;
1090
			}
1091
			if (voucher_auth($emac['username']) <= 0) {
1092
				$pipeno = captiveportal_get_dn_passthru_ruleno($emac['mac']);
1093
				if ($pipeno) {
1094
					captiveportal_free_dn_ruleno($pipeno);
1095
					$macrules .= "table {$cpzone}_pipe_mac delete any,{$emac['mac']}\n";
1096
					$macrules .= "table {$cpzone}_pipe_mac delete {$emac['mac']},any\n";
1097
					$macrules .= "pipe delete {$pipeno}\n";
1098
					++$pipeno;
1099
					$macrules .= "pipe delete {$pipeno}\n";
1100
				}
1101
				$writecfg = true;
1102
				captiveportal_logportalauth($emac['username'], $emac['mac'],
1103
				    $emac['ip'], "EXPIRED {$emac['username']} LOGIN - TERMINATING SESSION");
1104
				unset($config['captiveportal'][$cpzone]['passthrumac'][$eid]);
1105
			}
1106
		}
1107
		unset($tmpvoucherdb);
1108
		if (!empty($macrules)) {
1109
			@file_put_contents("{$g['tmp_path']}/macentry.prunerules.tmp", $macrules);
1110
			unset($macrules);
1111
			mwexec("/sbin/ipfw -q {$g['tmp_path']}/macentry.prunerules.tmp");
1112
		}
1113
		if ($writecfg === true) {
1114
			write_config("Prune session for auto-added macs");
1115
		}
1116
	}
1117
}
1118

    
1119
/* remove a single client according to the DB entry */
1120
function captiveportal_disconnect($dbent, $term_cause = 1, $stop_time = null) {
1121
	global $g, $config, $cpzone, $cpzoneid;
1122

    
1123
	$stop_time = (empty($stop_time)) ? time() : $stop_time;
1124

    
1125
	/* this client needs to be deleted - remove ipfw rules */
1126
	if (isset($config['captiveportal'][$cpzone]['radacct_enable']) && $dbent['authmethod'] == 'radius') {
1127
		if ($config['captiveportal'][$cpzone]['reauthenticateacct'] == "stopstartfreeradius") {
1128
			/*
1129
			 * Interim updates are on so the session time must be
1130
			 * reported as the elapsed time since the previous
1131
			 * interim update.
1132
			 */
1133
			$session_time = ($stop_time - $dbent[0]) % 60;
1134
			$start_time = $stop_time - $session_time;
1135
		} else {
1136
			$start_time = $dbent[0];
1137
		}
1138
		captiveportal_send_server_accounting('stop',
1139
			$dbent[1], // ruleno
1140
			$dbent[4], // username
1141
			$dbent[2], // clientip
1142
			$dbent[3], // clientmac
1143
			$dbent[5], // sessionid
1144
			$start_time, // start time
1145
			$stop_time, // stop time
1146
			$term_cause); // Acct-Terminate-Cause
1147
	}
1148

    
1149
	if (is_ipaddr($dbent[2])) {
1150
		/*
1151
		 * Delete client's ip entry from tables auth_up and auth_down.
1152
		 * It's not necessary to explicit specify mac address here
1153
		 */
1154
		$cpsession = captiveportal_isip_logged($dbent[2]);
1155
		if (!empty($cpsession)) {
1156
			$clientsn = (is_ipaddrv6($dbent[2])) ? 128 : 32;
1157
			pfSense_ipfw_table("{$cpzone}_auth_up",
1158
			    IP_FW_TABLE_XDEL, "{$dbent[2]}/{$clientsn}");
1159
			pfSense_ipfw_table("{$cpzone}_auth_down",
1160
			    IP_FW_TABLE_XDEL, "{$dbent[2]}/{$clientsn}");
1161
		}
1162
		/* XXX: Redundant?! Ensure all pf(4) states are killed. */
1163
		$_gb = @pfSense_kill_states($dbent[2]);
1164
		$_gb = @pfSense_kill_srcstates($dbent[2]);
1165
	}
1166

    
1167
	/*
1168
	 * These are the pipe numbers we use to control traffic shaping for
1169
	 * each logged in user via captive portal
1170
	 * We could get an error if the pipe doesn't exist but everything
1171
	 * should still be fine
1172
	 */
1173
	if (!empty($dbent[1])) {
1174
		/*
1175
		 * Call captiveportal_free_dnrules() in dry_run mode to verify
1176
		 * if there are pipes to be removed and prevent the attempt to
1177
		 * delete invalid pipes
1178
		 */
1179
		$removed_pipes = captiveportal_free_dnrules($dbent[1],
1180
		    $dbent[1]+1, true);
1181

    
1182
		if (!empty($removed_pipes)) {
1183
			$_gb = @pfSense_ipfw_pipe("pipe delete {$dbent[1]}");
1184
			$_gb = @pfSense_ipfw_pipe("pipe delete " .
1185
			    ($dbent[1]+1));
1186

    
1187
			/*
1188
			 * Release the ruleno so it can be reallocated to new
1189
			 * clients
1190
			 */
1191
			captiveportal_free_dn_ruleno($dbent[1]);
1192
		}
1193
	}
1194

    
1195
	// XMLRPC Call over to the master Voucher node
1196
	if (xmlrpc_sync_voucher_details($syncip, $syncport,
1197
	    $vouchersyncusername, $syncpass)) {
1198
		$remote_status = xmlrpc_sync_voucher_disconnect($dbent, $syncip,
1199
		    $syncport, $syncpass, $vouchersyncusername, $term_cause,
1200
		    $stop_time);
1201
	}
1202

    
1203
}
1204

    
1205
/* remove a single client by sessionid */
1206
function captiveportal_disconnect_client($sessionid, $term_cause = 1, $logoutReason = "LOGOUT") {
1207
	global $g, $config;
1208

    
1209
	$sessionid = SQLite3::escapeString($sessionid);
1210
	/* read database */
1211
	$result = captiveportal_read_db("WHERE sessionid = '{$sessionid}'");
1212

    
1213
	/* find entry */
1214
	if (!empty($result)) {
1215

    
1216
		foreach ($result as $cpentry) {
1217
			captiveportal_disconnect($cpentry, $term_cause);
1218
			captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "DISCONNECT");
1219
		}
1220
		captiveportal_remove_entries(array($sessionid));
1221
		unset($result);
1222
	}
1223
}
1224

    
1225
/* remove all clients */
1226
function captiveportal_disconnect_all($term_cause = 6, $logoutReason = "DISCONNECT") {
1227
	global $g, $config, $cpzone, $cpzoneid;
1228

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

    
1232
	/* if we still don't have the lock, unlock forcefully and take it */
1233
	if (!$rcprunelock) {
1234
		log_error("CP zone ${cpzone}: could not obtain the lock for more than 15 seconds, lock taken forcefully to disconnect all users");
1235
		unlock_force("rcprunecaptiveportal{$cpzone}");
1236
		$rcprunelock = lock("rcprunecaptiveportal{$cpzone}", LOCK_EX);
1237
	}
1238

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

    
1242
	captiveportal_radius_stop_all($term_cause, $logoutReason);
1243

    
1244
	/* remove users from the database */
1245
	$cpdb = captiveportal_read_db();
1246
	$unsetindexes = array_column($cpdb,5);
1247
	if (!empty($unsetindexes)) {
1248
		captiveportal_remove_entries($unsetindexes);
1249
	}
1250

    
1251
	/* reinit ipfw rules */
1252
	captiveportal_init_rules(true);
1253

    
1254
	unlock($cpdblck);
1255
	unlock($rcprunelock);
1256
}
1257

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

    
1262
	$cpdb = captiveportal_read_db();
1263

    
1264
	$radacct = isset($config['captiveportal'][$cpzone]['radacct_enable']) ? true : false;
1265
	foreach ($cpdb as $cpentry) {
1266
		if ($cpentry['authmethod'] === 'radius' && $radacct) {
1267
			if ($config['captiveportal'][$cpzone]['reauthenticateacct'] == "stopstartfreeradius") {
1268
				$session_time = (time() - $cpentry[0]) % 60;
1269
				$start_time = time() - $session_time;
1270
			} else {
1271
				$start_time = $cpentry[0];
1272
			}
1273
			captiveportal_send_server_accounting('stop',
1274
				$cpentry[1], // ruleno
1275
				$cpentry[4], // username
1276
				$cpentry[2], // clientip
1277
				$cpentry[3], // clientmac
1278
				$cpentry[5], // sessionid
1279
				$start_time, // start time
1280
				$stop_time, // stop time
1281
				$term_cause); // Acct-Terminate-Cause
1282
		}
1283
		captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], $logoutReason);
1284
	}
1285
	unset($cpdb);
1286
}
1287

    
1288
function captiveportal_passthrumac_configure_entry($macent, $pipeinrule = false) {
1289
	global $config, $g, $cpzone;
1290

    
1291
	$bwUp = 0;
1292
	if (!empty($macent['bw_up'])) {
1293
		$bwUp = $macent['bw_up'];
1294
	} else if (!empty($config['captiveportal'][$cpzone]['bwdefaultup'])) {
1295
		$bwUp = $config['captiveportal'][$cpzone]['bwdefaultup'];
1296
	}
1297
	$bwDown = 0;
1298
	if (!empty($macent['bw_down'])) {
1299
		$bwDown = $macent['bw_down'];
1300
	} else if (!empty($config['captiveportal'][$cpzone]['bwdefaultdn'])) {
1301
		$bwDown = $config['captiveportal'][$cpzone]['bwdefaultdn'];
1302
	}
1303

    
1304
	if ($macent['action'] == 'pass') {
1305
		$rules = "";
1306

    
1307
		$pipeno = captiveportal_get_next_dn_ruleno();
1308

    
1309
		$pipeup = $pipeno;
1310
		if ($pipeinrule == true) {
1311
			$_gb = @pfSense_ipfw_pipe("pipe {$pipeno} config bw {$bwUp}Kbit/s queue 100 buckets 16");
1312
		} else {
1313
			$rules .= "pipe {$pipeno} config bw {$bwUp}Kbit/s queue 100 buckets 16\n";
1314
		}
1315

    
1316
		$pipedown = $pipeno + 1;
1317
		if ($pipeinrule == true) {
1318
			$_gb = @pfSense_ipfw_pipe("pipe {$pipedown} config bw {$bwDown}Kbit/s queue 100 buckets 16");
1319
		} else {
1320
			$rules .= "pipe {$pipedown} config bw {$bwDown}Kbit/s queue 100 buckets 16\n";
1321
		}
1322

    
1323
		$rules .= "table {$cpzone}_pipe_mac add any,{$macent['mac']} {$pipeup}\n";
1324
		$rules .= "table {$cpzone}_pipe_mac add {$macent['mac']},any {$pipedown}\n";
1325
	}
1326

    
1327
	return $rules;
1328
}
1329

    
1330
function captiveportal_passthrumac_delete_entry($macent) {
1331
	global $cpzone;
1332
	$rules = "";
1333

    
1334
	if ($macent['action'] == 'pass') {
1335
		$pipeno = captiveportal_get_dn_passthru_ruleno($macent['mac']);
1336

    
1337
		if (!empty($pipeno)) {
1338
			captiveportal_free_dn_ruleno($pipeno);
1339
			$rules .= "table {$cpzone}_pipe_mac delete any,{$macent['mac']}\n";
1340
			$rules .= "table {$cpzone}_pipe_mac delete {$macent['mac']},any\n";
1341
			$rules .= "pipe delete " . $pipeno . "\n";
1342
			$rules .= "pipe delete " . ++$pipeno . "\n";
1343
		}
1344
	}
1345

    
1346
	return $rules;
1347
}
1348

    
1349
function captiveportal_passthrumac_configure($filename = false, $startindex = 0, $stopindex = 0) {
1350
	global $config, $g, $cpzone;
1351

    
1352
	$rules = "";
1353

    
1354
	if (is_array($config['captiveportal'][$cpzone]['passthrumac'])) {
1355
		if ($stopindex > 0) {
1356
			$fd = fopen($filename, "w");
1357
			for ($idx = $startindex; $idx <= $stopindex; $idx++) {
1358
				if (isset($config['captiveportal'][$cpzone]['passthrumac'][$idx])) {
1359
					$rules = captiveportal_passthrumac_configure_entry($config['captiveportal'][$cpzone]['passthrumac'][$idx]);
1360
					fwrite($fd, $rules);
1361
				}
1362
			}
1363
			fclose($fd);
1364

    
1365
			return;
1366
		} else {
1367
			$nentries = count($config['captiveportal'][$cpzone]['passthrumac']);
1368
			if ($nentries > 2000) {
1369
				$nloops = $nentries / 1000;
1370
				$remainder= $nentries % 1000;
1371
				for ($i = 0; $i < $nloops; $i++) {
1372
					mwexec_bg("/usr/local/sbin/fcgicli -f /etc/rc.captiveportal_configure_mac -d \"cpzone={$cpzone}&startidx=" . ($i * 1000) . "&stopidx=" . ((($i+1) * 1000) - 1) . "\"");
1373
				}
1374
				if ($remainder > 0) {
1375
					mwexec_bg("/usr/local/sbin/fcgicli -f /etc/rc.captiveportal_configure_mac -d \"cpzone={$cpzone}&startidx=" . ($i * 1000) . "&stopidx=" . (($i* 1000) + $remainder) ."\"");
1376
				}
1377
			} else {
1378
				foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $macent) {
1379
					$rules .= captiveportal_passthrumac_configure_entry($macent, true);
1380
				}
1381
			}
1382
		}
1383
	}
1384

    
1385
	return $rules;
1386
}
1387

    
1388
function captiveportal_passthrumac_findbyname($username) {
1389
	global $config, $cpzone;
1390

    
1391
	if (is_array($config['captiveportal'][$cpzone]['passthrumac'])) {
1392
		foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $macent) {
1393
			if ($macent['username'] == $username) {
1394
				return $macent;
1395
			}
1396
		}
1397
	}
1398
	return NULL;
1399
}
1400

    
1401
/*
1402
 * table (3=IN)/(4=OUT) hold allowed ip's without bw limits
1403
 */
1404
function captiveportal_allowedip_configure_entry($ipent, $ishostname = false) {
1405
	global $g, $config, $cpzone;
1406

    
1407
	/*  Instead of copying this entire function for something
1408
	 *  easy such as hostname vs ip address add this check
1409
	 */
1410
	if ($ishostname === true) {
1411
		if (!platform_booting()) {
1412
			$ipaddress = gethostbyname($ipent['hostname']);
1413
			if (!is_ipaddr($ipaddress)) {
1414
				return;
1415
			}
1416
		} else {
1417
			$ipaddress = "";
1418
		}
1419
	} else {
1420
		$ipaddress = $ipent['ip'];
1421
	}
1422

    
1423
	$rules = "";
1424
	$cp_filterdns_conf = "";
1425
	$enBwup = 0;
1426
	if (!empty($ipent['bw_up'])) {
1427
		$enBwup = intval($ipent['bw_up']);
1428
	} else if (!empty($config['captiveportal'][$cpzone]['bwdefaultup'])) {
1429
		$enBwup = $config['captiveportal'][$cpzone]['bwdefaultup'];
1430
	}
1431
	$enBwdown = 0;
1432
	if (!empty($ipent['bw_down'])) {
1433
		$enBwdown = intval($ipent['bw_down']);
1434
	} else if (!empty($config['captiveportal'][$cpzone]['bwdefaultdn'])) {
1435
		$enBwdown = $config['captiveportal'][$cpzone]['bwdefaultdn'];
1436
	}
1437

    
1438
	$pipeup = captiveportal_get_next_dn_ruleno();
1439
	$_gb = @pfSense_ipfw_pipe("pipe {$pipeup} config bw {$enBwup}Kbit/s queue 100 buckets 16");
1440
	$pipedown = $pipeup + 1;
1441
	$_gb = @pfSense_ipfw_pipe("pipe {$pipedown} config bw {$enBwdown}Kbit/s queue 100 buckets 16");
1442

    
1443
	if ($ishostname === true) {
1444
		$cp_filterdns_conf .= "ipfw {$ipent['hostname']} {$cpzone}_allowed_up pipe {$pipeup}\n";
1445
		$cp_filterdns_conf .= "ipfw {$ipent['hostname']} {$cpzone}_allowed_down pipe {$pipedown}\n";
1446
		if (!is_ipaddr($ipaddress)) {
1447
			return array("", $cp_filterdns_conf);
1448
		}
1449
	}
1450

    
1451
	$subnet = "";
1452
	if (!empty($ipent['sn'])) {
1453
		$subnet = "/{$ipent['sn']}";
1454
	}
1455
	$rules .= "table {$cpzone}_allowed_up add {$ipaddress}{$subnet} {$pipeup}\n";
1456
	$rules .= "table {$cpzone}_allowed_down add {$ipaddress}{$subnet} {$pipedown}\n";
1457

    
1458
	if ($ishostname === true) {
1459
		return array($rules, $cp_filterdns_conf);
1460
	} else {
1461
		return $rules;
1462
	}
1463
}
1464

    
1465
function captiveportal_allowedhostname_configure() {
1466
	global $config, $g, $cpzone, $cpzoneid;
1467

    
1468
	$rules = "";
1469
	if (!is_array($config['captiveportal'][$cpzone]['allowedhostname'])) {
1470
		return $rules;
1471
	}
1472

    
1473
	$rules = "\n# captiveportal_allowedhostname_configure()\n";
1474
	$cp_filterdns_conf = "";
1475
	foreach ($config['captiveportal'][$cpzone]['allowedhostname'] as $hostnameent) {
1476
		$tmprules = captiveportal_allowedip_configure_entry($hostnameent, true);
1477
		$rules .= $tmprules[0];
1478
		$cp_filterdns_conf .= $tmprules[1];
1479
	}
1480
	$cp_filterdns_filename = "{$g['varetc_path']}/filterdns-{$cpzone}-captiveportal.conf";
1481
	@file_put_contents($cp_filterdns_filename, $cp_filterdns_conf);
1482
	unset($cp_filterdns_conf);
1483

    
1484
	return $rules;
1485
}
1486

    
1487
function captiveportal_filterdns_configure() {
1488
	global $config, $g, $cpzone, $cpzoneid;
1489

    
1490
	$cp_filterdns_filename = $g['varetc_path'] .
1491
	    "/filterdns-{$cpzone}-captiveportal.conf";
1492

    
1493
	if (isset($config['captiveportal'][$cpzone]['enable']) &&
1494
	    is_array($config['captiveportal'][$cpzone]['allowedhostname']) &&
1495
	    file_exists($cp_filterdns_filename)) {
1496
		if (isvalidpid($g['varrun_path'] .
1497
		    "/filterdns-{$cpzone}-cpah.pid")) {
1498
			sigkillbypid($g['varrun_path'] .
1499
			    "/filterdns-{$cpzone}-cpah.pid", "HUP");
1500
		} else {
1501
			mwexec("/usr/local/sbin/filterdns -p " .
1502
			    "{$g['varrun_path']}/filterdns-{$cpzone}-cpah.pid" .
1503
			    " -i 300 -c {$cp_filterdns_filename} -d 1");
1504
		}
1505
	} else {
1506
		killbypid("{$g['varrun_path']}/filterdns-{$cpzone}-cpah.pid");
1507
		@unlink("{$g['varrun_path']}/filterdns-{$cpzone}-cpah.pid");
1508
	}
1509

    
1510
	return $rules;
1511
}
1512

    
1513
function captiveportal_allowedip_configure() {
1514
	global $config, $g, $cpzone;
1515

    
1516
	$rules = "";
1517
	if (is_array($config['captiveportal'][$cpzone]['allowedip'])) {
1518
		foreach ($config['captiveportal'][$cpzone]['allowedip'] as $ipent) {
1519
			$rules .= captiveportal_allowedip_configure_entry($ipent);
1520
		}
1521
	}
1522

    
1523
	return $rules;
1524
}
1525

    
1526
/* get last activity timestamp given client IP address */
1527
function captiveportal_get_last_activity($ip) {
1528
	global $cpzone;
1529

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

    
1533
	foreach ($tables as $table) {
1534
		$ipfw = pfSense_ipfw_table_lookup($table, $ip);
1535
		if (is_array($ipfw)) {
1536
			/* Workaround for #46652 */
1537
			if ($ipfw['packets'] > 0) {
1538
				return $ipfw['timestamp'];
1539
			} else {
1540
				return 0;
1541
			}
1542
		}
1543
	}
1544

    
1545
	return 0;
1546
}
1547

    
1548

    
1549
/* log successful captive portal authentication to syslog */
1550
/* part of this code from php.net */
1551
function captiveportal_logportalauth($user, $mac, $ip, $status, $message = null) {
1552
	// Log it
1553
	if (!$message) {
1554
		$message = "{$status}: {$user}, {$mac}, {$ip}";
1555
	} else {
1556
		$message = trim($message);
1557
		$message = "{$status}: {$user}, {$mac}, {$ip}, {$message}";
1558
	}
1559
	captiveportal_syslog($message);
1560
}
1561

    
1562
/* log simple messages to syslog */
1563
function captiveportal_syslog($message) {
1564
	global $cpzone;
1565

    
1566
	$message = trim($message);
1567
	$message = "Zone: {$cpzone} - {$message}";
1568
	openlog("logportalauth", LOG_PID, LOG_LOCAL4);
1569
	// Log it
1570
	syslog(LOG_INFO, $message);
1571
	closelog();
1572
}
1573

    
1574
/* Authenticate users using Authentication Backend */
1575
function captiveportal_authenticate_user(&$login = '', &$password = '', $clientmac = '', $clientip = '', $pipeno = 'null', $context = 'first') {
1576
	global $g, $config, $cpzone;
1577
	$cpcfg = $config['captiveportal'][$cpzone];
1578

    
1579
	$login_status = 'FAILURE';
1580
	$login_msg = gettext('Invalid credentials specified');
1581
	$reply_attributes = array();
1582
	$auth_method = '';
1583
	$auth_result = null;
1584

    
1585
	/*
1586
	Management of the reply Message (reason why the authentication failed) :
1587
	multiple authentication servers can be used, so multiple reply messages could theoretically be returned.
1588
	But only one message is returned (the most important one).
1589
	The return value of authenticate_user() define how important messages are :
1590
		- Reply message of a successful auth is more important than reply message of
1591
		a user failed auth(invalid credentials/authorization)
1592

    
1593
		- Reply message of a user failed auth is more important than reply message of
1594
		a server failed auth (unable to contact server)
1595

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

    
1599
	The $authlevel variable is a flag indicating the status of authentication
1600
	0 = failed server auth
1601
	1 = failed user auth
1602
	2 = failed user auth with custom server reply recieved
1603
	3 = successful auth
1604
	*/
1605
	$authlevel = 0;
1606

    
1607
	/* Getting authentication servers from captiveportal configuration */
1608
	$auth_servers = array();
1609

    
1610
	if ($cpcfg['auth_method'] === 'none') {
1611
		$auth_servers[] = array('type' => 'none');
1612
	} else {
1613
		if ($context === 'second') {
1614
			$fullauthservers = explode(",", $cpcfg['auth_server2']);
1615
		} else {
1616
			$fullauthservers = explode(",", $cpcfg['auth_server']);
1617
		}
1618

    
1619
		foreach ($fullauthservers as $authserver) {
1620
			if (strpos($authserver, ' - ') !== false) {
1621
				$authserver = explode(' - ', $authserver);
1622
				array_shift($authserver);
1623
				$authserver = implode(' - ', $authserver);
1624

    
1625
				if (auth_get_authserver($authserver) !== null) {
1626
					$auth_servers[] = auth_get_authserver($authserver);
1627
				} else {
1628
					log_error("Zone: {$cpzone} - Captive portal was unable to find the settings of the server '{$authserver}' used for authentication !");
1629
				}
1630
			}
1631
		}
1632
	}
1633

    
1634
	/* Unable to find the any authentication server config - shouldn't happen! - bail out */
1635
	if (count($auth_servers) === 0) {
1636
		log_error("Zone: {$cpzone} - No valid server could be used for authentication.");
1637
		$login_msg = gettext("Internal Error");
1638
	} else {
1639
		foreach ($auth_servers as $authcfg) {
1640
			if ($authlevel < 3) {
1641
				$radmac_error = false;
1642
				$attributes = array("nas_identifier" => empty($cpcfg["radiusnasid"]) ? "CaptivePortal-{$cpzone}" : $cpcfg["radiusnasid"],
1643
					"nas_port_type" => RADIUS_ETHERNET,
1644
					"nas_port" => $pipeno,
1645
					"framed_ip" => $clientip);
1646
				if (mac_format($clientmac) !== null) {
1647
					$attributes["calling_station_id"] = mac_format($clientmac);
1648
				}
1649

    
1650
				$result = null;
1651
				$status = null;
1652
				$msg = null;
1653

    
1654
				/* Radius MAC authentication */
1655
				if ($context === 'radmac' && $clientmac) {
1656
					if ($authcfg['type'] === 'radius') {
1657
						$login = mac_format($clientmac);
1658
						$status = "MACHINE LOGIN";
1659
					} else {
1660
						/* Trying to perform a Radius MAC authentication on a non-radius server - shouldn't happen! - bail out */
1661
						$msg = gettext("Internal Error");
1662
						log_error("Zone: {$cpzone} - Trying to perform RADIUS MAC authentication on a non-RADIUS server !");
1663
						$radmac_error = true;
1664
						$result = null;
1665
					}
1666
				}
1667

    
1668
				if (!$radmac_error) {
1669
					if ($authcfg['type'] === 'none') {
1670
						$result = true;
1671
					} else {
1672
						$result = authenticate_user($login, $password, $authcfg, $attributes);
1673
					}
1674

    
1675
					if (!empty($attributes['error_message'])) {
1676
						$msg = $attributes['error_message'];
1677
					}
1678

    
1679
					if ($authcfg['type'] == 'Local Auth' && $result && isset($cpcfg['localauth_priv'])) {
1680
						if (!userHasPrivilege(getUserEntry($login), "user-services-captiveportal-login")) {
1681
							$result = false;
1682
							$msg = gettext("Access Denied");
1683
						}
1684
					}
1685
					if ($context === 'radmac' && $result === null && empty($attributes['reply_message'])) {
1686
						$msg = gettext("RADIUS MAC Authentication Failed.");
1687
					}
1688

    
1689
					if (empty($status)) {
1690
						if ($result === true) {
1691
							$status = "ACCEPT";
1692
						} elseif ($result === null) {
1693
							$status = "ERROR";
1694
						} else {
1695
							$status = "FAILURE";
1696
						}
1697
					}
1698

    
1699
					if ($context === 'radmac' && $login == mac_format($clientmac) || $authcfg['type'] === 'none' && empty($login)) {
1700
						$login = "unauthenticated";
1701
					}
1702
				}
1703
				// We determine a flag
1704
				if ($result === true) {
1705
					$val = 3;
1706
				} elseif ($result === false && !empty($attributes['reply_message'])) {
1707
					$val = 2;
1708
					$msg = $attributes['reply_message'];
1709
				} elseif ($result === false) {
1710
					$val = 1;
1711
				} elseif ($result === null) {
1712
					$val = 0;
1713
				}
1714

    
1715
				if ($val >= $authlevel) {
1716
					$authlevel = $val;
1717
					$auth_method = $authcfg['type'];
1718
					$login_status = $status;
1719
					$login_msg = $msg;
1720
					$reply_attributes = $attributes;
1721
					$auth_result = $result;
1722
				}
1723
			}
1724
		}
1725
	}
1726

    
1727
	return array('result'=>$auth_result, 'attributes'=>$reply_attributes, 'auth_method' =>$auth_method, 'login_status'=> $login_status, 'login_message' => $login_msg);
1728
}
1729

    
1730
function captiveportal_opendb() {
1731
	global $g, $config, $cpzone, $cpzoneid;
1732

    
1733
	$db_path = "{$g['vardb_path']}/captiveportal{$cpzone}.db";
1734
	$createquery = "CREATE TABLE IF NOT EXISTS captiveportal (" .
1735
				"allow_time INTEGER, pipeno INTEGER, ip TEXT, mac TEXT, username TEXT, " .
1736
				"sessionid TEXT, bpassword TEXT, session_timeout INTEGER, idle_timeout INTEGER, " .
1737
				"session_terminate_time INTEGER, interim_interval INTEGER, traffic_quota INTEGER, " .
1738
				"bw_up INTEGER, bw_down INTEGER, authmethod TEXT, context TEXT); " .
1739
			"CREATE UNIQUE INDEX IF NOT EXISTS idx_active ON captiveportal (sessionid, username); " .
1740
			"CREATE INDEX IF NOT EXISTS user ON captiveportal (username); " .
1741
			"CREATE INDEX IF NOT EXISTS ip ON captiveportal (ip); " .
1742
			"CREATE INDEX IF NOT EXISTS starttime ON captiveportal (allow_time)";
1743

    
1744
	try {
1745
		$DB = new SQLite3($db_path);
1746
		$DB->busyTimeout(60000);
1747
	} catch (Exception $e) {
1748
		captiveportal_syslog("Could not open {$db_path} as an sqlite database for {$cpzone}. Error message: " . $e->getMessage() . " -- Trying again.");
1749
		unlink_if_exists($db_path);
1750
		try {
1751
			$DB = new SQLite3($db_path);
1752
			$DB->busyTimeout(60000);
1753
		} catch (Exception $e) {
1754
			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.");
1755
			return;
1756
		}
1757
	}
1758

    
1759
	if (!$DB) {
1760
		captiveportal_syslog("Could not open {$db_path} as an sqlite database for {$cpzone}. Error message: {$DB->lastErrorMsg()}. Trying again.");
1761
		unlink_if_exists($db_path);
1762
		$DB = new SQLite3($db_path);
1763
		$DB->busyTimeout(60000);
1764
		if (!$DB) {
1765
			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.");
1766
			return;
1767
		}
1768
	}
1769

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

    
1773
		/* If unable to initialize the database, reset and try again. */
1774
		$DB->close();
1775
		unset($DB);
1776
		unlink_if_exists($db_path);
1777
		$DB = new SQLite3($db_path);
1778
		$DB->busyTimeout(60000);
1779
		if ($DB->exec($createquery)) {
1780
			captiveportal_syslog("Successfully reinitialized tables for {$cpzone} -- database has been reset.");
1781
			if (!is_numericint($cpzoneid)) {
1782
				if (is_array($config['captiveportal'])) {
1783
					foreach ($config['captiveportal'] as $cpkey => $cp) {
1784
						if ($cpzone == $cpkey) {
1785
							$cpzoneid = $cp['zoneid'];
1786
						}
1787
					}
1788
				}
1789
			}
1790
			if (is_numericint($cpzoneid)) {
1791
				$table_names = captiveportal_get_ipfw_table_names();
1792
				foreach ($table_names as $table_name) {
1793
					mwexec("/sbin/ipfw table {$table_name} flush");
1794
				}
1795
				captiveportal_syslog("Flushed tables for {$cpzone} after database reset.");
1796
			}
1797
		} else {
1798
			captiveportal_syslog("Still unable to create tables for {$cpzone}. Error message: {$DB->lastErrorMsg()}. Remove the database file manually and try again.");
1799
		}
1800
	}
1801

    
1802
	return $DB;
1803
}
1804

    
1805
/* Get all tables for specific cpzone */
1806
function captiveportal_get_ipfw_table_names() {
1807
	global $cpzone;
1808

    
1809
	$result = array();
1810
	$tables = pfSense_ipfw_tables_list();
1811

    
1812
	if (!is_array($tables)) {
1813
		return $result;
1814
	}
1815

    
1816
	$len = strlen($cpzone) + 1;
1817
	foreach ($tables as $table) {
1818
		if (substr($table['name'], 0, $len) != $cpzone . '_') {
1819
			continue;
1820
		}
1821

    
1822
		$result[] = $table['name'];
1823
	}
1824

    
1825
	return $result;
1826
}
1827

    
1828
/* read captive portal DB into array */
1829
function captiveportal_read_db($query = "") {
1830
	$cpdb = array();
1831

    
1832
	$DB = captiveportal_opendb();
1833
	if ($DB) {
1834
		$response = $DB->query("SELECT * FROM captiveportal {$query}");
1835
		if ($response != FALSE) {
1836
			while ($row = $response->fetchArray()) {
1837
				$cpdb[] = $row;
1838
			}
1839
		}
1840
		$DB->close();
1841
	}
1842

    
1843
	return $cpdb;
1844
}
1845

    
1846
function captiveportal_remove_entries($remove) {
1847

    
1848
	if (!is_array($remove) || empty($remove)) {
1849
		return;
1850
	}
1851

    
1852
	$query = "DELETE FROM captiveportal WHERE sessionid in (";
1853
	foreach ($remove as $idx => $unindex) {
1854
		$query .= "'{$unindex}'";
1855
		if ($idx < (count($remove) - 1)) {
1856
			$query .= ",";
1857
		}
1858
	}
1859
	$query .= ")";
1860
	captiveportal_write_db($query);
1861
}
1862

    
1863
/* write captive portal DB */
1864
function captiveportal_write_db($queries) {
1865
	global $g;
1866

    
1867
	if (is_array($queries)) {
1868
		$query = implode(";", $queries);
1869
	} else {
1870
		$query = $queries;
1871
	}
1872

    
1873
	$DB = captiveportal_opendb();
1874
	if ($DB) {
1875
		$DB->exec("BEGIN TRANSACTION");
1876
		$result = $DB->exec($query);
1877
		if (!$result) {
1878
			captiveportal_syslog("Trying to modify DB returned error: {$DB->lastErrorMsg()}");
1879
		} else {
1880
			$DB->exec("END TRANSACTION");
1881
		}
1882
		$DB->close();
1883
		return $result;
1884
	} else {
1885
		return true;
1886
	}
1887
}
1888

    
1889
function captiveportal_write_elements() {
1890
	global $g, $config, $cpzone;
1891

    
1892
	$cpcfg = $config['captiveportal'][$cpzone];
1893

    
1894
	if (!is_dir($g['captiveportal_element_path'])) {
1895
		@mkdir($g['captiveportal_element_path']);
1896
	}
1897

    
1898
	if (is_array($cpcfg['element'])) {
1899
		foreach ($cpcfg['element'] as $data) {
1900
			/* Do not attempt to decode or write out empty files. */
1901
			if (isset($data['nocontent'])) {
1902
					continue;
1903
			}
1904
			if (empty($data['content']) || empty(base64_decode($data['content']))) {
1905
				unlink_if_exists("{$g['captiveportal_element_path']}/{$data['name']}");
1906
				touch("{$g['captiveportal_element_path']}/{$data['name']}");
1907
			} elseif (!@file_put_contents("{$g['captiveportal_element_path']}/{$data['name']}", base64_decode($data['content']))) {
1908
				printf(gettext('Error: cannot open \'%1$s\' in captiveportal_write_elements()%2$s'), $data['name'], "\n");
1909
				return 1;
1910
			}
1911
			if (!file_exists("{$g['captiveportal_path']}/{$data['name']}")) {
1912
				@symlink("{$g['captiveportal_element_path']}/{$data['name']}", "{$g['captiveportal_path']}/{$data['name']}");
1913
			}
1914
		}
1915
	}
1916

    
1917
	return 0;
1918
}
1919

    
1920
function captiveportal_free_dnrules($rulenos_start = 2000,
1921
    $rulenos_range_max = 64500, $dry_run = false) {
1922
	global $g, $cpzone;
1923

    
1924
	$removed_pipes = array();
1925

    
1926
	if (!file_exists("{$g['vardb_path']}/captiveportaldn.rules")) {
1927
		return $removed_pipes;
1928
	}
1929

    
1930
	if (!$dry_run) {
1931
		$cpruleslck = lock("captiveportalrulesdn", LOCK_EX);
1932
	}
1933

    
1934
	$rules = unserialize(file_get_contents(
1935
	    "{$g['vardb_path']}/captiveportaldn.rules"));
1936
	$ridx = $rulenos_start;
1937
	while ($ridx < $rulenos_range_max) {
1938
		if ($rules[$ridx] == $cpzone) {
1939
			if (!$dry_run) {
1940
				$rules[$ridx] = false;
1941
			}
1942
			$removed_pipes[] = $ridx;
1943
			$ridx++;
1944
			if (!$dry_run) {
1945
				$rules[$ridx] = false;
1946
			}
1947
			$removed_pipes[] = $ridx;
1948
			$ridx++;
1949
		} else {
1950
			$ridx += 2;
1951
		}
1952
	}
1953

    
1954
	if (!$dry_run) {
1955
		file_put_contents("{$g['vardb_path']}/captiveportaldn.rules",
1956
		    serialize($rules));
1957
		unlock($cpruleslck);
1958
	}
1959

    
1960
	unset($rules);
1961

    
1962
	return $removed_pipes;
1963
}
1964

    
1965
function captiveportal_reserve_ruleno($ruleno) {
1966
	global $g, $cpzone;
1967

    
1968
	$cpruleslck = lock("captiveportalrulesdn", LOCK_EX);
1969
	if (file_exists("{$g['vardb_path']}/captiveportaldn.rules")) {
1970
		$rules = unserialize(file_get_contents("{$g['vardb_path']}/captiveportaldn.rules"));
1971
	} else {
1972
		$rules = array_pad(array(), 64500, false);
1973
	}
1974
	$rules[$ruleno] = $cpzone;
1975
	$ruleno++;
1976
	$rules[$ruleno] = $cpzone;
1977

    
1978
	file_put_contents("{$g['vardb_path']}/captiveportaldn.rules", serialize($rules));
1979
	unlock($cpruleslck);
1980
	unset($rules);
1981

    
1982
	return $ruleno;
1983
}
1984

    
1985
function captiveportal_get_next_dn_ruleno($rulenos_start = 2000, $rulenos_range_max = 64500) {
1986
	global $config, $g, $cpzone;
1987

    
1988
	$cpruleslck = lock("captiveportalrulesdn", LOCK_EX);
1989
	$ruleno = 0;
1990
	if (file_exists("{$g['vardb_path']}/captiveportaldn.rules")) {
1991
		$rules = unserialize(file_get_contents("{$g['vardb_path']}/captiveportaldn.rules"));
1992
		$ridx = $rulenos_start;
1993
		while ($ridx < $rulenos_range_max) {
1994
			if (empty($rules[$ridx])) {
1995
				$ruleno = $ridx;
1996
				$rules[$ridx] = $cpzone;
1997
				$ridx++;
1998
				$rules[$ridx] = $cpzone;
1999
				break;
2000
			} else {
2001
				$ridx += 2;
2002
			}
2003
		}
2004
	} else {
2005
		$rules = array_pad(array(), $rulenos_range_max, false);
2006
		$ruleno = $rulenos_start;
2007
		$rules[$rulenos_start] = $cpzone;
2008
		$rulenos_start++;
2009
		$rules[$rulenos_start] = $cpzone;
2010
	}
2011
	file_put_contents("{$g['vardb_path']}/captiveportaldn.rules", serialize($rules));
2012
	unlock($cpruleslck);
2013
	unset($rules);
2014

    
2015
	return $ruleno;
2016
}
2017

    
2018
function captiveportal_free_dn_ruleno($ruleno) {
2019
	global $config, $g;
2020

    
2021
	$cpruleslck = lock("captiveportalrulesdn", LOCK_EX);
2022
	if (file_exists("{$g['vardb_path']}/captiveportaldn.rules")) {
2023
		$rules = unserialize(file_get_contents("{$g['vardb_path']}/captiveportaldn.rules"));
2024
		$rules[$ruleno] = false;
2025
		$ruleno++;
2026
		$rules[$ruleno] = false;
2027
		file_put_contents("{$g['vardb_path']}/captiveportaldn.rules", serialize($rules));
2028
		unset($rules);
2029
	}
2030
	unlock($cpruleslck);
2031
}
2032

    
2033
function captiveportal_get_dn_passthru_ruleno($value) {
2034
	global $config, $g, $cpzone, $cpzoneid;
2035

    
2036
	$cpcfg = $config['captiveportal'][$cpzone];
2037
	if (!isset($cpcfg['enable'])) {
2038
		return NULL;
2039
	}
2040

    
2041
	$cpruleslck = lock("captiveportalrulesdn", LOCK_EX);
2042
	$ruleno = NULL;
2043
	if (file_exists("{$g['vardb_path']}/captiveportaldn.rules")) {
2044
		unset($output);
2045
		$item = pfSense_ipfw_table_lookup("{$cpzone}_pipe_mac",
2046
		    "any,{$value}");
2047
		if (!is_array($item) || empty($item['pipe'])) {
2048
			unlock($cpruleslck);
2049
			return NULL;
2050
		}
2051

    
2052
		$ruleno = intval($item['pipe']);
2053
		$rules = unserialize(file_get_contents("{$g['vardb_path']}/captiveportaldn.rules"));
2054
		if (!$rules[$ruleno]) {
2055
			$ruleno = NULL;
2056
		}
2057
		unset($rules);
2058
	}
2059
	unlock($cpruleslck);
2060

    
2061
	return $ruleno;
2062
}
2063

    
2064
/**
2065
 * This function will calculate the traffic produced by a client
2066
 * based on its firewall rule
2067
 *
2068
 * Point of view: NAS
2069
 *
2070
 * Input means: from the client
2071
 * Output means: to the client
2072
 *
2073
 */
2074

    
2075
function getVolume($ip) {
2076
	global $config, $cpzone;
2077

    
2078
	$reverse = isset($config['captiveportal'][$cpzone]['reverseacct'])
2079
	    ? true : false;
2080
	$volume = array();
2081
	// Initialize vars properly, since we don't want NULL vars
2082
	$volume['input_pkts'] = $volume['input_bytes'] = 0;
2083
	$volume['output_pkts'] = $volume['output_bytes'] = 0;
2084

    
2085
	$tables = array("allowed", "auth");
2086

    
2087
	foreach($tables as $table) {
2088
		$ipfw = pfSense_ipfw_table_lookup("{$cpzone}_{$table}_up", $ip);
2089
		if (!is_array($ipfw)) {
2090
			continue;
2091
		}
2092
		if ($reverse) {
2093
			$volume['output_pkts'] = $ipfw['packets'];
2094
			$volume['output_bytes'] = $ipfw['bytes'];
2095
		} else {
2096
			$volume['input_pkts'] = $ipfw['packets'];
2097
			$volume['input_bytes'] = $ipfw['bytes'];
2098
		}
2099
	}
2100

    
2101
	foreach($tables as $table) {
2102
		$ipfw = pfSense_ipfw_table_lookup("{$cpzone}_{$table}_down",
2103
		    $ip);
2104
		if (!is_array($ipfw)) {
2105
			continue;
2106
		}
2107
		if ($reverse) {
2108
			$volume['input_pkts'] = $ipfw['packets'];
2109
			$volume['input_bytes'] = $ipfw['bytes'];
2110
		} else {
2111
			$volume['output_pkts'] = $ipfw['packets'];
2112
			$volume['output_bytes'] = $ipfw['bytes'];
2113
		}
2114
	}
2115

    
2116
	return $volume;
2117
}
2118

    
2119
function portal_ip_from_client_ip($cliip) {
2120
	global $config, $cpzone;
2121

    
2122
	$isipv6 = is_ipaddrv6($cliip);
2123
	$interfaces = explode(",", $config['captiveportal'][$cpzone]['interface']);
2124
	foreach ($interfaces as $cpif) {
2125
		if ($isipv6) {
2126
			$ip = get_interface_ipv6($cpif);
2127
			$sn = get_interface_subnetv6($cpif);
2128
		} else {
2129
			$ip = get_interface_ip($cpif);
2130
			$sn = get_interface_subnet($cpif);
2131
		}
2132
		if (ip_in_subnet($cliip, "{$ip}/{$sn}")) {
2133
			return $ip;
2134
		}
2135
	}
2136

    
2137
	$inet = ($isipv6) ? '-inet6' : '-inet';
2138
	$iface = exec_command("/sbin/route -n get {$inet} {$cliip} | /usr/bin/awk '/interface/ { print \$2; };'");
2139
	$iface = trim($iface, "\n");
2140
	if (!empty($iface)) {
2141
		$ip = ($isipv6) ? find_interface_ipv6($iface) : find_interface_ip($iface);
2142
		if (is_ipaddr($ip)) {
2143
			return $ip;
2144
		}
2145
	}
2146

    
2147
	// doesn't match up to any particular interface
2148
	// so let's set the portal IP to what PHP says
2149
	// the server IP issuing the request is.
2150
	// allows same behavior as 1.2.x where IP isn't
2151
	// in the subnet of any CP interface (static routes, etc.)
2152
	// rather than forcing to DNS hostname resolution
2153
	$ip = $_SERVER['SERVER_ADDR'];
2154
	if (is_ipaddr($ip)) {
2155
		return $ip;
2156
	}
2157

    
2158
	return false;
2159
}
2160

    
2161
function portal_hostname_from_client_ip($cliip) {
2162
	global $config, $cpzone;
2163

    
2164
	$cpcfg = $config['captiveportal'][$cpzone];
2165

    
2166
	if (isset($cpcfg['httpslogin'])) {
2167
		$listenporthttps = $cpcfg['listenporthttps'] ? $cpcfg['listenporthttps'] : ($cpcfg['zoneid'] + 8001);
2168
		$ourhostname = $cpcfg['httpsname'];
2169

    
2170
		if ($listenporthttps != 443) {
2171
			$ourhostname .= ":" . $listenporthttps;
2172
		}
2173
	} else {
2174
		$listenporthttp = $cpcfg['listenporthttp'] ? $cpcfg['listenporthttp'] : ($cpcfg['zoneid'] + 8000);
2175
		$ifip = portal_ip_from_client_ip($cliip);
2176
		if (!$ifip) {
2177
			$ourhostname = "{$config['system']['hostname']}.{$config['system']['domain']}";
2178
		} else {
2179
			$ourhostname = (is_ipaddrv6($ifip)) ? "[{$ifip}]" : "{$ifip}";
2180
		}
2181

    
2182
		if ($listenporthttp != 80) {
2183
			$ourhostname .= ":" . $listenporthttp;
2184
		}
2185
	}
2186

    
2187
	return $ourhostname;
2188
}
2189

    
2190
/* functions move from index.php */
2191

    
2192
function portal_reply_page($redirurl, $type = null, $message = null, $clientmac = null, $clientip = null, $username = null, $password = null) {
2193
	global $g, $config, $cpzone;
2194

    
2195
	/* Get captive portal layout */
2196
	if ($type == "redir") {
2197
		header("Location: {$redirurl}");
2198
		return;
2199
	} else if ($type == "login") {
2200
		$htmltext = get_include_contents("{$g['varetc_path']}/captiveportal_{$cpzone}.html");
2201
	} else {
2202
		$htmltext = get_include_contents("{$g['varetc_path']}/captiveportal-{$cpzone}-error.html");
2203
	}
2204

    
2205
	$cpcfg = $config['captiveportal'][$cpzone];
2206

    
2207
	/* substitute the PORTAL_REDIRURL variable */
2208
	if ($cpcfg['preauthurl']) {
2209
		$htmltext = str_replace("\$PORTAL_REDIRURL\$", "{$cpcfg['preauthurl']}", $htmltext);
2210
		$htmltext = str_replace("#PORTAL_REDIRURL#", "{$cpcfg['preauthurl']}", $htmltext);
2211
	}
2212

    
2213
	/* substitute other variables */
2214
	$ourhostname = portal_hostname_from_client_ip($clientip);
2215
	$protocol = (isset($cpcfg['httpslogin'])) ? 'https://' : 'http://';
2216
	$htmltext = str_replace("\$PORTAL_ACTION\$", "{$protocol}{$ourhostname}/index.php?zone={$cpzone}", $htmltext);
2217
	$htmltext = str_replace("#PORTAL_ACTION#", "{$protocol}{$ourhostname}/index.php?zone={$cpzone}", $htmltext);
2218

    
2219
	$htmltext = str_replace("\$PORTAL_ZONE\$", htmlspecialchars($cpzone), $htmltext);
2220
	$htmltext = str_replace("\$PORTAL_REDIRURL\$", htmlspecialchars($redirurl), $htmltext);
2221
	$htmltext = str_replace("\$PORTAL_MESSAGE\$", htmlspecialchars($message), $htmltext);
2222
	$htmltext = str_replace("\$CLIENT_MAC\$", htmlspecialchars($clientmac), $htmltext);
2223
	$htmltext = str_replace("\$CLIENT_IP\$", htmlspecialchars($clientip), $htmltext);
2224

    
2225
	// Special handling case for captive portal master page so that it can be ran
2226
	// through the PHP interpreter using the include method above.  We convert the
2227
	// $VARIABLE$ case to #VARIABLE# in /etc/inc/captiveportal.inc before writing out.
2228
	$htmltext = str_replace("#PORTAL_ZONE#", htmlspecialchars($cpzone), $htmltext);
2229
	$htmltext = str_replace("#PORTAL_REDIRURL#", htmlspecialchars($redirurl), $htmltext);
2230
	$htmltext = str_replace("#PORTAL_MESSAGE#", htmlspecialchars($message), $htmltext);
2231
	$htmltext = str_replace("#CLIENT_MAC#", htmlspecialchars($clientmac), $htmltext);
2232
	$htmltext = str_replace("#CLIENT_IP#", htmlspecialchars($clientip), $htmltext);
2233
	$htmltext = str_replace("#USERNAME#", htmlspecialchars($username), $htmltext);
2234
	$htmltext = str_replace("#PASSWORD#", htmlspecialchars($password), $htmltext);
2235

    
2236
	echo $htmltext;
2237
}
2238

    
2239
function captiveportal_reapply_attributes($cpentry, $attributes) {
2240
	global $config, $cpzone, $g;
2241

    
2242
	if (isset($config['captiveportal'][$cpzone]['peruserbw'])) {
2243
		$dwfaultbw_up = !empty($config['captiveportal'][$cpzone]['bwdefaultup']) ? $config['captiveportal'][$cpzone]['bwdefaultup'] : 0;
2244
		$dwfaultbw_down = !empty($config['captiveportal'][$cpzone]['bwdefaultdn']) ? $config['captiveportal'][$cpzone]['bwdefaultdn'] : 0;
2245
	} else {
2246
		$dwfaultbw_up = $dwfaultbw_down = 0;
2247
	}
2248
	/* pipe throughputs must always be an integer, enforce that restriction again here. */
2249
	if (isset($config['captiveportal'][$cpzone]['radiusperuserbw'])) {
2250
		$bw_up = round(!empty($attributes['bw_up']) ? intval($attributes['bw_up'])/1000 : $dwfaultbw_up, 0);
2251
		$bw_down = round(!empty($attributes['bw_down']) ? intval($attributes['bw_down'])/1000 : $dwfaultbw_down, 0);
2252
	} else {
2253
		$bw_up = round($dwfaultbw_up,0);
2254
		$bw_down = round($dwfaultbw_down,0);
2255
	}
2256

    
2257
	$bw_up_pipeno = $cpentry[1];
2258
	$bw_down_pipeno = $cpentry[1]+1;
2259

    
2260
	if ($cpentry['bw_up'] !== $bw_up) {
2261
		$_gb = @pfSense_ipfw_pipe("pipe {$bw_up_pipeno} config bw {$bw_up}Kbit/s queue 100 buckets 16");
2262
		captiveportal_update_entry($cpentry['sessionid'], $bw_up, 'bw_up');
2263
	}
2264
	if ($cpentry['bw_down'] !== $bw_down) {
2265
		$_gb = @pfSense_ipfw_pipe("pipe {$bw_down_pipeno} config bw {$bw_down}Kbit/s queue 100 buckets 16");
2266
		captiveportal_update_entry($cpentry['sessionid'], $bw_down, 'bw_down');
2267
	}
2268
	unset($bw_up_pipeno, $bw_down_pipeno, $bw_up, $bw_down);
2269
}
2270

    
2271
function captiveportal_update_entry($sessionid, $new_value, $field_to_update) {
2272
	global $cpzone;
2273

    
2274
	if (!intval($new_value)) {
2275
		$new_value = "'{$new_value}'";
2276
	}
2277
	captiveportal_write_db("UPDATE captiveportal SET {$field_to_update} = {$new_value} WHERE sessionid = '{$sessionid}'");
2278
}
2279

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

    
2283
	// Ensure we create an array if we are missing attributes
2284
	if (!is_array($attributes)) {
2285
		$attributes = array();
2286
	}
2287

    
2288
	unset($sessionid);
2289

    
2290
	/* Do not allow concurrent login execution. */
2291
	$cpdblck = lock("captiveportaldb{$cpzone}", LOCK_EX);
2292

    
2293
	if ($attributes['voucher']) {
2294
		$remaining_time = $attributes['session_timeout'];
2295
		$authmethod = "voucher"; // Set RADIUS-Attribute to Voucher to prevent ReAuth-Reqeuest for Vouchers Bug: #2155
2296
		$context = "voucher";
2297
	}
2298

    
2299
	$writecfg = false;
2300
	/* If both "Add MAC addresses of connected users as pass-through MAC" and "Disable concurrent logins" are checked, 
2301
	then we need to check if the user was already authenticated using another MAC Address, and if so remove the previous Pass-Through MAC. */	
2302
	if ((isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) && ($username != 'unauthenticated') && isset($config['captiveportal'][$cpzone]['passthrumacadd'])) {
2303
		$mac = captiveportal_passthrumac_findbyname($username);
2304
		if (!empty($mac)) {
2305
			foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $idx => $macent) {
2306
				if ($macent['mac'] != $mac['mac']) {
2307
					continue;
2308
				}
2309

    
2310
				$pipeno = captiveportal_get_dn_passthru_ruleno($mac['mac']);
2311
				if ($pipeno) {
2312
					captiveportal_free_dn_ruleno($pipeno);
2313
					@pfSense_ipfw_table("{$cpzone}_pipe_mac", IP_FW_TABLE_XDEL, "any,{$mac['mac']}");
2314
					@pfSense_ipfw_table("{$cpzone}_pipe_mac", IP_FW_TABLE_XDEL, "{$mac['mac']},any");
2315
					@pfSense_ipfw_pipe("pipe delete " . $pipeno+1);
2316
					@pfSense_ipfw_pipe("pipe delete " . $pipeno);
2317
				}
2318
				unset($config['captiveportal'][$cpzone]['passthrumac'][$idx]);
2319
			}
2320
		}
2321
	}
2322

    
2323
	/* read in client database */
2324
	$query = "WHERE ip = '{$clientip}'";
2325
	$tmpusername = SQLite3::escapeString(strtolower($username));
2326
	if (isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) {
2327
		$query .= " OR (username != 'unauthenticated' AND lower(username) = '{$tmpusername}')";
2328
	}
2329
	$cpdb = captiveportal_read_db($query);
2330

    
2331
	/* Snapshot the timestamp */
2332
	$allow_time = time();
2333
	$unsetindexes = array();
2334

    
2335
	foreach ($cpdb as $cpentry) {
2336
		/* on the same ip */
2337
		if ($cpentry[2] == $clientip) {
2338
			if (isset($config['captiveportal'][$cpzone]['nomacfilter']) || $cpentry[3] == $clientmac) {
2339
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT LOGIN - REUSING OLD SESSION");
2340
			} else {
2341
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT LOGIN - REUSING IP {$cpentry[2]} WITH DIFFERENT MAC ADDRESS {$cpentry[3]}");
2342
			}
2343
			$sessionid = $cpentry[5];
2344
			break;
2345
		} elseif (($attributes['voucher']) && ($username != 'unauthenticated') && ($cpentry[4] == $username)) {
2346
			// user logged in with an active voucher. Check for how long and calculate
2347
			// how much time we can give him (voucher credit - used time)
2348
			$remaining_time = $cpentry[0] + $cpentry[7] - $allow_time;
2349
			if ($remaining_time < 0) { // just in case.
2350
				$remaining_time = 0;
2351
			}
2352

    
2353
			/* This user was already logged in so we disconnect the old one */
2354
			captiveportal_disconnect($cpentry, 13);
2355
			captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT LOGIN - TERMINATING OLD SESSION");
2356
			$unsetindexes[] = $cpentry[5];
2357
			break;
2358
		} elseif ((isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) && ($username != 'unauthenticated')) {
2359
			/* on the same username */
2360
			if (strcasecmp($cpentry[4], $username) == 0) {
2361
				/* This user was already logged in so we disconnect the old one */
2362
				captiveportal_disconnect($cpentry, 13);
2363
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT LOGIN - TERMINATING OLD SESSION");
2364
				$unsetindexes[] = $cpentry[5];
2365
				break;
2366
			}
2367
		}
2368
	}
2369
	unset($cpdb);
2370

    
2371
	if (!empty($unsetindexes)) {
2372
		captiveportal_remove_entries($unsetindexes);
2373
	}
2374

    
2375
	if ($attributes['voucher'] && $remaining_time <= 0) {
2376
		return 0;       // voucher already used and no time left
2377
	}
2378

    
2379
	if (!isset($sessionid)) {
2380
		/* generate unique session ID */
2381
		$tod = gettimeofday();
2382
		$sessionid = substr(md5(mt_rand() . $tod['sec'] . $tod['usec'] . $clientip . $clientmac), 0, 16);
2383

    
2384
		if (isset($config['captiveportal'][$cpzone]['peruserbw'])) {
2385
			$dwfaultbw_up = !empty($config['captiveportal'][$cpzone]['bwdefaultup']) ? $config['captiveportal'][$cpzone]['bwdefaultup'] : 0;
2386
			$dwfaultbw_down = !empty($config['captiveportal'][$cpzone]['bwdefaultdn']) ? $config['captiveportal'][$cpzone]['bwdefaultdn'] : 0;
2387
		} else {
2388
			$dwfaultbw_up = $dwfaultbw_down = 0;
2389
		}
2390
		/* pipe throughputs must always be an integer, enforce that restriction again here. */
2391
		if (isset($config['captiveportal'][$cpzone]['radiusperuserbw'])) {
2392
			$bw_up = round(!empty($attributes['bw_up']) ? intval($attributes['bw_up'])/1000 : $dwfaultbw_up, 0);
2393
			$bw_down = round(!empty($attributes['bw_down']) ? intval($attributes['bw_down'])/1000 : $dwfaultbw_down, 0);
2394
		} else {
2395
			$bw_up = round($dwfaultbw_up,0);
2396
			$bw_down = round($dwfaultbw_down,0);
2397
		}
2398

    
2399
		if (isset($config['captiveportal'][$cpzone]['passthrumacadd'])) {
2400

    
2401
			$mac = array();
2402
			$mac['action'] = 'pass';
2403
			$mac['mac'] = $clientmac;
2404
			$mac['ip'] = $clientip; /* Used only for logging */
2405
			$mac['username'] = $username;
2406
			if ($attributes['voucher']) {
2407
				$mac['logintype'] = "voucher";
2408
			}
2409
			if ($username == "unauthenticated") {
2410
				$mac['descr'] = "Auto-added";
2411
			} else if ($authmethod == "voucher") {
2412
				$mac['descr'] = "Auto-added for voucher {$username}";
2413
			} else {
2414
				$mac['descr'] = "Auto-added for user {$username}";
2415
			}
2416
			if (!empty($bw_up)) {
2417
				$mac['bw_up'] = $bw_up;
2418
			}
2419
			if (!empty($bw_down)) {
2420
				$mac['bw_down'] = $bw_down;
2421
			}
2422
			if (!is_array($config['captiveportal'][$cpzone]['passthrumac'])) {
2423
				$config['captiveportal'][$cpzone]['passthrumac'] = array();
2424
			}
2425
			//check for mac duplicates before adding it to config.
2426
			$mac_duplicate = false;
2427
			foreach($config['captiveportal'][$cpzone]['passthrumac'] as $mac_check){
2428
				if($mac_check['mac'] == $mac['mac']){
2429
					$mac_duplicate = true;
2430
				}
2431
			}
2432
			if(!$mac_duplicate){
2433
				$config['captiveportal'][$cpzone]['passthrumac'][] = $mac;
2434
			}
2435
			unlock($cpdblck);
2436
			$macrules = captiveportal_passthrumac_configure_entry($mac);
2437
			file_put_contents("{$g['tmp_path']}/macentry_{$cpzone}.rules.tmp", $macrules);
2438
			mwexec("/sbin/ipfw -q {$g['tmp_path']}/macentry_{$cpzone}.rules.tmp");
2439
			$writecfg = true;
2440
		} else {
2441
			/* See if a pipeno is passed, if not start sessions because this means there isn't one atm */
2442
			if (is_null($pipeno)) {
2443
				$pipeno = captiveportal_get_next_dn_ruleno();
2444
			}
2445

    
2446
			/* if the pool is empty, return appropriate message and exit */
2447
			if (is_null($pipeno)) {
2448
				portal_reply_page($redirurl, "error", "System reached maximum login capacity");
2449
				log_error("Zone: {$cpzone} - WARNING!  Captive portal has reached maximum login capacity");
2450
				unlock($cpdblck);
2451
				return;
2452
			}
2453

    
2454
			$bw_up_pipeno = $pipeno;
2455
			$bw_down_pipeno = $pipeno + 1;
2456
			//$bw_up /= 1000; // Scale to Kbit/s
2457
			$_gb = @pfSense_ipfw_pipe("pipe {$bw_up_pipeno} config bw {$bw_up}Kbit/s queue 100 buckets 16");
2458
			$_gb = @pfSense_ipfw_pipe("pipe {$bw_down_pipeno} config bw {$bw_down}Kbit/s queue 100 buckets 16");
2459

    
2460
			$rule_entry = "{$clientip}/" . (is_ipaddrv6($clientip) ? "128" : "32");
2461
			if (!isset($config['captiveportal'][$cpzone]['nomacfilter'])) {
2462
				$rule_entry .= ",{$clientmac}";
2463
			}
2464
			$_gb = @pfSense_ipfw_table("{$cpzone}_auth_up", IP_FW_TABLE_XADD, "{$rule_entry}", $bw_up_pipeno);
2465
			$_gb = @pfSense_ipfw_table("{$cpzone}_auth_down", IP_FW_TABLE_XADD, "{$rule_entry}", $bw_down_pipeno);
2466

    
2467
			if ($attributes['voucher']) {
2468
				$attributes['session_timeout'] = $remaining_time;
2469
			}
2470

    
2471
			/* handle empty attributes */
2472
			$session_timeout = (!empty($attributes['session_timeout'])) ? $attributes['session_timeout'] : 'NULL';
2473
			$idle_timeout = (!empty($attributes['idle_timeout'])) ? $attributes['idle_timeout'] : 'NULL';
2474
			$session_terminate_time = (!empty($attributes['session_terminate_time'])) ? $attributes['session_terminate_time'] : 'NULL';
2475
			$interim_interval = (!empty($attributes['interim_interval'])) ? $attributes['interim_interval'] : 'NULL';
2476
			$traffic_quota = (!empty($attributes['maxbytes'])) ? $attributes['maxbytes'] : 'NULL';
2477

    
2478
			/* escape username */
2479
			$safe_username = SQLite3::escapeString($username);
2480

    
2481
			/* encode password in Base64 just in case it contains commas */
2482
			$bpassword = (isset($config['captiveportal'][$cpzone]['reauthenticate'])) ? base64_encode($password) : '';
2483
			$insertquery = "INSERT INTO captiveportal (allow_time, pipeno, ip, mac, username, sessionid, bpassword, session_timeout, idle_timeout, session_terminate_time, interim_interval, traffic_quota, bw_up, bw_down, authmethod, context) ";
2484
			$insertquery .= "VALUES ({$allow_time}, {$pipeno}, '{$clientip}', '{$clientmac}', '{$safe_username}', '{$sessionid}', '{$bpassword}', ";
2485
			$insertquery .= "{$session_timeout}, {$idle_timeout}, {$session_terminate_time}, {$interim_interval}, {$traffic_quota}, {$bw_up}, {$bw_down}, '{$authmethod}', '{$context}')";
2486

    
2487
			/* store information to database */
2488
			captiveportal_write_db($insertquery);
2489
			unlock($cpdblck);
2490
			unset($insertquery, $bpassword);
2491

    
2492
			$radacct = isset($config['captiveportal'][$cpzone]['radacct_enable']) ? true : false;
2493
			if ($authmethod === 'radius' && $radacct) {
2494
				captiveportal_send_server_accounting('start',
2495
					$pipeno, // ruleno
2496
					$username, // username
2497
					$clientip, // clientip
2498
					$clientmac, // clientmac
2499
					$sessionid, // sessionid
2500
					time());  // start time
2501
			}
2502
		}
2503
	} else {
2504
		/* NOTE: #3062-11 If the pipeno has been allocated free it to not DoS the CP */
2505
		if (!is_null($pipeno)) {
2506
			captiveportal_free_dn_ruleno($pipeno);
2507
		}
2508

    
2509
		unlock($cpdblck);
2510
	}
2511

    
2512
	if ($writecfg == true) {
2513
		write_config(gettext("Captive Portal allowed users configuration changed"));
2514
	}
2515

    
2516
	/* redirect user to desired destination */
2517
	if (!empty($attributes['url_redirection'])) {
2518
		$my_redirurl = $attributes['url_redirection'];
2519
	} else if (!empty($redirurl)) {
2520
		$my_redirurl = $redirurl;
2521
	} else if (!empty($config['captiveportal'][$cpzone]['redirurl'])) {
2522
		$my_redirurl = $config['captiveportal'][$cpzone]['redirurl'];
2523
	}
2524

    
2525
	if (isset($config['captiveportal'][$cpzone]['logoutwin_enable']) && !isset($config['captiveportal'][$cpzone]['passthrumacadd'])) {
2526
		$ourhostname = portal_hostname_from_client_ip($clientip);
2527
		$protocol = (isset($config['captiveportal'][$cpzone]['httpslogin'])) ? 'https://' : 'http://';
2528
		$logouturl = "{$protocol}{$ourhostname}/";
2529

    
2530
		if (isset($attributes['reply_message'])) {
2531
			$message = $attributes['reply_message'];
2532
		} else {
2533
			$message = 0;
2534
		}
2535

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

    
2538
	} else {
2539
		portal_reply_page($my_redirurl, "redir", "Just redirect the user.");
2540
	}
2541

    
2542
	return $sessionid;
2543
}
2544

    
2545

    
2546
/*
2547
 * Used for when pass-through credits are enabled.
2548
 * Returns true when there was at least one free login to deduct for the MAC.
2549
 * Expired entries are removed as they are seen.
2550
 * Active entries are updated according to the configuration.
2551
 */
2552
function portal_consume_passthrough_credit($clientmac) {
2553
	global $config, $cpzone;
2554

    
2555
	if (!empty($config['captiveportal'][$cpzone]['freelogins_count']) && is_numeric($config['captiveportal'][$cpzone]['freelogins_count'])) {
2556
		$freeloginscount = $config['captiveportal'][$cpzone]['freelogins_count'];
2557
	} else {
2558
		return false;
2559
	}
2560

    
2561
	if (!empty($config['captiveportal'][$cpzone]['freelogins_resettimeout']) && is_numeric($config['captiveportal'][$cpzone]['freelogins_resettimeout'])) {
2562
		$resettimeout = $config['captiveportal'][$cpzone]['freelogins_resettimeout'];
2563
	} else {
2564
		return false;
2565
	}
2566

    
2567
	if ($freeloginscount < 1 || $resettimeout <= 0 || !$clientmac) {
2568
		return false;
2569
	}
2570

    
2571
	$updatetimeouts = isset($config['captiveportal'][$cpzone]['freelogins_updatetimeouts']);
2572

    
2573
	/*
2574
	 * Read database of used MACs.  Lines are a comma-separated list
2575
	 * of the time, MAC, then the count of pass-through credits remaining.
2576
	 */
2577
	$usedmacs = captiveportal_read_usedmacs_db();
2578

    
2579
	$currenttime = time();
2580
	$found = false;
2581
	foreach ($usedmacs as $key => $usedmac) {
2582
		$usedmac = explode(",", $usedmac);
2583

    
2584
		if ($usedmac[1] == $clientmac) {
2585
			if ($usedmac[0] + ($resettimeout * 3600) > $currenttime) {
2586
				if ($usedmac[2] < 1) {
2587
					if ($updatetimeouts) {
2588
						$usedmac[0] = $currenttime;
2589
						unset($usedmacs[$key]);
2590
						$usedmacs[] = implode(",", $usedmac);
2591
						captiveportal_write_usedmacs_db($usedmacs);
2592
					}
2593

    
2594
					return false;
2595
				} else {
2596
					$usedmac[2] -= 1;
2597
					$usedmacs[$key] = implode(",", $usedmac);
2598
				}
2599

    
2600
				$found = true;
2601
			} else {
2602
				unset($usedmacs[$key]);
2603
			}
2604

    
2605
			break;
2606
		} else if ($usedmac[0] + ($resettimeout * 3600) <= $currenttime) {
2607
			unset($usedmacs[$key]);
2608
		}
2609
	}
2610

    
2611
	if (!$found) {
2612
		$usedmac = array($currenttime, $clientmac, $freeloginscount - 1);
2613
		$usedmacs[] = implode(",", $usedmac);
2614
	}
2615

    
2616
	captiveportal_write_usedmacs_db($usedmacs);
2617
	return true;
2618
}
2619

    
2620
function captiveportal_read_usedmacs_db() {
2621
	global $g, $cpzone;
2622

    
2623
	$cpumaclck = lock("captiveusedmacs{$cpzone}");
2624
	if (file_exists("{$g['vardb_path']}/captiveportal_usedmacs_{$cpzone}.db")) {
2625
		$usedmacs = file("{$g['vardb_path']}/captiveportal_usedmacs_{$cpzone}.db", FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
2626
		if (!$usedmacs) {
2627
			$usedmacs = array();
2628
		}
2629
	} else {
2630
		$usedmacs = array();
2631
	}
2632

    
2633
	unlock($cpumaclck);
2634
	return $usedmacs;
2635
}
2636

    
2637
function captiveportal_write_usedmacs_db($usedmacs) {
2638
	global $g, $cpzone;
2639

    
2640
	$cpumaclck = lock("captiveusedmacs{$cpzone}", LOCK_EX);
2641
	@file_put_contents("{$g['vardb_path']}/captiveportal_usedmacs_{$cpzone}.db", implode("\n", $usedmacs));
2642
	unlock($cpumaclck);
2643
}
2644

    
2645
function captiveportal_blocked_mac($mac) {
2646
	global $config, $g, $cpzone;
2647

    
2648
	if (empty($mac) || !is_macaddr($mac)) {
2649
		return false;
2650
	}
2651

    
2652
	if (!is_array($config['captiveportal'][$cpzone]['passthrumac'])) {
2653
		return false;
2654
	}
2655

    
2656
	foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $passthrumac) {
2657
		if (($passthrumac['action'] == 'block') &&
2658
		    ($passthrumac['mac'] == strtolower($mac))) {
2659
			return true;
2660
		}
2661
	}
2662

    
2663
	return false;
2664

    
2665
}
2666

    
2667
/* Captiveportal Radius Accounting */
2668

    
2669
function gigawords($bytes) {
2670

    
2671
	/*
2672
	 * RFC2866 Specifies a 32bit unsigned integer, which is a max of 4294967295
2673
	 * Currently there is a fault in the PECL radius_put_int function which can handle only 32bit signed integer.
2674
	 */
2675

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

    
2679
	// We need to manually set this to a zero instead of NULL for put_int() safety
2680
	if (is_null($gigawords)) {
2681
		$gigawords = 0;
2682
	}
2683

    
2684
	return $gigawords;
2685
}
2686

    
2687
function remainder($bytes) {
2688
	// Calculate the bytes we are going to send to the radius
2689
	$bytes = bcmod($bytes, GIGAWORDS_RIGHT_OPERAND);
2690

    
2691
	if (is_null($bytes)) {
2692
		$bytes = 0;
2693
	}
2694

    
2695
    return $bytes;
2696
}
2697

    
2698
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) {
2699
	global $cpzone, $config;
2700

    
2701
	$cpcfg = $config['captiveportal'][$cpzone];
2702
	$acctcfg = auth_get_authserver($cpcfg['radacct_server']);
2703

    
2704
	if (!isset($cpcfg['radacct_enable']) || empty($acctcfg)) {
2705
		return null;
2706
	}
2707

    
2708
	if ($type === 'on') {
2709
		$racct = new Auth_RADIUS_Acct_On;
2710
	} elseif ($type === 'off') {
2711
		$racct = new Auth_RADIUS_Acct_Off;
2712
	} elseif ($type === 'start') {
2713
		$racct = new Auth_RADIUS_Acct_Start;
2714
		if (!is_int($start_time)) {
2715
			$start_time = time();
2716
		}
2717
	} elseif ($type === 'stop') {
2718
		$racct = new Auth_RADIUS_Acct_Stop;
2719
		if (!is_int($stop_time)) {
2720
			$stop_time = time();
2721
		}
2722
	} elseif ($type === 'update') {
2723
        $racct = new Auth_RADIUS_Acct_Update;
2724
		if (!is_int($stop_time)) {
2725
			$stop_time = time(); // "top time" here will be used only for calculating session time.
2726
		}
2727
	} else {
2728
		return null;
2729
	}
2730

    
2731
	$racct->addServer($acctcfg['host'], $acctcfg['radius_acct_port'],
2732
		$acctcfg['radius_secret'], $acctcfg['radius_timeout']);
2733

    
2734
	$racct->authentic = RADIUS_AUTH_RADIUS;
2735
	if ($cpcfg['auth_method'] === 'radmac' && $username === "unauthenticated" && !empty($clientmac)) {
2736
		$racct->username = mac_format($clientmac);
2737
	} elseif (!empty($username)) {
2738
		$racct->username = $username;
2739
	}
2740

    
2741
	if (PEAR::isError($racct->start())) {
2742
		captiveportal_syslog('RADIUS ACCOUNTING FAILED : '.$racct->getError());
2743
		$racct->close();
2744
		return null;
2745
	}
2746

    
2747
	$nasip = $acctcfg['radius_nasip_attribute'];
2748
	if (!is_ipaddr($nasip)) {
2749
		$nasip = get_interface_ip($nasip);
2750
		if (!is_ipaddr($nasip)) {
2751
			$nasip = get_interface_ip();//We use WAN interface IP as fallback for NAS-IP-Address
2752
		}
2753
	}
2754
	$nasmac = get_interface_mac(find_ip_interface($nasip));
2755
	$racct->putAttribute(RADIUS_NAS_IP_ADDRESS, $nasip, "addr");
2756

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

    
2759
	if (is_int($ruleno)) {
2760
		$racct->putAttribute(RADIUS_NAS_PORT_TYPE, RADIUS_ETHERNET);
2761
		$racct->putAttribute(RADIUS_NAS_PORT, intval($ruleno), 'integer');
2762
	}
2763

    
2764
	if (!empty($sessionid)) {
2765
		$racct->putAttribute(RADIUS_ACCT_SESSION_ID, $sessionid);
2766
	}
2767

    
2768
	if (!empty($clientip) && is_ipaddr($clientip)) {
2769
		$racct->putAttribute(RADIUS_FRAMED_IP_ADDRESS, $clientip, "addr");
2770
	}
2771
	if (!empty($clientmac)) {
2772
		$racct->putAttribute(RADIUS_CALLING_STATION_ID, mac_format($clientmac));
2773
	}
2774
	if (!empty($nasmac)) {
2775
		$racct->putAttribute(RADIUS_CALLED_STATION_ID, mac_format($nasmac).':'.gethostname());
2776
	}
2777

    
2778
	// Accounting request Stop and Update : send the current data volume
2779
	if (($type === 'stop' || $type === 'update') && is_int($start_time)) {
2780
		$volume = getVolume($clientip);
2781
		$session_time = $stop_time - $start_time;
2782
		$volume['input_bytes_radius'] = remainder($volume['input_bytes']);
2783
		$volume['input_gigawords'] = gigawords($volume['input_bytes']);
2784
		$volume['output_bytes_radius'] = remainder($volume['output_bytes']);
2785
		$volume['output_gigawords'] = gigawords($volume['output_bytes']);
2786

    
2787
		// Volume stuff: Ingress
2788
		$racct->putAttribute(RADIUS_ACCT_INPUT_PACKETS, intval($volume['input_pkts']), "integer");
2789
		$racct->putAttribute(RADIUS_ACCT_INPUT_OCTETS, intval($volume['input_bytes_radius']), "integer");
2790
		// Volume stuff: Outgress
2791
		$racct->putAttribute(RADIUS_ACCT_OUTPUT_PACKETS, intval($volume['output_pkts']), "integer");
2792
		$racct->putAttribute(RADIUS_ACCT_OUTPUT_OCTETS, intval($volume['output_bytes_radius']), "integer");
2793
		$racct->putAttribute(RADIUS_ACCT_SESSION_TIME, intval($session_time), "integer");
2794

    
2795
		$racct->putAttribute(CUSTOM_RADIUS_ACCT_OUTPUT_GIGAWORDS, intval($volume['output_gigawords']), "integer");
2796
		$racct->putAttribute(CUSTOM_RADIUS_ACCT_INPUT_GIGAWORDS, intval($volume['input_gigawords']), "integer");
2797
		// Set session_time
2798
		$racct->session_time = $session_time;
2799
	}
2800

    
2801
	if ($type === 'stop') {
2802
		if (empty($term_cause)) {
2803
			$term_cause = 1;
2804
		}
2805
		$racct->putAttribute(RADIUS_ACCT_TERMINATE_CAUSE, $term_cause);
2806
	}
2807

    
2808
	// Send request
2809
	$result = $racct->send();
2810

    
2811
	if (PEAR::isError($result)) {
2812
		 captiveportal_syslog('RADIUS ACCOUNTING FAILED : '.$racct->getError());
2813
		 $result = null;
2814
	} elseif ($result !== true) {
2815
		$result = false;
2816
	}
2817

    
2818
	$racct->close();
2819
	return $result;
2820
}
2821

    
2822
function captiveportal_isip_logged($clientip) {
2823
	global $g, $cpzone;
2824

    
2825
	/* read in client database */
2826
	$query = "WHERE ip = '{$clientip}'";
2827
	$cpdb = captiveportal_read_db($query);
2828
	foreach ($cpdb as $cpentry) {
2829
		return $cpentry;
2830
	}
2831
}
2832
?>
(6-6/59)