Project

General

Profile

Download (103 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-2022 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
require_once("xmlrpc_client.inc");
37

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

    
45
function get_captive_portal_logo() {
46
	global $config, $g, $cpzone;
47
	$logo_src = "captiveportal-default-logo.png";
48
	// Check if customlogo is set and if the element exists
49
	// Check if the image is in the directory
50
	if (isset($config['captiveportal'][$cpzone]['customlogo']) &&
51
	    is_array($config['captiveportal'][$cpzone]['element']) &&
52
	    !empty($config['captiveportal'][$cpzone]['element'])) {
53
		foreach ($config['captiveportal'][$cpzone]['element'] as $element) {
54
			if (strpos($element['name'], "captiveportal-logo.") !== false) {
55
				if (file_exists("{$g['captiveportal_path']}/{$element['name']}")) {
56
					$logo_src = $element['name'];
57
					break;
58
				}
59
			}
60
		}
61
	}
62
	return $logo_src;
63
}
64

    
65
function get_captive_portal_bg() {
66
	$bg_src = "linear-gradient(135deg, #1475CF, #2B40B5, #1C1275)";
67
	global $config, $g, $cpzone;
68
	// check if custombg is set and if the element exists
69
	if (isset($config['captiveportal'][$cpzone]['custombg']) &&
70
	    is_array($config['captiveportal'][$cpzone]['element']) &&
71
	    !empty($config['captiveportal'][$cpzone]['element'])) { 
72
		foreach ($config['captiveportal'][$cpzone]['element'] as $element) {
73
			if (strpos($element['name'],"captiveportal-background.") !== false) {
74
				if( file_exists("{$g['captiveportal_path']}/{$element['name']}")) {
75
					$bg_src = "url(" . $element['name'] . ")" . "center center no-repeat fixed";
76
					break;
77
				}
78
			}
79
		}
80
	}
81
	return $bg_src;
82
}
83

    
84
function get_default_captive_portal_html() {
85
	global $config, $g, $cpzone;
86

    
87
	$translated_text1 = gettext("User");
88
	$translated_text2 = gettext("Password");
89
	$translated_text3 = gettext("First Authentication Method ");
90
	$translated_text4 = gettext("Second Authentication Method ");
91
	// default images to use.
92
	$logo_src = get_captive_portal_logo();
93
	$bg_src = get_captive_portal_bg();
94
	// bring in terms and conditions
95
	$termsconditions = base64_decode($config['captiveportal'][$cpzone]['termsconditions']);
96
	// if there is no terms and conditions do not require the checkbox to be selected.
97
	$disabled = "";
98
	if ($termsconditions) {
99
		$disabled = "disabled";
100
	}
101
	$htmltext = <<<EOD
102
<!DOCTYPE html>
103
<html>
104

    
105
<head>
106

    
107
  <meta charset="UTF-8">
108
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
109
  <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
110
  <title>Captive Portal Login Page</title>
111
  <style>
112
	  #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;}
113
  </style>
114
</head>
115

    
116
<body>
117
<div id="content">
118
	<div class="login-card">
119
		<img src="{$logo_src}"/><br>
120
 		<h1></h1>
121
		<div id="error-message">
122
			\$PORTAL_MESSAGE\$
123
		</div>
124
	  <form name="login_form" method="post" action="\$PORTAL_ACTION\$">
125
EOD;
126
	if ($config['captiveportal'][$cpzone]['auth_method'] != "none"){
127
		if ($config['captiveportal'][$cpzone]['auth_method'] === 'authserver' && !empty($config['captiveportal'][$cpzone]['auth_server2'])) {
128
			$htmltext .= <<<EOD
129
			<div class="auth_head_div">
130
				<h6 class="auth_head">{$translated_text3}</h6>
131
			</div>
132
			<div class="auth_source">
133

    
134
EOD;
135
		}
136
		$htmltext .=<<<EOD
137
		<input type="text" name="auth_user" placeholder="{$translated_text1}" id="auth_user">
138
		<input type="password" name="auth_pass" placeholder="{$translated_text2}" id="auth_pass">
139
EOD;
140

    
141
		if ($config['captiveportal'][$cpzone]['auth_method'] === 'authserver' && !empty($config['captiveportal'][$cpzone]['auth_server2'])) {
142
			$htmltext .= <<<EOD
143
			</div>
144
			<div class="auth_head_div">
145
				<h6 class="auth_head">{$translated_text4}</h6>
146
			</div>
147
			<div class="auth_source">
148

    
149
			<input type="text" name="auth_user2" placeholder="{$translated_text1}" id="auth_user2">
150
			<input type="password" name="auth_pass2" placeholder="{$translated_text2}" id="auth_pass2">
151
			</div>
152
EOD;
153
		}
154

    
155

    
156
		if (isset($config['voucher'][$cpzone]['enable'])) {
157
			$translated_text = gettext("Voucher Code");
158
			$htmltext .= <<<EOD
159
				<br  /><br  />
160
				<input name="auth_voucher" type="text" placeholder="{$translated_text}" value="#VOUCHER#">
161
EOD;
162
		}
163
	}
164

    
165
if ($termsconditions) {
166
	$htmltext .= <<<EOD
167
		  <div class="login-help">
168
			<ul class="list">
169
				<li class="list__item">
170
				  <label class="label--checkbox">
171
					<input type="checkbox" class="checkbox" onchange="document.getElementById('login').disabled = !this.checked;">
172
					<span>I agree with the <a  rel="noopener" href="#terms" onclick="document.getElementById('terms').style.display = 'block';">terms & conditions</a></span>
173
				  </label>
174
				</li>
175
			</ul>
176
		  </div>
177
EOD;
178
}
179
	$htmltext .= <<<EOD
180

    
181
		<input name="redirurl" type="hidden" value="\$PORTAL_REDIRURL\$">
182
		<input type="submit" name="accept" class="login login-submit" value="Login" id="login" {$disabled}>
183
	  </form>
184
	  <br  />
185
	  <span> <i>Made with &hearts; by</i> <strong>Netgate</strong></span>
186
	</div>
187
	<div id="terms">
188
		<textarea readonly>{$termsconditions}</textarea>
189
	</div>
190
</div>
191
</body>
192
</html>
193

    
194
EOD;
195

    
196
	return $htmltext;
197
}
198

    
199
function captiveportal_load_modules() {
200
	global $config;
201

    
202
	mute_kernel_msgs();
203
	if (!is_module_loaded("ipfw.ko")) {
204
		mwexec("/sbin/kldload ipfw");
205
		/* make sure ipfw is not on pfil hooks */
206
		set_sysctl(array(
207
		    "net.inet.ip.pfil.inbound" => "pf",
208
		    "net.inet6.ip6.pfil.inbound" => "pf",
209
		    "net.inet.ip.pfil.outbound" => "pf",
210
		    "net.inet6.ip6.pfil.outbound" => "pf"
211
		));
212
	}
213
	/* Activate layer2 filtering */
214
	set_sysctl(array(
215
	    "net.link.ether.ipfw" => "1",
216
	    "net.inet.ip.fw.one_pass" => "1",
217
	    "net.inet.ip.fw.tables_max" => "65534"
218
	));
219

    
220
	/* Always load dummynet now that even allowed ip and mac passthrough use it. */
221
	if (!is_module_loaded("dummynet.ko")) {
222
		mwexec("/sbin/kldload dummynet");
223
		set_sysctl(array(
224
		    "net.inet.ip.dummynet.io_fast" => "1",
225
		    "net.inet.ip.dummynet.hash_size" => "256"
226
		));
227
	}
228
	unmute_kernel_msgs();
229
}
230

    
231
function captiveportal_configure() {
232
	global $config, $g, $cpzone, $cpzoneid;
233

    
234
	if (is_array($config['captiveportal'])) {
235
		foreach ($config['captiveportal'] as $cpzone) {
236
			if (isset($cpzone['preservedb'])) {
237
				$keep_online_users = true;
238
				break;
239
			}
240
		}
241
		if (!$keep_online_users) {
242
			/* see https://redmine.pfsense.org/issues/12455 */
243
			unlink_if_exists("{$g['vardb_path']}/captiveportal_online_users");
244
		}
245
		foreach ($config['captiveportal'] as $cpkey => $cp) {
246
			$cpzone = $cpkey;
247
			$cpzoneid = $cp['zoneid'];
248
			captiveportal_configure_zone($cp);
249
		}
250
	}
251
}
252

    
253
function captiveportal_configure_zone($cpcfg) {
254
	global $config, $g, $cpzone, $cpzoneid;
255

    
256
	$captiveportallck = lock("captiveportal{$cpzone}", LOCK_EX);
257

    
258
	if (isset($cpcfg['enable'])) {
259

    
260
		if (platform_booting()) {
261
			echo "Starting captive portal({$cpcfg['zone']})... ";
262
		} else {
263
			captiveportal_syslog("Reconfiguring captive portal({$cpcfg['zone']}).");
264
		}
265

    
266
		/* init ipfw rules */
267
		captiveportal_init_rules();
268

    
269
		/* kill any running minicron */
270
		killbypid("{$g['varrun_path']}/cp_prunedb_{$cpzone}.pid");
271

    
272
		/* initialize minicron interval value */
273
		$croninterval = $cpcfg['croninterval'] ? $cpcfg['croninterval'] : 60;
274

    
275
		/* double check if the $croninterval is numeric and at least 10 seconds. If not we set it to 60 to avoid problems */
276
		if ((!is_numeric($croninterval)) || ($croninterval < 10)) {
277
			$croninterval = 60;
278
		}
279

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

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

    
307
		/* write error page */
308
		if (is_array($cpcfg['page']) && $cpcfg['page']['errtext']) {
309
			$errtext = base64_decode($cpcfg['page']['errtext']);
310
		} else {
311
			/* example page  */
312
			$errtext = get_default_captive_portal_html();
313
		}
314

    
315
		$fd = @fopen("{$g['varetc_path']}/captiveportal-{$cpzone}-error.html", "w");
316
		if ($fd) {
317
			// Special case handling.  Convert so that we can pass this page
318
			// through the PHP interpreter later without clobbering the vars.
319
			$errtext = str_replace("\$PORTAL_ZONE\$", "#PORTAL_ZONE#", $errtext);
320
			$errtext = str_replace("\$PORTAL_REDIRURL\$", "#PORTAL_REDIRURL#", $errtext);
321
			$errtext = str_replace("\$PORTAL_MESSAGE\$", "#PORTAL_MESSAGE#", $errtext);
322
			$errtext = str_replace("\$CLIENT_MAC\$", "#CLIENT_MAC#", $errtext);
323
			$errtext = str_replace("\$CLIENT_IP\$", "#CLIENT_IP#", $errtext);
324
			$errtext = str_replace("\$PORTAL_ACTION\$", "#PORTAL_ACTION#", $errtext);
325
			if ($cpcfg['preauthurl']) {
326
				$errtext = str_replace("\$PORTAL_REDIRURL\$", "{$cpcfg['preauthurl']}", $errtext);
327
				$errtext = str_replace("#PORTAL_REDIRURL#", "{$cpcfg['preauthurl']}", $errtext);
328
			}
329
			fwrite($fd, $errtext);
330
			fclose($fd);
331
		}
332
		unset($errtext);
333

    
334
		/* write logout page */
335
		if (is_array($cpcfg['page']) && $cpcfg['page']['logouttext']) {
336
			$logouttext = base64_decode($cpcfg['page']['logouttext']);
337
		} else {
338
			/* example page */
339
			$translated_text1 = gettext("Redirecting...");
340
			$translated_text2 = gettext("Redirecting to");
341
			$translated_text3 = gettext("Logout");
342
			$translated_text4 = gettext("Click the button below to disconnect");
343
			$logouttext = <<<EOD
344
<html>
345
<head><title>{$translated_text1}</title></head>
346
<body>
347
<span style="font-family: Tahoma, Verdana, Arial, Helvetica, sans-serif; font-size: 11px;">
348
<b>{$translated_text2} <a href="<?=\$my_redirurl;?>"><?=\$my_redirurl;?></a>...</b>
349
</span>
350
<script type="text/javascript">
351
//<![CDATA[
352
LogoutWin = window.open('', 'Logout', 'toolbar=0,scrollbars=0,location=0,statusbar=0,menubar=0,resizable=0,width=256,height=64');
353
if (LogoutWin) {
354
	LogoutWin.document.write('<html>');
355
	LogoutWin.document.write('<head><title>{$translated_text3}</title></head>') ;
356
	LogoutWin.document.write('<body style="background-color:#435370">');
357
	LogoutWin.document.write('<div class="text-center" style="color: #ffffff; font-family: Tahoma, Verdana, Arial, Helvetica, sans-serif; font-size: 11px;">') ;
358
	LogoutWin.document.write('<b>{$translated_text4}</b><p />');
359
	LogoutWin.document.write('<form method="POST" action="<?=\$logouturl;?>">');
360
	LogoutWin.document.write('<input name="logout_id" type="hidden" value="<?=\$sessionid;?>" />');
361
	LogoutWin.document.write('<input name="zone" type="hidden" value="<?=\$cpzone;?>" />');
362
	LogoutWin.document.write('<input name="logout" type="submit" value="{$translated_text3}" />');
363
	LogoutWin.document.write('</form>');
364
	LogoutWin.document.write('</div></body>');
365
	LogoutWin.document.write('</html>');
366
	LogoutWin.document.close();
367
}
368

    
369
document.location.href="<?=\$my_redirurl;?>";
370
//]]>
371
</script>
372
</body>
373
</html>
374

    
375
EOD;
376
		}
377

    
378
		$fd = @fopen("{$g['varetc_path']}/captiveportal-{$cpzone}-logout.html", "w");
379
		if ($fd) {
380
			fwrite($fd, $logouttext);
381
			fclose($fd);
382
		}
383
		unset($logouttext);
384

    
385
		/* write elements */
386
		captiveportal_write_elements();
387

    
388
		/* kill any running CP nginx instances */
389
		killbypid("{$g['varrun_path']}/nginx-{$cpzone}-CaptivePortal.pid", 0.1);
390
		killbypid("{$g['varrun_path']}/nginx-{$cpzone}-CaptivePortal-SSL.pid", 0.1);
391

    
392
		/* start up the webserving daemon */
393
		captiveportal_init_webgui_zone($cpcfg);
394

    
395
		/* Kill any existing prunecaptiveportal processes */
396
		if (file_exists("{$g['varrun_path']}/cp_prunedb_{$cpzone}.pid")) {
397
			killbypid("{$g['varrun_path']}/cp_prunedb_{$cpzone}.pid");
398
		}
399

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

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

    
407
		if (platform_booting()) {
408
			/* send Accounting-On to server */
409
			captiveportal_send_server_accounting('on');
410
			echo "done\n";
411

    
412
			if (isset($cpcfg['preservedb']) ||
413
			    captiveportal_xmlrpc_sync_get_details($syncip, $syncport, $syncuser, $syncpass)) {
414

    
415
				$connected_users = captiveportal_read_db();
416
				if (!empty($connected_users)) {
417
					echo "Reconnecting users to captive portal {$cpcfg['zone']}... ";
418
					foreach ($connected_users as $user) {
419
						captiveportal_reserve_ruleno($user['pipeno']);
420
						$bw_up = intval($user['bw_up']);
421
						$bw_down = intval($user['bw_down']);
422
						$clientip = $user['ip'];
423
						$clientmac = $user['mac'];
424
						$bw_up_pipeno = $user['pipeno'];
425
						$bw_down_pipeno = $user['pipeno'] + 1;
426

    
427
						$rule_entry = "{$clientip}/" . (is_ipaddrv6($clientip) ? "128" : "32");
428
						if (!isset($cpcfg['nomacfilter'])) {
429
							$rule_entry .= ",{$clientmac}";
430
						}
431

    
432
						$_gb = @pfSense_ipfw_pipe("pipe {$bw_up_pipeno} config bw {$bw_up}Kbit/s queue 100 buckets 16");
433
						$_gb = @pfSense_ipfw_pipe("pipe {$bw_down_pipeno} config bw {$bw_down}Kbit/s queue 100 buckets 16");
434
						$_gb = @pfSense_ipfw_table("{$cpzone}_auth_up", IP_FW_TABLE_XADD, "{$rule_entry}", $bw_up_pipeno);
435
						$_gb = @pfSense_ipfw_table("{$cpzone}_auth_down", IP_FW_TABLE_XADD, "{$rule_entry}", $bw_down_pipeno);
436
					}
437
					echo "done\n";
438
				}
439
			} else {
440
				/* reset database on unclean shutdown, see https://redmine.pfsense.org/issues/12355 */
441
				unlink_if_exists("{$g['vardb_path']}/captiveportal{$cpzone}.db");
442
			}
443
		}
444

    
445
	} else {
446
		killbypid("{$g['varrun_path']}/nginx-{$cpzone}-CaptivePortal.pid");
447
		killbypid("{$g['varrun_path']}/nginx-{$cpzone}-CaptivePortal-SSL.pid");
448
		killbypid("{$g['varrun_path']}/cp_prunedb_{$cpzone}.pid");
449
		@unlink("{$g['varetc_path']}/captiveportal_{$cpzone}.html");
450
		@unlink("{$g['varetc_path']}/captiveportal-{$cpzone}-error.html");
451
		@unlink("{$g['varetc_path']}/captiveportal-{$cpzone}-logout.html");
452

    
453
		captiveportal_radius_stop_all(10); // NAS-Request
454

    
455
		captiveportal_filterdns_configure();
456

    
457
		/* remove old information */
458
		unlink_if_exists("{$g['vardb_path']}/captiveportal{$cpzone}.db");
459
		unlink_if_exists("{$g['vardb_path']}/captiveportal_radius_{$cpzone}.db");
460
		unlink_if_exists("{$g['vardb_path']}/captiveportal_{$cpzone}.rules");
461
		/* Release allocated pipes for this zone */
462
		$pipes_to_remove = captiveportal_free_dnrules();
463

    
464
		captiveportal_delete_rules($pipes_to_remove);
465

    
466
		if (empty($config['captiveportal'])) {
467
			set_single_sysctl("net.link.ether.ipfw", "0");
468
		} else {
469
			/* Deactivate ipfw(4) if not needed */
470
			$cpactive = false;
471
			if (is_array($config['captiveportal'])) {
472
				foreach ($config['captiveportal'] as $cpkey => $cp) {
473
					if (isset($cp['enable'])) {
474
						$cpactive = true;
475
						break;
476
					}
477
				}
478
			}
479
			if ($cpactive === false) {
480
				set_single_sysctl("net.link.ether.ipfw", "0");
481
			}
482
		}
483
	}
484

    
485
	unlock($captiveportallck);
486

    
487
	return 0;
488
}
489

    
490
function captiveportal_init_webgui() {
491
	global $config, $cpzone;
492

    
493
	if (is_array($config['captiveportal'])) {
494
		foreach ($config['captiveportal'] as $cpkey => $cp) {
495
			$cpzone = $cpkey;
496
			captiveportal_init_webgui_zone($cp);
497
		}
498
	}
499
}
500

    
501
function captiveportal_init_webgui_zonename($zone) {
502
	global $config, $cpzone;
503

    
504
	if (isset($config['captiveportal'][$zone])) {
505
		$cpzone = $zone;
506
		captiveportal_init_webgui_zone($config['captiveportal'][$zone]);
507
	}
508
}
509

    
510
function captiveportal_init_webgui_zone($cpcfg) {
511
	global $g, $config, $cpzone;
512

    
513
	if (!isset($cpcfg['enable'])) {
514
		return;
515
	}
516

    
517
	if (isset($cpcfg['httpslogin'])) {
518
		$cert = lookup_cert($cpcfg['certref']);
519
		$crt = base64_decode($cert['crt']);
520
		$key = base64_decode($cert['prv']);
521
		$ca = ca_chain($cert);
522

    
523
		/* generate nginx configuration */
524
		if (!empty($cpcfg['listenporthttps'])) {
525
			$listenporthttps = $cpcfg['listenporthttps'];
526
		} else {
527
			$listenporthttps = 8001 + $cpcfg['zoneid'];
528
		}
529
		system_generate_nginx_config("{$g['varetc_path']}/nginx-{$cpzone}-CaptivePortal-SSL.conf",
530
			$crt, $key, $ca, "nginx-{$cpzone}-CaptivePortal-SSL.pid", $listenporthttps, "/usr/local/captiveportal",
531
			"cert-{$cpzone}-portal.pem", "ca-{$cpzone}-portal.pem", $cpzone, false);
532
	}
533

    
534
	/* generate nginx configuration */
535
	if (!empty($cpcfg['listenporthttp'])) {
536
		$listenporthttp = $cpcfg['listenporthttp'];
537
	} else {
538
		$listenporthttp = 8000 + $cpcfg['zoneid'];
539
	}
540
	system_generate_nginx_config("{$g['varetc_path']}/nginx-{$cpzone}-CaptivePortal.conf",
541
		"", "", "", "nginx-{$cpzone}-CaptivePortal.pid", $listenporthttp, "/usr/local/captiveportal",
542
		"", "", $cpzone, false);
543

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

    
548
	/* fire up https instance */
549
	if (isset($cpcfg['httpslogin'])) {
550
		@unlink("{$g['varrun']}/nginx-{$cpzone}-CaptivePortal-SSL.pid");
551
		$res = mwexec("/usr/local/sbin/nginx -c {$g['varetc_path']}/nginx-{$cpzone}-CaptivePortal-SSL.conf");
552
	}
553
}
554

    
555
function captiveportal_init_rules_byinterface($interface) {
556
	global $cpzone, $cpzoneid, $config;
557

    
558
	if (!is_array($config['captiveportal'])) {
559
		return;
560
	}
561

    
562
	foreach ($config['captiveportal'] as $cpkey => $cp) {
563
		$cpzone = $cpkey;
564
		$cpzoneid = $cp['zoneid'];
565
		$cpinterfaces = explode(",", $cp['interface']);
566
		if (in_array($interface, $cpinterfaces)) {
567
			captiveportal_init_rules();
568
			break;
569
		}
570
	}
571
}
572

    
573
/* Create basic rules used by all zones */
574
function captiveportal_init_general_rules($flush = false) {
575
	global $g;
576

    
577
	$flush_rule = '';
578
	if ($flush) {
579
		$flush_rule = 'flush';
580
	}
581

    
582
	/* Already loaded */
583
	if (!$flush && (mwexec("/sbin/ipfw list 1000", true) == 0)) {
584
		return;
585
	}
586

    
587
	$cprules = <<<EOD
588
{$flush_rule}
589
# Allow all layer3 so the chain is only evaluated once at layer 2
590
add 999 allow all from any to any not layer2
591

    
592
# Table with interfaces that have CP enabled
593
table cp_ifaces create type iface valtype skipto
594

    
595
# Redirect each CP interface to its specific rule
596
add 1000 skipto tablearg all from any to any via table(cp_ifaces)
597

    
598
# This interface has no cp zone configured
599
add 1100 allow all from any to any
600

    
601
# block everything else
602
add 65534 deny all from any to any
603
EOD;
604

    
605
	/* load rules */
606
	file_put_contents("{$g['tmp_path']}/ipfw.cp.rules", $cprules);
607
	mwexec("/sbin/ipfw -q {$g['tmp_path']}/ipfw.cp.rules", true);
608
	@unlink("{$g['tmp_path']}/ipfw_{$cpzone}.cp.rules");
609
	unset($cprules);
610
}
611

    
612
/* Create a string with ipfw rule and increase rulenum */
613
function captiveportal_create_ipfw_rule($cmd, &$rulenum, $args) {
614
	$rule = "{$cmd} {$rulenum} {$args}\n";
615
	$rulenum++;
616

    
617
	return $rule;
618
}
619

    
620
/* Return first rule number for a cp zone */
621
function captiveportal_ipfw_ruleno($id) {
622
	global $g;
623

    
624
	return 2000 + $id * $g['captiveportal_rules_interval'];
625
}
626

    
627
/* reinit will disconnect all users, be careful! */
628
function captiveportal_init_rules($reinit = false) {
629
	global $config, $g, $cpzone;
630

    
631
	if (!isset($config['captiveportal'][$cpzone]['enable'])) {
632
		return;
633
	}
634

    
635
	captiveportal_load_modules();
636
	captiveportal_init_general_rules();
637

    
638
	/* Cleanup so nothing is leaked */
639
	captiveportal_free_dnrules(2000, 64500, false, $reinit);
640
	unlink_if_exists("{$g['vardb_path']}/captiveportal_{$cpzone}.rules");
641

    
642
	$cpzoneid = $config['captiveportal'][$cpzone]['zoneid'];
643
	$skipto = captiveportal_ipfw_ruleno($cpzoneid);
644

    
645
	$cprules = '';
646

    
647
	$cpips = array();
648
	$ifaces = get_configured_interface_list();
649
	$cpinterfaces = explode(",", $config['captiveportal'][$cpzone]['interface']);
650
	$firsttime = 0;
651
	foreach ($cpinterfaces as $cpifgrp) {
652
		if (!isset($ifaces[$cpifgrp])) {
653
			continue;
654
		}
655
		$tmpif = get_real_interface($cpifgrp);
656
		if (empty($tmpif)) {
657
			continue;
658
		}
659

    
660
		$cpipm = get_interface_ip($cpifgrp);
661

    
662
		if (!is_ipaddr($cpipm)) {
663
			continue;
664
		}
665

    
666
		$cpips[] = $cpipm;
667
		if (is_array($config['virtualip']['vip'])) {
668
			foreach ($config['virtualip']['vip'] as $vip) {
669
				if (($vip['interface'] == $cpifgrp) &&
670
				    (($vip['mode'] == "carp") ||
671
				    ($vip['mode'] == "ipalias"))) {
672
					$cpips[] = $vip['subnet'];
673
				}
674
			}
675
		}
676

    
677
		$cprules .= "table cp_ifaces add {$tmpif} {$skipto}\n";
678
	}
679
	if (count($cpips) > 0) {
680
		$cpactive = true;
681
	} else {
682
		return false;
683
	}
684

    
685
	$captiveportallck = try_lock("captiveportal{$cpzone}", 0);
686

    
687
	$tables = captiveportal_get_ipfw_table_names();
688

    
689
	$rulenum = $skipto;
690
	$cprules .= "table {$cpzone}_pipe_mac create type mac valtype pipe\n";
691
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
692
	    "pipe tablearg MAC table({$cpzone}_pipe_mac)");
693
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
694
	    "allow pfsync from any to any");
