Project

General

Profile

Download (100 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-2021 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_configure() {
200
	global $config, $g, $cpzone, $cpzoneid;
201

    
202
	if (is_array($config['captiveportal'])) {
203
		foreach ($config['captiveportal'] as $cpzone) {
204
			if (isset($cpzone['preservedb'])) {
205
				$keep_online_users = true;
206
				break;
207
			}
208
		}
209
		if (!$keep_online_users) {
210
			/* see https://redmine.pfsense.org/issues/12455 */
211
			unlink_if_exists("{$g['vardb_path']}/captiveportal_online_users");
212
		}
213
		foreach ($config['captiveportal'] as $cpkey => $cp) {
214
			$cpzone = $cpkey;
215
			$cpzoneid = $cp['zoneid'];
216
			captiveportal_configure_zone($cp);
217
		}
218
	}
219
}
220

    
221
function captiveportal_configure_zone($cpcfg, $reload_rules = false) {
222
	global $config, $g, $cpzone, $cpzoneid;
223

    
224
	$captiveportallck = lock("captiveportal{$cpzone}", LOCK_EX);
225

    
226
	if (isset($cpcfg['enable'])) {
227

    
228
		if (platform_booting()) {
229
			echo "Starting captive portal({$cpcfg['zone']})... ";
230
		} else {
231
			captiveportal_syslog("Reconfiguring captive portal({$cpcfg['zone']}).");
232
		}
233

    
234
		/* init captive portal pipes and anchors */
235
		captiveportal_init_rules($reload_rules);
236

    
237
		/* kill any running minicron */
238
		killbypid("{$g['varrun_path']}/cp_prunedb_{$cpzone}.pid");
239

    
240
		/* initialize minicron interval value */
241
		$croninterval = $cpcfg['croninterval'] ? $cpcfg['croninterval'] : 60;
242

    
243
		/* double check if the $croninterval is numeric and at least 10 seconds. If not we set it to 60 to avoid problems */
244
		if ((!is_numeric($croninterval)) || ($croninterval < 10)) {
245
			$croninterval = 60;
246
		}
247

    
248
		/* write portal page */
249
		if (is_array($cpcfg['page']) && $cpcfg['page']['htmltext']) {
250
			$htmltext = base64_decode($cpcfg['page']['htmltext']);
251
		} else {
252
			/* example/template page */
253
			$htmltext = get_default_captive_portal_html();
254
		}
255

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

    
275
		/* write error page */
276
		if (is_array($cpcfg['page']) && $cpcfg['page']['errtext']) {
277
			$errtext = base64_decode($cpcfg['page']['errtext']);
278
		} else {
279
			/* example page  */
280
			$errtext = get_default_captive_portal_html();
281
		}
282

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

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

    
337
document.location.href="<?=\$my_redirurl;?>";
338
//]]>
339
</script>
340
</body>
341
</html>
342

    
343
EOD;
344
		}
345

    
346
		$fd = @fopen("{$g['varetc_path']}/captiveportal-{$cpzone}-logout.html", "w");
347
		if ($fd) {
348
			fwrite($fd, $logouttext);
349
			fclose($fd);
350
		}
351
		unset($logouttext);
352

    
353
		/* write elements */
354
		captiveportal_write_elements();
355

    
356
		/* kill any running CP nginx instances */
357
		killbypid("{$g['varrun_path']}/nginx-{$cpzone}-CaptivePortal.pid", 0.1);
358
		killbypid("{$g['varrun_path']}/nginx-{$cpzone}-CaptivePortal-SSL.pid", 0.1);
359

    
360
		/* start up the webserving daemon */
361
		captiveportal_init_webgui_zone($cpcfg);
362

    
363
		/* Kill any existing prunecaptiveportal processes */
364
		if (file_exists("{$g['varrun_path']}/cp_prunedb_{$cpzone}.pid")) {
365
			killbypid("{$g['varrun_path']}/cp_prunedb_{$cpzone}.pid");
366
		}
367

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

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

    
375
		if (platform_booting() || $reload_rules) {
376
			/* send Accounting-On to server */
377
			captiveportal_send_server_accounting('on');
378
			echo "done\n";
379

    
380
			if (isset($cpcfg['preservedb']) || $reload_rules ||
381
			    captiveportal_xmlrpc_sync_get_details($syncip, $syncport, $syncuser, $syncpass)) {
382

    
383
				$connected_users = captiveportal_read_db();
384
				if (!empty($connected_users)) {
385
					echo "Reconnecting users to captive portal {$cpcfg['zone']}... ";
386
					foreach ($connected_users as $user) {
387
						captiveportal_reserve_ruleno($user['pipeno']);
388
						captiveportal_ether_configure_entry($user, 'auth', true);
389
					}
390
					echo "done\n";
391
				}
392
			} else {
393
				/* reset database on unclean shutdown, see https://redmine.pfsense.org/issues/12355 */
394
				unlink_if_exists("{$g['vardb_path']}/captiveportal{$cpzone}.db");
395
			}
396
		}
397
	} else {
398
		killbypid("{$g['varrun_path']}/nginx-{$cpzone}-CaptivePortal.pid");
399
		killbypid("{$g['varrun_path']}/nginx-{$cpzone}-CaptivePortal-SSL.pid");
400
		killbypid("{$g['varrun_path']}/cp_prunedb_{$cpzone}.pid");
401
		@unlink("{$g['varetc_path']}/captiveportal_{$cpzone}.html");
402
		@unlink("{$g['varetc_path']}/captiveportal-{$cpzone}-error.html");
403
		@unlink("{$g['varetc_path']}/captiveportal-{$cpzone}-logout.html");
404

    
405
		captiveportal_radius_stop_all(10); // NAS-Request
406

    
407
		/* Release allocated pipes for this zone */
408
		$pipes_to_remove = captiveportal_free_dnrules();
409
		captiveportal_delete_rules($pipes_to_remove);
410

    
411
		/* remove old information */
412
		unlink_if_exists("{$g['vardb_path']}/captiveportal{$cpzone}.db");
413
		unlink_if_exists("{$g['vardb_path']}/captiveportal_radius_{$cpzone}.db");
414
		unlink_if_exists("{$g['vardb_path']}/captiveportal_{$cpzone}.rules");
415
	}
416

    
417
	unlock($captiveportallck);
418

    
419
	return 0;
420
}
421

    
422
function captiveportal_init_webgui() {
423
	global $config, $cpzone;
424

    
425
	if (is_array($config['captiveportal'])) {
426
		foreach ($config['captiveportal'] as $cpkey => $cp) {
427
			$cpzone = $cpkey;
428
			captiveportal_init_webgui_zone($cp);
429
		}
430
	}
431
}
432

    
433
function captiveportal_init_webgui_zonename($zone) {
434
	global $config, $cpzone;
435

    
436
	if (isset($config['captiveportal'][$zone])) {
437
		$cpzone = $zone;
438
		captiveportal_init_webgui_zone($config['captiveportal'][$zone]);
439
	}
440
}
441

    
442
function captiveportal_init_webgui_zone($cpcfg) {
443
	global $g, $config, $cpzone;
444

    
445
	if (!isset($cpcfg['enable'])) {
446
		return;
447
	}
448

    
449
	if (isset($cpcfg['httpslogin'])) {
450
		$cert = lookup_cert($cpcfg['certref']);
451
		$crt = base64_decode($cert['crt']);
452
		$key = base64_decode($cert['prv']);
453
		$ca = ca_chain($cert);
454

    
455
		/* generate nginx configuration */
456
		if (!empty($cpcfg['listenporthttps'])) {
457
			$listenporthttps = $cpcfg['listenporthttps'];
458
		} else {
459
			$listenporthttps = 8001 + $cpcfg['zoneid'];
460
		}
461
		system_generate_nginx_config("{$g['varetc_path']}/nginx-{$cpzone}-CaptivePortal-SSL.conf",
462
			$crt, $key, $ca, "nginx-{$cpzone}-CaptivePortal-SSL.pid", $listenporthttps, "/usr/local/captiveportal",
463
			"cert-{$cpzone}-portal.pem", "ca-{$cpzone}-portal.pem", $cpzone, false);
464
	}
465

    
466
	/* generate nginx configuration */
467
	if (!empty($cpcfg['listenporthttp'])) {
468
		$listenporthttp = $cpcfg['listenporthttp'];
469
	} else {
470
		$listenporthttp = 8000 + $cpcfg['zoneid'];
471
	}
472
	system_generate_nginx_config("{$g['varetc_path']}/nginx-{$cpzone}-CaptivePortal.conf",
473
		"", "", "", "nginx-{$cpzone}-CaptivePortal.pid", $listenporthttp, "/usr/local/captiveportal",
474
		"", "", $cpzone, false);
475

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

    
480
	/* fire up https instance */
481
	if (isset($cpcfg['httpslogin'])) {
482
		@unlink("{$g['varrun']}/nginx-{$cpzone}-CaptivePortal-SSL.pid");
483
		$res = mwexec("/usr/local/sbin/nginx -c {$g['varetc_path']}/nginx-{$cpzone}-CaptivePortal-SSL.conf");
484
	}
485
}
486

    
487
/* reinit will disconnect all users, be careful! */
488
function captiveportal_init_rules($reinit = false) {
489
	global $config, $g, $cpzone;
490

    
491
	if (!isset($config['captiveportal'][$cpzone]['enable'])) {
492
		return;
493
	}
494

    
495
	dummynet_load_module('100');
496

    
497
	/* Cleanup so nothing is leaked */
498
	captiveportal_free_dnrules(2000, 64500, false, $reinit);
499

    
500
	$captiveportallck = try_lock("captiveportal{$cpzone}", 0);
501

    
502
	/* delete all anchors */
503
	captiveportal_delete_rules(array(), $reinit);
504

    
505
	/* load passthru mac anchors */
506
	captiveportal_passthrumac_configure();
507

    
508
	/* load allowedip anchors */
509
	captiveportal_allowedip_configure();
510

    
511
	/* load allowed hostname anchors */
512
	captiveportal_allowedhostname_configure();
513

    
514
	if ($captiveportallck) {
515
		unlock($captiveportallck);
516
	}
517
}
518

    
519
/* Delete all rules related to specific cpzone */
520
function captiveportal_delete_rules($pipes_to_remove = array(), $clear_auth_rules = true) {
521
	global $g, $config, $cpzone;
522

    
523
	/* delete MAC passthru entries */
524
	init_config_arr(array('captiveportal', $cpzone, 'passthrumac'));
525
	foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $macent) {
526
		captiveportal_passthrumac_delete_entry($macent);
527
	}
528

    
529
	/* delete Allowed IP entries */
530
	init_config_arr(array('captiveportal', $cpzone, 'allowedip'));
531
	if (is_array($config['captiveportal'][$cpzone]['allowedip'])) {
532
		foreach ($config['captiveportal'][$cpzone]['allowedip'] as $ipent) {
533
			captiveportal_ether_delete_entry($ipent, 'allowedhosts');
534
		}
535
	}
536

    
537
	/* delete Allowed Hostnames entries */
538
	captiveportal_allowedhostname_cleanup();
539

    
540
	/* delete authenticated clients rules */
541
	$connected_users = captiveportal_read_db();
542
	if (!empty($connected_users) && $clear_auth_rules) {
543
		foreach ($connected_users as $user) {
544
			captiveportal_ether_delete_entry($user, 'auth');
545
		}
546
	}
547

    
548
	/* delete pipes */
549
	captiveportal_pipes_delete($pipes_to_remove);
550
}
551

    
552
/*
553
 * Remove clients that have been around for longer than the specified amount of time
554
 * db file structure:
555
 * timestamp,ipfw_rule_no(deprecated),clientip,clientmac,username,sessionid,password,session_timeout,idle_timeout,session_terminate_time,interim_interval,traffic_quota,auth_method,context
556
 * (password is in Base64 and only saved when reauthentication is enabled)
557
 */
558
function captiveportal_prune_old() {
559
	global $g, $config, $cpzone, $cpzoneid;
560

    
561
	if (empty($cpzone)) {
562
		return;
563
	}
564

    
565
	$cpcfg = config_get_path("captiveportal/{$cpzone}");
566
	$vcpcfg = config_get_path("voucher/{$cpzone}");
567

    
568
	/* check for expired entries */
569
	$idletimeout = 0;
570
	$timeout = 0;
571
	if (!empty($cpcfg['timeout']) && is_numeric($cpcfg['timeout'])) {
572
		$timeout = $cpcfg['timeout'] * 60;
573
	}
574

    
575
	if (!empty($cpcfg['idletimeout']) && is_numeric($cpcfg['idletimeout'])) {
576
		$idletimeout = $cpcfg['idletimeout'] * 60;
577
	}
578

    
579
	/* check for entries exceeding their traffic quota */
580
	$trafficquota = 0;
581
	if (!empty($cpcfg['trafficquota']) && is_numeric($cpcfg['trafficquota'])) {
582
		$trafficquota = $cpcfg['trafficquota'] * 1048576;
583
	}
584

    
585
	/* Is there any job to do? If we are in High Availability sync, are we in backup mode ? */
586
	if ((!$timeout && !$idletimeout && !$trafficquota && !isset($cpcfg['reauthenticate']) &&
587
	    !isset($cpcfg['radiussession_timeout']) && !isset($cpcfg['radiustraffic_quota']) &&
588
	    !isset($vcpcfg['enable']) && !isset($cpcfg['radacct_enable'])) ||
589
	    captiveportal_ha_is_node_in_backup_mode($cpzone)) {
590
		return;
591
	}
592

    
593

    
594
	/* Read database */
595
	/* NOTE: while this can be simplified in non radius case keep as is for now */
596
	$cpdb = captiveportal_read_db();
597

    
598
	$unsetindexes = array();
599
	$voucher_needs_sync = false;
600
	/*
601
	 * Snapshot the time here to use for calculation to speed up the process.
602
	 * If something is missed next run will catch it!
603
	 */
604
	$pruning_time = time();
605
	foreach ($cpdb as $cpentry) {
606
		$stop_time = $pruning_time;
607

    
608
		$timedout = false;
609
		$term_cause = 1;
610
		/* hard timeout or session_timeout from radius if enabled */
611
		if (isset($cpcfg['radiussession_timeout'])) {
612
			$utimeout = (is_numeric($cpentry[7])) ? $cpentry[7] : $timeout;
613
		} else {
614
			$utimeout = $timeout;
615
		}
616
		if ($utimeout) {
617
			if (($pruning_time - $cpentry[0]) >= $utimeout) {
618
				$timedout = true;
619
				$term_cause = 5; // Session-Timeout
620
				$logout_cause = 'SESSION TIMEOUT';
621
			}
622
		}
623

    
624
		/* Session-Terminate-Time */
625
		if (!$timedout && !empty($cpentry[9])) {
626
			if ($pruning_time >= $cpentry[9]) {
627
				$timedout = true;
628
				$term_cause = 5; // Session-Timeout
629
				$logout_cause = 'SESSION TIMEOUT';
630
			}
631
		}
632

    
633
		/* check if an idle_timeout has been set and if its set change the idletimeout to this value */
634
		$uidletimeout = (is_numeric($cpentry[8])) ? $cpentry[8] : $idletimeout;
635
		/* if an idle timeout is specified, get last activity timestamp from pf */
636
		if (!$timedout && $uidletimeout > 0) {
637
			$lastact = captiveportal_get_last_activity($cpentry[2]);
638
			/*	If the user has logged on but not sent any traffic they will never be logged out.
639
			 *	We "fix" this by setting lastact to the login timestamp.
640
			 */
641
			$lastact = $lastact ? $lastact : $cpentry[0];
642
			if ($lastact && (($pruning_time - $lastact) >= $uidletimeout)) {
643
				$timedout = true;
644
				$term_cause = 4; // Idle-Timeout
645
				$logout_cause = 'IDLE TIMEOUT';
646
				if (!isset($config['captiveportal'][$cpzone]['includeidletime'])) {
647
					$stop_time = $lastact;
648
				}
649
			}
650
		}
651

    
652
		/* if vouchers are configured, activate session timeouts */
653
		if (!$timedout && isset($vcpcfg['enable']) && !empty($cpentry[7])) {
654
			if ($pruning_time >= ($cpentry[0] + $cpentry[7])) {
655
				$timedout = true;
656
				$term_cause = 5; // Session-Timeout
657
				$logout_cause = 'SESSION TIMEOUT';
658
				$voucher_needs_sync = true;
659
			}
660
		}
661

    
662
		/* traffic quota, value retrieved from the radius attribute if the option is enabled */
663
		if (isset($cpcfg['radiustraffic_quota'])) {
664
			$utrafficquota = (is_numeric($cpentry[11])) ? $cpentry[11] : $trafficquota;
665
		} else {
666
			$utrafficquota = $trafficquota;
667
		}
668

    
669
		if (!$timedout && $utrafficquota > 0) {
670
			$volume = getVolume($cpentry[2]);
671
			if (($volume['input_bytes'] + $volume['output_bytes']) > $utrafficquota) {
672
				$timedout = true;
673
				$term_cause = 10; // NAS-Request
674
				$logout_cause = 'QUOTA EXCEEDED';
675
			}
676
		}
677

    
678
		if ($timedout) {
679
			captiveportal_disconnect($cpentry, $term_cause, $stop_time);
680
			captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], $logout_cause);
681
			$unsetindexes[] = $cpentry[5];
682
		}
