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
# Table with interfaces that have CP enabled
590
table cp_ifaces create type iface valtype skipto
591

    
592
# Redirect each CP interface to its specific rule
593
add 1000 skipto tablearg all from any to any via table(cp_ifaces)
594

    
595
# This interface has no cp zone configured
596
add 1100 allow all from any to any
597

    
598
# block everything else
599
add 65534 deny all from any to any
600
EOD;
601

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

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

    
614
	return $rule;
615
}
616

    
617
/* Return first rule number for a cp zone */
618
function captiveportal_ipfw_ruleno($id) {
619
	global $g;
620

    
621
	return 2000 + $id * $g['captiveportal_rules_interval'];
622
}
623

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

    
628
	if (!isset($config['captiveportal'][$cpzone]['enable'])) {
629
		return;
630
	}
631

    
632
	captiveportal_load_modules();
633
	captiveportal_init_general_rules();
634

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

    
639
	$cpzoneid = $config['captiveportal'][$cpzone]['zoneid'];
640
	$skipto = captiveportal_ipfw_ruleno($cpzoneid);
641

    
642
	$cprules = '';
643

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

    
657
		$cpipm = get_interface_ip($cpifgrp);
658

    
659
		if (!is_ipaddr($cpipm)) {
660
			continue;
661
		}
662

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

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

    
682
	$captiveportallck = try_lock("captiveportal{$cpzone}", 0);
683

    
684
	$tables = captiveportal_get_ipfw_table_names();
685

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

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

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

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

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

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

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

    
773
	/* generate passthru mac database */
774
	$cprules .= captiveportal_passthrumac_configure(true);
775
	$cprules .= "\n";
776

    
777
	/* allowed ipfw rules to make allowed ip work */
778
	$cprules .= captiveportal_allowedip_configure();
779

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

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

    
790
	captiveportal_filterdns_configure();
791

    
792
	if ($captiveportallck) {
793
		unlock($captiveportallck);
794
	}
795
}
796

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

    
801
	$cpzoneid = $config['captiveportal'][$cpzone]['zoneid'];
802

    
803
	$skipto1 = captiveportal_ipfw_ruleno($cpzoneid);
804
	$skipto2 = $skipto1 + $g['captiveportal_rules_interval'];
805

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

    
814
			pfSense_ipfw_table("cp_ifaces", IP_FW_TABLE_XDEL,
815
			    $cp_iface['iface']);
816
		}
817
	}
818

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

    
821
	$tables = captiveportal_get_ipfw_table_names();
822

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

    
832
	foreach ($pipes_to_remove as $pipeno) {
833
		$delrules .= "pipe delete {$pipeno}\n";
834
	}
835

    
836
	if (empty($delrules)) {
837
		return;
838
	}
839

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

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

    
854
	if (empty($cpzone)) {
855
		return;
856
	}
857

    
858
	$cpcfg = $config['captiveportal'][$cpzone];
859
	$vcpcfg = $config['voucher'][$cpzone];
860

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

    
868
	if (!empty($cpcfg['idletimeout']) && is_numeric($cpcfg['idletimeout'])) {
869
		$idletimeout = $cpcfg['idletimeout'] * 60;
870
	}
871

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

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

    
886

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

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

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

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

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

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

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

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

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

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

    
1090
	captiveportal_prune_old_automac();
1091

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

    
1097
	/* write database */
1098
	if (!empty($unsetindexes)) {
1099
		captiveportal_remove_entries($unsetindexes);
1100
	}