695
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
696
	    "allow carp from any to any\n");
697
	$cprules .= "# layer 2: pass ARP\n";
698
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
699
	    "pass layer2 mac-type arp,rarp");
700
	$cprules .= "# pfsense requires for WPA\n";
701
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
702
	    "pass layer2 mac-type 0x888e,0x88c7");
703
	$cprules .= "# PPP Over Ethernet Session Stage/Discovery Stage\n";
704
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
705
	    "pass layer2 mac-type 0x8863,0x8864\n");
706
	$cprules .= "# layer 2: block anything else non-IP(v4/v6)\n";
707
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
708
	    "deny layer2 not mac-type ip,ipv6");
709

    
710
	/* These tables contain host ips */
711
	$cprules .= "table {$cpzone}_host_ips create type addr\n";
712
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
713
	    "pass ip from any to table({$cpzone}_host_ips) in");
714
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
715
	    "pass ip from table({$cpzone}_host_ips) to any out");
716
	foreach ($cpips as $cpip) {
717
		$cprules .= "table {$cpzone}_host_ips add {$cpip}\n";
718
	}
719
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
720
	    "pass ip from any to 255.255.255.255 in");
721
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
722
	    "pass ip from 255.255.255.255 to any out");
723

    
724
	/* Allowed ips */
725
	$cprules .= "table {$cpzone}_allowed_up create type addr valtype pipe\n";
726
	$cprules .= "table {$cpzone}_allowed_down create type addr valtype pipe\n";
727
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
728
	    "pipe tablearg ip from table({$cpzone}_allowed_up) to any in");
729
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
730
	    "pipe tablearg ip from any to table({$cpzone}_allowed_down) in");
731
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
732
	    "pipe tablearg ip from table({$cpzone}_allowed_up) to any out");
733
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
734
	    "pipe tablearg ip from any to table({$cpzone}_allowed_down) out");
735

    
736
	/* Authenticated users rules. */
737
	if (!in_array("{$cpzone}_auth_up", $tables)) {
738
		$cprules .= "table {$cpzone}_auth_up create type addr valtype pipe\n";
739
	}
740
	if (!in_array("{$cpzone}_auth_down", $tables)) {
741
		$cprules .= "table {$cpzone}_auth_down create type addr valtype pipe\n";
742
	}
743
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
744
	    "pipe tablearg ip from table({$cpzone}_auth_up) to any in");
745
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
746
	    "pipe tablearg ip from any to table({$cpzone}_auth_down) out");
747

    
748
	if (!empty($config['captiveportal'][$cpzone]['listenporthttp'])) {
749
		$listenporthttp = $config['captiveportal'][$cpzone]['listenporthttp'];
750
	} else {
751
		$listenporthttp = 8000 + $cpzoneid;
752
	}
753

    
754
	if (isset($config['captiveportal'][$cpzone]['httpslogin'])) {
755
		if (!empty($config['captiveportal'][$cpzone]['listenporthttps'])) {
756
			$listenporthttps = $config['captiveportal'][$cpzone]['listenporthttps'];
757
		} else {
758
			$listenporthttps = 8001 + $cpzoneid;
759
		}
760
		if (!isset($config['captiveportal'][$cpzone]['nohttpsforwards'])) {
761
			$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
762
			    "fwd 127.0.0.1,{$listenporthttps} tcp from any to any dst-port 443 in");
763
		}
764
	}
765

    
766
	$cprules .= "# redirect non-authenticated clients to captive portal\n";
767
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
768
	    "fwd 127.0.0.1,{$listenporthttp} tcp from any to any dst-port 80 in");
769
	$cprules .= "# let the responses from the captive portal web server back out\n";
770
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
771
	    "pass tcp from any to any out");
772
	$cprules .= "# This CP zone is over, skip to last rule\n";
773
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
774
	    "skipto 65534 all from any to any");
775

    
776
	/* generate passthru mac database */
777
	$cprules .= captiveportal_passthrumac_configure(true);
778
	$cprules .= "\n";
779

    
780
	/* allowed ipfw rules to make allowed ip work */
781
	$cprules .= captiveportal_allowedip_configure();
782

    
783
	/* allowed ipfw rules to make allowed hostnames work */
784
	$cprules .= captiveportal_allowedhostname_configure();
785

    
786
	/* load rules */
787
	captiveportal_delete_rules(array(), $reinit);
788
	file_put_contents("{$g['tmp_path']}/ipfw_{$cpzone}.cp.rules", $cprules);
789
	mwexec("/sbin/ipfw -q {$g['tmp_path']}/ipfw_{$cpzone}.cp.rules", true);
790
	@unlink("{$g['tmp_path']}/ipfw_{$cpzone}.cp.rules");
791
	unset($cprules);
792

    
793
	captiveportal_filterdns_configure();
794

    
795
	if ($captiveportallck) {
796
		unlock($captiveportallck);
797
	}
798
}
799

    
800
/* Delete all rules related to specific cpzone */
801
function captiveportal_delete_rules($pipes_to_remove = array(), $clear_auth_rules = true) {
802
	global $g, $config, $cpzone;
803

    
804
	$cpzoneid = $config['captiveportal'][$cpzone]['zoneid'];
805

    
806
	$skipto1 = captiveportal_ipfw_ruleno($cpzoneid);
807
	$skipto2 = $skipto1 + $g['captiveportal_rules_interval'];
808

    
809
	$cp_ifaces = pfSense_ipfw_table_list("cp_ifaces");
810
	if (is_array($cp_ifaces)) {
811
		foreach ($cp_ifaces as $cp_iface) {
812
			if (empty($cp_iface['skipto']) ||
813
			    $cp_iface['skipto'] != $skipto1) {
814
				continue;
815
			}
816

    
817
			pfSense_ipfw_table("cp_ifaces", IP_FW_TABLE_XDEL,
818
			    $cp_iface['iface']);
819
		}
820
	}
821

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

    
824
	$tables = captiveportal_get_ipfw_table_names();
825

    
826
	$delrules = "";
827
	foreach ($tables as $table) {
828
		if (!$clear_auth_rules && substr($table, 0, strlen($cpzone . '_auth')) == $cpzone . '_auth') {
829
			continue;
830
		} else {
831
			$delrules .= "table {$table} destroy\n";
832
		}
833
	}
834

    
835
	foreach ($pipes_to_remove as $pipeno) {
836
		$delrules .= "pipe delete {$pipeno}\n";
837
	}
838

    
839
	if (empty($delrules)) {
840
		return;
841
	}
842

    
843
	file_put_contents("{$g['tmp_path']}/ipfw_{$cpzone}.deltable.rules", $delrules);
844
	mwexec("/sbin/ipfw -q {$g['tmp_path']}/ipfw_{$cpzone}.deltable.rules", true);
845
	@unlink("{$g['tmp_path']}/ipfw_{$cpzone}.deltable.rules");
846
}
847

    
848
/*
849
 * Remove clients that have been around for longer than the specified amount of time
850
 * db file structure:
851
 * timestamp,ipfw_rule_no,clientip,clientmac,username,sessionid,password,session_timeout,idle_timeout,session_terminate_time,interim_interval,traffic_quota,auth_method,context
852
 * (password is in Base64 and only saved when reauthentication is enabled)
853
 */