683

    
684
		/* do periodic reauthentication? For Radius servers, send accounting updates? */
685
		if (!$timedout) {
686
			//Radius servers : send accounting
687
			if (isset($cpcfg['radacct_enable']) && $cpentry['authmethod'] === 'radius') {
688
				if (substr($cpcfg['reauthenticateacct'], 0, 9) === "stopstart") {
689
					/* stop and restart accounting */
690
					if ($cpcfg['reauthenticateacct'] === "stopstartfreeradius") {
691
						$rastart_time = 0;
692
						$rastop_time = 60;
693
					} else {
694
						$rastart_time = $cpentry[0];
695
						$rastop_time = time();
696
					}
697
					captiveportal_send_server_accounting('stop',
698
						$cpentry[1], // ruleno
699
						$cpentry[4], // username
700
						$cpentry[2], // clientip
701
						$cpentry[3], // clientmac
702
						$cpentry[5], // sessionid
703
						$rastart_time, // start time
704
						$rastop_time, // Stop Time
705
						10); // NAS Request
706
					/* XXX rewrite to C wrapper pfSense_pf_anchor_zerocnt() */
707
					captiveportal_anchor_zerocnt($cpentry[2], 'auth');
708
					if ($cpcfg['reauthenticateacct'] == "stopstartfreeradius") {
709
						/* Need to pause here or the FreeRADIUS server gets confused about packet ordering. */
710
						sleep(1);
711
					}
712
					captiveportal_send_server_accounting('start',
713
						$cpentry[1], // ruleno
714
						$cpentry[4], // username
715
						$cpentry[2], // clientip
716
						$cpentry[3], // clientmac
717
						$cpentry[5]); // sessionid
718
				} else if ($cpcfg['reauthenticateacct'] == "interimupdate") {
719
					$session_time = $pruning_time - $cpentry[0];
720
					if (!empty($cpentry[10]) && $cpentry[10] > 60) {
721
						$interval = $cpentry[10];
722
					} else {
723
						$interval = 0;
724
					}
725
					$past_interval_min = ($session_time > $interval);
726
					if ($interval != 0) {
727
						$within_interval = ($session_time % $interval >= 0 && $session_time % $interval <= 59);
728
					}
729
					if ($interval === 0 || ($interval > 0 && $past_interval_min && $within_interval)) {
730
					captiveportal_send_server_accounting('update',
731
						$cpentry[1], // ruleno
732
						$cpentry[4], // username
733
						$cpentry[2], // clientip
734
						$cpentry[3], // clientmac
735
						$cpentry[5], // sessionid
736
						$cpentry[0]); // start time
737
					}
738
				}
739
			}
740

    
741
			/* check this user again */
742
			if (isset($cpcfg['reauthenticate']) && $cpentry['context'] !== 'voucher') {
743
				$auth_result = captiveportal_authenticate_user(
744
					$cpentry[4], // username
745
					base64_decode($cpentry[6]), // password
746
					$cpentry[3], // clientmac
747
					$cpentry[2], // clientip
748
					$cpentry[1], // ruleno
749
					$cpentry['context']); // context
750
				if ($auth_result['result'] === false) {
751
					captiveportal_disconnect($cpentry, 17);
752
					captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "DISCONNECT - REAUTHENTICATION FAILED", $auth_list['reply_message']);
753
					$unsetindexes[] = $cpentry[5];
754
				} else if ($auth_result['result'] === true) {
755
					if ($cpentry['authmethod'] !== $auth_result['auth_method']) {
756
						// if the user got authenticated against another server type:  we update the database
757
						if (!empty($cpentry[5])) {
758
							captiveportal_update_entry($cpentry['sessionid'], $auth_result['auth_method'], 'authmethod');
759
							captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CHANGED AUTHENTICATION SERVER", $auth_list['reply_message']);
760
						}
761
						// User was logged on a RADIUS server, but is now logged in by another server type : we send an accounting Stop
762
						if (isset($config['captiveportal'][$cpzone]['radacct_enable']) && $cpentry['authmethod'] == 'radius') {
763
							if ($cpcfg['reauthenticateacct'] === "stopstartfreeradius") {
764
								$rastart_time = 0;
765
								$rastop_time = 60;
766
							} else {
767
								$rastart_time = $cpentry[0];
768
								$rastop_time = time();
769
							}
770
							captiveportal_send_server_accounting('stop',
771
								$cpentry[1], // ruleno
772
								$cpentry[4], // username
773
								$cpentry[2], // clientip
774
								$cpentry[3], // clientmac
775
								$cpentry[5], // sessionid
776
								$rastart_time, // start time
777
								$rastop_time, // Stop Time
778
								3); // Lost Service
779
						// User was logged on a non-RADIUS Server but is now logged in by a RADIUS server : we send an accounting Start
780
						} else if(isset($config['captiveportal'][$cpzone]['radacct_enable']) && $auth_result['auth_method'] === 'radius') {
781
							captiveportal_send_server_accounting('start',
782
								$cpentry[1], // ruleno
783
								$cpentry[4], // username
784
								$cpentry[2], // clientip
785
								$cpentry[3], // clientmac
786
								$cpentry[5], // sessionid
787
								$cpentry[0]); // start_time
788
						}
789
					}
790
					captiveportal_reapply_attributes($cpentry, $auth_result['attributes']);
791
				}
792
			}
793
		}
794
	}
795
	unset($cpdb);
796

    
797
	captiveportal_prune_old_automac();
798

    
799
	if ($voucher_needs_sync == true) {
800
		/* perform in-use vouchers expiration using check_reload_status */
801
		send_event("service sync vouchers");
802
	}
803

    
804
	/* write database */
805
	if (!empty($unsetindexes)) {
806
		captiveportal_remove_entries($unsetindexes);
807
	}
808
}
809

    
810
function captiveportal_prune_old_automac() {
811
	global $g, $config, $cpzone, $cpzoneid;
812

    
813
	if (is_array($config['captiveportal'][$cpzone]['passthrumac']) &&
814
	    isset($config['captiveportal'][$cpzone]['passthrumacadd'])) {
815
		$tmpvoucherdb = array();
816
		$writecfg = false;
817
		foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $eid => $emac) {
818
			if ($emac['logintype'] != "voucher") {
819
				continue;
820
			}
821
			if (isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) {
822
				if (isset($tmpvoucherdb[$emac['username']])) {
823
					$temac = config_get_path("captiveportal/{$cpzone}/passthrumac/{$tmpvoucherdb[$emac['username']]}");
824
					$pipes = captiveportal_get_dn_passthru_pipes($temac['mac']);
825
					if ($pipes) {
826
						captiveportal_ether_delete_entry($temac['mac'], 'auth');
827
					}
828
					$writecfg = true;
829
					captiveportal_logportalauth($temac['username'], $temac['mac'],
830
					    $temac['ip'], "DUPLICATE {$temac['username']} LOGIN - TERMINATING OLD SESSION");
831
					config_del_path("captiveportal/{$cpzone}/passthrumac/{$tmpvoucherdb[$emac['username']]}");
832
				}
833
				$tmpvoucherdb[$emac['username']] = $eid;
834
			}
835
		}
836
		unset($tmpvoucherdb);
837
		if ($writecfg === true) {
838
			write_config("Prune session for auto-added macs");
839
		}
840
	}
841
}
842

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

    
847
	$stop_time = (empty($stop_time)) ? time() : $stop_time;
848

    
849
	/* this client needs to be deleted - remove pf anchor */
850
	if (isset($config['captiveportal'][$cpzone]['radacct_enable']) && $dbent['authmethod'] == 'radius') {
851
		if ($config['captiveportal'][$cpzone]['reauthenticateacct'] == "stopstartfreeradius") {
852
			/*
853
			 * Interim updates are on so the session time must be
854
			 * reported as the elapsed time since the previous
855
			 * interim update.
856
			 */
857
			$session_time = ($stop_time - $dbent[0]) % 60;
858
			$start_time = $stop_time - $session_time;
859
		} else {
860
			$start_time = $dbent[0];
861
		}
862
		captiveportal_send_server_accounting('stop',
863
			$dbent[1], // ruleno
864
			$dbent[4], // username
865
			$dbent[2], // clientip
866
			$dbent[3], // clientmac
867
			$dbent[5], // sessionid
868
			$start_time, // start time
869
			$stop_time, // stop time
870
			$term_cause); // Acct-Terminate-Cause
871
	}
872

    
873
	if (is_ipaddrv4($dbent[2])) {
874
		/*
875
		 * Delete client's anchor entry from auth anchor
876
		 */
877
		$cpsession = captiveportal_isip_logged($dbent[2]);
878
		if (!empty($cpsession)) {
879
			$host = array();
880
			$host['ip'] = $dbent[2];
881
			if (!isset($config['captiveportal'][$cpzone]['nomacfilter'])) {
882
				$host['mac'] = $dbent[3];
883
			}
884
			captiveportal_ether_delete_entry($host, 'auth');
885
		}
886
		/* XXX: Redundant?! Ensure all pf(4) states are killed. */
887
		$_gb = @pfSense_kill_states($dbent[2]);
888
		$_gb = @pfSense_kill_srcstates($dbent[2]);
889
	}
890

    
891
	// XMLRPC Call over to the backup node if necessary
892
	if (captiveportal_xmlrpc_sync_get_details($syncip, $syncport,
893
	    $syncuser, $syncpass, $carp_loop)) {
894
		$rpc_client = new pfsense_xmlrpc_client();
895
		$rpc_client->setConnectionData($syncip, $syncport, $syncuser, $syncpass);
896
		$rpc_client->set_noticefile("CaptivePortalUserSync");
897
		$arguments = array(
898
			'sessionid' => $dbent[5],
899
			'term_cause' => $term_cause,
900
			'stop_time' => $stop_time
901
		);
902

    
903
		$rpc_client->xmlrpc_method('captive_portal_sync',
904
			array(
905
				'op' => 'disconnect_user',
906
				'zone' => $cpzone,
907
				'session' => base64_encode(serialize($arguments))
908
			)
909
		);
910
	}
911
	return true;
912
}
913

    
914
/* remove a single client by sessionid */
915
function captiveportal_disconnect_client($sessionid, $term_cause = 1, $logoutReason = "LOGOUT") {
916
	global $g, $config;
917

    
918
	$sessionid = SQLite3::escapeString($sessionid);
919
	/* read database */
920
	$result = captiveportal_read_db("WHERE sessionid = '{$sessionid}'");
921

    
922
	/* find entry */
923
	if (!empty($result)) {
924
		foreach ($result as $cpentry) {
925
			captiveportal_disconnect($cpentry, $term_cause);
926
			captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "DISCONNECT");
927
		}
928
		captiveportal_remove_entries(array($sessionid));
929
	}
930
}
931

    
932
/* remove all clients */
933
function captiveportal_disconnect_all($term_cause = 6, $logoutReason = "DISCONNECT", $carp_loop = false) {
934
	global $g, $config, $cpzone, $cpzoneid;
935

    
936
	if (captiveportal_xmlrpc_sync_get_details($syncip, $syncport, $syncuser, $syncpass, $carp_loop)) {
937
		$rpc_client = new pfsense_xmlrpc_client();
938
		$rpc_client->setConnectionData($syncip, $syncport, $syncuser, $syncpass);
939
		$rpc_client->set_noticefile("CaptivePortalUserSync");
940
		$arguments = array(
941
			'term_cause' => $term_cause,
942
			'logout_reason' => $logoutReason
943
		);
944

    
945
		$rpc_client->xmlrpc_method('captive_portal_sync',
946
			array(
947
				'op' => 'disconnect_all',
948
				'zone' => $cpzone,
949
				'arguments' => base64_encode(serialize($arguments))
950
			)
951
		);
952
	}
953
	/* check if we're pruning old entries and eventually wait */
954
	$rcprunelock = try_lock("rcprunecaptiveportal{$cpzone}", 15);
955

    
956
	/* if we still don't have the lock, unlock forcefully and take it */
957
	if (!$rcprunelock) {
958
		log_error("CP zone ${cpzone}: could not obtain the lock for more than 15 seconds, lock taken forcefully to disconnect all users");
959
		unlock_force("rcprunecaptiveportal{$cpzone}");
960
		$rcprunelock = lock("rcprunecaptiveportal{$cpzone}", LOCK_EX);
961
	}
962

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

    
966
	captiveportal_radius_stop_all($term_cause, $logoutReason);
967

    
968
	/* reinit captiveportal pipes and anchors */
969
	captiveportal_init_rules(true);
970

    
971
	/* remove users from the database */
972
	$cpdb = captiveportal_read_db();
973
	$unsetindexes = array_column($cpdb,5);
974
	if (!empty($unsetindexes)) {
975
		// High Availability : do not sync removed entries
976
		captiveportal_remove_entries($unsetindexes, true);
977
	}
978

    
979
	unlock($cpdblck);
980
	unlock($rcprunelock);
981
	return true;
982
}
983

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

    
988
	$cpdb = captiveportal_read_db();