1101
}
1102

    
1103
function captiveportal_prune_old_automac() {
1104
	global $g, $config, $cpzone, $cpzoneid;
1105

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

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

    
1151
	$stop_time = (empty($stop_time)) ? time() : $stop_time;
1152

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

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

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

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

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

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

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

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

    
1250
	$sessionid = SQLite3::escapeString($sessionid);
1251
	/* read database */
1252
	$result = captiveportal_read_db("WHERE sessionid = '{$sessionid}'");
1253

    
1254
	/* find entry */
1255
	if (!empty($result)) {
1256

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

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

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

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

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

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

    
1300
	captiveportal_radius_stop_all($term_cause, $logoutReason);
1301

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

    
1310
	/* reinit ipfw rules */
1311
	captiveportal_init_rules(true);
1312

    
1313
	unlock($cpdblck);
1314
	unlock($rcprunelock);
1315
	return true;
1316
}
1317

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

    
1322
	$cpdb = captiveportal_read_db();
1323

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

    
1348
function captiveportal_passthrumac_configure_entry($macent, $pipeinrule = false) {
1349
	global $config, $g, $cpzone;
1350

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

    
1366
	if ($macent['action'] == 'pass') {
1367
		$rules = "";
1368

    
1369
		$pipeno = captiveportal_get_next_dn_ruleno('pipe_mac');
1370

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

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

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

    
1389
	return $rules;
1390
}
1391

    
1392
function captiveportal_passthrumac_delete_entry($macent) {
1393
	global $cpzone;
1394
	$rules = "";
1395

    
1396
	if ($macent['action'] == 'pass') {
1397
		$pipeno = captiveportal_get_dn_passthru_ruleno($macent['mac']);
1398

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

    
1408
	return $rules;
1409
}
1410

    
1411
function captiveportal_passthrumac_configure($filename = false, $startindex = 0, $stopindex = 0) {
1412
	global $config, $g, $cpzone;
1413

    
1414
	$rules = "";
1415

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

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

    
1447
	return $rules;
1448
}
1449

    
1450
function captiveportal_passthrumac_findbyname($username) {
1451
	global $config, $cpzone;
1452

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

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

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

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

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

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

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

    
1524
	if ($ishostname === true) {
1525
		return array($rules, $cp_filterdns_conf);
1526
	} else {
1527
		return $rules;
1528
	}
1529
}
1530

    
1531
function captiveportal_allowedhostname_configure() {
1532
	global $config, $g, $cpzone, $cpzoneid;
1533

    
1534
	$rules = "";
1535
	if (!is_array($config['captiveportal'][$cpzone]['allowedhostname'])) {
1536
		return $rules;
1537
	}
1538

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

    
1550
	return $rules;
1551
}
1552

    
1553
function captiveportal_filterdns_configure() {
1554
	global $config, $g, $cpzone, $cpzoneid;
1555

    
1556
	$cp_filterdns_filename = $g['varetc_path'] .
1557
	    "/filterdns-{$cpzone}-captiveportal.conf";
1558

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

    
1576
	return $rules;
1577
}
1578

    
1579
function captiveportal_allowedip_configure() {
1580
	global $config, $g, $cpzone;
1581

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

    
1589
	return $rules;
1590
}
1591

    
1592
/* get last activity timestamp given client IP address */
1593
function captiveportal_get_last_activity($ip) {
1594
	global $cpzone;
1595

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

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

    
1611
	return 0;
1612
}
1613

    
1614

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

    
1628
/* log simple messages to syslog */
1629
function captiveportal_syslog($message) {
1630
	global $cpzone;
1631

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

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

    
1645
	$login_status = 'FAILURE';
1646
	$login_msg = gettext('Invalid credentials specified');
1647
	$reply_attributes = array();
1648
	$auth_method = '';
1649
	$auth_result = null;
1650

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

    
1659
		- Reply message of a user failed auth is more important than reply message of
1660
		a server failed auth (unable to contact server)
1661

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

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

    
1673
	/* Getting authentication servers from captiveportal configuration */
1674
	$auth_servers = array();
1675

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

    
1685
		foreach ($fullauthservers as $authserver) {
1686
			if (strpos($authserver, ' - ') !== false) {
1687
				$authserver = explode(' - ', $authserver);
1688
				array_shift($authserver);
1689
				$authserver = implode(' - ', $authserver);
1690

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

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

    
1716
				$result = null;
1717
				$status = null;
1718
				$msg = null;
1719

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

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

    
1741
					if (!empty($attributes['error_message'])) {
1742
						$msg = $attributes['error_message'];
1743
					}
1744

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

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

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

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

    
1793
	return array('result'=>$auth_result, 'attributes'=>$reply_attributes, 'auth_method' =>$auth_method, 'login_status'=> $login_status, 'login_message' => $login_msg);
1794
}
1795

    
1796
function captiveportal_opendb() {
1797
	global $g, $config, $cpzone, $cpzoneid;
1798

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

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

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

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

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

    
1868
	return $DB;
1869
}
1870

    
1871
/* Get all tables for specific cpzone */
1872
function captiveportal_get_ipfw_table_names() {
1873
	global $cpzone;
1874

    
1875
	$result = array();
1876
	$tables = pfSense_ipfw_tables_list();
1877

    
1878
	if (!is_array($tables)) {
1879
		return $result;
1880
	}
1881

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

    
1888
		$result[] = $table['name'];
1889
	}
1890

    
1891
	return $result;
1892
}
1893

    
1894
/* read captive portal DB into array */
1895
function captiveportal_read_db($query = "") {
1896
	$cpdb = array();
1897

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

    
1909
	return $cpdb;
1910
}
1911

    
1912
function captiveportal_remove_entries($remove, $carp_loop = false) {
1913
	global $cpzone;
1914

    
1915
	if (!is_array($remove) || empty($remove)) {
1916
		return;
1917
	}
1918

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

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

    
1944
/* write captive portal DB */
1945
function captiveportal_write_db($queries) {
1946
	global $g;
1947

    
1948
	if (is_array($queries)) {
1949
		$query = implode(";", $queries);
1950
	} else {
1951
		$query = $queries;
1952
	}
1953

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

    
1970
function captiveportal_write_elements() {
1971
	global $g, $config, $cpzone;
1972

    
1973
	$cpcfg = $config['captiveportal'][$cpzone];
1974

    
1975
	if (!is_dir($g['captiveportal_element_path'])) {
1976
		@mkdir($g['captiveportal_element_path']);
1977
	}
1978

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

    
1998
	return 0;
1999
}
2000

    
2001
function captiveportal_free_dnrules($rulenos_start = 2000,
2002
    $rulenos_range_max = 64500, $dry_run = false, $clear_auth_pipes = true) {
2003
	global $g, $cpzone;
2004

    
2005
	$removed_pipes = array();
2006

    
2007
	if (!file_exists("{$g['vardb_path']}/captiveportaldn.rules")) {
2008
		return $removed_pipes;
2009
	}
2010

    
2011
	if (!$dry_run) {
2012
		$cpruleslck = lock("captiveportalrulesdn", LOCK_EX);
2013
	}
2014

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

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

    
2045
	unset($rules);
2046

    
2047
	return $removed_pipes;
2048
}
2049

    
2050
function captiveportal_reserve_ruleno($ruleno) {
2051
	global $g, $cpzone;
2052

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

    
2063
	file_put_contents("{$g['vardb_path']}/captiveportaldn.rules", serialize($rules));
2064
	unlock($cpruleslck);
2065
	unset($rules);
2066

    
2067
	return $ruleno;
2068
}
2069

    
2070
function captiveportal_get_next_dn_ruleno($rule_type = 'default', $rulenos_start = 2000, $rulenos_range_max = 64500) {
2071
	global $config, $g, $cpzone;
2072

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

    
2100
	return $ruleno;
2101
}
2102

    
2103
function captiveportal_free_dn_ruleno($ruleno) {
2104
	global $config, $g;
2105

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

    
2118
function captiveportal_get_dn_passthru_ruleno($value) {
2119
	global $config, $g, $cpzone, $cpzoneid;
2120

    
2121
	$cpcfg = $config['captiveportal'][$cpzone];
2122
	if (!isset($cpcfg['enable'])) {
2123
		return NULL;
2124
	}
2125

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

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

    
2146
	return $ruleno;
2147
}
2148

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

    
2160
function getVolume($ip) {
2161
	global $config, $cpzone;
2162

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

    
2170
	$tables = array("allowed", "auth");
2171

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

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

    
2201
	return $volume;
2202
}
2203

    
2204
function portal_ip_from_client_ip($cliip) {
2205
	global $config, $cpzone;
2206

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

    
2222
	$route = route_get($cliip, 'inet', true);
2223
	if (empty($route)) {
2224
		return false;
2225
	}
2226

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

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

    
2247
	return false;
2248
}
2249

    
2250
function portal_hostname_from_client_ip($cliip) {
2251
	global $config, $cpzone;
2252

    
2253
	$cpcfg = $config['captiveportal'][$cpzone];
2254

    
2255
	if (isset($cpcfg['httpslogin'])) {
2256
		$listenporthttps = $cpcfg['listenporthttps'] ? $cpcfg['listenporthttps'] : ($cpcfg['zoneid'] + 8001);
2257
		$ourhostname = $cpcfg['httpsname'];
2258

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

    
2271
		if ($listenporthttp != 80) {
2272
			$ourhostname .= ":" . $listenporthttp;
2273
		}
2274
	}
2275

    
2276
	return $ourhostname;
2277
}
2278

    
2279
/* functions move from index.php */
2280

    
2281
function portal_reply_page($redirurl, $type = null, $message = null, $clientmac = null, $clientip = null, $username = null, $password = null, $voucher = null) {
2282
	global $g, $config, $cpzone;
2283

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

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

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

    
2306
	/* substitute other variables */
2307
	$htmltext = str_replace("\$PORTAL_ACTION\$", $portal_url, $htmltext);
2308
	$htmltext = str_replace("#PORTAL_ACTION#", $portal_url, $htmltext);
2309

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

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

    
2328
	echo $htmltext;
2329
}
2330

    
2331
function captiveportal_reapply_attributes($cpentry, $attributes) {
2332
	global $config, $cpzone, $g;
2333

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

    
2349
	$bw_up_pipeno = $cpentry[1];
2350
	$bw_down_pipeno = $cpentry[1]+1;
2351

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

    
2363
function captiveportal_update_entry($sessionid, $new_value, $field_to_update) {
2364
	global $cpzone;
2365

    
2366
	if (!intval($new_value)) {
2367
		$new_value = "'{$new_value}'";
2368
	}
2369
	captiveportal_write_db("UPDATE captiveportal SET {$field_to_update} = {$new_value} WHERE sessionid = '{$sessionid}'");
2370
}
2371

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

    
2376
	// Ensure we create an array if we are missing attributes
2377
	if (!is_array($attributes)) {
2378
		$attributes = array();
2379
	}
2380

    
2381
	unset($sessionid);
2382

    
2383
	/* Do not allow concurrent login execution. */
2384
	$cpdblck = lock("captiveportaldb{$cpzone}", LOCK_EX);
2385

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

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

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

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

    
2424
	/* Snapshot the timestamp */
2425
	$allow_time = time();
2426

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

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

    
2452
			/* This user was already logged in so we disconnect the old one, or 
2453
			keep the old one, refusing the new login, or
2454
			allow the login */
2455

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

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

    
2510
	if (!empty($unsetindexes)) {
2511
		captiveportal_remove_entries($unsetindexes);
2512
	}
2513

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

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

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

    
2542
		if (isset($config['captiveportal'][$cpzone]['passthrumacadd'])) {
2543

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

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

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

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

    
2609
			if ($attributes['voucher']) {
2610
				$attributes['session_timeout'] = $remaining_time;
2611
			}
2612

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

    
2620
			/* escape username */
2621
			$safe_username = SQLite3::escapeString($username);
2622

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

    
2629
			/* store information to database */
2630
			captiveportal_write_db($insertquery);
2631
			unlock($cpdblck);
2632
			unset($insertquery, $bpassword);
2633

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

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

    
2677
		unlock($cpdblck);
2678
	}
2679

    
2680
	if ($writecfg == true) {
2681
		write_config(gettext("Captive Portal allowed users configuration changed"));
2682
	}
2683

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

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

    
2705
		if (isset($attributes['reply_message'])) {
2706
			$message = $attributes['reply_message'];
2707
		} else {
2708
			$message = 0;
2709
		}
2710

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

    
2713
	} else {
2714
		portal_reply_page($my_redirurl, "redir", "Just redirect the user.");
2715
	}
2716

    
2717
	return $sessionid;
2718
}
2719

    
2720

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

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

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

    
2742
	if ($freeloginscount < 1 || $resettimeout <= 0 || !$clientmac) {
2743
		return false;
2744
	}
2745

    
2746
	$updatetimeouts = isset($config['captiveportal'][$cpzone]['freelogins_updatetimeouts']);
2747

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

    
2754
	$currenttime = time();
2755
	$found = false;
2756
	foreach ($usedmacs as $key => $usedmac) {
2757
		$usedmac = explode(",", $usedmac);
2758

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

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

    
2776
				$found = true;
2777
			} else {
2778
				unset($usedmacs[$key]);
2779
			}
2780

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

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

    
2792
	captiveportal_write_usedmacs_db($usedmacs);
2793
	xmlrpc_sync_usedmacs($usedmacs);
2794
	return true;
2795
}
2796

    
2797
function xmlrpc_sync_usedmacs($usedmacs) { 
2798
	global $config, $cpzone;
2799

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

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

    
2820
function captiveportal_read_usedmacs_db() {
2821
	global $g, $cpzone;
2822

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

    
2833
	unlock($cpumaclck);
2834
	return $usedmacs;
2835
}
2836

    
2837
function captiveportal_write_usedmacs_db($usedmacs) {
2838
	global $g, $cpzone;
2839

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

    
2845
function captiveportal_blocked_mac($mac) {
2846
	global $config, $g, $cpzone;
2847

    
2848
	if (empty($mac) || !is_macaddr($mac)) {
2849
		return false;
2850
	}
2851

    
2852
	if (!is_array($config['captiveportal'][$cpzone]['passthrumac'])) {
2853
		return false;
2854
	}
2855

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

    
2863
	return false;
2864

    
2865
}
2866

    
2867
/* Captiveportal Radius Accounting */
2868

    
2869
function gigawords($bytes) {
2870

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

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

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

    
2884
	return $gigawords;
2885
}
2886

    
2887
function remainder($bytes) {
2888
	// Calculate the bytes we are going to send to the radius
2889
	$bytes = bcmod($bytes, GIGAWORDS_RIGHT_OPERAND);
2890

    
2891
	if (is_null($bytes)) {
2892
		$bytes = 0;
2893
	}
2894

    
2895
    return $bytes;
2896
}
2897

    
2898
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) {
2899
	global $cpzone, $config;
2900

    
2901
	$cpcfg = $config['captiveportal'][$cpzone];
2902
	$acctcfg = auth_get_authserver($cpcfg['radacct_server']);
2903

    
2904
	if (!isset($cpcfg['radacct_enable']) || empty($acctcfg) ||
2905
	    captiveportal_ha_is_node_in_backup_mode($cpzone)) {
2906
		return null;
2907
	}
2908

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

    
2932
	$racct->addServer($acctcfg['host'], $acctcfg['radius_acct_port'],
2933
		$acctcfg['radius_secret'], $acctcfg['radius_timeout']);
2934

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

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

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

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

    
2960
	if (is_int($ruleno)) {
2961
		$racct->putAttribute(RADIUS_NAS_PORT_TYPE, RADIUS_ETHERNET);
2962
		$racct->putAttribute(RADIUS_NAS_PORT, intval($ruleno), 'integer');
2963
	}
2964

    
2965
	if (!empty($sessionid)) {
2966
		$racct->putAttribute(RADIUS_ACCT_SESSION_ID, $sessionid);
2967
	}
2968

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

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

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

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

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

    
3009
	// Send request
3010
	$result = $racct->send();
3011

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

    
3019
	$racct->close();
3020
	return $result;
3021
}
3022

    
3023
function captiveportal_isip_logged($clientip) {
3024
	global $g, $cpzone;
3025

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

    
3034
?>
(6-6/61)