854
function captiveportal_prune_old() {
855
	global $g, $config, $cpzone, $cpzoneid;
856

    
857
	if (empty($cpzone)) {
858
		return;
859
	}
860

    
861
	$cpcfg = $config['captiveportal'][$cpzone];
862
	$vcpcfg = $config['voucher'][$cpzone];
863

    
864
	/* check for expired entries */
865
	$idletimeout = 0;
866
	$timeout = 0;
867
	if (!empty($cpcfg['timeout']) && is_numeric($cpcfg['timeout'])) {
868
		$timeout = $cpcfg['timeout'] * 60;
869
	}
870

    
871
	if (!empty($cpcfg['idletimeout']) && is_numeric($cpcfg['idletimeout'])) {
872
		$idletimeout = $cpcfg['idletimeout'] * 60;
873
	}
874

    
875
	/* check for entries exceeding their traffic quota */
876
	$trafficquota = 0;
877
	if (!empty($cpcfg['trafficquota']) && is_numeric($cpcfg['trafficquota'])) {
878
		$trafficquota = $cpcfg['trafficquota'] * 1048576;
879
	}
880

    
881
	/* Is there any job to do? If we are in High Availability sync, are we in backup mode ? */
882
	if ((!$timeout && !$idletimeout && !$trafficquota && !isset($cpcfg['reauthenticate']) &&
883
	    !isset($cpcfg['radiussession_timeout']) && !isset($cpcfg['radiustraffic_quota']) &&
884
	    !isset($vcpcfg['enable']) && !isset($cpcfg['radacct_enable'])) ||
885
	    captiveportal_ha_is_node_in_backup_mode($cpzone)) {
886
		return;
887
	}
888

    
889

    
890
	/* Read database */
891
	/* NOTE: while this can be simplified in non radius case keep as is for now */
892
	$cpdb = captiveportal_read_db();
893

    
894
	$unsetindexes = array();
895
	$voucher_needs_sync = false;
896
	/*
897
	 * Snapshot the time here to use for calculation to speed up the process.
898
	 * If something is missed next run will catch it!
899
	 */
900
	$pruning_time = time();
901
	foreach ($cpdb as $cpentry) {
902
		$stop_time = $pruning_time;
903

    
904
		$timedout = false;
905
		$term_cause = 1;
906
		/* hard timeout or session_timeout from radius if enabled */
907
		if (isset($cpcfg['radiussession_timeout'])) {
908
			$utimeout = (is_numeric($cpentry[7])) ? $cpentry[7] : $timeout;
909
		} else {
910
			$utimeout = $timeout;
911
		}
912
		if ($utimeout) {
913
			if (($pruning_time - $cpentry[0]) >= $utimeout) {
914
				$timedout = true;
915
				$term_cause = 5; // Session-Timeout
916
				$logout_cause = 'SESSION TIMEOUT';
917
			}
918
		}
919

    
920
		/* Session-Terminate-Time */
921
		if (!$timedout && !empty($cpentry[9])) {
922
			if ($pruning_time >= $cpentry[9]) {
923
				$timedout = true;
924
				$term_cause = 5; // Session-Timeout
925
				$logout_cause = 'SESSION TIMEOUT';
926
			}
927
		}
928

    
929
		/* check if an idle_timeout has been set and if its set change the idletimeout to this value */
930
		$uidletimeout = (is_numeric($cpentry[8])) ? $cpentry[8] : $idletimeout;
931
		/* if an idle timeout is specified, get last activity timestamp from ipfw */
932
		if (!$timedout && $uidletimeout > 0) {
933
			$lastact = captiveportal_get_last_activity($cpentry[2]);
934
			/*	If the user has logged on but not sent any traffic they will never be logged out.
935
			 *	We "fix" this by setting lastact to the login timestamp.
936
			 */
937
			$lastact = $lastact ? $lastact : $cpentry[0];
938
			if ($lastact && (($pruning_time - $lastact) >= $uidletimeout)) {
939
				$timedout = true;
940
				$term_cause = 4; // Idle-Timeout
941
				$logout_cause = 'IDLE TIMEOUT';
942
				if (!isset($config['captiveportal'][$cpzone]['includeidletime'])) {
943
					$stop_time = $lastact;
944
				}
945
			}
946
		}
947

    
948
		/* if vouchers are configured, activate session timeouts */
949
		if (!$timedout && isset($vcpcfg['enable']) && !empty($cpentry[7])) {
950
			if ($pruning_time >= ($cpentry[0] + $cpentry[7])) {
951
				$timedout = true;
952
				$term_cause = 5; // Session-Timeout
953
				$logout_cause = 'SESSION TIMEOUT';
954
				$voucher_needs_sync = true;
955
			}
956
		}
957

    
958
		/* traffic quota, value retrieved from the radius attribute if the option is enabled */
959
		if (isset($cpcfg['radiustraffic_quota'])) {
960
			$utrafficquota = (is_numeric($cpentry[11])) ? $cpentry[11] : $trafficquota;
961
		} else {
962
			$utrafficquota = $trafficquota;
963
		}
964
		if (!$timedout && $utrafficquota > 0) {
965
			$volume = getVolume($cpentry[2], $cpentry[3]);
966
			if (($volume['input_bytes'] + $volume['output_bytes']) > $utrafficquota) {
967
				$timedout = true;
968
				$term_cause = 10; // NAS-Request
969
				$logout_cause = 'QUOTA EXCEEDED';
970
			}
971
		}
972

    
973
		if ($timedout) {
974
			captiveportal_disconnect($cpentry, $term_cause, $stop_time);
975
			captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], $logout_cause);
976
			$unsetindexes[] = $cpentry[5];
977
		}
978

    
979
		/* do periodic reauthentication? For Radius servers, send accounting updates? */
980
		if (!$timedout) {
981
			//Radius servers : send accounting
982
			if (isset($cpcfg['radacct_enable']) && $cpentry['authmethod'] === 'radius') {
983
				if (substr($cpcfg['reauthenticateacct'], 0, 9) === "stopstart") {
984
					/* stop and restart accounting */
985
					if ($cpcfg['reauthenticateacct'] === "stopstartfreeradius") {
986
						$rastart_time = 0;
987
						$rastop_time = 60;
988
					} else {
989
						$rastart_time = $cpentry[0];
990
						$rastop_time = time();
991
					}
992
					captiveportal_send_server_accounting('stop',
993
						$cpentry[1], // ruleno
994
						$cpentry[4], // username
995
						$cpentry[2], // clientip
996
						$cpentry[3], // clientmac
997
						$cpentry[5], // sessionid
998
						$rastart_time, // start time
999
						$rastop_time, // Stop Time
1000
						10); // NAS Request
1001
					$clientsn = (is_ipaddrv6($cpentry[2])) ? 128 : 32;
1002
					pfSense_ipfw_table_zerocnt("{$cpzone}_auth_up", "{$cpentry[2]}/{$clientsn}");
1003
					pfSense_ipfw_table_zerocnt("{$cpzone}_auth_down", "{$cpentry[2]}/{$clientsn}");
1004
					if ($cpcfg['reauthenticateacct'] == "stopstartfreeradius") {
1005
						/* Need to pause here or the FreeRADIUS server gets confused about packet ordering. */
1006
						sleep(1);
1007
					}
1008
					captiveportal_send_server_accounting('start',
1009
						$cpentry[1], // ruleno
1010
						$cpentry[4], // username
1011
						$cpentry[2], // clientip
1012
						$cpentry[3], // clientmac
1013
						$cpentry[5]); // sessionid
1014
				} else if ($cpcfg['reauthenticateacct'] == "interimupdate") {
1015
					$session_time = $pruning_time - $cpentry[0];
1016
					if (!empty($cpentry[10]) && $cpentry[10] > 60) {
1017
						$interval = $cpentry[10];
1018
					} else {
1019
						$interval = 0;
1020
					}
1021
					$past_interval_min = ($session_time > $interval);
1022
					if ($interval != 0) {
1023
						$within_interval = ($session_time % $interval >= 0 && $session_time % $interval <= 59);
1024
					}
1025
					if ($interval === 0 || ($interval > 0 && $past_interval_min && $within_interval)) {
1026
					captiveportal_send_server_accounting('update',
1027
						$cpentry[1], // ruleno
1028
						$cpentry[4], // username
1029
						$cpentry[2], // clientip
1030
						$cpentry[3], // clientmac
1031
						$cpentry[5], // sessionid
1032
						$cpentry[0]); // start time
1033
					}
1034
				}
1035
			}
1036

    
1037
			/* check this user again */
1038
			if (isset($cpcfg['reauthenticate']) && $cpentry['context'] !== 'voucher') {
1039
				$auth_result = captiveportal_authenticate_user(
1040
					$cpentry[4], // username
1041
					base64_decode($cpentry[6]), // password
1042
					$cpentry[3], // clientmac
1043
					$cpentry[2], // clientip
1044
					$cpentry[1], // ruleno
1045
					$cpentry['context']); // context
1046
				if ($auth_result['result'] === false) {
1047
					captiveportal_disconnect($cpentry, 17);
1048
					captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "DISCONNECT - REAUTHENTICATION FAILED", $auth_list['reply_message']);
1049
					$unsetindexes[] = $cpentry[5];
1050
				} else if ($auth_result['result'] === true) {
1051
					if ($cpentry['authmethod'] !== $auth_result['auth_method']) {
1052
						// if the user got authenticated against another server type:  we update the database
1053
						if (!empty($cpentry[5])) {
1054
							captiveportal_update_entry($cpentry['sessionid'], $auth_result['auth_method'], 'authmethod');
1055
							captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CHANGED AUTHENTICATION SERVER", $auth_list['reply_message']);
1056
						}
1057
						// User was logged on a RADIUS server, but is now logged in by another server type : we send an accounting Stop
1058
						if (isset($config['captiveportal'][$cpzone]['radacct_enable']) && $cpentry['authmethod'] == 'radius') {
1059
							if ($cpcfg['reauthenticateacct'] === "stopstartfreeradius") {
1060
								$rastart_time = 0;
1061
								$rastop_time = 60;
1062
							} else {
1063
								$rastart_time = $cpentry[0];
1064
								$rastop_time = time();
1065
							}
1066
							captiveportal_send_server_accounting('stop',
1067
								$cpentry[1], // ruleno
1068
								$cpentry[4], // username
1069
								$cpentry[2], // clientip
1070
								$cpentry[3], // clientmac
1071
								$cpentry[5], // sessionid
1072
								$rastart_time, // start time
1073
								$rastop_time, // Stop Time
1074
								3); // Lost Service
1075
						// User was logged on a non-RADIUS Server but is now logged in by a RADIUS server : we send an accounting Start
1076
						} else if(isset($config['captiveportal'][$cpzone]['radacct_enable']) && $auth_result['auth_method'] === 'radius') {
1077
							captiveportal_send_server_accounting('start',
1078
								$cpentry[1], // ruleno
1079
								$cpentry[4], // username
1080
								$cpentry[2], // clientip
1081
								$cpentry[3], // clientmac
1082
								$cpentry[5], // sessionid
1083
								$cpentry[0]); // start_time
1084
						}
1085
					}
1086
					captiveportal_reapply_attributes($cpentry, $auth_result['attributes']);
1087
				}
1088
			}
1089
		}
1090
	}
1091
	unset($cpdb);
1092

    
1093
	captiveportal_prune_old_automac();
1094

    
1095
	if ($voucher_needs_sync == true) {
1096
		/* perform in-use vouchers expiration using check_reload_status */
1097
		send_event("service sync vouchers");
1098
	}
1099

    
1100
	/* write database */
1101
	if (!empty($unsetindexes)) {
1102
		captiveportal_remove_entries($unsetindexes);
1103
	}
1104
}
1105

    
1106
function captiveportal_prune_old_automac() {
1107
	global $g, $config, $cpzone, $cpzoneid;
1108

    
1109
	if (is_array($config['captiveportal'][$cpzone]['passthrumac']) &&
1110
	    isset($config['captiveportal'][$cpzone]['passthrumacadd'])) {
1111
		$tmpvoucherdb = array();
1112
		$macrules = "";
1113
		$writecfg = false;
1114
		foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $eid => $emac) {
1115
			if ($emac['logintype'] != "voucher") {
1116
				continue;
1117
			}
1118
			if (isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) {
1119
				if (isset($tmpvoucherdb[$emac['username']])) {
1120
					$temac = $config['captiveportal'][$cpzone]['passthrumac'][$tmpvoucherdb[$emac['username']]];
1121
					$pipeno = captiveportal_get_dn_passthru_ruleno($temac['mac']);
1122
					if ($pipeno) {
1123
						captiveportal_free_dn_ruleno($pipeno);
1124
						$macrules .= "table {$cpzone}_pipe_mac delete any,{$temac['mac']}\n";
1125
						$macrules .= "table {$cpzone}_pipe_mac delete {$temac['mac']},any\n";
1126
						$macrules .= "pipe delete {$pipeno}\n";
1127
						++$pipeno;
1128
						$macrules .= "pipe delete {$pipeno}\n";
1129
					}
1130
					$writecfg = true;
1131
					captiveportal_logportalauth($temac['username'], $temac['mac'],
1132
					    $temac['ip'], "DUPLICATE {$temac['username']} LOGIN - TERMINATING OLD SESSION");
1133
					unset($config['captiveportal'][$cpzone]['passthrumac'][$tmpvoucherdb[$emac['username']]]);
1134
				}
1135
				$tmpvoucherdb[$emac['username']] = $eid;
1136
			}
1137
		}
1138
		unset($tmpvoucherdb);
1139
		if (!empty($macrules)) {
1140
			@file_put_contents("{$g['tmp_path']}/macentry.prunerules.tmp", $macrules);
1141
			unset($macrules);
1142
			mwexec("/sbin/ipfw -q {$g['tmp_path']}/macentry.prunerules.tmp");
1143
		}
1144
		if ($writecfg === true) {
1145
			write_config("Prune session for auto-added macs");
1146
		}
1147
	}