989

    
990
	$radacct = isset($config['captiveportal'][$cpzone]['radacct_enable']) ? true : false;
991
	foreach ($cpdb as $cpentry) {
992
		if ($cpentry['authmethod'] === 'radius' && $radacct) {
993
			if ($config['captiveportal'][$cpzone]['reauthenticateacct'] == "stopstartfreeradius") {
994
				$session_time = (time() - $cpentry[0]) % 60;
995
				$start_time = time() - $session_time;
996
			} else {
997
				$start_time = $cpentry[0];
998
			}
999
			captiveportal_send_server_accounting('stop',
1000
				$cpentry[1], // ruleno
1001
				$cpentry[4], // username
1002
				$cpentry[2], // clientip
1003
				$cpentry[3], // clientmac
1004
				$cpentry[5], // sessionid
1005
				$start_time, // start time
1006
				$stop_time, // stop time
1007
				$term_cause); // Acct-Terminate-Cause
1008
		}
1009
		captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], $logoutReason);
1010
	}
1011
	unset($cpdb);
1012
}
1013

    
1014
function captiveportal_passthrumac_delete_entry($macent) {
1015
	global $g, $cpzone, $config;
1016

    
1017
	$host = str_replace("/", "_", str_replace(":", "", $macent['mac']));
1018
	$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
1019

    
1020
	if ($macent['action'] == 'pass') {
1021
		$pipes = captiveportal_get_dn_passthru_pipes($macent['mac']);
1022
		if (!empty($pipes)) {
1023
			captiveportal_pipes_delete($pipes);
1024
		}
1025
	} else {
1026
		/* no rules on passthru block */
1027
		return;
1028
	}
1029

    
1030
	pfSense_pf_cp_flush("{$cpzoneprefix}_passthrumac/{$host}", "ether");
1031
}
1032

    
1033
function captiveportal_passthrumac_configure($startindex = 0, $stopindex = 0) {
1034
	global $config, $g, $cpzone;
1035

    
1036
	if (is_array($config['captiveportal'][$cpzone]['passthrumac'])) {
1037
		if ($stopindex > 0) {
1038
			for ($idx = $startindex; $idx <= $stopindex; $idx++) {
1039
				if (isset($config['captiveportal'][$cpzone]['passthrumac'][$idx])) {
1040
					captiveportal_ether_configure_entry($config['captiveportal'][$cpzone]['passthrumac'][$idx], 'passthrumac');
1041
				}
1042
			}
1043
		} else {
1044
			$nentries = count($config['captiveportal'][$cpzone]['passthrumac']);
1045
			if ($nentries > 2000) {
1046
				$nloops = $nentries / 1000;
1047
				$remainder= $nentries % 1000;
1048
				for ($i = 0; $i < $nloops; $i++) {
1049
					mwexec_bg("/usr/local/sbin/fcgicli -f /etc/rc.captiveportal_configure_mac -d \"cpzone={$cpzone}&startidx=" . ($i * 1000) . "&stopidx=" . ((($i+1) * 1000) - 1) . "\"");
1050
				}
1051
				if ($remainder > 0) {
1052
					mwexec_bg("/usr/local/sbin/fcgicli -f /etc/rc.captiveportal_configure_mac -d \"cpzone={$cpzone}&startidx=" . ($i * 1000) . "&stopidx=" . (($i* 1000) + $remainder) ."\"");
1053
				}
1054
			} else {
1055
				foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $macent) {
1056
					captiveportal_ether_configure_entry($macent, 'passthrumac');
1057
				}
1058
			}
1059
		}
1060
	}
1061
}
1062

    
1063
function captiveportal_passthrumac_findbyname($username) {
1064
	global $config, $cpzone;
1065

    
1066
	if (is_array($config['captiveportal'][$cpzone]['passthrumac'])) {
1067
		foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $macent) {
1068
			if ($macent['username'] == $username) {
1069
				return $macent;
1070
			}
1071
		}
1072
	}
1073
	return NULL;
1074
}
1075

    
1076
function captiveportal_ether_delete_entry($hostent, $anchor = 'allowedhosts') {
1077
	global $g, $cpzone, $config;
1078

    
1079
	$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
1080

    
1081
	if (!empty($hostent['sn'])) {
1082
		$host = $hostent['ip'] . '_' . $hostent['sn'];
1083
	} else {
1084
		$host = $hostent['ip'] . '_32';
1085
	}
1086

    
1087
	$pipes = pfSense_pf_cp_get_eth_pipes("{$cpzoneprefix}_{$anchor}/{$host}");
1088
	if (!empty($pipes)) {
1089
		captiveportal_pipes_delete($pipes);
1090
	}
1091
	/* flush anchor rules */
1092
	pfSense_pf_cp_flush("{$cpzoneprefix}_{$anchor}/{$host}", "ether");
1093
}
1094

    
1095
function captiveportal_allowedhostname_configure() {
1096
	global $config, $g, $cpzone, $cpzoneid;
1097

    
1098
	if (!is_array($config['captiveportal'][$cpzone]['allowedhostname'])) {
1099
		return false;
1100
	}
1101

    
1102
	$cp_filterdns_conf = "";
1103
	foreach ($config['captiveportal'][$cpzone]['allowedhostname'] as $id => $hostnameent) {
1104
		$cp_filterdns_conf .= captiveportal_allowedhostname_configure_entry($hostnameent, $id);
1105
	}
1106
	$cp_filterdns_filename = "{$g['varetc_path']}/filterdns-{$cpzone}-captiveportal.conf";
1107
	if ((!file_exists($cp_filterdns_filename) && !empty($cp_filterdns_conf)) ||
1108
	    (file_exists($cp_filterdns_filename) && ($cp_filterdns_conf != file_get_contents($cp_filterdns_filename)))) {
1109
		@file_put_contents($cp_filterdns_filename, $cp_filterdns_conf);
1110
		filter_configure();
1111
		captiveportal_filterdns_configure();
1112
	}
1113
}
1114

    
1115
function captiveportal_filterdns_configure() {
1116
	global $config, $g, $cpzone, $cpzoneid;
1117

    
1118
	$cp_filterdns_filename = g_get('varetc_path') .
1119
	    "/filterdns-{$cpzone}-captiveportal.conf";
1120

    
1121
	if (isset($config['captiveportal'][$cpzone]['enable']) &&
1122
	    is_array($config['captiveportal'][$cpzone]['allowedhostname']) &&
1123
	    file_exists($cp_filterdns_filename) &&
1124
	    !empty(file_get_contents($cp_filterdns_filename))) {
1125
		if (isvalidpid(g_get('varrun_path') .
1126
		    "/filterdns-{$cpzone}-cpah.pid")) {
1127
			sigkillbypid(g_get('varrun_path') .
1128
			    "/filterdns-{$cpzone}-cpah.pid", "HUP");
1129
		} else {
1130
			mwexec("/usr/local/sbin/filterdns -p " .
1131
			    "{$g['varrun_path']}/filterdns-{$cpzone}-cpah.pid" .
1132
			    " -i 300 -c {$cp_filterdns_filename} -d 1");
1133
		}
1134
	} else {
1135
		killbypid("{$g['varrun_path']}/filterdns-{$cpzone}-cpah.pid");
1136
		@unlink("{$g['varrun_path']}/filterdns-{$cpzone}-cpah.pid");
1137
	}
1138
}
1139

    
1140
function captiveportal_allowedip_configure() {
1141
	global $config, $g, $cpzone;
1142

    
1143
	if (is_array($config['captiveportal'][$cpzone]['allowedip'])) {
1144
		foreach ($config['captiveportal'][$cpzone]['allowedip'] as $ipent) {
1145
			captiveportal_allowedip_configure_entry($ipent);
1146
		}
1147
	}
1148
}
1149

    
1150
/* get last activity timestamp given client IP address */
1151
function captiveportal_get_last_activity($ip) {
1152
	global $config, $cpzone;
1153

    
1154
	$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
1155
	$anchor = $cpzoneprefix . '_auth';
1156

    
1157
	$active_times = pfSense_pf_cp_get_eth_last_active("{$anchor}/{$ip}_32");
1158
	$time = 0;
1159
	if (!empty($active_times)) {
1160
		foreach ($active_times as $active_time) {
1161
			if ( $active_time > $time)
1162
				$time = $active_time;
1163
	   }
1164
	}
1165

    
1166
	return $time;
1167
}
1168

    
1169

    
1170
/* log successful captive portal authentication to syslog */
1171
/* part of this code from php.net */
1172
function captiveportal_logportalauth($user, $mac, $ip, $status, $message = null) {
1173
	// Log it
1174
	if (!$message) {
1175
		$message = "{$status}: {$user}, {$mac}, {$ip}";
1176
	} else {
1177
		$message = trim($message);
1178
		$message = "{$status}: {$user}, {$mac}, {$ip}, {$message}";
1179
	}
1180
	captiveportal_syslog($message);
1181
}
1182

    
1183
/* log simple messages to syslog */
1184
function captiveportal_syslog($message) {
1185
	global $cpzone;
1186

    
1187
	$message = trim($message);
1188
	$message = "Zone: {$cpzone} - {$message}";
1189
	openlog("logportalauth", LOG_PID, LOG_LOCAL4);
1190
	// Log it
1191
	syslog(LOG_INFO, $message);
1192
	closelog();
1193
}
1194

    
1195
/* Authenticate users using Authentication Backend */
1196
function captiveportal_authenticate_user(&$login = '', &$password = '', $clientmac = '', $clientip = '', $pipeno = 'null', $context = 'first') {
1197
	global $g, $config, $cpzone;
1198
	$cpcfg = config_get_path("captiveportal/{$cpzone}");
1199

    
1200
	$login_status = 'FAILURE';
1201
	$login_msg = gettext('Invalid credentials specified');
1202
	$reply_attributes = array();
1203
	$auth_method = '';
1204
	$auth_result = null;
1205

    
1206
	/*
1207
	Management of the reply Message (reason why the authentication failed) :
1208
	multiple authentication servers can be used, so multiple reply messages could theoretically be returned.
1209
	But only one message is returned (the most important one).
1210
	The return value of authenticate_user() define how important messages are :
1211
		- Reply message of a successful auth is more important than reply message of
1212
		a user failed auth(invalid credentials/authorization)
1213

    
1214
		- Reply message of a user failed auth is more important than reply message of
1215
		a server failed auth (unable to contact server)
1216

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

    
1220
	The $authlevel variable is a flag indicating the status of authentication
1221
	0 = failed server auth
1222
	1 = failed user auth
1223
	2 = failed user auth with custom server reply received
1224
	3 = successful auth
1225
	*/
1226
	$authlevel = 0;
1227

    
1228
	/* Getting authentication servers from captiveportal configuration */
1229
	$auth_servers = array();
1230

    
1231
	if ($cpcfg['auth_method'] === 'none') {
1232
		$auth_servers[] = array('type' => 'none');
1233
	} else {
1234
		if ($context === 'second') {
1235
			$fullauthservers = explode(",", $cpcfg['auth_server2']);
1236
		} else {
1237
			$fullauthservers = explode(",", $cpcfg['auth_server']);
1238
		}
1239

    
1240
		foreach ($fullauthservers as $authserver) {
1241
			if (strpos($authserver, ' - ') !== false) {
1242
				$authserver = explode(' - ', $authserver);
1243
				array_shift($authserver);
1244
				$authserver = implode(' - ', $authserver);
1245

    
1246
				if (auth_get_authserver($authserver) !== null) {
1247
					$auth_servers[] = auth_get_authserver($authserver);
1248
				} else {
1249
					log_error("Zone: {$cpzone} - Captive portal was unable to find the settings of the server '{$authserver}' used for authentication !");
1250
				}
1251
			}
1252
		}
1253
	}
1254

    
1255
	/* Unable to find the any authentication server config - shouldn't happen! - bail out */
1256
	if (count($auth_servers) === 0) {
1257
		log_error("Zone: {$cpzone} - No valid server could be used for authentication.");
1258
		$login_msg = gettext("Internal Error");
1259
	} else {
1260
		foreach ($auth_servers as $authcfg) {
1261
			if ($authlevel < 3) {
1262
				$radmac_error = false;
1263
				$attributes = array("nas_identifier" => empty($cpcfg["radiusnasid"]) ? "CaptivePortal-{$cpzone}" : $cpcfg["radiusnasid"],
1264
					"nas_port_type" => RADIUS_ETHERNET,
1265
					"nas_port" => $pipeno,
1266
					"framed_ip" => $clientip);
1267
				if (mac_format($clientmac) !== null) {
1268
					$attributes["calling_station_id"] = mac_format($clientmac);
1269
				}
1270

    
1271
				$result = null;
1272
				$status = null;
1273
				$msg = null;
1274

    
1275
				/* Radius MAC authentication */
1276
				if ($context === 'radmac' && $clientmac) {
1277
					if ($authcfg['type'] === 'radius') {
1278
						$login = mac_format($clientmac);
1279
						$status = "MACHINE LOGIN";
1280
					} else {
1281
						/* Trying to perform a Radius MAC authentication on a non-radius server - shouldn't happen! - bail out */
1282
						$msg = gettext("Internal Error");
1283
						log_error("Zone: {$cpzone} - Trying to perform RADIUS MAC authentication on a non-RADIUS server !");
1284
						$radmac_error = true;
1285
						$result = null;
1286
					}
1287
				}
1288

    
1289
				if (!$radmac_error) {
1290
					if ($authcfg['type'] === 'none') {
1291
						$result = true;
1292
					} else {
1293
						$result = authenticate_user($login, $password, $authcfg, $attributes);
1294
					}
1295

    
1296
					if (!empty($attributes['error_message'])) {
1297
						$msg = $attributes['error_message'];
1298
					}
1299

    
1300
					if ($authcfg['type'] == 'Local Auth' && $result && isset($cpcfg['localauth_priv'])) {
1301
						if (!userHasPrivilege(getUserEntry($login), "user-services-captiveportal-login")) {
1302
							$result = false;
1303
							$msg = gettext("Access Denied");
1304
						}
1305
					}
1306
					if ($context === 'radmac' && $result === null && empty($attributes['reply_message'])) {
1307
						$msg = gettext("RADIUS MAC Authentication Failed.");
1308
					}
1309

    
1310
					if (empty($status)) {
1311
						if ($result === true) {
1312
							$status = "ACCEPT";
1313
						} elseif ($result === null) {
1314
							$status = "ERROR";
1315
						} else {
1316
							$status = "FAILURE";
1317
						}
1318
					}
1319

    
1320
					if ($context === 'radmac' && $login == mac_format($clientmac) || $authcfg['type'] === 'none' && empty($login)) {
1321
						$login = "unauthenticated";
1322
					}
1323
				}
1324
				// We determine a flag
1325
				if ($result === true) {
1326
					$val = 3;
1327
				} elseif ($result === false && !empty($attributes['reply_message'])) {
1328
					$val = 2;
1329
					$msg = $attributes['reply_message'];
1330
				} elseif ($result === false) {
1331
					$val = 1;
1332
				} elseif ($result === null) {
1333
					$val = 0;
1334
				}
1335

    
1336
				if ($val >= $authlevel) {
1337
					$authlevel = $val;
1338
					$auth_method = $authcfg['type'];
1339
					$login_status = $status;
1340
					$login_msg = $msg;
1341
					$reply_attributes = $attributes;
1342
					$auth_result = $result;
1343
				}
1344
			}
1345
		}
1346
	}
1347

    
1348
	return array('result'=>$auth_result, 'attributes'=>$reply_attributes, 'auth_method' =>$auth_method, 'login_status'=> $login_status, 'login_message' => $login_msg);
1349
}
1350

    
1351
function captiveportal_opendb() {
1352
	global $g, $config, $cpzone, $cpzoneid;
1353

    
1354
	$db_path = "{$g['vardb_path']}/captiveportal{$cpzone}.db";
1355
	$createquery = "CREATE TABLE IF NOT EXISTS captiveportal (" .
1356
				"allow_time INTEGER, pipeno INTEGER, ip TEXT, mac TEXT, username TEXT, " .
1357
				"sessionid TEXT, bpassword TEXT, session_timeout INTEGER, idle_timeout INTEGER, " .
1358
				"session_terminate_time INTEGER, interim_interval INTEGER, traffic_quota INTEGER, " .
1359
				"bw_up INTEGER, bw_down INTEGER, authmethod TEXT, context TEXT); " .
1360
			"CREATE UNIQUE INDEX IF NOT EXISTS idx_active ON captiveportal (sessionid, username); " .
1361
			"CREATE INDEX IF NOT EXISTS user ON captiveportal (username); " .
1362
			"CREATE INDEX IF NOT EXISTS ip ON captiveportal (ip); " .
1363
			"CREATE INDEX IF NOT EXISTS starttime ON captiveportal (allow_time)";
1364

    
1365
	try {
1366
		$DB = new SQLite3($db_path);
1367
		$DB->busyTimeout(60000);
1368
	} catch (Exception $e) {
1369
		captiveportal_syslog("Could not open {$db_path} as an sqlite database for {$cpzone}. Error message: " . $e->getMessage() . " -- Trying again.");
1370
		unlink_if_exists($db_path);
1371
		try {
1372
			$DB = new SQLite3($db_path);
1373
			$DB->busyTimeout(60000);
1374
		} catch (Exception $e) {
1375
			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.");
1376
			return;
1377
		}
1378
	}
1379

    
1380
	if (!$DB) {
1381
		captiveportal_syslog("Could not open {$db_path} as an sqlite database for {$cpzone}. Error message: {$DB->lastErrorMsg()}. Trying again.");
1382
		unlink_if_exists($db_path);
1383
		$DB = new SQLite3($db_path);
1384
		$DB->busyTimeout(60000);
1385
		if (!$DB) {
1386
			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.");
1387
			return;
1388
		}
1389
	}
1390

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

    
1394
		/* If unable to initialize the database, reset and try again. */
1395
		$DB->close();
1396
		unset($DB);
1397
		unlink_if_exists($db_path);
1398
		$DB = new SQLite3($db_path);
1399
		$DB->busyTimeout(60000);
1400
		if ($DB->exec($createquery)) {
1401
			captiveportal_syslog("Successfully reinitialized tables for {$cpzone} -- database has been reset.");
1402
			if (!is_numericint($cpzoneid)) {
1403
				if (is_array($config['captiveportal'])) {
1404
					foreach ($config['captiveportal'] as $cpkey => $cp) {
1405
						if ($cpzone == $cpkey) {
1406
							$cpzoneid = $cp['zoneid'];
1407
						}
1408
					}
1409
				}
1410
			}
1411
			if (is_numericint($cpzoneid)) {
1412
				captiveportal_delete_rules(array(), true);
1413
				filter_configure();
1414
				captiveportal_syslog("Flushed tables for {$cpzone} after database reset.");
1415
			}
1416
		} else {
1417
			captiveportal_syslog("Still unable to create tables for {$cpzone}. Error message: {$DB->lastErrorMsg()}. Remove the database file manually and try again.");
1418
		}
1419
	}