1148
}
1149

    
1150
/* remove a single client according to the DB entry */
1151
function captiveportal_disconnect($dbent, $term_cause = 1, $stop_time = null, $carp_loop = false) {
1152
	global $g, $config, $cpzone, $cpzoneid;
1153

    
1154
	$stop_time = (empty($stop_time)) ? time() : $stop_time;
1155

    
1156
	/* this client needs to be deleted - remove ipfw rules */
1157
	if (isset($config['captiveportal'][$cpzone]['radacct_enable']) && $dbent['authmethod'] == 'radius') {
1158
		if ($config['captiveportal'][$cpzone]['reauthenticateacct'] == "stopstartfreeradius") {
1159
			/*
1160
			 * Interim updates are on so the session time must be
1161
			 * reported as the elapsed time since the previous
1162
			 * interim update.
1163
			 */
1164
			$session_time = ($stop_time - $dbent[0]) % 60;
1165
			$start_time = $stop_time - $session_time;
1166
		} else {
1167
			$start_time = $dbent[0];
1168
		}
1169
		captiveportal_send_server_accounting('stop',
1170
			$dbent[1], // ruleno
1171
			$dbent[4], // username
1172
			$dbent[2], // clientip
1173
			$dbent[3], // clientmac
1174
			$dbent[5], // sessionid
1175
			$start_time, // start time
1176
			$stop_time, // stop time
1177
			$term_cause); // Acct-Terminate-Cause
1178
	}
1179

    
1180
	if (is_ipaddr($dbent[2])) {
1181
		/*
1182
		 * Delete client's ip entry from tables auth_up and auth_down.
1183
		 * It's not necessary to explicit specify mac address here
1184
		 */
1185
		$cpsession = captiveportal_isip_logged($dbent[2]);
1186
		if (!empty($cpsession)) {
1187
			$clientsn = (is_ipaddrv6($dbent[2])) ? 128 : 32;
1188
			pfSense_ipfw_table("{$cpzone}_auth_up",
1189
			    IP_FW_TABLE_XDEL, "{$dbent[2]}/{$clientsn}");
1190
			pfSense_ipfw_table("{$cpzone}_auth_down",
1191
			    IP_FW_TABLE_XDEL, "{$dbent[2]}/{$clientsn}");
1192
		}
1193
		/* XXX: Redundant?! Ensure all pf(4) states are killed. */
1194
		$_gb = @pfSense_kill_states(utf8_encode($dbent[2]));
1195
		$_gb = @pfSense_kill_srcstates(utf8_encode($dbent[2]));
1196
	}
1197

    
1198
	/*
1199
	 * These are the pipe numbers we use to control traffic shaping for
1200
	 * each logged in user via captive portal
1201
	 * We could get an error if the pipe doesn't exist but everything
1202
	 * should still be fine
1203
	 */
1204
	if (!empty($dbent[1])) {
1205
		/*
1206
		 * Call captiveportal_free_dnrules() in dry_run mode to verify
1207
		 * if there are pipes to be removed and prevent the attempt to
1208
		 * delete invalid pipes
1209
		 */
1210
		$removed_pipes = captiveportal_free_dnrules($dbent[1],
1211
		    $dbent[1]+1, true);
1212

    
1213
		if (!empty($removed_pipes)) {
1214
			$_gb = @pfSense_ipfw_pipe("pipe delete {$dbent[1]}");
1215
			$_gb = @pfSense_ipfw_pipe("pipe delete " .
1216
			    ($dbent[1]+1));
1217

    
1218
			/*
1219
			 * Release the ruleno so it can be reallocated to new
1220
			 * clients
1221
			 */
1222
			captiveportal_free_dn_ruleno($dbent[1]);
1223
		}
1224
	}
1225

    
1226
	// XMLRPC Call over to the backup node if necessary
1227
	if (captiveportal_xmlrpc_sync_get_details($syncip, $syncport,
1228
	    $syncuser, $syncpass, $carp_loop)) {
1229
		$rpc_client = new pfsense_xmlrpc_client();
1230
		$rpc_client->setConnectionData($syncip, $syncport, $syncuser, $syncpass);
1231
		$rpc_client->set_noticefile("CaptivePortalUserSync");
1232
		$arguments = array(
1233
			'sessionid' => $dbent[5],
1234
			'term_cause' => $term_cause,
1235
			'stop_time' => $stop_time
1236
		);
1237

    
1238
		$rpc_client->xmlrpc_method('captive_portal_sync',
1239
			array(
1240
				'op' => 'disconnect_user',
1241
				'zone' => $cpzone,
1242
				'session' => base64_encode(serialize($arguments))
1243
			)
1244
		);
1245
	}
1246
	return true;
1247
}
1248

    
1249
/* remove a single client by sessionid */
1250
function captiveportal_disconnect_client($sessionid, $term_cause = 1, $logoutReason = "LOGOUT") {
1251
	global $g, $config;
1252

    
1253
	$sessionid = SQLite3::escapeString($sessionid);
1254
	/* read database */
1255
	$result = captiveportal_read_db("WHERE sessionid = '{$sessionid}'");
1256

    
1257
	/* find entry */
1258
	if (!empty($result)) {
1259

    
1260
		foreach ($result as $cpentry) {
1261
			captiveportal_disconnect($cpentry, $term_cause);
1262
			captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "DISCONNECT");
1263
		}
1264
		captiveportal_remove_entries(array($sessionid));
1265
		unset($result);
1266
	}
1267
}
1268

    
1269
/* remove all clients */
1270
function captiveportal_disconnect_all($term_cause = 6, $logoutReason = "DISCONNECT", $carp_loop = false) {
1271
	global $g, $config, $cpzone, $cpzoneid;
1272

    
1273
	if (captiveportal_xmlrpc_sync_get_details($syncip, $syncport, $syncuser, $syncpass, $carp_loop)) {
1274
		$rpc_client = new pfsense_xmlrpc_client();
1275
		$rpc_client->setConnectionData($syncip, $syncport, $syncuser, $syncpass);
1276
		$rpc_client->set_noticefile("CaptivePortalUserSync");
1277
		$arguments = array(
1278
			'term_cause' => $term_cause,
1279
			'logout_reason' => $logoutReason
1280
		);
1281

    
1282
		$rpc_client->xmlrpc_method('captive_portal_sync',
1283
			array(
1284
				'op' => 'disconnect_all',
1285
				'zone' => $cpzone,
1286
				'arguments' => base64_encode(serialize($arguments))
1287
			)
1288
		);
1289
	}
1290
	/* check if we're pruning old entries and eventually wait */
1291
	$rcprunelock = try_lock("rcprunecaptiveportal{$cpzone}", 15);
1292

    
1293
	/* if we still don't have the lock, unlock forcefully and take it */
1294
	if (!$rcprunelock) {
1295
		log_error("CP zone ${cpzone}: could not obtain the lock for more than 15 seconds, lock taken forcefully to disconnect all users");
1296
		unlock_force("rcprunecaptiveportal{$cpzone}");
1297
		$rcprunelock = lock("rcprunecaptiveportal{$cpzone}", LOCK_EX);
1298
	}
1299

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

    
1303
	captiveportal_radius_stop_all($term_cause, $logoutReason);
1304

    
1305
	/* remove users from the database */
1306
	$cpdb = captiveportal_read_db();
1307
	$unsetindexes = array_column($cpdb,5);
1308
	if (!empty($unsetindexes)) {
1309
		// High Availability : do not sync removed entries
1310
		captiveportal_remove_entries($unsetindexes, true);
1311
	}
1312

    
1313
	/* reinit ipfw rules */
1314
	captiveportal_init_rules(true);
1315

    
1316
	unlock($cpdblck);
1317
	unlock($rcprunelock);
1318
	return true;
1319
}
1320

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

    
1325
	$cpdb = captiveportal_read_db();
1326

    
1327
	$radacct = isset($config['captiveportal'][$cpzone]['radacct_enable']) ? true : false;
1328
	foreach ($cpdb as $cpentry) {
1329
		if ($cpentry['authmethod'] === 'radius' && $radacct) {
1330
			if ($config['captiveportal'][$cpzone]['reauthenticateacct'] == "stopstartfreeradius") {
1331
				$session_time = (time() - $cpentry[0]) % 60;
1332
				$start_time = time() - $session_time;
1333
			} else {
1334
				$start_time = $cpentry[0];
1335
			}
1336
			captiveportal_send_server_accounting('stop',
1337
				$cpentry[1], // ruleno
1338
				$cpentry[4], // username
1339
				$cpentry[2], // clientip
1340
				$cpentry[3], // clientmac
1341
				$cpentry[5], // sessionid
1342
				$start_time, // start time
1343
				$stop_time, // stop time
1344
				$term_cause); // Acct-Terminate-Cause
1345
		}
1346
		captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], $logoutReason);
1347
	}
1348
	unset($cpdb);
1349
}
1350

    
1351
function captiveportal_passthrumac_configure_entry($macent, $pipeinrule = false) {
1352
	global $config, $g, $cpzone;
1353

    
1354
	$bwUp = 0;
1355
	if (!empty($macent['bw_up'])) {
1356
		$bwUp = $macent['bw_up'];
1357
	} elseif (isset($config['captiveportal'][$cpzone]['peruserbw']) &&
1358
	    !empty($config['captiveportal'][$cpzone]['bwdefaultup'])) {
1359
		$bwUp = $config['captiveportal'][$cpzone]['bwdefaultup'];
1360
	}
1361
	$bwDown = 0;
1362
	if (!empty($macent['bw_down'])) {
1363
		$bwDown = $macent['bw_down'];
1364
	} elseif (isset($config['captiveportal'][$cpzone]['peruserbw']) &&
1365
	    !empty($config['captiveportal'][$cpzone]['bwdefaultdn'])) {
1366
		$bwDown = $config['captiveportal'][$cpzone]['bwdefaultdn'];
1367
	}
1368

    
1369
	if ($macent['action'] == 'pass') {
1370
		$rules = "";
1371

    
1372
		$pipeno = captiveportal_get_next_dn_ruleno('pipe_mac');
1373

    
1374
		$pipeup = $pipeno;
1375
		if ($pipeinrule == true) {
1376
			$_gb = @pfSense_ipfw_pipe("pipe {$pipeno} config bw {$bwUp}Kbit/s queue 100 buckets 16");
1377
		} else {
1378
			$rules .= "pipe {$pipeno} config bw {$bwUp}Kbit/s queue 100 buckets 16\n";
1379
		}
1380

    
1381
		$pipedown = $pipeno + 1;
1382
		if ($pipeinrule == true) {
1383
			$_gb = @pfSense_ipfw_pipe("pipe {$pipedown} config bw {$bwDown}Kbit/s queue 100 buckets 16");
1384
		} else {
1385
			$rules .= "pipe {$pipedown} config bw {$bwDown}Kbit/s queue 100 buckets 16\n";
1386
		}
1387

    
1388
		$rules .= "table {$cpzone}_pipe_mac add any,{$macent['mac']} {$pipeup}\n";
1389
		$rules .= "table {$cpzone}_pipe_mac add {$macent['mac']},any {$pipedown}\n";
1390
	}
1391

    
1392
	return $rules;
1393
}
1394

    
1395
function captiveportal_passthrumac_delete_entry($macent) {
1396
	global $cpzone;
1397
	$rules = "";
1398

    
1399
	if ($macent['action'] == 'pass') {
1400
		$pipeno = captiveportal_get_dn_passthru_ruleno($macent['mac']);
1401

    
1402
		if (!empty($pipeno)) {
1403
			captiveportal_free_dn_ruleno($pipeno);
1404
			$rules .= "table {$cpzone}_pipe_mac delete any,{$macent['mac']}\n";
1405
			$rules .= "table {$cpzone}_pipe_mac delete {$macent['mac']},any\n";
1406
			$rules .= "pipe delete " . $pipeno . "\n";
1407
			$rules .= "pipe delete " . ++$pipeno . "\n";
1408
		}
1409
	}
1410

    
1411
	return $rules;
1412
}
1413

    
1414
function captiveportal_passthrumac_configure($filename = false, $startindex = 0, $stopindex = 0) {
1415
	global $config, $g, $cpzone;
1416

    
1417
	$rules = "";
1418

    
1419
	if (is_array($config['captiveportal'][$cpzone]['passthrumac'])) {
1420
		if ($stopindex > 0) {
1421
			$fd = fopen($filename, "w");
1422
			for ($idx = $startindex; $idx <= $stopindex; $idx++) {
1423
				if (isset($config['captiveportal'][$cpzone]['passthrumac'][$idx])) {
1424
					$rules = captiveportal_passthrumac_configure_entry($config['captiveportal'][$cpzone]['passthrumac'][$idx]);
1425
					fwrite($fd, $rules);
1426
				}
1427
			}
1428
			fclose($fd);
1429

    
1430
			return;
1431
		} else {
1432
			$nentries = count($config['captiveportal'][$cpzone]['passthrumac']);
1433
			if ($nentries > 2000) {
1434
				$nloops = $nentries / 1000;
1435
				$remainder= $nentries % 1000;
1436
				for ($i = 0; $i < $nloops; $i++) {
1437
					mwexec_bg("/usr/local/sbin/fcgicli -f /etc/rc.captiveportal_configure_mac -d \"cpzone={$cpzone}&startidx=" . ($i * 1000) . "&stopidx=" . ((($i+1) * 1000) - 1) . "\"");
1438
				}
1439
				if ($remainder > 0) {
1440
					mwexec_bg("/usr/local/sbin/fcgicli -f /etc/rc.captiveportal_configure_mac -d \"cpzone={$cpzone}&startidx=" . ($i * 1000) . "&stopidx=" . (($i* 1000) + $remainder) ."\"");
1441
				}
1442
			} else {
1443
				foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $macent) {
1444
					$rules .= captiveportal_passthrumac_configure_entry($macent, true);
1445
				}
1446
			}
1447
		}
1448
	}
1449

    
1450
	return $rules;
1451
}
1452

    
1453
function captiveportal_passthrumac_findbyname($username) {
1454
	global $config, $cpzone;
1455

    
1456
	if (is_array($config['captiveportal'][$cpzone]['passthrumac'])) {
1457
		foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $macent) {
1458
			if ($macent['username'] == $username) {
1459
				return $macent;
1460
			}
1461
		}
1462
	}
1463
	return NULL;
1464
}
1465

    
1466
/*
1467
 * table (3=IN)/(4=OUT) hold allowed ip's without bw limits
1468
 */
1469
function captiveportal_allowedip_configure_entry($ipent, $ishostname = false) {
1470
	global $g, $config, $cpzone;
1471

    
1472
	/*  Instead of copying this entire function for something
1473
	 *  easy such as hostname vs ip address add this check
1474
	 */
1475
	if ($ishostname === true) {
1476
		if (!platform_booting()) {
1477
			$ipaddress = resolve_host_addresses($ipent['hostname'], array(DNS_A), false);
1478
			if (empty($ipaddress)) {
1479
				return;
1480
			}
1481
		} else {
1482
			$ipaddress = array();
1483
		}
1484
	} else {
1485
		$ipaddress = array($ipent['ip']);
1486
	}
1487

    
1488
	$rules = "";
1489
	$cp_filterdns_conf = "";
1490
	$enBwup = 0;
1491
	if (!empty($ipent['bw_up'])) {
1492
		$enBwup = intval($ipent['bw_up']);
1493
	} elseif (isset($config['captiveportal'][$cpzone]['peruserbw']) &&
1494
	    !empty($config['captiveportal'][$cpzone]['bwdefaultup'])) {
1495
		$enBwup = $config['captiveportal'][$cpzone]['bwdefaultup'];
1496
	}
1497
	$enBwdown = 0;
1498
	if (!empty($ipent['bw_down'])) {
1499
		$enBwdown = intval($ipent['bw_down']);
1500
	} elseif (isset($config['captiveportal'][$cpzone]['peruserbw']) &&
1501
	    !empty($config['captiveportal'][$cpzone]['bwdefaultdn'])) {
1502
		$enBwdown = $config['captiveportal'][$cpzone]['bwdefaultdn'];
1503
	}
1504

    
1505
	$pipeup = captiveportal_get_next_dn_ruleno('allowed');
1506
	$_gb = @pfSense_ipfw_pipe("pipe {$pipeup} config bw {$enBwup}Kbit/s queue 100 buckets 16");
1507
	$pipedown = $pipeup + 1;
1508
	$_gb = @pfSense_ipfw_pipe("pipe {$pipedown} config bw {$enBwdown}Kbit/s queue 100 buckets 16");
1509

    
1510
	if ($ishostname === true) {
1511
		$cp_filterdns_conf .= "ipfw {$ipent['hostname']} {$cpzone}_allowed_up pipe {$pipeup}\n";
1512
		$cp_filterdns_conf .= "ipfw {$ipent['hostname']} {$cpzone}_allowed_down pipe {$pipedown}\n";
1513
		if (!is_ipaddr($ipaddress[0])) {
1514
			return array("", $cp_filterdns_conf);
1515
		}
1516
	}
1517

    
1518
	$subnet = "";
1519
	if (!empty($ipent['sn'])) {
1520
		$subnet = "/{$ipent['sn']}";
1521
	}
1522
	foreach ($ipaddress as $ip) {
1523
		$rules .= "table {$cpzone}_allowed_up add {$ip}{$subnet} {$pipeup}\n";
1524
		$rules .= "table {$cpzone}_allowed_down add {$ip}{$subnet} {$pipedown}\n";
1525
	}
1526

    
1527
	if ($ishostname === true) {
1528
		return array($rules, $cp_filterdns_conf);
1529
	} else {
1530
		return $rules;
1531
	}
1532
}
1533

    
1534
function captiveportal_allowedhostname_configure() {
1535
	global $config, $g, $cpzone, $cpzoneid;
1536

    
1537
	$rules = "";
1538
	if (!is_array($config['captiveportal'][$cpzone]['allowedhostname'])) {
1539
		return $rules;
1540
	}
1541

    
1542
	$rules = "\n# captiveportal_allowedhostname_configure()\n";
1543
	$cp_filterdns_conf = "";
1544
	foreach ($config['captiveportal'][$cpzone]['allowedhostname'] as $hostnameent) {
1545
		$tmprules = captiveportal_allowedip_configure_entry($hostnameent, true);
1546
		$rules .= $tmprules[0];
1547
		$cp_filterdns_conf .= $tmprules[1];
1548
	}
1549
	$cp_filterdns_filename = "{$g['varetc_path']}/filterdns-{$cpzone}-captiveportal.conf";
1550
	@file_put_contents($cp_filterdns_filename, $cp_filterdns_conf);
1551
	unset($cp_filterdns_conf);
1552

    
1553
	return $rules;
1554
}
1555

    
1556
function captiveportal_filterdns_configure() {
1557
	global $config, $g, $cpzone, $cpzoneid;
1558

    
1559
	$cp_filterdns_filename = $g['varetc_path'] .
1560
	    "/filterdns-{$cpzone}-captiveportal.conf";
1561

    
1562
	if (isset($config['captiveportal'][$cpzone]['enable']) &&
1563
	    is_array($config['captiveportal'][$cpzone]['allowedhostname']) &&
1564
	    file_exists($cp_filterdns_filename)) {
1565
		if (isvalidpid($g['varrun_path'] .
1566
		    "/filterdns-{$cpzone}-cpah.pid")) {
1567
			sigkillbypid($g['varrun_path'] .
1568
			    "/filterdns-{$cpzone}-cpah.pid", "HUP");
1569
		} else {
1570
			mwexec("/usr/local/sbin/filterdns -p " .
1571
			    "{$g['varrun_path']}/filterdns-{$cpzone}-cpah.pid" .
1572
			    " -i 300 -c {$cp_filterdns_filename} -d 1");
1573
		}
1574
	} else {
1575
		killbypid("{$g['varrun_path']}/filterdns-{$cpzone}-cpah.pid");
1576
		@unlink("{$g['varrun_path']}/filterdns-{$cpzone}-cpah.pid");
1577
	}
1578

    
1579
	return $rules;
1580
}
1581

    
1582
function captiveportal_allowedip_configure() {
1583
	global $config, $g, $cpzone;
1584

    
1585
	$rules = "";
1586
	if (is_array($config['captiveportal'][$cpzone]['allowedip'])) {
1587
		foreach ($config['captiveportal'][$cpzone]['allowedip'] as $ipent) {
1588
			$rules .= captiveportal_allowedip_configure_entry($ipent);
1589
		}
1590
	}
1591

    
1592
	return $rules;
1593
}
1594

    
1595
/* get last activity timestamp given client IP address */
1596
function captiveportal_get_last_activity($ip) {
1597
	global $cpzone;
1598

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

    
1602
	foreach ($tables as $table) {
1603
		$ipfw = pfSense_ipfw_table_lookup($table, $ip);
1604
		if (is_array($ipfw)) {
1605
			/* Workaround for #46652 */
1606
			if ($ipfw['packets'] > 0) {
1607
				return $ipfw['timestamp'];
1608
			} else {
1609
				return 0;
1610
			}
1611
		}
1612
	}
1613

    
1614
	return 0;
1615
}
1616

    
1617

    
1618
/* log successful captive portal authentication to syslog */
1619
/* part of this code from php.net */
1620
function captiveportal_logportalauth($user, $mac, $ip, $status, $message = null) {
1621
	// Log it
1622
	if (!$message) {
1623
		$message = "{$status}: {$user}, {$mac}, {$ip}";
1624
	} else {
1625
		$message = trim($message);
1626
		$message = "{$status}: {$user}, {$mac}, {$ip}, {$message}";
1627
	}
1628
	captiveportal_syslog($message);
1629
}
1630

    
1631
/* log simple messages to syslog */
1632
function captiveportal_syslog($message) {
1633
	global $cpzone;
1634

    
1635
	$message = trim($message);
1636
	$message = "Zone: {$cpzone} - {$message}";
1637
	openlog("logportalauth", LOG_PID, LOG_LOCAL4);
1638
	// Log it
1639
	syslog(LOG_INFO, $message);
1640
	closelog();
1641
}
1642

    
1643
/* Authenticate users using Authentication Backend */
1644
function captiveportal_authenticate_user(&$login = '', &$password = '', $clientmac = '', $clientip = '', $pipeno = 'null', $context = 'first') {
1645
	global $g, $config, $cpzone;
1646
	$cpcfg = $config['captiveportal'][$cpzone];
1647

    
1648
	$login_status = 'FAILURE';
1649
	$login_msg = gettext('Invalid credentials specified');
1650
	$reply_attributes = array();
1651
	$auth_method = '';
1652
	$auth_result = null;
1653

    
1654
	/*
1655
	Management of the reply Message (reason why the authentication failed) :
1656
	multiple authentication servers can be used, so multiple reply messages could theoretically be returned.
1657
	But only one message is returned (the most important one).
1658
	The return value of authenticate_user() define how important messages are :
1659
		- Reply message of a successful auth is more important than reply message of
1660
		a user failed auth(invalid credentials/authorization)
1661

    
1662
		- Reply message of a user failed auth is more important than reply message of
1663
		a server failed auth (unable to contact server)
1664

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

    
1668
	The $authlevel variable is a flag indicating the status of authentication
1669
	0 = failed server auth
1670
	1 = failed user auth
1671
	2 = failed user auth with custom server reply recieved
1672
	3 = successful auth
1673
	*/
1674
	$authlevel = 0;
1675

    
1676
	/* Getting authentication servers from captiveportal configuration */
1677
	$auth_servers = array();
1678

    
1679
	if ($cpcfg['auth_method'] === 'none') {
1680
		$auth_servers[] = array('type' => 'none');
1681
	} else {
1682
		if ($context === 'second') {
1683
			$fullauthservers = explode(",", $cpcfg['auth_server2']);
1684
		} else {
1685
			$fullauthservers = explode(",", $cpcfg['auth_server']);
1686
		}
1687

    
1688
		foreach ($fullauthservers as $authserver) {
1689
			if (strpos($authserver, ' - ') !== false) {
1690
				$authserver = explode(' - ', $authserver);
1691
				array_shift($authserver);
1692
				$authserver = implode(' - ', $authserver);
1693

    
1694
				if (auth_get_authserver($authserver) !== null) {
1695
					$auth_servers[] = auth_get_authserver($authserver);
1696
				} else {
1697
					log_error("Zone: {$cpzone} - Captive portal was unable to find the settings of the server '{$authserver}' used for authentication !");
1698
				}
1699
			}
1700
		}
1701
	}
1702

    
1703
	/* Unable to find the any authentication server config - shouldn't happen! - bail out */
1704
	if (count($auth_servers) === 0) {
1705
		log_error("Zone: {$cpzone} - No valid server could be used for authentication.");
1706
		$login_msg = gettext("Internal Error");
1707
	} else {
1708
		foreach ($auth_servers as $authcfg) {
1709
			if ($authlevel < 3) {
1710
				$radmac_error = false;
1711
				$attributes = array("nas_identifier" => empty($cpcfg["radiusnasid"]) ? "CaptivePortal-{$cpzone}" : $cpcfg["radiusnasid"],
1712
					"nas_port_type" => RADIUS_ETHERNET,
1713
					"nas_port" => $pipeno,
1714
					"framed_ip" => $clientip);
1715
				if (mac_format($clientmac) !== null) {
1716
					$attributes["calling_station_id"] = mac_format($clientmac);
1717
				}
1718

    
1719
				$result = null;
1720
				$status = null;
1721
				$msg = null;
1722

    
1723
				/* Radius MAC authentication */
1724
				if ($context === 'radmac' && $clientmac) {
1725
					if ($authcfg['type'] === 'radius') {
1726
						$login = mac_format($clientmac);
1727
						$status = "MACHINE LOGIN";
1728
					} else {
1729
						/* Trying to perform a Radius MAC authentication on a non-radius server - shouldn't happen! - bail out */
1730
						$msg = gettext("Internal Error");
1731
						log_error("Zone: {$cpzone} - Trying to perform RADIUS MAC authentication on a non-RADIUS server !");
1732
						$radmac_error = true;
1733
						$result = null;
1734
					}
1735
				}
1736

    
1737
				if (!$radmac_error) {
1738
					if ($authcfg['type'] === 'none') {
1739
						$result = true;
1740
					} else {
1741
						$result = authenticate_user($login, $password, $authcfg, $attributes);
1742
					}
1743

    
1744
					if (!empty($attributes['error_message'])) {
1745
						$msg = $attributes['error_message'];
1746
					}
1747

    
1748
					if ($authcfg['type'] == 'Local Auth' && $result && isset($cpcfg['localauth_priv'])) {
1749
						if (!userHasPrivilege(getUserEntry($login), "user-services-captiveportal-login")) {
1750
							$result = false;
1751
							$msg = gettext("Access Denied");
1752
						}
1753
					}
1754
					if ($context === 'radmac' && $result === null && empty($attributes['reply_message'])) {
1755
						$msg = gettext("RADIUS MAC Authentication Failed.");
1756
					}
1757

    
1758
					if (empty($status)) {
1759
						if ($result === true) {
1760
							$status = "ACCEPT";
1761
						} elseif ($result === null) {
1762
							$status = "ERROR";
1763
						} else {
1764
							$status = "FAILURE";
1765
						}
1766
					}
1767

    
1768
					if ($context === 'radmac' && $login == mac_format($clientmac) || $authcfg['type'] === 'none' && empty($login)) {
1769
						$login = "unauthenticated";
1770
					}
1771
				}
1772
				// We determine a flag
1773
				if ($result === true) {
1774
					$val = 3;
1775
				} elseif ($result === false && !empty($attributes['reply_message'])) {
1776
					$val = 2;
1777
					$msg = $attributes['reply_message'];
1778
				} elseif ($result === false) {
1779
					$val = 1;
1780
				} elseif ($result === null) {
1781
					$val = 0;
1782
				}
1783

    
1784
				if ($val >= $authlevel) {
1785
					$authlevel = $val;
1786
					$auth_method = $authcfg['type'];
1787
					$login_status = $status;
1788
					$login_msg = $msg;
1789
					$reply_attributes = $attributes;
1790
					$auth_result = $result;
1791
				}
1792
			}
1793
		}
1794
	}
1795

    
1796
	return array('result'=>$auth_result, 'attributes'=>$reply_attributes, 'auth_method' =>$auth_method, 'login_status'=> $login_status, 'login_message' => $login_msg);
1797
}
1798

    
1799
function captiveportal_opendb() {
1800
	global $g, $config, $cpzone, $cpzoneid;
1801

    
1802
	$db_path = "{$g['vardb_path']}/captiveportal{$cpzone}.db";
1803
	$createquery = "CREATE TABLE IF NOT EXISTS captiveportal (" .
1804
				"allow_time INTEGER, pipeno INTEGER, ip TEXT, mac TEXT, username TEXT, " .
1805
				"sessionid TEXT, bpassword TEXT, session_timeout INTEGER, idle_timeout INTEGER, " .
1806
				"session_terminate_time INTEGER, interim_interval INTEGER, traffic_quota INTEGER, " .
1807
				"bw_up INTEGER, bw_down INTEGER, authmethod TEXT, context TEXT); " .
1808
			"CREATE UNIQUE INDEX IF NOT EXISTS idx_active ON captiveportal (sessionid, username); " .
1809
			"CREATE INDEX IF NOT EXISTS user ON captiveportal (username); " .
1810
			"CREATE INDEX IF NOT EXISTS ip ON captiveportal (ip); " .
1811
			"CREATE INDEX IF NOT EXISTS starttime ON captiveportal (allow_time)";
1812

    
1813
	try {
1814
		$DB = new SQLite3($db_path);
1815
		$DB->busyTimeout(60000);
1816
	} catch (Exception $e) {
1817
		captiveportal_syslog("Could not open {$db_path} as an sqlite database for {$cpzone}. Error message: " . $e->getMessage() . " -- Trying again.");
1818
		unlink_if_exists($db_path);
1819
		try {
1820
			$DB = new SQLite3($db_path);
1821
			$DB->busyTimeout(60000);
1822
		} catch (Exception $e) {
1823
			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.");
1824
			return;
1825
		}
1826
	}
1827

    
1828
	if (!$DB) {
1829
		captiveportal_syslog("Could not open {$db_path} as an sqlite database for {$cpzone}. Error message: {$DB->lastErrorMsg()}. Trying again.");
1830
		unlink_if_exists($db_path);
1831
		$DB = new SQLite3($db_path);
1832
		$DB->busyTimeout(60000);
1833
		if (!$DB) {
1834
			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.");
1835
			return;
1836
		}
1837
	}
1838

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

    
1842
		/* If unable to initialize the database, reset and try again. */
1843
		$DB->close();
1844
		unset($DB);
1845
		unlink_if_exists($db_path);
1846
		$DB = new SQLite3($db_path);
1847
		$DB->busyTimeout(60000);
1848
		if ($DB->exec($createquery)) {
1849
			captiveportal_syslog("Successfully reinitialized tables for {$cpzone} -- database has been reset.");
1850
			if (!is_numericint($cpzoneid)) {
1851
				if (is_array($config['captiveportal'])) {
1852
					foreach ($config['captiveportal'] as $cpkey => $cp) {
1853
						if ($cpzone == $cpkey) {
1854
							$cpzoneid = $cp['zoneid'];
1855
						}
1856
					}
1857
				}
1858
			}
1859
			if (is_numericint($cpzoneid)) {
1860
				$table_names = captiveportal_get_ipfw_table_names();
1861
				foreach ($table_names as $table_name) {
1862
					mwexec("/sbin/ipfw table {$table_name} flush");
1863
				}
1864
				captiveportal_syslog("Flushed tables for {$cpzone} after database reset.");
1865
			}
1866
		} else {
1867
			captiveportal_syslog("Still unable to create tables for {$cpzone}. Error message: {$DB->lastErrorMsg()}. Remove the database file manually and try again.");
1868
		}
1869
	}
1870

    
1871
	return $DB;