1420

    
1421
	return $DB;
1422
}
1423

    
1424
/* read captive portal DB into array */
1425
function captiveportal_read_db($query = "") {
1426
	$cpdb = array();
1427

    
1428
	$DB = captiveportal_opendb();
1429
	if ($DB) {
1430
		$response = $DB->query("SELECT * FROM captiveportal {$query}");
1431
		if ($response != FALSE) {
1432
			while ($row = $response->fetchArray()) {
1433
				$cpdb[] = $row;
1434
			}
1435
		}
1436
		$DB->close();
1437
	}
1438

    
1439
	return $cpdb;
1440
}
1441

    
1442
function captiveportal_remove_entries($remove, $carp_loop = false) {
1443
	global $cpzone;
1444

    
1445
	if (!is_array($remove) || empty($remove)) {
1446
		return;
1447
	}
1448

    
1449
	$query = "DELETE FROM captiveportal WHERE sessionid in (";
1450
	foreach ($remove as $idx => $unindex) {
1451
		$query .= "'{$unindex}'";
1452
		if ($idx < (count($remove) - 1)) {
1453
			$query .= ",";
1454
		}
1455
	}
1456
	$query .= ")";
1457
	captiveportal_write_db($query);
1458

    
1459
	if (captiveportal_xmlrpc_sync_get_details($syncip, $syncport, $syncuser, $syncpass, $carp_loop)) {
1460
		$rpc_client = new pfsense_xmlrpc_client();
1461
		$rpc_client->setConnectionData($syncip, $syncport, $syncuser, $syncpass);
1462
		$rpc_client->set_noticefile("CaptivePortalUserSync");
1463
		$rpc_client->xmlrpc_method('captive_portal_sync',
1464
			array(
1465
				'op' => 'remove_entries',
1466
				'zone' => $cpzone,
1467
				'entries' => base64_encode(serialize($remove))
1468
			)
1469
		);
1470
	}
1471
	return true;
1472
}
1473

    
1474
/* write captive portal DB */
1475
function captiveportal_write_db($queries) {
1476
	global $g;
1477

    
1478
	if (is_array($queries)) {
1479
		$query = implode(";", $queries);
1480
	} else {
1481
		$query = $queries;
1482
	}
1483

    
1484
	$DB = captiveportal_opendb();
1485
	if ($DB) {
1486
		$DB->exec("BEGIN TRANSACTION");
1487
		$result = $DB->exec($query);
1488
		if (!$result) {
1489
			captiveportal_syslog("Trying to modify DB returned error: {$DB->lastErrorMsg()}");
1490
		} else {
1491
			$DB->exec("END TRANSACTION");
1492
		}
1493
		$DB->close();
1494
		return $result;
1495
	} else {
1496
		return true;
1497
	}
1498
}
1499

    
1500
function captiveportal_write_elements() {
1501
	global $g, $config, $cpzone;
1502

    
1503
	$cpcfg = config_get_path("captiveportal/{$cpzone}");
1504

    
1505
	if (!is_dir(g_get('captiveportal_element_path'))) {
1506
		@mkdir(g_get('captiveportal_element_path'));
1507
	}
1508

    
1509
	if (is_array($cpcfg['element'])) {
1510
		foreach ($cpcfg['element'] as $data) {
1511
			/* Do not attempt to decode or write out empty files. */
1512
			if (isset($data['nocontent'])) {
1513
					continue;
1514
			}
1515
			if (empty($data['content']) || empty(base64_decode($data['content']))) {
1516
				unlink_if_exists("{$g['captiveportal_element_path']}/{$data['name']}");
1517
				touch("{$g['captiveportal_element_path']}/{$data['name']}");
1518
			} elseif (!@file_put_contents("{$g['captiveportal_element_path']}/{$data['name']}", base64_decode($data['content']))) {
1519
				printf(gettext('Error: cannot open \'%1$s\' in captiveportal_write_elements()%2$s'), $data['name'], "\n");
1520
				return 1;
1521
			}
1522
			if (!file_exists("{$g['captiveportal_path']}/{$data['name']}")) {
1523
				@symlink("{$g['captiveportal_element_path']}/{$data['name']}", "{$g['captiveportal_path']}/{$data['name']}");
1524
			}
1525
		}
1526
	}
1527

    
1528
	return 0;
1529
}
1530

    
1531
function captiveportal_free_dnrules($rulenos_start = 2000,
1532
    $rulenos_range_max = 64500, $dry_run = false, $clear_auth_pipes = true) {
1533
	global $g, $cpzone;
1534

    
1535
	$removed_pipes = array();
1536

    
1537
	if (!file_exists("{$g['vardb_path']}/captiveportaldn.rules")) {
1538
		return $removed_pipes;
1539
	}
1540

    
1541
	if (!$dry_run) {
1542
		$cpruleslck = lock("captiveportalrulesdn", LOCK_EX);
1543
	}
1544

    
1545
	$rules = unserialize(file_get_contents(
1546
	    "{$g['vardb_path']}/captiveportaldn.rules"));
1547
	$ridx = $rulenos_start;
1548
	while ($ridx < $rulenos_range_max) {
1549
		if (substr($rules[$ridx], 0, strlen($cpzone . '_')) == $cpzone . '_') {
1550
			if (!$clear_auth_pipes && substr($rules[$ridx], 0, strlen($cpzone . '_auth')) == $cpzone . '_auth') {
1551
				$ridx += 2;
1552
			} else {
1553
				if (!$dry_run) {
1554
					$rules[$ridx] = false;
1555
				}
1556
				$removed_pipes[] = $ridx;
1557
				$ridx++;
1558
				if (!$dry_run) {
1559
					$rules[$ridx] = false;
1560
				}
1561
				$removed_pipes[] = $ridx;
1562
				$ridx++;
1563
			}
1564
		} else {
1565
			$ridx += 2;
1566
		}
1567
	}
1568

    
1569
	if (!$dry_run) {
1570
		file_put_contents("{$g['vardb_path']}/captiveportaldn.rules",
1571
		    serialize($rules));
1572
		unlock($cpruleslck);
1573
	}
1574

    
1575
	unset($rules);
1576

    
1577
	return $removed_pipes;
1578
}
1579

    
1580
function captiveportal_reserve_ruleno($ruleno) {
1581
	global $g, $cpzone;
1582

    
1583
	$cpruleslck = lock("captiveportalrulesdn", LOCK_EX);
1584
	if (file_exists("{$g['vardb_path']}/captiveportaldn.rules")) {
1585
		$rules = unserialize(file_get_contents("{$g['vardb_path']}/captiveportaldn.rules"));
1586
	} else {
1587
		$rules = array_pad(array(), 64500, false);
1588
	}
1589
	$rules[$ruleno] = $cpzone . '_auth';
1590
	$ruleno++;
1591
	$rules[$ruleno] = $cpzone . '_auth';
1592

    
1593
	file_put_contents("{$g['vardb_path']}/captiveportaldn.rules", serialize($rules));
1594
	unlock($cpruleslck);
1595
	unset($rules);
1596

    
1597
	return $ruleno;
1598
}
1599

    
1600
function captiveportal_get_next_dn_ruleno($rule_type = 'default', $rulenos_start = 2000, $rulenos_range_max = 64500, $check_only = false) {
1601
	global $config, $g, $cpzone;
1602

    
1603
	$cpruleslck = lock("captiveportalrulesdn", LOCK_EX);
1604
	$ruleno = 0;
1605
	if (file_exists("{$g['vardb_path']}/captiveportaldn.rules")) {
1606
		$rules = unserialize(file_get_contents("{$g['vardb_path']}/captiveportaldn.rules"));
1607
		$ridx = $rulenos_start;
1608
		while ($ridx < $rulenos_range_max) {
1609
			if (empty($rules[$ridx])) {
1610
				$ruleno = $ridx;
1611
				$rules[$ridx] = $cpzone . '_' . $rule_type;
1612
				$ridx++;
1613
				$rules[$ridx] = $cpzone . '_' . $rule_type;
1614
				break;
1615
			} else {
1616
				$ridx += 2;
1617
			}
1618
		}
1619
	} else {
1620
		$rules = array_pad(array(), $rulenos_range_max, false);
1621
		$ruleno = $rulenos_start;
1622
		$rules[$rulenos_start] = $cpzone . '_' . $rule_type;
1623
		$rulenos_start++;
1624
		$rules[$rulenos_start] = $cpzone . '_' . $rule_type;
1625
	}
1626
	if (!$check_only) {
1627
		file_put_contents("{$g['vardb_path']}/captiveportaldn.rules", serialize($rules));
1628
	}
1629
	unlock($cpruleslck);
1630
	unset($rules);
1631

    
1632
	return $ruleno;
1633
}
1634

    
1635
function captiveportal_free_dn_rulenos($rulenos) {
1636
	global $config, $g;
1637

    
1638
	$cpruleslck = lock("captiveportalrulesdn", LOCK_EX);
1639
	if (file_exists("{$g['vardb_path']}/captiveportaldn.rules")) {
1640
		$rules = unserialize(file_get_contents("{$g['vardb_path']}/captiveportaldn.rules"));
1641
		foreach ($rulenos as $ruleno) {
1642
			$rules[$ruleno] = false;
1643
		}
1644
		file_put_contents("{$g['vardb_path']}/captiveportaldn.rules", serialize($rules));
1645
		unset($rules);
1646
	}
1647
	unlock($cpruleslck);
1648
}
1649

    
1650
function captiveportal_get_dn_passthru_pipes($mac, $anchor = 'passthrumac') {
1651
	global $config, $g, $cpzone, $cpzoneid;
1652

    
1653
	$cpcfg = config_get_path("captiveportal/{$cpzone}");
1654
	$cpzoneprefix = CPPREFIX . $cpcfg['zoneid'];
1655
	if (!isset($cpcfg['enable'])) {
1656
		return NULL;
1657
	}
1658

    
1659
	$host = str_replace(":", "", $mac);
1660
	$pipes = pfSense_pf_cp_get_eth_pipes("{$cpzoneprefix}_{$anchor}/$host");
1661

    
1662
	return $pipes;
1663
}
1664

    
1665
/**
1666
 * This function will calculate the traffic produced by a client
1667
 * based on its firewall rule
1668
 *
1669
 * Point of view: NAS
1670
 *
1671
 * Input means: from the client
1672
 * Output means: to the client
1673
 *
1674
 */