1872
}
1873

    
1874
/* Get all tables for specific cpzone */
1875
function captiveportal_get_ipfw_table_names() {
1876
	global $cpzone;
1877

    
1878
	$result = array();
1879
	$tables = pfSense_ipfw_tables_list();
1880

    
1881
	if (!is_array($tables)) {
1882
		return $result;
1883
	}
1884

    
1885
	$len = strlen($cpzone) + 1;
1886
	foreach ($tables as $table) {
1887
		if (substr($table['name'], 0, $len) != $cpzone . '_') {
1888
			continue;
1889
		}
1890

    
1891
		$result[] = $table['name'];
1892
	}
1893

    
1894
	return $result;
1895
}
1896

    
1897
/* read captive portal DB into array */
1898
function captiveportal_read_db($query = "") {
1899
	$cpdb = array();
1900

    
1901
	$DB = captiveportal_opendb();
1902
	if ($DB) {
1903
		$response = $DB->query("SELECT * FROM captiveportal {$query}");
1904
		if ($response != FALSE) {
1905
			while ($row = $response->fetchArray()) {
1906
				$cpdb[] = $row;
1907
			}
1908
		}
1909
		$DB->close();
1910
	}
1911

    
1912
	return $cpdb;
1913
}
1914

    
1915
function captiveportal_remove_entries($remove, $carp_loop = false) {
1916
	global $cpzone;
1917

    
1918
	if (!is_array($remove) || empty($remove)) {
1919
		return;
1920
	}
1921

    
1922
	$query = "DELETE FROM captiveportal WHERE sessionid in (";
1923
	foreach ($remove as $idx => $unindex) {
1924
		$query .= "'{$unindex}'";
1925
		if ($idx < (count($remove) - 1)) {
1926
			$query .= ",";
1927
		}
1928
	}
1929
	$query .= ")";
1930
	captiveportal_write_db($query);
1931

    
1932
	if (captiveportal_xmlrpc_sync_get_details($syncip, $syncport, $syncuser, $syncpass, $carp_loop)) {
1933
		$rpc_client = new pfsense_xmlrpc_client();
1934
		$rpc_client->setConnectionData($syncip, $syncport, $syncuser, $syncpass);
1935
		$rpc_client->set_noticefile("CaptivePortalUserSync");
1936
		$rpc_client->xmlrpc_method('captive_portal_sync',
1937
			array(
1938
				'op' => 'remove_entries',
1939
				'zone' => $cpzone,
1940
				'entries' => base64_encode(serialize($remove))
1941
			)
1942
		);
1943
	}
1944
	return true;
1945
}
1946

    
1947
/* write captive portal DB */
1948
function captiveportal_write_db($queries) {
1949
	global $g;
1950

    
1951
	if (is_array($queries)) {
1952
		$query = implode(";", $queries);
1953
	} else {
1954
		$query = $queries;
1955
	}
1956

    
1957
	$DB = captiveportal_opendb();
1958
	if ($DB) {
1959
		$DB->exec("BEGIN TRANSACTION");
1960
		$result = $DB->exec($query);
1961
		if (!$result) {
1962
			captiveportal_syslog("Trying to modify DB returned error: {$DB->lastErrorMsg()}");
1963
		} else {
1964
			$DB->exec("END TRANSACTION");
1965
		}
1966
		$DB->close();
1967
		return $result;
1968
	} else {
1969
		return true;
1970
	}
1971
}
1972

    
1973
function captiveportal_write_elements() {
1974
	global $g, $config, $cpzone;
1975

    
1976
	$cpcfg = $config['captiveportal'][$cpzone];
1977

    
1978
	if (!is_dir($g['captiveportal_element_path'])) {
1979
		@mkdir($g['captiveportal_element_path']);
1980
	}
1981

    
1982
	if (is_array($cpcfg['element'])) {
1983
		foreach ($cpcfg['element'] as $data) {
1984
			/* Do not attempt to decode or write out empty files. */
1985
			if (isset($data['nocontent'])) {
1986
					continue;
1987
			}
1988
			if (empty($data['content']) || empty(base64_decode($data['content']))) {
1989
				unlink_if_exists("{$g['captiveportal_element_path']}/{$data['name']}");
1990
				touch("{$g['captiveportal_element_path']}/{$data['name']}");
1991
			} elseif (!@file_put_contents("{$g['captiveportal_element_path']}/{$data['name']}", base64_decode($data['content']))) {
1992
				printf(gettext('Error: cannot open \'%1$s\' in captiveportal_write_elements()%2$s'), $data['name'], "\n");
1993
				return 1;
1994
			}
1995
			if (!file_exists("{$g['captiveportal_path']}/{$data['name']}")) {
1996
				@symlink("{$g['captiveportal_element_path']}/{$data['name']}", "{$g['captiveportal_path']}/{$data['name']}");
1997
			}
1998
		}
1999
	}
2000

    
2001
	return 0;
2002
}
2003

    
2004
function captiveportal_free_dnrules($rulenos_start = 2000,
2005
    $rulenos_range_max = 64500, $dry_run = false, $clear_auth_pipes = true) {
2006
	global $g, $cpzone;
2007

    
2008
	$removed_pipes = array();
2009

    
2010
	if (!file_exists("{$g['vardb_path']}/captiveportaldn.rules")) {
2011
		return $removed_pipes;
2012
	}
2013

    
2014
	if (!$dry_run) {
2015
		$cpruleslck = lock("captiveportalrulesdn", LOCK_EX);
2016
	}
2017

    
2018
	$rules = unserialize(file_get_contents(
2019
	    "{$g['vardb_path']}/captiveportaldn.rules"));
2020
	$ridx = $rulenos_start;
2021
	while ($ridx < $rulenos_range_max) {
2022
		if (substr($rules[$ridx], 0, strlen($cpzone . '_')) == $cpzone . '_') {
2023
			if (!$clear_auth_pipes && substr($rules[$ridx], 0, strlen($cpzone . '_auth')) == $cpzone . '_auth') {
2024
				$ridx += 2;
2025
			} else {
2026
				if (!$dry_run) {
2027
					$rules[$ridx] = false;
2028
				}
2029
				$removed_pipes[] = $ridx;
2030
				$ridx++;
2031
				if (!$dry_run) {
2032
					$rules[$ridx] = false;
2033
				}
2034
				$removed_pipes[] = $ridx;
2035
				$ridx++;
2036
			}
2037
		} else {
2038
			$ridx += 2;
2039
		}
2040
	}
2041

    
2042
	if (!$dry_run) {
2043
		file_put_contents("{$g['vardb_path']}/captiveportaldn.rules",
2044
		    serialize($rules));
2045
		unlock($cpruleslck);
2046
	}
2047

    
2048
	unset($rules);
2049

    
2050
	return $removed_pipes;
2051
}
2052

    
2053
function captiveportal_reserve_ruleno($ruleno) {
2054
	global $g, $cpzone;
2055

    
2056
	$cpruleslck = lock("captiveportalrulesdn", LOCK_EX);
2057
	if (file_exists("{$g['vardb_path']}/captiveportaldn.rules")) {
2058
		$rules = unserialize(file_get_contents("{$g['vardb_path']}/captiveportaldn.rules"));
2059
	} else {
2060
		$rules = array_pad(array(), 64500, false);
2061
	}
2062
	$rules[$ruleno] = $cpzone . '_auth';
2063
	$ruleno++;
2064
	$rules[$ruleno] = $cpzone . '_auth';
2065

    
2066
	file_put_contents("{$g['vardb_path']}/captiveportaldn.rules", serialize($rules));
2067
	unlock($cpruleslck);
2068
	unset($rules);
2069

    
2070
	return $ruleno;
2071
}
2072

    
2073
function captiveportal_get_next_dn_ruleno($rule_type = 'default', $rulenos_start = 2000, $rulenos_range_max = 64500) {
2074
	global $config, $g, $cpzone;
2075

    
2076
	$cpruleslck = lock("captiveportalrulesdn", LOCK_EX);
2077
	$ruleno = 0;
2078
	if (file_exists("{$g['vardb_path']}/captiveportaldn.rules")) {
2079
		$rules = unserialize(file_get_contents("{$g['vardb_path']}/captiveportaldn.rules"));
2080
		$ridx = $rulenos_start;
2081
		while ($ridx < $rulenos_range_max) {
2082
			if (empty($rules[$ridx])) {
2083
				$ruleno = $ridx;
2084
				$rules[$ridx] = $cpzone . '_' . $rule_type;
2085
				$ridx++;
2086
				$rules[$ridx] = $cpzone . '_' . $rule_type;
2087
				break;
2088
			} else {
2089
				$ridx += 2;
2090
			}
2091
		}
2092
	} else {
2093
		$rules = array_pad(array(), $rulenos_range_max, false);
2094
		$ruleno = $rulenos_start;
2095
		$rules[$rulenos_start] = $cpzone . '_' . $rule_type;
2096
		$rulenos_start++;
2097
		$rules[$rulenos_start] = $cpzone . '_' . $rule_type;
2098
	}
2099
	file_put_contents("{$g['vardb_path']}/captiveportaldn.rules", serialize($rules));
2100
	unlock($cpruleslck);
2101
	unset($rules);
2102

    
2103
	return $ruleno;
2104
}
2105

    
2106
function captiveportal_free_dn_ruleno($ruleno) {
2107
	global $config, $g;
2108

    
2109
	$cpruleslck = lock("captiveportalrulesdn", LOCK_EX);
2110
	if (file_exists("{$g['vardb_path']}/captiveportaldn.rules")) {
2111
		$rules = unserialize(file_get_contents("{$g['vardb_path']}/captiveportaldn.rules"));
2112
		$rules[$ruleno] = false;
2113
		$ruleno++;
2114
		$rules[$ruleno] = false;
2115
		file_put_contents("{$g['vardb_path']}/captiveportaldn.rules", serialize($rules));
2116
		unset($rules);
2117
	}
2118
	unlock($cpruleslck);
2119
}
2120

    
2121
function captiveportal_get_dn_passthru_ruleno($value) {
2122
	global $config, $g, $cpzone, $cpzoneid;
2123

    
2124
	$cpcfg = $config['captiveportal'][$cpzone];
2125
	if (!isset($cpcfg['enable'])) {
2126
		return NULL;
2127
	}
2128

    
2129
	$cpruleslck = lock("captiveportalrulesdn", LOCK_EX);
2130
	$ruleno = NULL;
2131
	if (file_exists("{$g['vardb_path']}/captiveportaldn.rules")) {
2132
		unset($output);
2133
		$item = pfSense_ipfw_table_lookup("{$cpzone}_pipe_mac",
2134
		    "any,{$value}");
2135
		if (!is_array($item) || empty($item['pipe'])) {
2136
			unlock($cpruleslck);
2137
			return NULL;
2138
		}
2139

    
2140
		$ruleno = intval($item['pipe']);
2141
		$rules = unserialize(file_get_contents("{$g['vardb_path']}/captiveportaldn.rules"));
2142
		if (!$rules[$ruleno]) {
2143
			$ruleno = NULL;
2144
		}
2145
		unset($rules);
2146
	}
2147
	unlock($cpruleslck);
2148

    
2149
	return $ruleno;
2150
}
2151

    
2152
/**
2153
 * This function will calculate the traffic produced by a client
2154
 * based on its firewall rule
2155
 *
2156
 * Point of view: NAS
2157
 *
2158
 * Input means: from the client
2159
 * Output means: to the client
2160
 *
2161
 */
2162

    
2163
function getVolume($ip) {
2164
	global $config, $cpzone;
2165

    
2166
	$reverse = isset($config['captiveportal'][$cpzone]['reverseacct'])
2167
	    ? true : false;
2168
	$volume = array();
2169
	// Initialize vars properly, since we don't want NULL vars
2170
	$volume['input_pkts'] = $volume['input_bytes'] = 0;
2171
	$volume['output_pkts'] = $volume['output_bytes'] = 0;
2172

    
2173
	$tables = array("allowed", "auth");
2174

    
2175
	foreach($tables as $table) {
2176
		$ipfw = pfSense_ipfw_table_lookup("{$cpzone}_{$table}_up", $ip);
2177
		if (!is_array($ipfw)) {
2178
			continue;
2179
		}
2180
		if ($reverse) {
2181
			$volume['output_pkts'] = $ipfw['packets'];
2182
			$volume['output_bytes'] = $ipfw['bytes'];
2183
		} else {
2184
			$volume['input_pkts'] = $ipfw['packets'];
2185
			$volume['input_bytes'] = $ipfw['bytes'];
2186
		}
2187
	}
2188

    
2189
	foreach($tables as $table) {
2190
		$ipfw = pfSense_ipfw_table_lookup("{$cpzone}_{$table}_down",
2191
		    $ip);
2192
		if (!is_array($ipfw)) {
2193
			continue;
2194
		}
2195
		if ($reverse) {
2196
			$volume['input_pkts'] = $ipfw['packets'];
2197
			$volume['input_bytes'] = $ipfw['bytes'];
2198
		} else {
2199
			$volume['output_pkts'] = $ipfw['packets'];
2200
			$volume['output_bytes'] = $ipfw['bytes'];
2201
		}
2202
	}
2203

    
2204
	return $volume;
2205
}
2206

    
2207
function portal_ip_from_client_ip($cliip) {
2208
	global $config, $cpzone;
2209

    
2210
	$isipv6 = is_ipaddrv6($cliip);
2211
	$interfaces = explode(",", $config['captiveportal'][$cpzone]['interface']);
2212
	foreach ($interfaces as $cpif) {
2213
		if ($isipv6) {
2214
			$ip = get_interface_ipv6($cpif);
2215
			$sn = get_interface_subnetv6($cpif);
2216
		} else {
2217
			$ip = get_interface_ip($cpif);
2218
			$sn = get_interface_subnet($cpif);
2219
		}
2220
		if (ip_in_subnet($cliip, "{$ip}/{$sn}")) {
2221
			return $ip;
2222
		}
2223
	}
2224

    
2225
	$route = route_get($cliip, 'inet', true);
2226
	if (empty($route)) {
2227
		return false;
2228
	}
2229

    
2230
	$iface = $route[0]['interface-name'];
2231
	if (!empty($iface)) {
2232
		$ip = ($isipv6) ? find_interface_ipv6($iface)
2233
		    : find_interface_ip($iface);
2234
		if (is_ipaddr($ip)) {
2235
			return $ip;
2236
		}
2237
	}
2238

    
2239
	// doesn't match up to any particular interface
2240
	// so let's set the portal IP to what PHP says
2241
	// the server IP issuing the request is.
2242
	// allows same behavior as 1.2.x where IP isn't
2243
	// in the subnet of any CP interface (static routes, etc.)
2244
	// rather than forcing to DNS hostname resolution
2245
	$ip = $_SERVER['SERVER_ADDR'];
2246
	if (is_ipaddr($ip)) {
2247
		return $ip;
2248
	}
2249

    
2250
	return false;
2251
}
2252

    
2253
function portal_hostname_from_client_ip($cliip) {
2254
	global $config, $cpzone;
2255

    
2256
	$cpcfg = $config['captiveportal'][$cpzone];
2257

    
2258
	if (isset($cpcfg['httpslogin'])) {
2259
		$listenporthttps = $cpcfg['listenporthttps'] ? $cpcfg['listenporthttps'] : ($cpcfg['zoneid'] + 8001);
2260
		$ourhostname = $cpcfg['httpsname'];
2261

    
2262
		if ($listenporthttps != 443) {
2263
			$ourhostname .= ":" . $listenporthttps;
2264
		}
2265
	} else {
2266
		$listenporthttp = $cpcfg['listenporthttp'] ? $cpcfg['listenporthttp'] : ($cpcfg['zoneid'] + 8000);
2267
		$ifip = portal_ip_from_client_ip($cliip);
2268
		if (!$ifip) {
2269
			$ourhostname = "{$config['system']['hostname']}.{$config['system']['domain']}";
2270
		} else {
2271
			$ourhostname = (is_ipaddrv6($ifip)) ? "[{$ifip}]" : "{$ifip}";
2272
		}
2273

    
2274
		if ($listenporthttp != 80) {
2275
			$ourhostname .= ":" . $listenporthttp;
2276
		}
2277
	}
2278

    
2279
	return $ourhostname;
2280
}
2281

    
2282
/* functions move from index.php */
2283

    
2284
function portal_reply_page($redirurl, $type = null, $message = null, $clientmac = null, $clientip = null, $username = null, $password = null, $voucher = null) {
2285
	global $g, $config, $cpzone;
2286

    
2287
	$cpcfg = $config['captiveportal'][$cpzone];
2288
	$ourhostname = portal_hostname_from_client_ip($clientip);
2289
	$protocol = (isset($cpcfg['httpslogin'])) ? 'https://' : 'http://';
2290
	$portal_url = "{$protocol}{$ourhostname}/index.php?zone={$cpzone}";
2291

    
2292
	/* Get captive portal layout */
2293
	if ($type == "redir") {
2294
		$redirurl = is_URL($redirurl, true) ? $redirurl : $portal_url;
2295
		header("Location: {$redirurl}");
2296
		return;
2297
	} else if ($type == "login") {
2298
		$htmltext = get_include_contents("{$g['varetc_path']}/captiveportal_{$cpzone}.html");
2299
	} else {
2300
		$htmltext = get_include_contents("{$g['varetc_path']}/captiveportal-{$cpzone}-error.html");
2301
	}
2302

    
2303
	/* substitute the PORTAL_REDIRURL variable */
2304
	if ($cpcfg['preauthurl']) {
2305
		$htmltext = str_replace("\$PORTAL_REDIRURL\$", "{$cpcfg['preauthurl']}", $htmltext);
2306
		$htmltext = str_replace("#PORTAL_REDIRURL#", "{$cpcfg['preauthurl']}", $htmltext);
2307
	}
2308

    
2309
	/* substitute other variables */
2310
	$htmltext = str_replace("\$PORTAL_ACTION\$", $portal_url, $htmltext);
2311
	$htmltext = str_replace("#PORTAL_ACTION#", $portal_url, $htmltext);
2312

    
2313
	$htmltext = str_replace("\$PORTAL_ZONE\$", htmlspecialchars($cpzone), $htmltext);
2314
	$htmltext = str_replace("\$PORTAL_REDIRURL\$", htmlspecialchars($redirurl), $htmltext);
2315
	$htmltext = str_replace("\$PORTAL_MESSAGE\$", htmlspecialchars($message), $htmltext);
2316
	$htmltext = str_replace("\$CLIENT_MAC\$", htmlspecialchars($clientmac), $htmltext);
2317
	$htmltext = str_replace("\$CLIENT_IP\$", htmlspecialchars($clientip), $htmltext);
2318

    
2319
	// Special handling case for captive portal master page so that it can be ran
2320
	// through the PHP interpreter using the include method above.  We convert the
2321
	// $VARIABLE$ case to #VARIABLE# in /etc/inc/captiveportal.inc before writing out.
2322
	$htmltext = str_replace("#PORTAL_ZONE#", htmlspecialchars($cpzone), $htmltext);
2323
	$htmltext = str_replace("#PORTAL_REDIRURL#", htmlspecialchars($redirurl), $htmltext);
2324
	$htmltext = str_replace("#PORTAL_MESSAGE#", htmlspecialchars($message), $htmltext);
2325
	$htmltext = str_replace("#CLIENT_MAC#", htmlspecialchars($clientmac), $htmltext);
2326
	$htmltext = str_replace("#CLIENT_IP#", htmlspecialchars($clientip), $htmltext);
2327
	$htmltext = str_replace("#USERNAME#", htmlspecialchars($username), $htmltext);
2328
	$htmltext = str_replace("#PASSWORD#", htmlspecialchars($password), $htmltext);
2329
	$htmltext = str_replace("#VOUCHER#", htmlspecialchars($voucher), $htmltext);
2330

    
2331
	echo $htmltext;
2332
}
2333

    
2334
function captiveportal_reapply_attributes($cpentry, $attributes) {
2335
	global $config, $cpzone, $g;
2336

    
2337
	if (isset($config['captiveportal'][$cpzone]['peruserbw'])) {
2338
		$dwfaultbw_up = !empty($config['captiveportal'][$cpzone]['bwdefaultup']) ? $config['captiveportal'][$cpzone]['bwdefaultup'] : 0;
2339
		$dwfaultbw_down = !empty($config['captiveportal'][$cpzone]['bwdefaultdn']) ? $config['captiveportal'][$cpzone]['bwdefaultdn'] : 0;
2340
	} else {
2341
		$dwfaultbw_up = $dwfaultbw_down = 0;
2342
	}
2343
	/* pipe throughputs must always be an integer, enforce that restriction again here. */
2344
	if (isset($config['captiveportal'][$cpzone]['radiusperuserbw'])) {
2345
		$bw_up = round(!empty($attributes['bw_up']) ? intval($attributes['bw_up'])/1000 : $dwfaultbw_up, 0);
2346
		$bw_down = round(!empty($attributes['bw_down']) ? intval($attributes['bw_down'])/1000 : $dwfaultbw_down, 0);
2347
	} else {
2348
		$bw_up = round($dwfaultbw_up,0);
2349
		$bw_down = round($dwfaultbw_down,0);
2350
	}
2351

    
2352
	$bw_up_pipeno = $cpentry[1];
2353
	$bw_down_pipeno = $cpentry[1]+1;
2354

    
2355
	if ($cpentry['bw_up'] !== $bw_up) {
2356
		$_gb = @pfSense_ipfw_pipe("pipe {$bw_up_pipeno} config bw {$bw_up}Kbit/s queue 100 buckets 16");
2357
		captiveportal_update_entry($cpentry['sessionid'], $bw_up, 'bw_up');
2358
	}
2359
	if ($cpentry['bw_down'] !== $bw_down) {
2360
		$_gb = @pfSense_ipfw_pipe("pipe {$bw_down_pipeno} config bw {$bw_down}Kbit/s queue 100 buckets 16");
2361
		captiveportal_update_entry($cpentry['sessionid'], $bw_down, 'bw_down');
2362
	}
2363
	unset($bw_up_pipeno, $bw_down_pipeno, $bw_up, $bw_down);
2364
}
2365

    
2366
function captiveportal_update_entry($sessionid, $new_value, $field_to_update) {
2367
	global $cpzone;
2368

    
2369
	if (!intval($new_value)) {
2370
		$new_value = "'{$new_value}'";
2371
	}
2372
	captiveportal_write_db("UPDATE captiveportal SET {$field_to_update} = {$new_value} WHERE sessionid = '{$sessionid}'");
2373
}
2374

    
2375
function portal_allow($clientip, $clientmac, $username, $password = null, $redirurl = null,
2376
    $attributes = null, $pipeno = null, $authmethod = null, $context = 'first', $existing_sessionid = null) {
2377
	global $g, $config, $cpzone;
2378

    
2379
	// Ensure we create an array if we are missing attributes
2380
	if (!is_array($attributes)) {
2381
		$attributes = array();
2382
	}
2383

    
2384
	unset($sessionid);
2385

    
2386
	/* Do not allow concurrent login execution. */
2387
	$cpdblck = lock("captiveportaldb{$cpzone}", LOCK_EX);
2388

    
2389
	if ($attributes['voucher']) {
2390
		$remaining_time = $attributes['session_timeout'];
2391
		$authmethod = "voucher"; // Set RADIUS-Attribute to Voucher to prevent ReAuth-Request for Vouchers Bug: #2155
2392
		$context = "voucher";
2393
	}
2394

    
2395
	$writecfg = false;
2396
	/* If both "Add MAC addresses of connected users as pass-through MAC" and "Disable concurrent logins" are checked,
2397
	then we need to check if the user was already authenticated using another MAC Address, and if so remove the previous Pass-Through MAC. */
2398
	if ((isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) && ($config['captiveportal'][$cpzone]['noconcurrentlogins'] == 'last') && ($username != 'unauthenticated') && isset($config['captiveportal'][$cpzone]['passthrumacadd'])) {
2399
		$mac = captiveportal_passthrumac_findbyname($username);
2400
		if (!empty($mac)) {
2401
			foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $idx => $macent) {
2402
				if ($macent['mac'] != $mac['mac']) {
2403
					continue;
2404
				}
2405

    
2406
				$pipeno = captiveportal_get_dn_passthru_ruleno($mac['mac']);
2407
				if ($pipeno) {
2408
					captiveportal_free_dn_ruleno($pipeno);
2409
					@pfSense_ipfw_table("{$cpzone}_pipe_mac", IP_FW_TABLE_XDEL, "any,{$mac['mac']}");
2410
					@pfSense_ipfw_table("{$cpzone}_pipe_mac", IP_FW_TABLE_XDEL, "{$mac['mac']},any");
2411
					@pfSense_ipfw_pipe("pipe delete " . ($pipeno+1));
2412
					@pfSense_ipfw_pipe("pipe delete " . $pipeno);
2413
				}
2414
				unset($config['captiveportal'][$cpzone]['passthrumac'][$idx]);
2415
			}
2416
		}
2417
	}
2418

    
2419
	/* read in client database */
2420
	$query = "WHERE ip = '{$clientip}'";
2421
	$tmpusername = SQLite3::escapeString(strtolower($username));
2422
	if (isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) {
2423
		$query .= " OR (username != 'unauthenticated' AND lower(username) = '{$tmpusername}')";
2424
	}
2425
	$cpdb = captiveportal_read_db($query);
2426

    
2427
	/* Snapshot the timestamp */
2428
	$allow_time = time();
2429

    
2430
	if ($existing_sessionid !== null) {
2431
		// If we recieved this connection through XMLRPC sync :
2432
		// we fetch allow_time from the info given by the other node
2433
		$allow_time = $attributes['allow_time'];
2434
	}
2435
	$unsetindexes = array();
2436

    
2437
	foreach ($cpdb as $cpentry) {
2438
		/* on the same ip */
2439
		if ($cpentry[2] == $clientip) {
2440
			if (isset($config['captiveportal'][$cpzone]['nomacfilter']) || $cpentry[3] == $clientmac) {
2441
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT LOGIN - REUSING OLD SESSION");
2442
			} else {
2443
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT LOGIN - REUSING IP {$cpentry[2]} WITH DIFFERENT MAC ADDRESS {$cpentry[3]}");
2444
			}
2445
			$sessionid = $cpentry[5];
2446
			break;
2447
		} elseif (($attributes['voucher']) && ($username != 'unauthenticated') && ($cpentry[4] == $username)) {
2448
			// user logged in with an active voucher. Check for how long and calculate
2449
			// how much time we can give him (voucher credit - used time)
2450
			$remaining_time = $cpentry[0] + $cpentry[7] - $allow_time;
2451
			if ($remaining_time < 0) { // just in case.
2452
				$remaining_time = 0;
2453
			}
2454

    
2455
			/* This user was already logged in so we disconnect the old one, or 
2456
			keep the old one, refusing the new login, or
2457
			allow the login */
2458

    
2459
			if (!isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) {
2460
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "config['captiveportal'][$cpzone]['noconcurrentlogins'] 2 does not exists = NOT set");		
2461
			} else {
2462
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "config['captiveportal'][$cpzone]['noconcurrentlogins'] 2 exists = set");		
2463
			}
2464

    
2465
			if ($config['captiveportal'][$cpzone]['noconcurrentlogins'] == "last") {
2466
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "Found last");
2467
			} else {
2468
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "Found NOT last");
2469
			}
2470
				
2471
			if (!isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) {
2472
				/* 'noconcurrentlogins' not set : accept login 'username' creating multiple sessions. */
2473
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "config['captiveportal'][$cpzone]['noconcurrentlogins'] 3 does not exists = NOT set");
2474
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT LOGIN - NOT TERMINATING EXISTING SESSION(S)");			
2475
			} elseif ($config['captiveportal'][$cpzone]['noconcurrentlogins'] == "last") {
2476
				/* Classic situation : accept the new login, disconnect the old - present - connection */
2477
				if (isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) {
2478
					captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "config['captiveportal'][$cpzone]['noconcurrentlogins'] 4 exists = set");		
2479
				} else {
2480
					captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "config['captiveportal'][$cpzone]['noconcurrentlogins'] 4 does not exists = NOT set");		
2481
				}
2482
				
2483
				captiveportal_disconnect($cpentry, 13);
2484
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT LOGIN - TERMINATING OLD SESSION");
2485
				$unsetindexes[] = $cpentry[5];
2486
				break;
2487
			} else {
2488
				/* Implicit 'first' : refuse the new login - 'username' is already logged in */
2489
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT VOUCHER LOGIN - NOT ALLOWED KEEPING OLD SESSION ");
2490
				unlock($cpdblck);
2491
				return 2;
2492
			}
2493
		} elseif ((isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) && ($username != 'unauthenticated')) {
2494
			if ($config['captiveportal'][$cpzone]['noconcurrentlogins'] == "last") {
2495
				/* on the same username */
2496
				if (strcasecmp($cpentry[4], $username) == 0) {
2497
					/* This user was already logged in so we disconnect the old one */
2498
					captiveportal_disconnect($cpentry, 13);
2499
					captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT USER LOGIN - TERMINATING OLD SESSION");
2500
					$unsetindexes[] = $cpentry[5];
2501
					break;
2502
				}
2503
			} else {
2504
				/* Implicit 'first' : refuse the new login - 'username' is already logged in */
2505
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT USER LOGIN - NOT ALLOWED KEEPING OLD SESSION ");
2506
				unlock($cpdblck);
2507
				return 2;				
2508
			}
2509
		}
2510
	}
2511
	unset($cpdb);
2512

    
2513
	if (!empty($unsetindexes)) {
2514
		captiveportal_remove_entries($unsetindexes);
2515
	}
2516

    
2517
	if ($attributes['voucher'] && $remaining_time <= 0) {
2518
		return 0;       // voucher already used and no time left
2519
	}
2520

    
2521
	if (!isset($sessionid)) {
2522
		if ($existing_sessionid != null) { // existing_sessionid should only be set during XMLRPC sync
2523
			$sessionid = $existing_sessionid;
2524
		} else {
2525
			/* generate unique session ID */
2526
			$tod = gettimeofday();
2527
			$sessionid = substr(md5(mt_rand() . $tod['sec'] . $tod['usec'] . $clientip . $clientmac), 0, 16);
2528
		}
2529

    
2530
		if (isset($config['captiveportal'][$cpzone]['peruserbw'])) {
2531
			$dwfaultbw_up = !empty($config['captiveportal'][$cpzone]['bwdefaultup']) ? $config['captiveportal'][$cpzone]['bwdefaultup'] : 0;
2532
			$dwfaultbw_down = !empty($config['captiveportal'][$cpzone]['bwdefaultdn']) ? $config['captiveportal'][$cpzone]['bwdefaultdn'] : 0;
2533
		} else {
2534
			$dwfaultbw_up = $dwfaultbw_down = 0;
2535
		}
2536
		/* pipe throughputs must always be an integer, enforce that restriction again here. */
2537
		if (isset($config['captiveportal'][$cpzone]['radiusperuserbw'])) {
2538
			$bw_up = round(!empty($attributes['bw_up']) ? intval($attributes['bw_up'])/1000 : $dwfaultbw_up, 0);
2539
			$bw_down = round(!empty($attributes['bw_down']) ? intval($attributes['bw_down'])/1000 : $dwfaultbw_down, 0);
2540
		} else {
2541
			$bw_up = round($dwfaultbw_up,0);
2542
			$bw_down = round($dwfaultbw_down,0);
2543
		}
2544

    
2545
		if (isset($config['captiveportal'][$cpzone]['passthrumacadd'])) {
2546

    
2547
			$mac = array();
2548
			$mac['action'] = 'pass';
2549
			$mac['mac'] = $clientmac;
2550
			$mac['ip'] = $clientip; /* Used only for logging */
2551
			$mac['username'] = $username;
2552
			if ($attributes['voucher']) {
2553
				$mac['logintype'] = "voucher";
2554
			}
2555
			if ($username == "unauthenticated") {
2556
				$mac['descr'] = "Auto-added";
2557
			} else if ($authmethod == "voucher") {
2558
				$mac['descr'] = "Auto-added for voucher {$username}";
2559
			} else {
2560
				$mac['descr'] = "Auto-added for user {$username}";
2561
			}
2562
			if (!empty($bw_up)) {
2563
				$mac['bw_up'] = $bw_up;
2564
			}
2565
			if (!empty($bw_down)) {
2566
				$mac['bw_down'] = $bw_down;
2567
			}
2568
			if (!is_array($config['captiveportal'][$cpzone]['passthrumac'])) {
2569
				$config['captiveportal'][$cpzone]['passthrumac'] = array();
2570
			}
2571
			//check for mac duplicates before adding it to config.
2572
			$mac_duplicate = false;
2573
			foreach($config['captiveportal'][$cpzone]['passthrumac'] as $mac_check){
2574
				if($mac_check['mac'] == $mac['mac']){
2575
					$mac_duplicate = true;
2576
				}
2577
			}
2578
			if(!$mac_duplicate){
2579
				$config['captiveportal'][$cpzone]['passthrumac'][] = $mac;
2580
			}
2581
			unlock($cpdblck);
2582
			$macrules = captiveportal_passthrumac_configure_entry($mac);
2583
			file_put_contents("{$g['tmp_path']}/macentry_{$cpzone}.rules.tmp", $macrules);
2584
			mwexec("/sbin/ipfw -q {$g['tmp_path']}/macentry_{$cpzone}.rules.tmp");
2585
			$writecfg = true;
2586
		} else {
2587
			/* See if a pipeno is passed, if not start sessions because this means there isn't one atm */
2588
			if (is_null($pipeno)) {
2589
				$pipeno = captiveportal_get_next_dn_ruleno('auth');
2590
			}
2591

    
2592
			/* if the pool is empty, return appropriate message and exit */
2593
			if (is_null($pipeno)) {
2594
				captiveportal_syslog("Zone: {$cpzone} - WARNING!  Captive portal has reached maximum login capacity");
2595
				unlock($cpdblck);
2596
				return false;
2597
			}
2598

    
2599
			$bw_up_pipeno = $pipeno;
2600
			$bw_down_pipeno = $pipeno + 1;
2601
			//$bw_up /= 1000; // Scale to Kbit/s
2602
			$_gb = @pfSense_ipfw_pipe("pipe {$bw_up_pipeno} config bw {$bw_up}Kbit/s queue 100 buckets 16");
2603
			$_gb = @pfSense_ipfw_pipe("pipe {$bw_down_pipeno} config bw {$bw_down}Kbit/s queue 100 buckets 16");
2604

    
2605
			$rule_entry = "{$clientip}/" . (is_ipaddrv6($clientip) ? "128" : "32");
2606
			if (!isset($config['captiveportal'][$cpzone]['nomacfilter'])) {
2607
				$rule_entry .= ",{$clientmac}";
2608
			}
2609
			$_gb = @pfSense_ipfw_table("{$cpzone}_auth_up", IP_FW_TABLE_XADD, "{$rule_entry}", $bw_up_pipeno);
2610
			$_gb = @pfSense_ipfw_table("{$cpzone}_auth_down", IP_FW_TABLE_XADD, "{$rule_entry}", $bw_down_pipeno);
2611

    
2612
			if ($attributes['voucher']) {
2613
				$attributes['session_timeout'] = $remaining_time;
2614
			}
2615

    
2616
			/* handle empty attributes */
2617
			$session_timeout = (!empty($attributes['session_timeout'])) ? $attributes['session_timeout'] : 'NULL';
2618
			$idle_timeout = (!empty($attributes['idle_timeout'])) ? $attributes['idle_timeout'] : 'NULL';
2619
			$session_terminate_time = (!empty($attributes['session_terminate_time'])) ? $attributes['session_terminate_time'] : 'NULL';
2620
			$interim_interval = (!empty($attributes['interim_interval'])) ? $attributes['interim_interval'] : 'NULL';
2621
			$traffic_quota = (!empty($attributes['maxbytes'])) ? $attributes['maxbytes'] : 'NULL';
2622

    
2623
			/* escape username */
2624
			$safe_username = SQLite3::escapeString($username);
2625

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

    
2632
			/* store information to database */
2633
			captiveportal_write_db($insertquery);
2634
			unlock($cpdblck);
2635
			unset($insertquery, $bpassword);
2636

    
2637
			$radacct = isset($config['captiveportal'][$cpzone]['radacct_enable']) ? true : false;
2638
			if ($authmethod === 'radius' && $radacct) {
2639
				captiveportal_send_server_accounting('start',
2640
					$pipeno, // ruleno
2641
					$username, // username
2642
					$clientip, // clientip
2643
					$clientmac, // clientmac
2644
					$sessionid, // sessionid
2645
					time());  // start time
2646
			}
2647
			if (captiveportal_xmlrpc_sync_get_details($syncip, $syncport, $syncuser, $syncpass, isset($existing_sessionid))) {
2648
				// $existing_sessionid prevent carp loop : only forward
2649
				// the connection to the other node if we generated the sessionid by ourselves
2650
				$rpc_client = new pfsense_xmlrpc_client();
2651
				$rpc_client->setConnectionData($syncip, $syncport, $syncuser, $syncpass);
2652
				$rpc_client->set_noticefile("CaptivePortalUserSync");
2653
				$arguments = array(
2654
					'clientip' => $clientip,
2655
					'clientmac' => $clientmac,
2656
					'username' => $username,
2657
					'password' => $password,
2658
					'attributes' => $attributes,
2659
					'allow_time' => $allow_time,
2660
					'authmethod' => $authmethod,
2661
					'context' => $context,
2662
					'sessionid' => $sessionid
2663
				);
2664

    
2665
				$rpc_client->xmlrpc_method('captive_portal_sync',
2666
					array(
2667
						'op' => 'connect_user',
2668
						'zone' => $cpzone,
2669
						'user' => base64_encode(serialize($arguments))
2670
					)
2671
				);
2672
			}
2673
		}
2674
	} else {
2675
		/* NOTE: #3062-11 If the pipeno has been allocated free it to not DoS the CP */
2676
		if (!is_null($pipeno)) {
2677
			captiveportal_free_dn_ruleno($pipeno);
2678
		}
2679

    
2680
		unlock($cpdblck);
2681
	}