1675

    
1676
function getVolume($ip) {
1677
	global $g, $config, $cpzone;
1678

    
1679
	$reverse = isset($config['captiveportal'][$cpzone]['reverseacct']) ? true : false;
1680
	$volume = array();
1681
	// Initialize vars properly, since we don't want NULL vars
1682
	$volume['input_pkts'] = $volume['input_bytes'] = 0;
1683
	$volume['output_pkts'] = $volume['output_bytes'] = 0;
1684

    
1685
	/* no needs to check allowedip */
1686
	$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
1687
	$anchor = $cpzoneprefix . '_auth';
1688

    
1689
	// It is presumed that a list of arrays is returned, each containing rule direction, and packet and bytes counters.
1690
	$result = pfSense_pf_cp_get_eth_rule_counters("{$anchor}/{$ip}_32");
1691
	if (!empty($result) && is_array($result)) {
1692
		$input_pkts = 0;
1693
		$input_bytes = 0;
1694
		$output_pkts = 0;
1695
		$output_bytes = 0;
1696

    
1697
		foreach ($result as $rule_counters) {
1698
			switch ($rule_counters[0]) {
1699
				case 1: // rule direction 'PF_IN'
1700
					$input_pkts += $rule_counters['input_pkts'];
1701
					$input_bytes += $rule_counters['input_bytes'];
1702
					break;
1703
				case 2: // rule direction 'PF_OUT'
1704
					$output_pkts += $rule_counters['output_pkts'];
1705
					$output_bytes += $rule_counters['output_bytes'];
1706
					break;
1707
				case 0: // rule direction 'PF_INOUT'
1708
					$input_pkts += $rule_counters['input_pkts'];
1709
					$input_bytes += $rule_counters['input_bytes'];
1710
					$output_pkts += $rule_counters['output_pkts'];
1711
					$output_bytes += $rule_counters['output_bytes'];
1712
					break;
1713
				default:
1714
					break;
1715
			}
1716
		}
1717

    
1718
		if ($reverse) {
1719
			$volume['output_pkts'] = $input_pkts;
1720
			$volume['output_bytes'] = $input_bytes;
1721
			$volume['input_pkts'] = $output_pkts;
1722
			$volume['input_bytes'] = $output_bytes;
1723
		} else {
1724
			$volume['output_pkts'] = $output_pkts;
1725
			$volume['output_bytes'] = $output_bytes;
1726
			$volume['input_pkts'] = $input_pkts;
1727
			$volume['input_bytes'] = $input_bytes;
1728
		}
1729
	}
1730

    
1731
	return $volume;
1732
}
1733

    
1734
function portal_ip_from_client_ip($cliip) {
1735
	global $config, $cpzone;
1736

    
1737
	$isipv6 = is_ipaddrv6($cliip);
1738
	$interfaces = explode(",", $config['captiveportal'][$cpzone]['interface']);
1739
	foreach ($interfaces as $cpif) {
1740
		if ($isipv6) {
1741
			$ip = get_interface_ipv6($cpif);
1742
			$sn = get_interface_subnetv6($cpif);
1743
		} else {
1744
			$ip = get_interface_ip($cpif);
1745
			$sn = get_interface_subnet($cpif);
1746
		}
1747
		if (ip_in_subnet($cliip, "{$ip}/{$sn}")) {
1748
			return $ip;
1749
		}
1750
	}
1751

    
1752
	$route = route_get($cliip, 'inet', true);
1753
	if (empty($route)) {
1754
		return false;
1755
	}
1756

    
1757
	$iface = $route[0]['interface-name'];
1758
	if (!empty($iface)) {
1759
		$ip = ($isipv6) ? find_interface_ipv6($iface)
1760
		    : find_interface_ip($iface);
1761
		if (is_ipaddr($ip)) {
1762
			return $ip;
1763
		}
1764
	}
1765

    
1766
	// doesn't match up to any particular interface
1767
	// so let's set the portal IP to what PHP says
1768
	// the server IP issuing the request is.
1769
	// allows same behavior as 1.2.x where IP isn't
1770
	// in the subnet of any CP interface (static routes, etc.)
1771
	// rather than forcing to DNS hostname resolution
1772
	$ip = $_SERVER['SERVER_ADDR'];
1773
	if (is_ipaddr($ip)) {
1774
		return $ip;
1775
	}
1776

    
1777
	return false;
1778
}
1779

    
1780
function portal_hostname_from_client_ip($cliip) {
1781
	global $config, $cpzone;
1782

    
1783
	$cpcfg = config_get_path("captiveportal/{$cpzone}");
1784

    
1785
	if (isset($cpcfg['httpslogin'])) {
1786
		$listenporthttps = $cpcfg['listenporthttps'] ? $cpcfg['listenporthttps'] : ($cpcfg['zoneid'] + 8001);
1787
		$ourhostname = $cpcfg['httpsname'];
1788

    
1789
		if ($listenporthttps != 443) {
1790
			$ourhostname .= ":" . $listenporthttps;
1791
		}
1792
	} else {
1793
		$listenporthttp = $cpcfg['listenporthttp'] ? $cpcfg['listenporthttp'] : ($cpcfg['zoneid'] + 8000);
1794
		$ifip = portal_ip_from_client_ip($cliip);
1795
		if (!$ifip) {
1796
			$ourhostname = "{$config['system']['hostname']}.{$config['system']['domain']}";
1797
		} else {
1798
			$ourhostname = (is_ipaddrv6($ifip)) ? "[{$ifip}]" : "{$ifip}";
1799
		}
1800

    
1801
		if ($listenporthttp != 80) {
1802
			$ourhostname .= ":" . $listenporthttp;
1803
		}
1804
	}
1805

    
1806
	return $ourhostname;
1807
}
1808

    
1809
/* functions move from index.php */
1810

    
1811
function portal_reply_page($redirurl, $type = null, $message = null, $clientmac = null, $clientip = null, $username = null, $password = null, $voucher = null) {
1812
	global $g, $config, $cpzone;
1813

    
1814
	$cpcfg = config_get_path("captiveportal/{$cpzone}");
1815
	$ourhostname = portal_hostname_from_client_ip($clientip);
1816
	$protocol = (isset($cpcfg['httpslogin'])) ? 'https://' : 'http://';
1817
	$portal_url = "{$protocol}{$ourhostname}/index.php?zone={$cpzone}";
1818

    
1819
	/* Get captive portal layout */
1820
	if ($type == "redir") {
1821
		$redirurl = is_URL($redirurl, true) ? $redirurl : $portal_url;
1822
		header("Location: {$redirurl}");
1823
		return;
1824
	} else if ($type == "login") {
1825
		$htmltext = get_include_contents("{$g['varetc_path']}/captiveportal_{$cpzone}.html");
1826
	} else {
1827
		$htmltext = get_include_contents("{$g['varetc_path']}/captiveportal-{$cpzone}-error.html");
1828
	}
1829

    
1830
	/* substitute the PORTAL_REDIRURL variable */
1831
	if ($cpcfg['preauthurl']) {
1832
		$htmltext = str_replace("\$PORTAL_REDIRURL\$", "{$cpcfg['preauthurl']}", $htmltext);
1833
		$htmltext = str_replace("#PORTAL_REDIRURL#", "{$cpcfg['preauthurl']}", $htmltext);
1834
	}
1835

    
1836
	/* substitute other variables */
1837
	$htmltext = str_replace("\$PORTAL_ACTION\$", $portal_url, $htmltext);
1838
	$htmltext = str_replace("#PORTAL_ACTION#", $portal_url, $htmltext);
1839

    
1840
	$htmltext = str_replace("\$PORTAL_ZONE\$", htmlspecialchars($cpzone), $htmltext);
1841
	$htmltext = str_replace("\$PORTAL_REDIRURL\$", htmlspecialchars($redirurl), $htmltext);
1842
	$htmltext = str_replace("\$PORTAL_MESSAGE\$", htmlspecialchars($message), $htmltext);
1843
	$htmltext = str_replace("\$CLIENT_MAC\$", htmlspecialchars($clientmac), $htmltext);
1844
	$htmltext = str_replace("\$CLIENT_IP\$", htmlspecialchars($clientip), $htmltext);
1845

    
1846
	// Special handling case for captive portal master page so that it can be ran
1847
	// through the PHP interpreter using the include method above.  We convert the
1848
	// $VARIABLE$ case to #VARIABLE# in /etc/inc/captiveportal.inc before writing out.
1849
	$htmltext = str_replace("#PORTAL_ZONE#", htmlspecialchars($cpzone), $htmltext);
1850
	$htmltext = str_replace("#PORTAL_REDIRURL#", htmlspecialchars($redirurl), $htmltext);
1851
	$htmltext = str_replace("#PORTAL_MESSAGE#", htmlspecialchars($message), $htmltext);
1852
	$htmltext = str_replace("#CLIENT_MAC#", htmlspecialchars($clientmac), $htmltext);
1853
	$htmltext = str_replace("#CLIENT_IP#", htmlspecialchars($clientip), $htmltext);
1854
	$htmltext = str_replace("#USERNAME#", htmlspecialchars($username), $htmltext);
1855
	$htmltext = str_replace("#PASSWORD#", htmlspecialchars($password), $htmltext);
1856
	$htmltext = str_replace("#VOUCHER#", htmlspecialchars($voucher), $htmltext);
1857

    
1858
	echo $htmltext;
1859
}
1860

    
1861
function captiveportal_reapply_attributes($cpentry, $attributes) {
1862
	global $config, $cpzone, $g;
1863

    
1864
	if (isset($config['captiveportal'][$cpzone]['peruserbw'])) {
1865
		$dwfaultbw_up = !empty($config['captiveportal'][$cpzone]['bwdefaultup']) ? $config['captiveportal'][$cpzone]['bwdefaultup'] : 0;
1866
		$dwfaultbw_down = !empty($config['captiveportal'][$cpzone]['bwdefaultdn']) ? $config['captiveportal'][$cpzone]['bwdefaultdn'] : 0;
1867
	} else {
1868
		$dwfaultbw_up = $dwfaultbw_down = 0;
1869
	}
1870
	/* pipe throughputs must always be an integer, enforce that restriction again here. */
1871
	if (isset($config['captiveportal'][$cpzone]['radiusperuserbw'])) {
1872
		$bw_up = round(!empty($attributes['bw_up']) ? intval($attributes['bw_up'])/1000 : $dwfaultbw_up, 0);
1873
		$bw_down = round(!empty($attributes['bw_down']) ? intval($attributes['bw_down'])/1000 : $dwfaultbw_down, 0);
1874
	} else {
1875
		$bw_up = round($dwfaultbw_up,0);
1876
		$bw_down = round($dwfaultbw_down,0);
1877
	}
1878

    
1879
	$bw_up_pipeno = $cpentry[1];
1880
	$bw_down_pipeno = $cpentry[1]+1;
1881

    
1882
	if ($cpentry['bw_up'] !== $bw_up) {
1883
		$_gb = mwexec("/sbin/dnctl pipe {$bw_up_pipeno} config bw {$bw_up}Kbit/s queue 100 buckets 16");
1884
		captiveportal_update_entry($cpentry['sessionid'], $bw_up, 'bw_up');
1885
	}
1886
	if ($cpentry['bw_down'] !== $bw_down) {
1887
		$_gb = mwexec("/sbin/dnctl pipe {$bw_down_pipeno} config bw {$bw_down}Kbit/s queue 100 buckets 16");
1888
		captiveportal_update_entry($cpentry['sessionid'], $bw_down, 'bw_down');
1889
	}
1890
	unset($bw_up_pipeno, $bw_down_pipeno, $bw_up, $bw_down);
1891
}
1892

    
1893
function captiveportal_update_entry($sessionid, $new_value, $field_to_update) {
1894
	global $cpzone;
1895

    
1896
	if (!intval($new_value)) {
1897
		$new_value = "'{$new_value}'";
1898
	}
1899
	captiveportal_write_db("UPDATE captiveportal SET {$field_to_update} = {$new_value} WHERE sessionid = '{$sessionid}'");
1900
}
1901

    
1902
function portal_allow($clientip, $clientmac, $username, $password = null, $redirurl = null,
1903
    $attributes = null, $pipeno = null, $authmethod = null, $context = 'first', $existing_sessionid = null) {
1904
	global $g, $config, $cpzone;
1905

    
1906
	// Ensure we create an array if we are missing attributes
1907
	if (!is_array($attributes)) {
1908
		$attributes = array();
1909
	}
1910

    
1911
	unset($sessionid);
1912

    
1913
	/* Do not allow concurrent login execution. */
1914
	$cpdblck = lock("captiveportaldb{$cpzone}", LOCK_EX);
1915

    
1916
	if ($attributes['voucher']) {
1917
		$remaining_time = $attributes['session_timeout'];
1918
		$authmethod = "voucher"; // Set RADIUS-Attribute to Voucher to prevent ReAuth-Request for Vouchers Bug: #2155
1919
		$context = "voucher";
1920
	}
1921

    
1922
	$writecfg = false;
1923
	/* If both "Add MAC addresses of connected users as pass-through MAC" and "Disable concurrent logins" are checked,
1924
	then we need to check if the user was already authenticated using another MAC Address, and if so remove the previous Pass-Through MAC. */
1925
	if ((isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) && ($config['captiveportal'][$cpzone]['noconcurrentlogins'] == 'last') && ($username != 'unauthenticated') && isset($config['captiveportal'][$cpzone]['passthrumacadd'])) {
1926
		$mac = captiveportal_passthrumac_findbyname($username);
1927
		if (!empty($mac)) {
1928
			foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $idx => $macent) {
1929
				if ($macent['mac'] != $mac['mac']) {
1930
					continue;
1931
				}
1932

    
1933
				captiveportal_passthrumac_delete_entry($macent);
1934
				config_del_path("captiveportal/{$cpzone}/passthrumac/{$idx}");
1935
			}
1936
		}
1937
	}
1938

    
1939
	/* read in client database */
1940
	$query = "WHERE ip = '{$clientip}'";
1941
	$tmpusername = SQLite3::escapeString(strtolower($username));
1942
	if (isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) {
1943
		$query .= " OR (username != 'unauthenticated' AND lower(username) = '{$tmpusername}')";
1944
	}
1945
	$cpdb = captiveportal_read_db($query);
1946

    
1947
	/* Snapshot the timestamp */