2682

    
2683
	if ($writecfg == true) {
2684
		write_config(gettext("Captive Portal allowed users configuration changed"));
2685
	}
2686

    
2687
	if ($existing_sessionid !== null) {
2688
		if (!empty($sessionid)) {
2689
			return $sessionid;
2690
		} else {
2691
			return false;
2692
		}
2693
	}
2694
	/* redirect user to desired destination */
2695
	if (is_URL($attributes['url_redirection'], true)) {
2696
		$my_redirurl = $attributes['url_redirection'];
2697
	} else if (is_URL($config['captiveportal'][$cpzone]['redirurl'], true)) {
2698
		$my_redirurl = $config['captiveportal'][$cpzone]['redirurl'];
2699
	} else if (is_URL($redirurl, true)) {
2700
		$my_redirurl = $redirurl;
2701
	}
2702

    
2703
	if (isset($config['captiveportal'][$cpzone]['logoutwin_enable']) && !isset($config['captiveportal'][$cpzone]['passthrumacadd'])) {
2704
		$ourhostname = portal_hostname_from_client_ip($clientip);
2705
		$protocol = (isset($config['captiveportal'][$cpzone]['httpslogin'])) ? 'https://' : 'http://';
2706
		$logouturl = "{$protocol}{$ourhostname}/";
2707

    
2708
		if (isset($attributes['reply_message'])) {
2709
			$message = $attributes['reply_message'];
2710
		} else {
2711
			$message = 0;
2712
		}
2713

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

    
2716
	} else {
2717
		portal_reply_page($my_redirurl, "redir", "Just redirect the user.");
2718
	}
2719

    
2720
	return $sessionid;
2721
}
2722

    
2723

    
2724
/*
2725
 * Used for when pass-through credits are enabled.
2726
 * Returns true when there was at least one free login to deduct for the MAC.
2727
 * Expired entries are removed as they are seen.
2728
 * Active entries are updated according to the configuration.
2729
 */
2730
function portal_consume_passthrough_credit($clientmac) {
2731
	global $config, $cpzone;
2732

    
2733
	if (!empty($config['captiveportal'][$cpzone]['freelogins_count']) && is_numeric($config['captiveportal'][$cpzone]['freelogins_count'])) {
2734
		$freeloginscount = $config['captiveportal'][$cpzone]['freelogins_count'];
2735
	} else {
2736
		return false;
2737
	}
2738

    
2739
	if (!empty($config['captiveportal'][$cpzone]['freelogins_resettimeout']) && is_numeric($config['captiveportal'][$cpzone]['freelogins_resettimeout'])) {
2740
		$resettimeout = $config['captiveportal'][$cpzone]['freelogins_resettimeout'];
2741
	} else {
2742
		return false;
2743
	}
2744

    
2745
	if ($freeloginscount < 1 || $resettimeout <= 0 || !$clientmac) {
2746
		return false;
2747
	}
2748

    
2749
	$updatetimeouts = isset($config['captiveportal'][$cpzone]['freelogins_updatetimeouts']);
2750

    
2751
	/*
2752
	 * Read database of used MACs.  Lines are a comma-separated list
2753
	 * of the time, MAC, then the count of pass-through credits remaining.
2754
	 */
2755
	$usedmacs = captiveportal_read_usedmacs_db();
2756

    
2757
	$currenttime = time();
2758
	$found = false;
2759
	foreach ($usedmacs as $key => $usedmac) {
2760
		$usedmac = explode(",", $usedmac);
2761

    
2762
		if ($usedmac[1] == $clientmac) {
2763
			if ($usedmac[0] + ($resettimeout * 3600) > $currenttime) {
2764
				if ($usedmac[2] < 1) {
2765
					if ($updatetimeouts) {
2766
						$usedmac[0] = $currenttime;
2767
						unset($usedmacs[$key]);
2768
						$usedmacs[] = implode(",", $usedmac);
2769
						captiveportal_write_usedmacs_db($usedmacs);
2770
						xmlrpc_sync_usedmacs($usedmacs);
2771
					}
2772

    
2773
					return false;
2774
				} else {
2775
					$usedmac[2] -= 1;
2776
					$usedmacs[$key] = implode(",", $usedmac);
2777
				}
2778

    
2779
				$found = true;
2780
			} else {
2781
				unset($usedmacs[$key]);
2782
			}
2783

    
2784
			break;
2785
		} else if ($usedmac[0] + ($resettimeout * 3600) <= $currenttime) {
2786
			unset($usedmacs[$key]);
2787
		}
2788
	}
2789

    
2790
	if (!$found) {
2791
		$usedmac = array($currenttime, $clientmac, $freeloginscount - 1);
2792
		$usedmacs[] = implode(",", $usedmac);
2793
	}
2794

    
2795
	captiveportal_write_usedmacs_db($usedmacs);
2796
	xmlrpc_sync_usedmacs($usedmacs);
2797
	return true;
2798
}
2799

    
2800
function xmlrpc_sync_usedmacs($usedmacs) { 
2801
	global $config, $cpzone;
2802

    
2803
	// XMLRPC Call over to the other node
2804
	if (captiveportal_xmlrpc_sync_get_details($syncip, $syncport,
2805
	    $syncuser, $syncpass, $carp_loop)) {
2806
		$rpc_client = new pfsense_xmlrpc_client();
2807
		$rpc_client->setConnectionData($syncip, $syncport, $syncuser, $syncpass);
2808
		$rpc_client->set_noticefile("CaptivePortalUsedmacsSync");
2809
		$arguments = array(
2810
			'usedmacs' => $usedmacs
2811
		);
2812

    
2813
		$rpc_client->xmlrpc_method('captive_portal_sync',
2814
			array(
2815
				'op' => 'write_usedmacs',
2816
				'zone' => $cpzone,
2817
				'arguments' => base64_encode(serialize($arguments))
2818
			)
2819
		);
2820
	}
2821
}
2822

    
2823
function captiveportal_read_usedmacs_db() {
2824
	global $g, $cpzone;
2825

    
2826
	$cpumaclck = lock("captiveusedmacs{$cpzone}");
2827
	if (file_exists("{$g['vardb_path']}/captiveportal_usedmacs_{$cpzone}.db")) {
2828
		$usedmacs = file("{$g['vardb_path']}/captiveportal_usedmacs_{$cpzone}.db", FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
2829
		if (!$usedmacs) {
2830
			$usedmacs = array();
2831
		}
2832
	} else {
2833
		$usedmacs = array();
2834
	}
2835

    
2836
	unlock($cpumaclck);
2837
	return $usedmacs;
2838
}
2839

    
2840
function captiveportal_write_usedmacs_db($usedmacs) {
2841
	global $g, $cpzone;
2842

    
2843
	$cpumaclck = lock("captiveusedmacs{$cpzone}", LOCK_EX);
2844
	@file_put_contents("{$g['vardb_path']}/captiveportal_usedmacs_{$cpzone}.db", implode("\n", $usedmacs));
2845
	unlock($cpumaclck);
2846
}
2847

    
2848
function captiveportal_blocked_mac($mac) {
2849
	global $config, $g, $cpzone;
2850

    
2851
	if (empty($mac) || !is_macaddr($mac)) {
2852
		return false;
2853
	}
2854

    
2855
	if (!is_array($config['captiveportal'][$cpzone]['passthrumac'])) {
2856
		return false;
2857
	}
2858

    
2859
	foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $passthrumac) {
2860
		if (($passthrumac['action'] == 'block') &&
2861
		    ($passthrumac['mac'] == strtolower($mac))) {
2862
			return true;
2863
		}
2864
	}
2865

    
2866
	return false;
2867

    
2868
}
2869

    
2870
/* Captiveportal Radius Accounting */
2871

    
2872
function gigawords($bytes) {
2873

    
2874
	/*
2875
	 * RFC2866 Specifies a 32bit unsigned integer, which is a max of 4294967295
2876
	 * Currently there is a fault in the PECL radius_put_int function which can handle only 32bit signed integer.
2877
	 */
2878

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

    
2882
	// We need to manually set this to a zero instead of NULL for put_int() safety
2883
	if (is_null($gigawords)) {
2884
		$gigawords = 0;
2885
	}
2886

    
2887
	return $gigawords;
2888
}
2889

    
2890
function remainder($bytes) {
2891
	// Calculate the bytes we are going to send to the radius
2892
	$bytes = bcmod($bytes, GIGAWORDS_RIGHT_OPERAND);
2893

    
2894
	if (is_null($bytes)) {
2895
		$bytes = 0;
2896
	}
2897

    
2898
    return $bytes;
2899
}
2900

    
2901
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) {
2902
	global $cpzone, $config;
2903

    
2904
	$cpcfg = $config['captiveportal'][$cpzone];
2905
	$acctcfg = auth_get_authserver($cpcfg['radacct_server']);
2906

    
2907
	if (!isset($cpcfg['radacct_enable']) || empty($acctcfg) ||
2908
	    captiveportal_ha_is_node_in_backup_mode($cpzone)) {
2909
		return null;
2910
	}
2911

    
2912
	if ($type === 'on') {
2913
		$racct = new Auth_RADIUS_Acct_On;
2914
	} elseif ($type === 'off') {
2915
		$racct = new Auth_RADIUS_Acct_Off;
2916
	} elseif ($type === 'start') {
2917
		$racct = new Auth_RADIUS_Acct_Start;
2918
		if (!is_int($start_time)) {
2919
			$start_time = time();
2920
		}
2921
	} elseif ($type === 'stop') {
2922
		$racct = new Auth_RADIUS_Acct_Stop;
2923
		if (!is_int($stop_time)) {
2924
			$stop_time = time();
2925
		}
2926
	} elseif ($type === 'update') {
2927
        $racct = new Auth_RADIUS_Acct_Update;
2928
		if (!is_int($stop_time)) {
2929
			$stop_time = time(); // "top time" here will be used only for calculating session time.
2930
		}
2931
	} else {
2932
		return null;
2933
	}
2934

    
2935
	$racct->addServer($acctcfg['host'], $acctcfg['radius_acct_port'],
2936
		$acctcfg['radius_secret'], $acctcfg['radius_timeout']);
2937

    
2938
	$racct->authentic = RADIUS_AUTH_RADIUS;
2939
	if ($cpcfg['auth_method'] === 'radmac' && $username === "unauthenticated" && !empty($clientmac)) {
2940
		$racct->username = mac_format($clientmac);
2941
	} elseif (!empty($username)) {
2942
		$racct->username = $username;
2943
	}
2944

    
2945
	if (PEAR::isError($racct->start())) {
2946
		captiveportal_syslog('RADIUS ACCOUNTING FAILED : '.$racct->getError());
2947
		$racct->close();
2948
		return null;
2949
	}
2950

    
2951
	$nasip = $acctcfg['radius_nasip_attribute'];
2952
	if (!is_ipaddr($nasip)) {
2953
		$nasip = get_interface_ip($nasip);
2954
		if (!is_ipaddr($nasip)) {
2955
			$nasip = get_interface_ip();//We use WAN interface IP as fallback for NAS-IP-Address
2956
		}
2957
	}
2958
	$nasmac = get_interface_mac(find_ip_interface($nasip));
2959
	$racct->putAttribute(RADIUS_NAS_IP_ADDRESS, $nasip, "addr");
2960

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

    
2963
	if (is_int($ruleno)) {
2964
		$racct->putAttribute(RADIUS_NAS_PORT_TYPE, RADIUS_ETHERNET);
2965
		$racct->putAttribute(RADIUS_NAS_PORT, intval($ruleno), 'integer');
2966
	}
2967

    
2968
	if (!empty($sessionid)) {
2969
		$racct->putAttribute(RADIUS_ACCT_SESSION_ID, $sessionid);
2970
	}
2971

    
2972
	if (!empty($clientip) && is_ipaddr($clientip)) {
2973
		$racct->putAttribute(RADIUS_FRAMED_IP_ADDRESS, $clientip, "addr");
2974
	}
2975
	if (!empty($clientmac)) {
2976
		$racct->putAttribute(RADIUS_CALLING_STATION_ID, mac_format($clientmac));
2977
	}
2978
	if (!empty($nasmac)) {
2979
		$racct->putAttribute(RADIUS_CALLED_STATION_ID, mac_format($nasmac).':'.gethostname());
2980
	}
2981

    
2982
	// Accounting request Stop and Update : send the current data volume
2983
	if (($type === 'stop' || $type === 'update') && is_int($start_time)) {
2984
		$volume = getVolume($clientip);
2985
		$session_time = $stop_time - $start_time;
2986
		$volume['input_bytes_radius'] = remainder($volume['input_bytes']);
2987
		$volume['input_gigawords'] = gigawords($volume['input_bytes']);
2988
		$volume['output_bytes_radius'] = remainder($volume['output_bytes']);
2989
		$volume['output_gigawords'] = gigawords($volume['output_bytes']);
2990

    
2991
		// Volume stuff: Ingress
2992
		$racct->putAttribute(RADIUS_ACCT_INPUT_PACKETS, intval($volume['input_pkts']), "integer");
2993
		$racct->putAttribute(RADIUS_ACCT_INPUT_OCTETS, intval($volume['input_bytes_radius']), "integer");
2994
		// Volume stuff: Outgress
2995
		$racct->putAttribute(RADIUS_ACCT_OUTPUT_PACKETS, intval($volume['output_pkts']), "integer");
2996
		$racct->putAttribute(RADIUS_ACCT_OUTPUT_OCTETS, intval($volume['output_bytes_radius']), "integer");
2997
		$racct->putAttribute(RADIUS_ACCT_SESSION_TIME, intval($session_time), "integer");
2998

    
2999
		$racct->putAttribute(CUSTOM_RADIUS_ACCT_OUTPUT_GIGAWORDS, intval($volume['output_gigawords']), "integer");
3000
		$racct->putAttribute(CUSTOM_RADIUS_ACCT_INPUT_GIGAWORDS, intval($volume['input_gigawords']), "integer");
3001
		// Set session_time
3002
		$racct->session_time = $session_time;
3003
	}
3004

    
3005
	if ($type === 'stop') {
3006
		if (empty($term_cause)) {
3007
			$term_cause = 1;
3008
		}
3009
		$racct->putAttribute(RADIUS_ACCT_TERMINATE_CAUSE, $term_cause);
3010
	}
3011

    
3012
	// Send request
3013
	$result = $racct->send();
3014

    
3015
	if (PEAR::isError($result)) {
3016
		 captiveportal_syslog('RADIUS ACCOUNTING FAILED : '.$racct->getError());
3017
		 $result = null;
3018
	} elseif ($result !== true) {
3019
		$result = false;
3020
	}
3021

    
3022
	$racct->close();
3023
	return $result;
3024
}
3025

    
3026
function captiveportal_isip_logged($clientip) {
3027
	global $g, $cpzone;
3028

    
3029
	/* read in client database */
3030
	$query = "WHERE ip = '{$clientip}'";
3031
	$cpdb = captiveportal_read_db($query);
3032
	foreach ($cpdb as $cpentry) {
3033
		return $cpentry;
3034
	}
3035
}
3036

    
3037
?>
(6-6/61)