1948
	$allow_time = time();
1949

    
1950
	if ($existing_sessionid !== null) {
1951
		// If we received this connection through XMLRPC sync :
1952
		// we fetch allow_time from the info given by the other node
1953
		$allow_time = $attributes['allow_time'];
1954
	}
1955
	$unsetindexes = array();
1956

    
1957
	foreach ($cpdb as $cpentry) {
1958
		/* on the same ip */
1959
		if ($cpentry[2] == $clientip) {
1960
			if (isset($config['captiveportal'][$cpzone]['nomacfilter']) || $cpentry[3] == $clientmac) {
1961
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT LOGIN - REUSING OLD SESSION");
1962
			} else {
1963
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT LOGIN - REUSING IP {$cpentry[2]} WITH DIFFERENT MAC ADDRESS {$cpentry[3]}");
1964
			}
1965
			$sessionid = $cpentry[5];
1966
			break;
1967
		} elseif (($attributes['voucher']) && ($username != 'unauthenticated') && ($cpentry[4] == $username)) {
1968
			// user logged in with an active voucher. Check for how long and calculate
1969
			// how much time we can give him (voucher credit - used time)
1970
			$remaining_time = $cpentry[0] + $cpentry[7] - $allow_time;
1971
			if ($remaining_time < 0) { // just in case.
1972
				$remaining_time = 0;
1973
			}
1974

    
1975
			/* This user was already logged in so we disconnect the old one, or
1976
			keep the old one, refusing the new login, or
1977
			allow the login */
1978

    
1979
			if (!isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) {
1980
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "config['captiveportal'][$cpzone]['noconcurrentlogins'] 2 does not exists = NOT set");
1981
			} else {
1982
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "config['captiveportal'][$cpzone]['noconcurrentlogins'] 2 exists = set");
1983
			}
1984

    
1985
			if ($config['captiveportal'][$cpzone]['noconcurrentlogins'] == "last") {
1986
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "Found last");
1987
			} else {
1988
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "Found NOT last");
1989
			}
1990

    
1991
			if (!isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) {
1992
				/* 'noconcurrentlogins' not set : accept login 'username' creating multiple sessions. */
1993
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "config['captiveportal'][$cpzone]['noconcurrentlogins'] 3 does not exists = NOT set");
1994
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT LOGIN - NOT TERMINATING EXISTING SESSION(S)");
1995
			} elseif ($config['captiveportal'][$cpzone]['noconcurrentlogins'] == "last") {
1996
				/* Classic situation : accept the new login, disconnect the old - present - connection */
1997
				if (isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) {
1998
					captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "config['captiveportal'][$cpzone]['noconcurrentlogins'] 4 exists = set");
1999
				} else {
2000
					captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "config['captiveportal'][$cpzone]['noconcurrentlogins'] 4 does not exists = NOT set");
2001
				}
2002

    
2003
				captiveportal_disconnect($cpentry, 13);
2004
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT LOGIN - TERMINATING OLD SESSION");
2005
				$unsetindexes[] = $cpentry[5];
2006
				break;
2007
			} else {
2008
				/* Implicit 'first' : refuse the new login - 'username' is already logged in */
2009
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT VOUCHER LOGIN - NOT ALLOWED KEEPING OLD SESSION ");
2010
				unlock($cpdblck);
2011
				return 2;
2012
			}
2013
		} elseif ((isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) && ($username != 'unauthenticated')) {
2014
			if ($config['captiveportal'][$cpzone]['noconcurrentlogins'] == "last") {
2015
				/* on the same username */
2016
				if (strcasecmp($cpentry[4], $username) == 0) {
2017
					/* This user was already logged in so we disconnect the old one */
2018
					captiveportal_disconnect($cpentry, 13);
2019
					captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT USER LOGIN - TERMINATING OLD SESSION");
2020
					$unsetindexes[] = $cpentry[5];
2021
					break;
2022
				}
2023
			} else {
2024
				/* Implicit 'first' : refuse the new login - 'username' is already logged in */
2025
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT USER LOGIN - NOT ALLOWED KEEPING OLD SESSION ");
2026
				unlock($cpdblck);
2027
				return 2;
2028
			}
2029
		}
2030
	}
2031
	unset($cpdb);
2032

    
2033
	if (!empty($unsetindexes)) {
2034
		captiveportal_remove_entries($unsetindexes);
2035
	}
2036

    
2037
	if ($attributes['voucher'] && $remaining_time <= 0) {
2038
		return 0;       // voucher already used and no time left
2039
	}
2040

    
2041
	if (!isset($sessionid)) {
2042
		if ($existing_sessionid != null) { // existing_sessionid should only be set during XMLRPC sync
2043
			$sessionid = $existing_sessionid;
2044
		} else {
2045
			/* generate unique session ID */
2046
			$tod = gettimeofday();
2047
			$sessionid = substr(md5(mt_rand() . $tod['sec'] . $tod['usec'] . $clientip . $clientmac), 0, 16);
2048
		}
2049

    
2050
		if (isset($config['captiveportal'][$cpzone]['peruserbw'])) {
2051
			$dwfaultbw_up = !empty($config['captiveportal'][$cpzone]['bwdefaultup']) ? $config['captiveportal'][$cpzone]['bwdefaultup'] : 0;
2052
			$dwfaultbw_down = !empty($config['captiveportal'][$cpzone]['bwdefaultdn']) ? $config['captiveportal'][$cpzone]['bwdefaultdn'] : 0;
2053
		} else {
2054
			$dwfaultbw_up = $dwfaultbw_down = 0;
2055
		}
2056
		/* pipe throughputs must always be an integer, enforce that restriction again here. */
2057
		if (isset($config['captiveportal'][$cpzone]['radiusperuserbw'])) {
2058
			$bw_up = round(!empty($attributes['bw_up']) ? intval($attributes['bw_up'])/1000 : $dwfaultbw_up, 0);
2059
			$bw_down = round(!empty($attributes['bw_down']) ? intval($attributes['bw_down'])/1000 : $dwfaultbw_down, 0);
2060
		} else {
2061
			$bw_up = round($dwfaultbw_up,0);
2062
			$bw_down = round($dwfaultbw_down,0);
2063
		}
2064

    
2065
		$mac = array();
2066
		$mac['action'] = 'pass';
2067
		$mac['ip'] = $clientip;
2068
		$mac['username'] = $username;
2069
		if (!empty($bw_up)) {
2070
			$mac['bw_up'] = $bw_up;
2071
		}
2072
		if (!empty($bw_down)) {
2073
			$mac['bw_down'] = $bw_down;
2074
		}
2075
		if (isset($config['captiveportal'][$cpzone]['passthrumacadd'])) {
2076
			$mac['mac'] = $clientmac;
2077
			if ($attributes['voucher']) {
2078
				$mac['logintype'] = "voucher";
2079
			}
2080
			if ($username == "unauthenticated") {
2081
				$mac['descr'] = "Auto-added";
2082
			} else if ($authmethod == "voucher") {
2083
				$mac['descr'] = "Auto-added for voucher {$username}";
2084
			} else {
2085
				$mac['descr'] = "Auto-added for user {$username}";
2086
			}
2087
			if (!is_array($config['captiveportal'][$cpzone]['passthrumac'])) {
2088
				$config['captiveportal'][$cpzone]['passthrumac'] = array();
2089
			}
2090
			//check for mac duplicates before adding it to config.
2091
			$mac_duplicate = false;
2092
			foreach($config['captiveportal'][$cpzone]['passthrumac'] as $mac_check){
2093
				if($mac_check['mac'] == $mac['mac']){
2094
					$mac_duplicate = true;
2095
				}
2096
			}
2097
			if(!$mac_duplicate){
2098
				$config['captiveportal'][$cpzone]['passthrumac'][] = $mac;
2099
			}
2100
			unlock($cpdblck);
2101
			captiveportal_ether_configure_entry($mac, 'passthrumac', true);
2102
			$writecfg = true;
2103
		} else {
2104
			/* See if a pipeno is passed, if not start sessions because this means there isn't one atm */
2105
			if (is_null($pipeno)) {
2106
				$pipeno = captiveportal_get_next_dn_ruleno('auth');
2107
			}
2108
			/* if the pool is empty, return appropriate message and exit */
2109
			if (is_null($pipeno)) {
2110
				captiveportal_syslog("Zone: {$cpzone} - WARNING!  Captive portal has reached maximum login capacity");
2111
				unlock($cpdblck);
2112
				return false;
2113
			}
2114

    
2115
			$mac['pipeno'] = $pipeno;
2116
			$mac['ip'] = $clientip;
2117
			if (!isset($config['captiveportal'][$cpzone]['nomacfilter'])) {
2118
				$mac['mac'] = $clientmac;
2119
			}
2120
			captiveportal_ether_configure_entry($mac, 'auth', true);
2121

    
2122
			if ($attributes['voucher']) {
2123
				$attributes['session_timeout'] = $remaining_time;
2124
			}
2125

    
2126
			/* handle empty attributes */
2127
			$session_timeout = (!empty($attributes['session_timeout'])) ? $attributes['session_timeout'] : 'NULL';
2128
			$idle_timeout = (!empty($attributes['idle_timeout'])) ? $attributes['idle_timeout'] : 'NULL';
2129
			$session_terminate_time = (!empty($attributes['session_terminate_time'])) ? $attributes['session_terminate_time'] : 'NULL';
2130
			$interim_interval = (!empty($attributes['interim_interval'])) ? $attributes['interim_interval'] : 'NULL';
2131
			$traffic_quota = (!empty($attributes['maxbytes'])) ? $attributes['maxbytes'] : 'NULL';
2132

    
2133
			/* escape username */
2134
			$safe_username = SQLite3::escapeString($username);
2135

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

    
2142
			/* store information to database */
2143
			captiveportal_write_db($insertquery);
2144
			unlock($cpdblck);
2145
			unset($insertquery, $bpassword);
2146

    
2147
			$radacct = isset($config['captiveportal'][$cpzone]['radacct_enable']) ? true : false;
2148
			if ($authmethod === 'radius' && $radacct) {
2149
				captiveportal_send_server_accounting('start',
2150
					$pipeno, // ruleno
2151
					$username, // username
2152
					$clientip, // clientip
2153
					$clientmac, // clientmac
2154
					$sessionid, // sessionid
2155
					time());  // start time
2156
			}
2157
			if (captiveportal_xmlrpc_sync_get_details($syncip, $syncport, $syncuser, $syncpass, isset($existing_sessionid))) {
2158
				// $existing_sessionid prevent carp loop : only forward
2159
				// the connection to the other node if we generated the sessionid by ourselves
2160
				$rpc_client = new pfsense_xmlrpc_client();
2161
				$rpc_client->setConnectionData($syncip, $syncport, $syncuser, $syncpass);
2162
				$rpc_client->set_noticefile("CaptivePortalUserSync");
2163
				$arguments = array(
2164
					'clientip' => $clientip,
2165
					'clientmac' => $clientmac,
2166
					'username' => $username,
2167
					'password' => $password,
2168
					'attributes' => $attributes,
2169
					'allow_time' => $allow_time,
2170
					'authmethod' => $authmethod,
2171
					'context' => $context,
2172
					'sessionid' => $sessionid
2173
				);
2174

    
2175
				$rpc_client->xmlrpc_method('captive_portal_sync',
2176
					array(
2177
						'op' => 'connect_user',
2178
						'zone' => $cpzone,
2179
						'user' => base64_encode(serialize($arguments))
2180
					)
2181
				);
2182
			}
2183
		}
2184
	} else {
2185
		/* NOTE: #3062-11 If the pipeno has been allocated free it to not DoS the CP */
2186
		if (!is_null($pipeno)) {
2187
			captiveportal_free_dn_rulenos(array($pipeno, $pipeno+1));
2188
		}
2189

    
2190
		unlock($cpdblck);
2191
	}
2192

    
2193
	if ($writecfg == true) {
2194
		write_config(gettext("Captive Portal allowed users configuration changed"));
2195
	}
2196

    
2197
	if ($existing_sessionid !== null) {
2198
		if (!empty($sessionid)) {
2199
			return $sessionid;
2200
		} else {
2201
			return false;
2202
		}
2203
	}
2204
	/* redirect user to desired destination */
2205
	if (is_URL($attributes['url_redirection'], true)) {
2206
		$my_redirurl = $attributes['url_redirection'];
2207
	} else if (is_URL($config['captiveportal'][$cpzone]['redirurl'], true)) {
2208
		$my_redirurl = config_get_path("captiveportal/{$cpzone}/redirurl");
2209
	} else if (is_URL($redirurl, true)) {
2210
		$my_redirurl = $redirurl;
2211
	}
2212

    
2213
	if (isset($config['captiveportal'][$cpzone]['logoutwin_enable']) && !isset($config['captiveportal'][$cpzone]['passthrumacadd'])) {
2214
		$ourhostname = portal_hostname_from_client_ip($clientip);
2215
		$protocol = (isset($config['captiveportal'][$cpzone]['httpslogin'])) ? 'https://' : 'http://';
2216
		$logouturl = "{$protocol}{$ourhostname}/";
2217

    
2218
		if (isset($attributes['reply_message'])) {
2219
			$message = $attributes['reply_message'];
2220
		} else {
2221
			$message = 0;
2222
		}
2223

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

    
2226
	} else {
2227
		portal_reply_page($my_redirurl, "redir", "Just redirect the user.");
2228
	}
2229

    
2230
	return $sessionid;
2231
}
2232

    
2233

    
2234
/*
2235
 * Used for when pass-through credits are enabled.
2236
 * Returns true when there was at least one free login to deduct for the MAC.
2237
 * Expired entries are removed as they are seen.
2238
 * Active entries are updated according to the configuration.
2239
 */
2240
function portal_consume_passthrough_credit($clientmac) {
2241
	global $config, $cpzone;
2242

    
2243
	if (!empty($config['captiveportal'][$cpzone]['freelogins_count']) && is_numeric($config['captiveportal'][$cpzone]['freelogins_count'])) {
2244
		$freeloginscount = config_get_path("captiveportal/{$cpzone}/freelogins_count");
2245
	} else {
2246
		return false;
2247
	}
2248

    
2249
	if (!empty($config['captiveportal'][$cpzone]['freelogins_resettimeout']) && is_numeric($config['captiveportal'][$cpzone]['freelogins_resettimeout'])) {
2250
		$resettimeout = config_get_path("captiveportal/{$cpzone}/freelogins_resettimeout");
2251
	} else {
2252
		return false;
2253
	}
2254

    
2255
	if ($freeloginscount < 1 || $resettimeout <= 0 || !$clientmac) {
2256
		return false;
2257
	}
2258

    
2259
	$updatetimeouts = isset($config['captiveportal'][$cpzone]['freelogins_updatetimeouts']);
2260

    
2261
	/*
2262
	 * Read database of used MACs.  Lines are a comma-separated list
2263
	 * of the time, MAC, then the count of pass-through credits remaining.
2264
	 */
2265
	$usedmacs = captiveportal_read_usedmacs_db();
2266

    
2267
	$currenttime = time();
2268
	$found = false;
2269
	foreach ($usedmacs as $key => $usedmac) {
2270
		$usedmac = explode(",", $usedmac);
2271

    
2272
		if ($usedmac[1] == $clientmac) {
2273
			if ($usedmac[0] + ($resettimeout * 3600) > $currenttime) {
2274
				if ($usedmac[2] < 1) {
2275
					if ($updatetimeouts) {
2276
						$usedmac[0] = $currenttime;
2277
						unset($usedmacs[$key]);
2278
						$usedmacs[] = implode(",", $usedmac);
2279
						captiveportal_write_usedmacs_db($usedmacs);
2280
						xmlrpc_sync_usedmacs($usedmacs);
2281
					}
2282

    
2283
					return false;
2284
				} else {
2285
					$usedmac[2] -= 1;
2286
					$usedmacs[$key] = implode(",", $usedmac);
2287
				}
2288

    
2289
				$found = true;
2290
			} else {
2291
				unset($usedmacs[$key]);
2292
			}
2293

    
2294
			break;
2295
		} else if ($usedmac[0] + ($resettimeout * 3600) <= $currenttime) {
2296
			unset($usedmacs[$key]);
2297
		}
2298
	}
2299

    
2300
	if (!$found) {
2301
		$usedmac = array($currenttime, $clientmac, $freeloginscount - 1);
2302
		$usedmacs[] = implode(",", $usedmac);
2303
	}
2304

    
2305
	captiveportal_write_usedmacs_db($usedmacs);
2306
	xmlrpc_sync_usedmacs($usedmacs);
2307
	return true;
2308
}
2309

    
2310
function xmlrpc_sync_usedmacs($usedmacs) {
2311
	global $config, $cpzone;
2312

    
2313
	// XMLRPC Call over to the other node
2314
	if (captiveportal_xmlrpc_sync_get_details($syncip, $syncport,
2315
	    $syncuser, $syncpass, $carp_loop)) {
2316
		$rpc_client = new pfsense_xmlrpc_client();
2317
		$rpc_client->setConnectionData($syncip, $syncport, $syncuser, $syncpass);
2318
		$rpc_client->set_noticefile("CaptivePortalUsedmacsSync");
2319
		$arguments = array(
2320
			'usedmacs' => $usedmacs
2321
		);
2322

    
2323
		$rpc_client->xmlrpc_method('captive_portal_sync',
2324
			array(
2325
				'op' => 'write_usedmacs',
2326
				'zone' => $cpzone,
2327
				'arguments' => base64_encode(serialize($arguments))
2328
			)
2329
		);
2330
	}
2331
}
2332

    
2333
function captiveportal_read_usedmacs_db() {
2334
	global $g, $cpzone;
2335

    
2336
	$cpumaclck = lock("captiveusedmacs{$cpzone}");
2337
	if (file_exists("{$g['vardb_path']}/captiveportal_usedmacs_{$cpzone}.db")) {
2338
		$usedmacs = file("{$g['vardb_path']}/captiveportal_usedmacs_{$cpzone}.db", FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
2339
		if (!$usedmacs) {
2340
			$usedmacs = array();
2341
		}
2342
	} else {
2343
		$usedmacs = array();
2344
	}
2345

    
2346
	unlock($cpumaclck);
2347
	return $usedmacs;
2348
}
2349

    
2350
function captiveportal_write_usedmacs_db($usedmacs) {
2351
	global $g, $cpzone;
2352

    
2353
	if (!is_array($usedmacs)) {
2354
		$usedmacs = [];
2355
	}
2356
	$cpumaclck = lock("captiveusedmacs{$cpzone}", LOCK_EX);
2357
	@file_put_contents("{$g['vardb_path']}/captiveportal_usedmacs_{$cpzone}.db", implode("\n", $usedmacs));
2358
	unlock($cpumaclck);
2359
}
2360

    
2361
function captiveportal_blocked_mac($mac) {
2362
	global $config, $g, $cpzone;
2363

    
2364
	if (empty($mac) || !is_macaddr($mac)) {
2365
		return false;
2366
	}
2367

    
2368
	if (!is_array($config['captiveportal'][$cpzone]['passthrumac'])) {
2369
		return false;
2370
	}
2371

    
2372
	foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $passthrumac) {
2373
		if (($passthrumac['action'] == 'block') &&
2374
		    ($passthrumac['mac'] == strtolower($mac))) {
2375
			return true;
2376
		}
2377
	}
2378

    
2379
	return false;
2380

    
2381
}
2382

    
2383
/* Captiveportal Radius Accounting */
2384

    
2385
function gigawords($bytes) {
2386

    
2387
	/*
2388
	 * RFC2866 Specifies a 32bit unsigned integer, which is a max of 4294967295
2389
	 * Currently there is a fault in the PECL radius_put_int function which can handle only 32bit signed integer.
2390
	 */
2391

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

    
2395
	// We need to manually set this to a zero instead of NULL for put_int() safety
2396
	if (is_null($gigawords)) {
2397
		$gigawords = 0;
2398
	}
2399

    
2400
	return $gigawords;
2401
}
2402

    
2403
function remainder($bytes) {
2404
	// Calculate the bytes we are going to send to the radius
2405
	$bytes = bcmod($bytes, GIGAWORDS_RIGHT_OPERAND);
2406

    
2407
	if (is_null($bytes)) {
2408
		$bytes = 0;
2409
	}
2410

    
2411
    return $bytes;
2412
}
2413

    
2414
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) {
2415
	global $cpzone, $config;
2416

    
2417
	$cpcfg = config_get_path("captiveportal/{$cpzone}");
2418
	$acctcfg = auth_get_authserver($cpcfg['radacct_server']);
2419

    
2420
	if (!isset($cpcfg['radacct_enable']) || empty($acctcfg) ||
2421
	    captiveportal_ha_is_node_in_backup_mode($cpzone)) {
2422
		return null;
2423
	}
2424

    
2425
	if ($type === 'on') {
2426
		$racct = new Auth_RADIUS_Acct_On;
2427
	} elseif ($type === 'off') {
2428
		$racct = new Auth_RADIUS_Acct_Off;
2429
	} elseif ($type === 'start') {
2430
		$racct = new Auth_RADIUS_Acct_Start;
2431
		if (!is_int($start_time)) {
2432
			$start_time = time();
2433
		}
2434
	} elseif ($type === 'stop') {
2435
		$racct = new Auth_RADIUS_Acct_Stop;
2436
		if (!is_int($stop_time)) {
2437
			$stop_time = time();
2438
		}
2439
	} elseif ($type === 'update') {
2440
        $racct = new Auth_RADIUS_Acct_Update;
2441
		if (!is_int($stop_time)) {
2442
			$stop_time = time(); // "top time" here will be used only for calculating session time.
2443
		}
2444
	} else {
2445
		return null;
2446
	}
2447

    
2448
	$racct->addServer($acctcfg['host'], $acctcfg['radius_acct_port'],
2449
		$acctcfg['radius_secret'], $acctcfg['radius_timeout']);
2450

    
2451
	$racct->authentic = RADIUS_AUTH_RADIUS;
2452
	if ($cpcfg['auth_method'] === 'radmac' && $username === "unauthenticated" && !empty($clientmac)) {
2453
		$racct->username = mac_format($clientmac);
2454
	} elseif (!empty($username)) {
2455
		$racct->username = $username;
2456
	}
2457

    
2458
	if (PEAR::isError($racct->start())) {
2459
		captiveportal_syslog('RADIUS ACCOUNTING FAILED : '.$racct->getError());
2460
		$racct->close();
2461
		return null;
2462
	}
2463

    
2464
	$nasip = nasip_fallback($acctcfg['radius_nasip_attribute']);
2465
	$nasmac = get_interface_mac(find_ip_interface($nasip));
2466
	$racct->putAttribute(RADIUS_NAS_IP_ADDRESS, $nasip, "addr");
2467

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

    
2470
	if (is_int($ruleno)) {
2471
		$racct->putAttribute(RADIUS_NAS_PORT_TYPE, RADIUS_ETHERNET);
2472
		$racct->putAttribute(RADIUS_NAS_PORT, intval($ruleno), 'integer');
2473
	}
2474

    
2475
	if (!empty($sessionid)) {
2476
		$racct->putAttribute(RADIUS_ACCT_SESSION_ID, $sessionid);
2477
	}
2478

    
2479
	if (!empty($clientip) && is_ipaddr($clientip)) {
2480
		$racct->putAttribute(RADIUS_FRAMED_IP_ADDRESS, $clientip, "addr");
2481
	}
2482
	if (!empty($clientmac)) {
2483
		$racct->putAttribute(RADIUS_CALLING_STATION_ID, mac_format($clientmac));
2484
	}
2485
	if (!empty($nasmac)) {
2486
		$racct->putAttribute(RADIUS_CALLED_STATION_ID, mac_format($nasmac).':'.gethostname());
2487
	}
2488

    
2489
	// Accounting request Stop and Update : send the current data volume
2490
	if (($type === 'stop' || $type === 'update') && is_int($start_time)) {
2491
		$volume = getVolume($clientip);
2492
		$session_time = $stop_time - $start_time;
2493
		$volume['input_bytes_radius'] = remainder($volume['input_bytes']);
2494
		$volume['input_gigawords'] = gigawords($volume['input_bytes']);
2495
		$volume['output_bytes_radius'] = remainder($volume['output_bytes']);
2496
		$volume['output_gigawords'] = gigawords($volume['output_bytes']);
2497

    
2498
		// Volume stuff: Ingress
2499
		$racct->putAttribute(RADIUS_ACCT_INPUT_PACKETS, intval($volume['input_pkts']), "integer");
2500
		$racct->putAttribute(RADIUS_ACCT_INPUT_OCTETS, intval($volume['input_bytes_radius']), "integer");
2501
		// Volume stuff: Outgress
2502
		$racct->putAttribute(RADIUS_ACCT_OUTPUT_PACKETS, intval($volume['output_pkts']), "integer");
2503
		$racct->putAttribute(RADIUS_ACCT_OUTPUT_OCTETS, intval($volume['output_bytes_radius']), "integer");
2504
		$racct->putAttribute(RADIUS_ACCT_SESSION_TIME, intval($session_time), "integer");
2505

    
2506
		$racct->putAttribute(CUSTOM_RADIUS_ACCT_OUTPUT_GIGAWORDS, intval($volume['output_gigawords']), "integer");
2507
		$racct->putAttribute(CUSTOM_RADIUS_ACCT_INPUT_GIGAWORDS, intval($volume['input_gigawords']), "integer");
2508
		// Set session_time
2509
		$racct->session_time = $session_time;
2510
	}
2511

    
2512
	if ($type === 'stop') {
2513
		if (empty($term_cause)) {
2514
			$term_cause = 1;
2515
		}
2516
		$racct->putAttribute(RADIUS_ACCT_TERMINATE_CAUSE, $term_cause);
2517
	}
2518

    
2519
	// Send request
2520
	$result = $racct->send();
2521

    
2522
	if (PEAR::isError($result)) {
2523
		 captiveportal_syslog('RADIUS ACCOUNTING FAILED : '.$racct->getError());
2524
		 $result = null;
2525
	} elseif ($result !== true) {
2526
		$result = false;
2527
	}
2528

    
2529
	$racct->close();
2530
	return $result;
2531
}
2532

    
2533
function captiveportal_isip_logged($clientip) {
2534
	global $g, $cpzone;
2535

    
2536
	/* read in client database */
2537
	$query = "WHERE ip = '{$clientip}'";
2538
	$cpdb = captiveportal_read_db($query);
2539
	foreach ($cpdb as $cpentry) {
2540
		return $cpentry;
2541
	}
2542
}
2543

    
2544
function captiveportal_allowedhostname_cleanup() {
2545
	global $g, $config, $cpzone;
2546

    
2547
	$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
2548

    
2549
	init_config_arr(array('captiveportal', $cpzone, 'allowedhostname'));
2550
	foreach ($config['captiveportal'][$cpzone]['allowedhostname'] as $id => $hostnameent) {
2551
		$pipes = pfSense_pf_cp_get_eth_pipes("{$cpzoneprefix}_allowedhosts/hostname_{$id}");
2552
		pfSense_pf_cp_flush("{$cpzoneprefix}_allowedhosts/hostname_{$id}", "ether");
2553
		if (!empty($pipes)) {
2554
			captiveportal_pipes_delete($pipes);
2555
		}
2556
	}
2557
}
2558

    
2559
function filter_captiveportal_aliases() {
2560
	/* return all aliases used in captive portal zones,
2561
	 * to prevent it from deletion in filter_configure_sync() as unused aliases */
2562
	global $g, $config;
2563

    
2564
	$aliasesnames = array();
2565

    
2566
	init_config_arr(array('captiveportal'));
2567
	foreach ($config['captiveportal'] as $cpzone => $cpcfg) {
2568
		if (isset($cpcfg['enable'])) {
2569
			$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
2570
			$aliasesnames[] = $cpzoneprefix . '_cpips';
2571
			init_config_arr(array('captiveportal', $cpzone, 'allowedhostname'));
2572
			foreach ($config['captiveportal'][$cpzone]['allowedhostname'] as $id => $hostnameent) {
2573
				$aliasesnames[] = $cpzoneprefix . '_hostname_' . $id;
2574
			}
2575
		}
2576
	}
2577

    
2578
	return $aliasesnames;
2579
}
2580

    
2581
function filter_captiveportal_tables() {
2582
	/* return pf rules which defines tables used in captive portal zones */
2583
	global $config, $FilterIflist;
2584

    
2585
	$rules = '';
2586
	init_config_arr(array('captiveportal'));
2587
	foreach ($config['captiveportal'] as $cpzone => $cpcfg) {
2588
		if (!isset($cpcfg['enable'])) {
2589
			continue;
2590
		}
2591

    
2592
		$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
2593
		$cpips = $cpzoneprefix . '_cpips';
2594
		$cpiplist = array();
2595

    
2596
		foreach (explode(",", $cpcfg['interface']) as $cpifgrp) {
2597
			if (isset($FilterIflist[$cpifgrp])) {
2598
				$realif = get_real_interface($cpifgrp);
2599
				if (!empty($realif)) {
2600
					$cpip = get_interface_ip($cpifgrp);
2601
					if (is_ipaddrv4($cpip)) {
2602
						$cpipliststring = $cpip . ' ' . get_interface_vip_ips($cpifgrp);
2603
						$cpiplist = array_filter(array_merge($cpiplist, explode(' ', $cpipliststring)),
2604
												 function ($val) {
2605
													 return (trim($val) != "");
2606
												 });
2607
					}
2608
				}
2609
			}
2610
		}
2611
		if (!empty($cpiplist)) {
2612
			/* captive portal web server IP addresses */
2613
			$rules .= "table <{$cpips}> { " . join(' ', $cpiplist)  . "}\n";
2614
		}
2615
	}
2616

    
2617
	if (!empty($rules)) {
2618
		$rules = "\n# Captive Portal\n" . $rules . "\n";
2619
	}
2620

    
2621
	return $rules;
2622
}
2623

    
2624
function filter_captiveportal_ether() {
2625
	global $g, $config;
2626

    
2627
	$rules = '';
2628
	init_config_arr(array('captiveportal'));
2629
	foreach ($config['captiveportal'] as $cpzone => $cpcfg) {
2630
		if (!isset($cpcfg['enable'])) {
2631
			continue;
2632
		}
2633

    
2634
		$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
2635
		$rdrtag = $cpzoneprefix . '_rdr';
2636
		$interfaces = captiveportal_zone_interfaces($cpcfg);
2637

    
2638
		if (!empty($interfaces)) {
2639
			/* set 'rdr' tag for further captive portal web portal redirection */
2640
			$rules .= "ether pass on { {$interfaces} } tag \"{$rdrtag}\"\n";
2641
			/* anchor to set the PASS tag for authenticated clients */
2642
			$rules .= "ether anchor \"{$cpzoneprefix}_auth/*\" on { {$interfaces} }\n";
2643
			/* anchor for Services / Captive Portal / CPZONE / MACs */
2644
			$rules .= "ether anchor \"{$cpzoneprefix}_passthrumac/*\" on { {$interfaces} }\n";
2645
			/* anchor to set the PASSTHRU tag for Allowed IP/Hostnames */
2646
			$rules .= "ether anchor \"{$cpzoneprefix}_allowedhosts/*\" on { {$interfaces} }\n";
2647
		}
2648
	}
2649

    
2650
	if (!empty($rules)) {
2651
		$rules = "\n# Captive Portal\n" . $rules . "\n";
2652
	}
2653

    
2654
	return $rules;
2655
}
2656

    
2657
function filter_captiveportal_rdr() {
2658
	global $g, $config, $FilterIflist;
2659

    
2660
	$rules = '';
2661
	foreach ($config['captiveportal'] as $cpzone => $cpcfg) {
2662
		if (!isset($cpcfg['enable'])) {
2663
			continue;
2664
		}
2665

    
2666
		$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
2667
		$rdrtag = $cpzoneprefix . '_rdr';
2668
		$cpips = $cpzoneprefix . '_cpips';
2669
		$rdr_ports = captiveportal_zone_portalports($cpcfg);
2670
		foreach (explode(",", $cpcfg['interface']) as $cpifgrp) {
2671
			if (isset($FilterIflist[$cpifgrp])) {
2672
				$realif = get_real_interface($cpifgrp);
2673
				if (!empty($realif)) {
2674
					$cpip = get_interface_ip($cpifgrp);
2675
					if (is_ipaddrv4($cpip)) {
2676
						foreach ($rdr_ports as list($portalias, $cprdrport)) {
2677
							$rules .= "rdr on {$realif} inet proto tcp from any to ! <{$cpips}> port {$cprdrport} tagged {$rdrtag} -> {$cpip} port {$portalias}\n";
2678
						}
2679
					}
2680
				}
2681
			}
2682
		}
2683
	}
2684

    
2685
	if (!empty($rules)) {
2686
		$rules = "\n# Captive Portal\n" . $rules . "\n";
2687
	}
2688

    
2689
	return $rules;
2690
}
2691

    
2692
function filter_captiveportal_pass() {
2693
	global $g, $config, $FilterIflist;
2694

    
2695
	$captiveportal_increment = 'filter_captiveportal_tracker';
2696

    
2697
	$rules = '';
2698
	foreach ($config['captiveportal'] as $cpzone => $cpcfg) {
2699
		if (!isset($cpcfg['enable'])) {
2700
			continue;
2701
		}
2702

    
2703
		$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
2704
		$cpips = $cpzoneprefix . '_cpips';
2705
		$authtag = $cpzoneprefix . '_auth';
2706
		$rdr_ports = captiveportal_zone_portalports($cpcfg);
2707

    
2708
		foreach (explode(",", $cpcfg['interface']) as $cpifgrp) {
2709
			if (!isset($FilterIflist[$cpifgrp])) {
2710
				continue;
2711
			}
2712
			$realif = get_real_interface($cpifgrp);
2713
			if (!empty($realif)) {
2714
				$cpip = get_interface_ip($cpifgrp);
2715
				if (is_ipaddrv4($cpip)) {
2716
					foreach ($rdr_ports as list($portalias, $cprdrport)) {						/* pass non-authenticated clients to captive portal */
2717
						$rules .= "pass in quick on {$realif} proto tcp from any to <{$cpips}> port {$portalias} ridentifier {$captiveportal_increment()} keep state(sloppy)\n";
2718
						/* without this rule captive portal doesn't show login page after manual disconnect */
2719
						$rules .= "pass out quick on {$realif} proto tcp from {$cpip} port {$portalias} to any flags any ridentifier {$captiveportal_increment()} keep state(sloppy)\n";
2720
					}
2721
					/* block non-authenticated clients access to internet */
2722
					$rules .= "block in quick on {$realif} from any to ! <{$cpips}> ! tagged {$authtag} ridentifier {$captiveportal_increment()}\n";
2723
				}
2724
			}
2725
		}
2726
	}
2727

    
2728
	if (!empty($rules)) {
2729
		$rules = "\n# Captive Portal\n" . $rules . "\n";
2730
	}
2731

    
2732
	return $rules;
2733
}
2734

    
2735
function captiveportal_zone_interfaces($cpcfg) {
2736
	/* return a list of captive portal zone interfaces */
2737
	global $FilterIflist;
2738

    
2739
	$interfaces = '';
2740
	foreach (explode(",", $cpcfg['interface']) as $cpifgrp) {
2741
		if (isset($FilterIflist[$cpifgrp])) {
2742
			$realif = get_real_interface($cpifgrp);
2743
			if (!empty($realif) && get_interface_ip($realif)) {
2744
				$interfaces .= $realif . ' ';
2745
			}
2746
		}
2747
	}
2748
	return $interfaces;
2749
}
2750

    
2751
/*
2752
 * Returns an array of (alias, rdrport) pairs describing ports to be forwarded for the captive portal
2753
 */
2754
function captiveportal_zone_portalports($cpcfg) {
2755
	$rdr_ports = array();
2756
	if (isset($cpcfg['httpslogin']) && !isset($cpcfg['nohttpsforwards'])) {
2757
		$portalias = $cpcfg['listenporthttps'] ? $cpcfg['listenporthttps'] : 8001 + $cpcfg['zoneid'];
2758
		$cprdrport = '443';
2759
		array_push($rdr_ports, array($portalias, $cprdrport));
2760
	}
2761
	$portalias = $cpcfg['listenporthttp'] ? $cpcfg['listenporthttp'] : 8000 + $cpcfg['zoneid'];
2762
	$cprdrport = '80';
2763
	array_push($rdr_ports, array($portalias, $cprdrport));
2764

    
2765
	return $rdr_ports;
2766
}
2767

    
2768
function captiveportal_pipes_delete($pipes) {
2769
	if (!empty($pipes)) {
2770
		foreach ($pipes as $pipe) {
2771
			mwexec("/sbin/dnctl pipe delete {$pipe}");
2772
		}
2773
		captiveportal_free_dn_rulenos($pipes);
2774
	}
2775
}
2776

    
2777
function captiveportal_ether_configure_entry($hostent, $anchor, $user_auth = false) {
2778
	global $config, $g, $cpzone;
2779

    
2780
	if (($hostent['action'] == 'block') && ($anchor == 'passthrumac')) {
2781
		return;
2782
	}
2783

    
2784
	$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
2785
	if ($anchor == 'passthrumac') {
2786
		$tag = $cpzoneprefix . '_auth';
2787
	} else {
2788
		$tag = $cpzoneprefix . '_' . $anchor;
2789
	}
2790

    
2791
	if ($anchor == 'passthrumac') {
2792
		list($pipeup, $pipedown) = captiveportal_pipe_configure($hostent, 'pipe_mac', $user_auth);
2793
		$host = str_replace("/", "_", str_replace(":", "", $hostent['mac']));
2794
		$l3from = '';
2795
		$l3to = '';
2796
		$macfrom = "from {$hostent['mac']}";
2797
		$macto = "to {$hostent['mac']}";
2798
	} else {
2799
		list($pipeup, $pipedown) = captiveportal_pipe_configure($hostent, 'auth', $user_auth);
2800
		$host = $hostent['ip'] . '_32';
2801
		$l3from = "l3 from {$hostent['ip']}";
2802
		$l3to = "l3 to {$hostent['ip']}";
2803
		if (!isset($config['captiveportal'][$cpzone]['nomacfilter'])) {
2804
			if (!empty($hostent['mac'])) {
2805
				$macfrom = "from {$hostent['mac']}";
2806
				$macto = "to {$hostent['mac']}";
2807
			} else {
2808
				return;
2809
			}
2810
		} else {
2811
			$macfrom = '';
2812
			$macto = '';
2813
		}
2814
	}
2815

    
2816
	$rules = "ether pass in quick {$macfrom} {$l3from} tag {$tag} dnpipe {$pipeup}\n";
2817
	$rules .= "ether pass out quick {$macto} {$l3to} tag {$tag} dnpipe {$pipedown}\n";
2818

    
2819
	captiveportal_load_pfctl("{$cpzoneprefix}_{$anchor}", $host, $rules);
2820
}
2821

    
2822
function captiveportal_pipe_configure($host, $type, $user_auth = true) {
2823
	global $config, $cpzone;
2824

    
2825
	$bwUp = 0;
2826
	if (!empty($host['bw_up'])) {
2827
		$bwUp = $host['bw_up'];
2828
	} elseif ($user_auth &&
2829
		isset($config['captiveportal'][$cpzone]['peruserbw']) &&
2830
	    !empty($config['captiveportal'][$cpzone]['bwdefaultup'])) {
2831
		$bwUp = config_get_path("captiveportal/{$cpzone}/bwdefaultup");
2832
	}
2833
	$bwDown = 0;
2834
	if (!empty($host['bw_down'])) {
2835
		$bwDown = $host['bw_down'];
2836
	} elseif ($user_auth &&
2837
		isset($config['captiveportal'][$cpzone]['peruserbw']) &&
2838
	    !empty($config['captiveportal'][$cpzone]['bwdefaultdn'])) {
2839
		$bwDown = config_get_path("captiveportal/{$cpzone}/bwdefaultdn");
2840
	}
2841

    
2842
	if (isset($host['pipeno']) && !empty($host['pipeno'])) {
2843
		$pipeup = $host['pipeno'];
2844
	} else {
2845
		$pipeup = captiveportal_get_next_dn_ruleno($type);
2846
	}
2847

    
2848
	mwexec("/sbin/dnctl pipe {$pipeup} config bw {$bwUp}Kbit/s queue 100 buckets 16");
2849
	$pipedown = $pipeup + 1;
2850
	mwexec("/sbin/dnctl pipe {$pipedown} config bw {$bwDown}Kbit/s queue 100 buckets 16");
2851

    
2852
	return array($pipeup, $pipedown);
2853
}
2854

    
2855
function captiveportal_allowedip_configure_entry($ipent) {
2856
	global $g, $config, $cpzone;
2857

    
2858
	$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
2859
	$tag = $cpzoneprefix . '_auth';
2860

    
2861
	if (empty($ipent['sn'])) {
2862
		$ipent['sn'] = '32';
2863
	}
2864

    
2865
	$host = $ipent['ip'] . '_' . $ipent['sn'];
2866
	list($pipeup, $pipedown) = captiveportal_pipe_configure($ipent, 'allowed', false);
2867

    
2868
	$rules = '';
2869
	if (($ipent['dir'] == 'to') || ($ipent['dir'] == 'both')) {
2870
		$rules = "ether pass in quick l3 to {$ipent['ip']}/{$ipent['sn']} tag {$tag} dnpipe {$pipeup}\n";
2871
	}
2872
	if (($ipent['dir'] == 'from') || ($ipent['dir'] == 'both')) {
2873
		$rules .= "ether pass in quick l3 from {$ipent['ip']}/{$ipent['sn']} tag {$tag} dnpipe {$pipedown}\n";
2874
	}
2875

    
2876
	captiveportal_load_pfctl("{$cpzoneprefix}_allowedhosts", $host, $rules);
2877
}
2878

    
2879
function captiveportal_allowedhostname_configure_entry($ipent, $hostnameid = 1) {
2880
	global $config, $cpzone;
2881

    
2882
	if (!isset($ipent['hostname'])) {
2883
		return;
2884
	}
2885

    
2886
	$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
2887
	$tag = $cpzoneprefix . '_auth';
2888
	$table = $cpzoneprefix . '_hostname_' . $hostnameid;
2889
	$host = 'hostname_' . $hostnameid;
2890
	list($pipeup, $pipedown) = captiveportal_pipe_configure($ipent, 'allowed', false);
2891

    
2892
	$rules = "table <{$table}> persist\n";
2893
	if (($ipent['dir'] == 'to') || ($ipent['dir'] == 'both')) {
2894
		$rules .= "ether pass in quick l3 to <{$table}> tag {$tag} dnpipe {$pipeup}\n";
2895
	}
2896
	if (($ipent['dir'] == 'from') || ($ipent['dir'] == 'both')) {
2897
		$rules .= "ether pass in quick l3 from <{$table}> tag {$tag} dnpipe {$pipedown}\n";
2898
	}
2899

    
2900
	captiveportal_load_pfctl("{$cpzoneprefix}_allowedhosts", $host, $rules);
2901

    
2902
	/* return filterdns entry */
2903
	return "pf {$ipent['hostname']} {$table} {$cpzoneprefix}_allowedhosts/{$host}\n";
2904
}
2905

    
2906
function captiveportal_load_pfctl($anchor, $host, $rules) {
2907
	global $g, $cpzone;
2908

    
2909
	if (!empty($rules)) {
2910
		mwexec("/usr/bin/printf \"{$rules}\" | /sbin/pfctl -a {$anchor}/{$host} -f-");
2911
	} else {
2912
		log_error("CP zone ${cpzone}: {$anchor} rules are empty for {$host}");
2913
	}
2914
}
2915

    
2916
function captiveportal_anchor_zerocnt($ip, $anchor = 'auth') {
2917
	global $config, $cpzone;
2918
	$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
2919

    
2920
	pfSense_pf_cp_zerocnt("{$cpzoneprefix}_{$anchor}/{$ip}_32");
2921
}
2922

    
2923
?>
(6-6/61)