Project

General

Profile

Download (99.6 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-2024 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 $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
	$cpzone_config = config_get_path("captiveportal/{$cpzone}", []);
51
	if (isset($cpzone_config['customlogo']) &&
52
	    is_array($cpzone_config['element']) &&
53
	    !empty($cpzone_config['element'])) {
54
		foreach ($cpzone_config['element'] as $element) {
55
			if (strpos($element['name'], "captiveportal-logo.") !== false) {
56
				if (file_exists("{$g['captiveportal_path']}/{$element['name']}")) {
57
					$logo_src = $element['name'];
58
					break;
59
				}
60
			}
61
		}
62
	}
63
	return $logo_src;
64
}
65

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

    
86
function get_default_captive_portal_html() {
87
	global $g, $cpzone;
88

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

    
108
<head>
109

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

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

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

    
144
		if ($cpzone_config['auth_method'] === 'authserver' && !empty($cpzone_config['auth_server2'])) {
145
			$htmltext .= <<<EOD
146
			</div>
147
			<div class="auth_head_div">
148
				<h6 class="auth_head">{$translated_text4}</h6>
149
			</div>
150
			<div class="auth_source">
151

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

    
158

    
159
		if (config_path_enabled("voucher/{$cpzone}")) {
160
			$translated_text = gettext("Voucher Code");
161
			$htmltext .= <<<EOD
162
				<br  /><br  />
163
				<input name="auth_voucher" type="text" placeholder="{$translated_text}" value="#VOUCHER#">
164
EOD;
165
		}
166
	}
167

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

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

    
197
EOD;
198

    
199
	return $htmltext;
200
}
201

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

    
225
function captiveportal_configure_zone($cpcfg, $reload_rules = false) {
226
	global $g, $cpzone, $cpzoneid;
227

    
228
	$captiveportallck = lock("captiveportal{$cpzone}", LOCK_EX);
229

    
230
	if (isset($cpcfg['enable'])) {
231

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

    
238
		/* init captive portal pipes and anchors */
239
		captiveportal_init_rules($reload_rules);
240

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

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

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

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

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

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

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

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

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

    
347
EOD;
348
		}
349

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

    
357
		/* write elements */
358
		captiveportal_write_elements();
359

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

    
364
		/* start up the webserving daemon */
365
		captiveportal_init_webgui_zone($cpcfg);
366

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

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

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

    
379
		if (is_platform_booting() || $reload_rules) {
380
			/* send Accounting-On to server */
381
			captiveportal_send_server_accounting('on');
382
			echo "done\n";
383

    
384
			if (isset($cpcfg['preservedb']) || $reload_rules ||
385
			    captiveportal_xmlrpc_sync_get_details($syncip, $syncport, $syncuser, $syncpass)) {
386

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

    
409
		captiveportal_radius_stop_all(10); // NAS-Request
410

    
411
		/* Release allocated pipes for this zone */
412
		$pipes_to_remove = captiveportal_free_dnrules();
413
		captiveportal_delete_rules($pipes_to_remove);
414

    
415
		/* remove old information */
416
		unlink_if_exists("{$g['vardb_path']}/captiveportal{$cpzone}.db");
417
		unlink_if_exists("{$g['vardb_path']}/captiveportal_radius_{$cpzone}.db");
418
		unlink_if_exists("{$g['vardb_path']}/captiveportal_{$cpzone}.rules");
419
	}
420

    
421
	unlock($captiveportallck);
422

    
423
	return 0;
424
}
425

    
426
function captiveportal_init_webgui() {
427
	global $cpzone;
428

    
429
	foreach (config_get_path('captiveportal', []) as $cpkey => $cp) {
430
		$cpzone = $cpkey;
431
		captiveportal_init_webgui_zone($cp);
432
	}
433
}
434

    
435
function captiveportal_init_webgui_zonename($zone) {
436
	global $cpzone;
437

    
438
	$cpzone_config = config_get_path("captiveportal/{$zone}");
439
	if (isset($cpzone_config)) {
440
		$cpzone = $zone;
441
		captiveportal_init_webgui_zone($cpzone_config);
442
	}
443
}
444

    
445
function captiveportal_init_webgui_zone($cpcfg) {
446
	global $g, $cpzone;
447

    
448
	if (!isset($cpcfg['enable'])) {
449
		return;
450
	}
451

    
452
	if (isset($cpcfg['httpslogin'])) {
453
		$cert = lookup_cert($cpcfg['certref']);
454
		$cert = $cert['item'];
455
		$crt = base64_decode($cert['crt']);
456
		$key = base64_decode($cert['prv']);
457
		$ca = ca_chain($cert);
458

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

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

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

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

    
491
/* reinit will disconnect all users, be careful! */
492
function captiveportal_init_rules($reinit = false) {
493
	global $g, $cpzone;
494

    
495
	if (!config_path_enabled("captiveportal/{$cpzone}")) {
496
		return;
497
	}
498

    
499
	dummynet_load_module('100');
500

    
501
	/* Cleanup so nothing is leaked */
502
	captiveportal_free_dnrules(2000, 64500, false, $reinit);
503

    
504
	$captiveportallck = try_lock("captiveportal{$cpzone}", 0);
505

    
506
	/* delete all anchors */
507
	captiveportal_delete_rules(array(), $reinit);
508

    
509
	/* load passthru mac anchors */
510
	captiveportal_passthrumac_configure();
511

    
512
	/* load allowedip anchors */
513
	captiveportal_allowedip_configure();
514

    
515
	/* load allowed hostname anchors */
516
	captiveportal_allowedhostname_configure();
517

    
518
	if ($captiveportallck) {
519
		unlock($captiveportallck);
520
	}
521
}
522

    
523
/* Delete all rules related to specific cpzone */
524
function captiveportal_delete_rules($pipes_to_remove = array(), $clear_auth_rules = true) {
525
	global $g, $cpzone;
526

    
527
	/* delete MAC passthru entries */
528
	foreach (config_get_path("captiveportal/{$cpzone}/passthrumac", []) as $macent) {
529
		captiveportal_passthrumac_delete_entry($macent);
530
	}
531

    
532
	/* delete Allowed IP entries */
533
	foreach (config_get_path("captiveportal/{$cpzone}/allowedip", []) as $ipent) {
534
		captiveportal_ether_delete_entry($ipent, 'allowedhosts');
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, $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 (!config_path_enabled("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_result['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_result['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 (config_path_enabled("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(config_path_enabled("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, $cpzone, $cpzoneid;
812
	$cpzone_config = config_get_path("captiveportal/{$cpzone}", []);
813

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

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

    
845
	$stop_time = (empty($stop_time)) ? time() : $stop_time;
846

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

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

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

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

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

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

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

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

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

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

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

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

    
965
	captiveportal_radius_stop_all($term_cause, $logoutReason);
966

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

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

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

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

    
987
	$cpdb = captiveportal_read_db();
988

    
989
	$cpzone_config = config_get_path("captiveportal/{$cpzone}", []);
990
	$radacct = isset($cpzone_config['radacct_enable']) ? true : false;
991
	foreach ($cpdb as $cpentry) {
992
		if ($cpentry['authmethod'] === 'radius' && $radacct) {
993
			if ($cpzone_config['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
				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;
1016

    
1017
	$host = str_replace("/", "_", str_replace(":", "", $macent['mac']));
1018
	$cpzoneprefix = CPPREFIX . config_get_path("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 $g, $cpzone;
1035

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

    
1064
function captiveportal_passthrumac_findbyname($username) {
1065
	global $cpzone;
1066

    
1067
	$passthrumac_config = config_get_path("captiveportal/{$cpzone}/passthrumac");
1068
	if (is_array($passthrumac_config)) {
1069
		foreach ($passthrumac_config as $macent) {
1070
			if ($macent['username'] == $username) {
1071
				return $macent;
1072
			}
1073
		}
1074
	}
1075
	return NULL;
1076
}
1077

    
1078
function captiveportal_ether_delete_entry($hostent, $anchor = 'allowedhosts') {
1079
	global $g, $cpzone;
1080

    
1081
	$cpzoneprefix = CPPREFIX . config_get_path("captiveportal/{$cpzone}/zoneid");
1082

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

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

    
1097
function captiveportal_allowedhostname_configure() {
1098
	global $g, $cpzone, $cpzoneid;
1099

    
1100
	$allowedhostname_config = config_get_path("captiveportal/{$cpzone}/allowedhostname");
1101
	if (!is_array($allowedhostname_config)) {
1102
		return false;
1103
	}
1104

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

    
1118
function captiveportal_filterdns_configure() {
1119
	global $g, $cpzone, $cpzoneid;
1120

    
1121
	$cp_filterdns_filename = g_get('varetc_path') .
1122
	    "/filterdns-{$cpzone}-captiveportal.conf";
1123

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

    
1144
function captiveportal_allowedip_configure() {
1145
	global $g, $cpzone;
1146

    
1147
	$allowedip_config = config_get_path("captiveportal/{$cpzone}/allowedip");
1148
	if (is_array($allowedip_config)) {
1149
		foreach ($allowedip_config as $ipent) {
1150
			captiveportal_allowedip_configure_entry($ipent);
1151
		}
1152
	}
1153
}
1154

    
1155
/* get last activity timestamp given client IP address */
1156
function captiveportal_get_last_activity($ip) {
1157
	global $cpzone;
1158

    
1159
	$cpzoneprefix = CPPREFIX . config_get_path("captiveportal/{$cpzone}/zoneid");
1160
	$anchor = $cpzoneprefix . '_auth';
1161

    
1162
	$active_times = pfSense_pf_cp_get_eth_last_active("{$anchor}/{$ip}_32");
1163
	$time = 0;
1164
	if (!empty($active_times)) {
1165
		foreach ($active_times as $active_time) {
1166
			if ( $active_time > $time)
1167
				$time = $active_time;
1168
	   }
1169
	}
1170

    
1171
	return $time;
1172
}
1173

    
1174

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

    
1188
/* log simple messages to syslog */
1189
function captiveportal_syslog($message) {
1190
	global $cpzone;
1191

    
1192
	$message = trim($message);
1193
	$message = "Zone: {$cpzone} - {$message}";
1194
	openlog("logportalauth", LOG_PID, LOG_LOCAL4);
1195
	// Log it
1196
	syslog(LOG_INFO, $message);
1197
	closelog();
1198
}
1199

    
1200
/* Authenticate users using Authentication Backend */
1201
function captiveportal_authenticate_user(&$login = '', $password = '', $clientmac = '', $clientip = '', $pipeno = 'null', $context = 'first') {
1202
	global $g, $cpzone;
1203
	$cpcfg = config_get_path("captiveportal/{$cpzone}", []);
1204

    
1205
	$login_status = 'FAILURE';
1206
	$login_msg = gettext('Invalid credentials specified');
1207
	$reply_attributes = array();
1208
	$auth_method = '';
1209
	$auth_result = null;
1210

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

    
1219
		- Reply message of a user failed auth is more important than reply message of
1220
		a server failed auth (unable to contact server)
1221

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

    
1225
	The $authlevel variable is a flag indicating the status of authentication
1226
	0 = failed server auth
1227
	1 = failed user auth
1228
	2 = failed user auth with custom server reply received
1229
	3 = successful auth
1230
	*/
1231
	$authlevel = 0;
1232

    
1233
	/* Getting authentication servers from captiveportal configuration */
1234
	$auth_servers = array();
1235

    
1236
	if ($cpcfg['auth_method'] === 'none') {
1237
		$auth_servers[] = array('type' => 'none');
1238
	} else {
1239
		if ($context === 'second') {
1240
			$fullauthservers = explode(",", $cpcfg['auth_server2']);
1241
		} else {
1242
			$fullauthservers = explode(",", $cpcfg['auth_server']);
1243
		}
1244

    
1245
		foreach ($fullauthservers as $authserver) {
1246
			if (strpos($authserver, ' - ') !== false) {
1247
				$authserver = explode(' - ', $authserver);
1248
				array_shift($authserver);
1249
				$authserver = implode(' - ', $authserver);
1250

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

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

    
1276
				$result = null;
1277
				$status = null;
1278
				$msg = null;
1279

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

    
1294
				if (!$radmac_error) {
1295
					if ($authcfg['type'] === 'none') {
1296
						$result = true;
1297
					} else {
1298
						$result = authenticate_user($login, $password, $authcfg, $attributes);
1299
					}
1300

    
1301
					if (!empty($attributes['error_message'])) {
1302
						$msg = $attributes['error_message'];
1303
					}
1304

    
1305
					if ($authcfg['type'] == 'Local Auth' && $result && isset($cpcfg['localauth_priv'])) {
1306
						$tmp_user_item_config = getUserEntry($login);
1307
						if (!userHasPrivilege($tmp_user_item_config['item'], "user-services-captiveportal-login")) {
1308
							$result = false;
1309
							$msg = gettext("Access Denied");
1310
						}
1311
					}
1312
					if ($context === 'radmac' && $result === null && empty($attributes['reply_message'])) {
1313
						$msg = gettext("RADIUS MAC Authentication Failed.");
1314
					}
1315

    
1316
					if (empty($status)) {
1317
						if ($result === true) {
1318
							$status = "ACCEPT";
1319
						} elseif ($result === null) {
1320
							$status = "ERROR";
1321
						} else {
1322
							$status = "FAILURE";
1323
						}
1324
					}
1325

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

    
1342
				if ($val >= $authlevel) {
1343
					$authlevel = $val;
1344
					$auth_method = $authcfg['type'];
1345
					$login_status = $status;
1346
					$login_msg = $msg;
1347
					$reply_attributes = $attributes;
1348
					$auth_result = $result;
1349
				}
1350
			}
1351
		}
1352
	}
1353

    
1354
	return array('result'=>$auth_result, 'attributes'=>$reply_attributes, 'auth_method' =>$auth_method, 'login_status'=> $login_status, 'login_message' => $login_msg);
1355
}
1356

    
1357
function captiveportal_opendb() {
1358
	global $g, $cpzone, $cpzoneid;
1359

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

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

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

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

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

    
1428
	return $DB;
1429
}
1430

    
1431
/* read captive portal DB into array */
1432
function captiveportal_read_db($query = "") {
1433
	$cpdb = array();
1434

    
1435
	$DB = captiveportal_opendb();
1436
	if ($DB) {
1437
		$response = $DB->query("SELECT * FROM captiveportal {$query}");
1438
		if ($response != FALSE) {
1439
			while ($row = $response->fetchArray()) {
1440
				$cpdb[] = $row;
1441
			}
1442
		}
1443
		$DB->close();
1444
	}
1445

    
1446
	return $cpdb;
1447
}
1448

    
1449
function captiveportal_remove_entries($remove, $carp_loop = false) {
1450
	global $cpzone;
1451

    
1452
	if (!is_array($remove) || empty($remove)) {
1453
		return;
1454
	}
1455

    
1456
	$query = "DELETE FROM captiveportal WHERE sessionid in (";
1457
	foreach ($remove as $idx => $unindex) {
1458
		$query .= "'{$unindex}'";
1459
		if ($idx < (count($remove) - 1)) {
1460
			$query .= ",";
1461
		}
1462
	}
1463
	$query .= ")";
1464
	captiveportal_write_db($query);
1465

    
1466
	if (captiveportal_xmlrpc_sync_get_details($syncip, $syncport, $syncuser, $syncpass, $carp_loop)) {
1467
		$rpc_client = new pfsense_xmlrpc_client();
1468
		$rpc_client->setConnectionData($syncip, $syncport, $syncuser, $syncpass);
1469
		$rpc_client->set_noticefile("CaptivePortalUserSync");
1470
		$rpc_client->xmlrpc_method('captive_portal_sync',
1471
			array(
1472
				'op' => 'remove_entries',
1473
				'zone' => $cpzone,
1474
				'entries' => base64_encode(serialize($remove))
1475
			)
1476
		);
1477
	}
1478
	return true;
1479
}
1480

    
1481
/* write captive portal DB */
1482
function captiveportal_write_db($queries) {
1483
	global $g;
1484

    
1485
	if (is_array($queries)) {
1486
		$query = implode(";", $queries);
1487
	} else {
1488
		$query = $queries;
1489
	}
1490

    
1491
	$DB = captiveportal_opendb();
1492
	if ($DB) {
1493
		$DB->exec("BEGIN TRANSACTION");
1494
		$result = $DB->exec($query);
1495
		if (!$result) {
1496
			captiveportal_syslog("Trying to modify DB returned error: {$DB->lastErrorMsg()}");
1497
		} else {
1498
			$DB->exec("END TRANSACTION");
1499
		}
1500
		$DB->close();
1501
		return $result;
1502
	} else {
1503
		return true;
1504
	}
1505
}
1506

    
1507
function captiveportal_write_elements() {
1508
	global $g, $cpzone;
1509

    
1510
	$cpcfg = config_get_path("captiveportal/{$cpzone}", []);
1511

    
1512
	if (!is_dir(g_get('captiveportal_element_path'))) {
1513
		@mkdir(g_get('captiveportal_element_path'));
1514
	}
1515

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

    
1535
	return 0;
1536
}
1537

    
1538
function captiveportal_free_dnrules($rulenos_start = 2000,
1539
    $rulenos_range_max = 64500, $dry_run = false, $clear_auth_pipes = true) {
1540
	global $g, $cpzone;
1541

    
1542
	$removed_pipes = array();
1543

    
1544
	if (!file_exists("{$g['vardb_path']}/captiveportaldn.rules")) {
1545
		return $removed_pipes;
1546
	}
1547

    
1548
	if (!$dry_run) {
1549
		$cpruleslck = lock("captiveportalrulesdn", LOCK_EX);
1550
	}
1551

    
1552
	$rules = unserialize_data(file_get_contents(
1553
	    "{$g['vardb_path']}/captiveportaldn.rules"), []);
1554
	$ridx = $rulenos_start;
1555
	while ($ridx < $rulenos_range_max) {
1556
		if (substr($rules[$ridx], 0, strlen($cpzone . '_')) == $cpzone . '_') {
1557
			if (!$clear_auth_pipes && substr($rules[$ridx], 0, strlen($cpzone . '_auth')) == $cpzone . '_auth') {
1558
				$ridx += 2;
1559
			} else {
1560
				if (!$dry_run) {
1561
					$rules[$ridx] = false;
1562
				}
1563
				$removed_pipes[] = $ridx;
1564
				$ridx++;
1565
				if (!$dry_run) {
1566
					$rules[$ridx] = false;
1567
				}
1568
				$removed_pipes[] = $ridx;
1569
				$ridx++;
1570
			}
1571
		} else {
1572
			$ridx += 2;
1573
		}
1574
	}
1575

    
1576
	if (!$dry_run) {
1577
		file_put_contents("{$g['vardb_path']}/captiveportaldn.rules",
1578
		    serialize($rules));
1579
		unlock($cpruleslck);
1580
	}
1581

    
1582
	unset($rules);
1583

    
1584
	return $removed_pipes;
1585
}
1586

    
1587
function captiveportal_reserve_ruleno($ruleno) {
1588
	global $g, $cpzone;
1589

    
1590
	$cpruleslck = lock("captiveportalrulesdn", LOCK_EX);
1591
	if (file_exists("{$g['vardb_path']}/captiveportaldn.rules")) {
1592
		$rules = unserialize_data(file_get_contents("{$g['vardb_path']}/captiveportaldn.rules"), array_pad(array(), 64500, false));
1593
	} else {
1594
		$rules = array_pad(array(), 64500, false);
1595
	}
1596
	$rules[$ruleno] = $cpzone . '_auth';
1597
	$ruleno++;
1598
	$rules[$ruleno] = $cpzone . '_auth';
1599

    
1600
	file_put_contents("{$g['vardb_path']}/captiveportaldn.rules", serialize($rules));
1601
	unlock($cpruleslck);
1602
	unset($rules);
1603

    
1604
	return $ruleno;
1605
}
1606

    
1607
function captiveportal_get_next_dn_ruleno($rule_type = 'default', $rulenos_start = 2000, $rulenos_range_max = 64500, $check_only = false) {
1608
	global $g, $cpzone;
1609

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

    
1639
	return $ruleno;
1640
}
1641

    
1642
function captiveportal_free_dn_rulenos($rulenos) {
1643
	global $g;
1644

    
1645
	$cpruleslck = lock("captiveportalrulesdn", LOCK_EX);
1646
	if (file_exists("{$g['vardb_path']}/captiveportaldn.rules")) {
1647
		$rules = unserialize_data(file_get_contents("{$g['vardb_path']}/captiveportaldn.rules"), []);
1648
		foreach ($rulenos as $ruleno) {
1649
			$rules[$ruleno] = false;
1650
		}
1651
		file_put_contents("{$g['vardb_path']}/captiveportaldn.rules", serialize($rules));
1652
		unset($rules);
1653
	}
1654
	unlock($cpruleslck);
1655
}
1656

    
1657
function captiveportal_get_dn_passthru_pipes($mac, $anchor = 'passthrumac') {
1658
	global $g, $cpzone, $cpzoneid;
1659

    
1660
	$cpcfg = config_get_path("captiveportal/{$cpzone}", []);
1661
	$cpzoneprefix = CPPREFIX . $cpcfg['zoneid'];
1662
	if (!isset($cpcfg['enable'])) {
1663
		return NULL;
1664
	}
1665

    
1666
	$host = str_replace(":", "", $mac);
1667
	$pipes = pfSense_pf_cp_get_eth_pipes("{$cpzoneprefix}_{$anchor}/{$host}");
1668

    
1669
	return $pipes;
1670
}
1671

    
1672
/**
1673
 * This function will calculate the traffic produced by a client
1674
 * based on its firewall rule
1675
 *
1676
 * Point of view: NAS
1677
 *
1678
 * Input means: from the client
1679
 * Output means: to the client
1680
 *
1681
 */
1682

    
1683
function getVolume($ip) {
1684
	global $g, $cpzone;
1685

    
1686
	$cpzone_config = config_get_path("captiveportal/{$cpzone}", []);
1687
	$reverse = isset($cpzone_config['reverseacct']) ? true : false;
1688
	$volume = array();
1689
	// Initialize vars properly, since we don't want NULL vars
1690
	$volume['input_pkts'] = $volume['input_bytes'] = 0;
1691
	$volume['output_pkts'] = $volume['output_bytes'] = 0;
1692

    
1693
	/* no needs to check allowedip */
1694
	$cpzoneprefix = CPPREFIX . $cpzone_config['zoneid'];
1695
	$anchor = $cpzoneprefix . '_auth';
1696

    
1697
	// It is presumed that a list of arrays is returned, each containing rule direction, and packet and bytes counters.
1698
	$result = pfSense_pf_cp_get_eth_rule_counters("{$anchor}/{$ip}_32");
1699
	if (!empty($result) && is_array($result)) {
1700
		$input_pkts = 0;
1701
		$input_bytes = 0;
1702
		$output_pkts = 0;
1703
		$output_bytes = 0;
1704

    
1705
		foreach ($result as $rule_counters) {
1706
			switch ($rule_counters[0]) {
1707
				case 1: // rule direction 'PF_IN'
1708
					$input_pkts += $rule_counters['input_pkts'];
1709
					$input_bytes += $rule_counters['input_bytes'];
1710
					break;
1711
				case 2: // rule direction 'PF_OUT'
1712
					$output_pkts += $rule_counters['output_pkts'];
1713
					$output_bytes += $rule_counters['output_bytes'];
1714
					break;
1715
				case 0: // rule direction 'PF_INOUT'
1716
					$input_pkts += $rule_counters['input_pkts'];
1717
					$input_bytes += $rule_counters['input_bytes'];
1718
					$output_pkts += $rule_counters['output_pkts'];
1719
					$output_bytes += $rule_counters['output_bytes'];
1720
					break;
1721
				default:
1722
					break;
1723
			}
1724
		}
1725

    
1726
		if ($reverse) {
1727
			$volume['output_pkts'] = $input_pkts;
1728
			$volume['output_bytes'] = $input_bytes;
1729
			$volume['input_pkts'] = $output_pkts;
1730
			$volume['input_bytes'] = $output_bytes;
1731
		} else {
1732
			$volume['output_pkts'] = $output_pkts;
1733
			$volume['output_bytes'] = $output_bytes;
1734
			$volume['input_pkts'] = $input_pkts;
1735
			$volume['input_bytes'] = $input_bytes;
1736
		}
1737
	}
1738

    
1739
	return $volume;
1740
}
1741

    
1742
function portal_ip_from_client_ip($cliip) {
1743
	global $cpzone;
1744

    
1745
	$isipv6 = is_ipaddrv6($cliip);
1746
	$interfaces = array_filter(explode(",", config_get_path("captiveportal/{$cpzone}/interface", [])));
1747
	foreach ($interfaces as $cpif) {
1748
		if ($isipv6) {
1749
			$ip = get_interface_ipv6($cpif);
1750
			$sn = get_interface_subnetv6($cpif);
1751
		} else {
1752
			$ip = get_interface_ip($cpif);
1753
			$sn = get_interface_subnet($cpif);
1754
		}
1755
		if (ip_in_subnet($cliip, "{$ip}/{$sn}")) {
1756
			return $ip;
1757
		}
1758
	}
1759

    
1760
	$route = route_get($cliip, 'inet', true);
1761
	if (empty($route)) {
1762
		return false;
1763
	}
1764

    
1765
	$iface = $route[0]['interface-name'];
1766
	if (!empty($iface)) {
1767
		$ip = ($isipv6) ? find_interface_ipv6($iface)
1768
		    : find_interface_ip($iface);
1769
		if (is_ipaddr($ip)) {
1770
			return $ip;
1771
		}
1772
	}
1773

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

    
1785
	return false;
1786
}
1787

    
1788
function portal_hostname_from_client_ip($cliip) {
1789
	global $cpzone;
1790

    
1791
	$cpcfg = config_get_path("captiveportal/{$cpzone}", []);
1792

    
1793
	if (isset($cpcfg['httpslogin'])) {
1794
		$listenporthttps = $cpcfg['listenporthttps'] ? $cpcfg['listenporthttps'] : ($cpcfg['zoneid'] + 8001);
1795
		$ourhostname = $cpcfg['httpsname'];
1796

    
1797
		if ($listenporthttps != 443) {
1798
			$ourhostname .= ":" . $listenporthttps;
1799
		}
1800
	} else {
1801
		$listenporthttp = $cpcfg['listenporthttp'] ? $cpcfg['listenporthttp'] : ($cpcfg['zoneid'] + 8000);
1802
		$ifip = portal_ip_from_client_ip($cliip);
1803
		if (!$ifip) {
1804
			$ourhostname = config_get_path('system/hostname') . config_get_path('system/domain');
1805
		} else {
1806
			$ourhostname = (is_ipaddrv6($ifip)) ? "[{$ifip}]" : "{$ifip}";
1807
		}
1808

    
1809
		if ($listenporthttp != 80) {
1810
			$ourhostname .= ":" . $listenporthttp;
1811
		}
1812
	}
1813

    
1814
	return $ourhostname;
1815
}
1816

    
1817
/* functions move from index.php */
1818

    
1819
function portal_reply_page($redirurl, $type = null, $message = null, $clientmac = null, $clientip = null, $username = null, $password = null, $voucher = null) {
1820
	global $g, $cpzone;
1821

    
1822
	$cpcfg = config_get_path("captiveportal/{$cpzone}", []);
1823
	$ourhostname = portal_hostname_from_client_ip($clientip);
1824
	$protocol = (isset($cpcfg['httpslogin'])) ? 'https://' : 'http://';
1825
	$portal_url = "{$protocol}{$ourhostname}/index.php?zone={$cpzone}";
1826

    
1827
	/* Get captive portal layout */
1828
	if ($type == "redir") {
1829
		$redirurl = is_URL($redirurl, true) ? $redirurl : $portal_url;
1830
		header("Location: {$redirurl}");
1831
		return;
1832
	} else if ($type == "login") {
1833
		$htmltext = get_include_contents("{$g['varetc_path']}/captiveportal_{$cpzone}.html");
1834
	} else {
1835
		$htmltext = get_include_contents("{$g['varetc_path']}/captiveportal-{$cpzone}-error.html");
1836
	}
1837

    
1838
	/* substitute the PORTAL_REDIRURL variable */
1839
	if ($cpcfg['preauthurl']) {
1840
		$htmltext = str_replace("\$PORTAL_REDIRURL\$", "{$cpcfg['preauthurl']}", $htmltext);
1841
		$htmltext = str_replace("#PORTAL_REDIRURL#", "{$cpcfg['preauthurl']}", $htmltext);
1842
	}
1843

    
1844
	/* substitute other variables */
1845
	$htmltext = str_replace("\$PORTAL_ACTION\$", $portal_url, $htmltext);
1846
	$htmltext = str_replace("#PORTAL_ACTION#", $portal_url, $htmltext);
1847

    
1848
	$htmltext = str_replace("\$PORTAL_ZONE\$", htmlspecialchars($cpzone), $htmltext);
1849
	$htmltext = str_replace("\$PORTAL_REDIRURL\$", htmlspecialchars($redirurl), $htmltext);
1850
	$htmltext = str_replace("\$PORTAL_MESSAGE\$", htmlspecialchars($message), $htmltext);
1851
	$htmltext = str_replace("\$CLIENT_MAC\$", htmlspecialchars($clientmac), $htmltext);
1852
	$htmltext = str_replace("\$CLIENT_IP\$", htmlspecialchars($clientip), $htmltext);
1853

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

    
1866
	echo $htmltext;
1867
}
1868

    
1869
function captiveportal_reapply_attributes($cpentry, $attributes) {
1870
	global $cpzone, $g;
1871

    
1872
	$cpzone_config = config_get_path("captiveportal/{$cpzone}", []);
1873
	if (isset($cpzone_config['peruserbw'])) {
1874
		$dwfaultbw_up = !empty($cpzone_config['bwdefaultup']) ? $cpzone_config['bwdefaultup'] : 0;
1875
		$dwfaultbw_down = !empty($cpzone_config['bwdefaultdn']) ? $cpzone_config['bwdefaultdn'] : 0;
1876
	} else {
1877
		$dwfaultbw_up = $dwfaultbw_down = 0;
1878
	}
1879
	/* pipe throughputs must always be an integer, enforce that restriction again here. */
1880
	if (isset($cpzone_config['radiusperuserbw'])) {
1881
		$bw_up = round(!empty($attributes['bw_up']) ? intval($attributes['bw_up'])/1000 : $dwfaultbw_up, 0);
1882
		$bw_down = round(!empty($attributes['bw_down']) ? intval($attributes['bw_down'])/1000 : $dwfaultbw_down, 0);
1883
	} else {
1884
		$bw_up = round($dwfaultbw_up,0);
1885
		$bw_down = round($dwfaultbw_down,0);
1886
	}
1887

    
1888
	$bw_up_pipeno = $cpentry[1];
1889
	$bw_down_pipeno = $cpentry[1]+1;
1890

    
1891
	if ($cpentry['bw_up'] !== $bw_up) {
1892
		$_gb = mwexec("/sbin/dnctl pipe {$bw_up_pipeno} config bw {$bw_up}Kbit/s queue 100 buckets 16");
1893
		captiveportal_update_entry($cpentry['sessionid'], $bw_up, 'bw_up');
1894
	}
1895
	if ($cpentry['bw_down'] !== $bw_down) {
1896
		$_gb = mwexec("/sbin/dnctl pipe {$bw_down_pipeno} config bw {$bw_down}Kbit/s queue 100 buckets 16");
1897
		captiveportal_update_entry($cpentry['sessionid'], $bw_down, 'bw_down');
1898
	}
1899
	unset($bw_up_pipeno, $bw_down_pipeno, $bw_up, $bw_down);
1900
}
1901

    
1902
function captiveportal_update_entry($sessionid, $new_value, $field_to_update) {
1903
	global $cpzone;
1904

    
1905
	if (!intval($new_value)) {
1906
		$new_value = "'{$new_value}'";
1907
	}
1908
	captiveportal_write_db("UPDATE captiveportal SET {$field_to_update} = {$new_value} WHERE sessionid = '{$sessionid}'");
1909
}
1910

    
1911
function portal_allow($clientip, $clientmac, $username, $password = null, $redirurl = null,
1912
    $attributes = null, $pipeno = null, $authmethod = null, $context = 'first', $existing_sessionid = null) {
1913
	global $g, $cpzone;
1914

    
1915
	// Ensure we create an array if we are missing attributes
1916
	if (!is_array($attributes)) {
1917
		$attributes = array();
1918
	}
1919

    
1920
	unset($sessionid);
1921

    
1922
	/* Do not allow concurrent login execution. */
1923
	$cpdblck = lock("captiveportaldb{$cpzone}", LOCK_EX);
1924

    
1925
	if ($attributes['voucher']) {
1926
		$remaining_time = $attributes['session_timeout'];
1927
		$authmethod = "voucher"; // Set RADIUS-Attribute to Voucher to prevent ReAuth-Request for Vouchers Bug: #2155
1928
		$context = "voucher";
1929
	}
1930

    
1931
	$writecfg = false;
1932
	/* If both "Add MAC addresses of connected users as pass-through MAC" and "Disable concurrent logins" are checked,
1933
	then we need to check if the user was already authenticated using another MAC Address, and if so remove the previous Pass-Through MAC. */
1934
	$cpzone_config = config_get_path("captiveportal/{$cpzone}", []);
1935
	if ((isset($cpzone_config['noconcurrentlogins'])) && ($cpzone_config['noconcurrentlogins'] == 'last') && ($username != 'unauthenticated') && isset($cpzone_config['passthrumacadd'])) {
1936
		$mac = captiveportal_passthrumac_findbyname($username);
1937
		if (!empty($mac)) {
1938
			foreach (config_get_path("captiveportal/{$cpzone}/passthrumac", []) as $idx => $macent) {
1939
				if ($macent['mac'] != $mac['mac']) {
1940
					continue;
1941
				}
1942

    
1943
				captiveportal_passthrumac_delete_entry($macent);
1944
				config_del_path("captiveportal/{$cpzone}/passthrumac/{$idx}");
1945
			}
1946
		}
1947
	}
1948

    
1949
	/* read in client database */
1950
	$query = "WHERE ip = '{$clientip}'";
1951
	$tmpusername = SQLite3::escapeString(strtolower($username));
1952
	if (config_get_path("captiveportal/{$cpzone}/noconcurrentlogins") !== null) {
1953
		$query .= " OR (username != 'unauthenticated' AND lower(username) = '{$tmpusername}')";
1954
	}
1955
	$cpdb = captiveportal_read_db($query);
1956

    
1957
	/* Snapshot the timestamp */
1958
	$allow_time = time();
1959

    
1960
	if ($existing_sessionid !== null) {
1961
		// If we received this connection through XMLRPC sync :
1962
		// we fetch allow_time from the info given by the other node
1963
		$allow_time = $attributes['allow_time'];
1964
	}
1965
	$unsetindexes = array();
1966

    
1967
	$cpzone_config = config_get_path("captiveportal/{$cpzone}", []);
1968
	foreach ($cpdb as $cpentry) {
1969
		/* on the same ip */
1970
		if ($cpentry[2] == $clientip) {
1971
			if (isset($cpzone_config['nomacfilter']) || $cpentry[3] == $clientmac) {
1972
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT LOGIN - REUSING OLD SESSION");
1973
			} else {
1974
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT LOGIN - REUSING IP {$cpentry[2]} WITH DIFFERENT MAC ADDRESS {$cpentry[3]}");
1975
			}
1976
			$sessionid = $cpentry[5];
1977
			break;
1978
		} elseif (($attributes['voucher']) && ($username != 'unauthenticated') && ($cpentry[4] == $username)) {
1979
			// user logged in with an active voucher. Check for how long and calculate
1980
			// how much time we can give him (voucher credit - used time)
1981
			$remaining_time = $cpentry[0] + $cpentry[7] - $allow_time;
1982
			if ($remaining_time < 0) { // just in case.
1983
				$remaining_time = 0;
1984
			}
1985

    
1986
			/* This user was already logged in so we disconnect the old one, or
1987
			keep the old one, refusing the new login, or
1988
			allow the login */
1989

    
1990
			if (!isset($cpzone_config['noconcurrentlogins'])) {
1991
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "config['captiveportal'][$cpzone]['noconcurrentlogins'] 2 does not exists = NOT set");
1992
			} else {
1993
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "config['captiveportal'][$cpzone]['noconcurrentlogins'] 2 exists = set");
1994
			}
1995

    
1996
			if ($cpzone_config['noconcurrentlogins'] == "last") {
1997
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "Found last");
1998
			} else {
1999
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "Found NOT last");
2000
			}
2001

    
2002
			if (!isset($cpzone_config['noconcurrentlogins'])) {
2003
				/* 'noconcurrentlogins' not set : accept login 'username' creating multiple sessions. */
2004
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "config['captiveportal'][$cpzone]['noconcurrentlogins'] 3 does not exists = NOT set");
2005
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT LOGIN - NOT TERMINATING EXISTING SESSION(S)");
2006
			} elseif ($cpzone_config['noconcurrentlogins'] == "last") {
2007
				/* Classic situation : accept the new login, disconnect the old - present - connection */
2008
				if (isset($cpzone_config['noconcurrentlogins'])) {
2009
					captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "config['captiveportal'][$cpzone]['noconcurrentlogins'] 4 exists = set");
2010
				} else {
2011
					captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "config['captiveportal'][$cpzone]['noconcurrentlogins'] 4 does not exists = NOT set");
2012
				}
2013

    
2014
				captiveportal_disconnect($cpentry, 13);
2015
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT LOGIN - TERMINATING OLD SESSION");
2016
				$unsetindexes[] = $cpentry[5];
2017
				break;
2018
			} else {
2019
				/* Implicit 'first' : refuse the new login - 'username' is already logged in */
2020
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT VOUCHER LOGIN - NOT ALLOWED KEEPING OLD SESSION ");
2021
				unlock($cpdblck);
2022
				return 2;
2023
			}
2024
		} elseif ((isset($cpzone_config['noconcurrentlogins'])) && ($username != 'unauthenticated')) {
2025
			if ($cpzone_config['noconcurrentlogins'] == "last") {
2026
				/* on the same username */
2027
				if (strcasecmp($cpentry[4], $username) == 0) {
2028
					/* This user was already logged in so we disconnect the old one */
2029
					captiveportal_disconnect($cpentry, 13);
2030
					captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT USER LOGIN - TERMINATING OLD SESSION");
2031
					$unsetindexes[] = $cpentry[5];
2032
					break;
2033
				}
2034
			} else {
2035
				/* Implicit 'first' : refuse the new login - 'username' is already logged in */
2036
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT USER LOGIN - NOT ALLOWED KEEPING OLD SESSION ");
2037
				unlock($cpdblck);
2038
				return 2;
2039
			}
2040
		}
2041
	}
2042
	unset($cpdb);
2043

    
2044
	if (!empty($unsetindexes)) {
2045
		captiveportal_remove_entries($unsetindexes);
2046
	}
2047

    
2048
	if ($attributes['voucher'] && $remaining_time <= 0) {
2049
		return 0;       // voucher already used and no time left
2050
	}
2051

    
2052
	if (!isset($sessionid)) {
2053
		if ($existing_sessionid != null) { // existing_sessionid should only be set during XMLRPC sync
2054
			$sessionid = $existing_sessionid;
2055
		} else {
2056
			/* generate unique session ID */
2057
			$tod = gettimeofday();
2058
			$sessionid = substr(md5(mt_rand() . $tod['sec'] . $tod['usec'] . $clientip . $clientmac), 0, 16);
2059
		}
2060

    
2061
		$cpzone_config = config_get_path("captiveportal/{$cpzone}", []);
2062
		if (isset($cpzone_config['peruserbw'])) {
2063
			$dwfaultbw_up = !empty($cpzone_config['bwdefaultup']) ? $cpzone_config['bwdefaultup'] : 0;
2064
			$dwfaultbw_down = !empty($cpzone_config['bwdefaultdn']) ? $cpzone_config['bwdefaultdn'] : 0;
2065
		} else {
2066
			$dwfaultbw_up = $dwfaultbw_down = 0;
2067
		}
2068
		/* pipe throughputs must always be an integer, enforce that restriction again here. */
2069
		if (isset($cpzone_config['radiusperuserbw'])) {
2070
			$bw_up = round(!empty($attributes['bw_up']) ? intval($attributes['bw_up'])/1000 : $dwfaultbw_up, 0);
2071
			$bw_down = round(!empty($attributes['bw_down']) ? intval($attributes['bw_down'])/1000 : $dwfaultbw_down, 0);
2072
		} else {
2073
			$bw_up = round($dwfaultbw_up,0);
2074
			$bw_down = round($dwfaultbw_down,0);
2075
		}
2076

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

    
2124
			$mac['pipeno'] = $pipeno;
2125
			$mac['ip'] = $clientip;
2126
			if (!isset($cpzone_config['nomacfilter'])) {
2127
				$mac['mac'] = $clientmac;
2128
			}
2129
			captiveportal_ether_configure_entry($mac, 'auth', true);
2130

    
2131
			if ($attributes['voucher']) {
2132
				$attributes['session_timeout'] = $remaining_time;
2133
			}
2134

    
2135
			/* handle empty attributes */
2136
			$session_timeout = (!empty($attributes['session_timeout'])) ? $attributes['session_timeout'] : 'NULL';
2137
			$idle_timeout = (!empty($attributes['idle_timeout'])) ? $attributes['idle_timeout'] : 'NULL';
2138
			$session_terminate_time = (!empty($attributes['session_terminate_time'])) ? $attributes['session_terminate_time'] : 'NULL';
2139
			$interim_interval = (!empty($attributes['interim_interval'])) ? $attributes['interim_interval'] : 'NULL';
2140
			$traffic_quota = (!empty($attributes['maxbytes'])) ? $attributes['maxbytes'] : 'NULL';
2141

    
2142
			/* escape username */
2143
			$safe_username = SQLite3::escapeString($username);
2144

    
2145
			/* encode password in Base64 just in case it contains commas */
2146
			$bpassword = (isset($cpzone_config['reauthenticate'])) ? base64_encode($password) : '';
2147
			$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) ";
2148
			$insertquery .= "VALUES ({$allow_time}, {$pipeno}, '{$clientip}', '{$clientmac}', '{$safe_username}', '{$sessionid}', '{$bpassword}', ";
2149
			$insertquery .= "{$session_timeout}, {$idle_timeout}, {$session_terminate_time}, {$interim_interval}, {$traffic_quota}, {$bw_up}, {$bw_down}, '{$authmethod}', '{$context}')";
2150

    
2151
			/* store information to database */
2152
			captiveportal_write_db($insertquery);
2153
			unlock($cpdblck);
2154
			unset($insertquery, $bpassword);
2155

    
2156
			$radacct = isset($cpzone_config['radacct_enable']) ? true : false;
2157
			if ($authmethod === 'radius' && $radacct) {
2158
				captiveportal_send_server_accounting('start',
2159
					$pipeno, // ruleno
2160
					$username, // username
2161
					$clientip, // clientip
2162
					$clientmac, // clientmac
2163
					$sessionid, // sessionid
2164
					time());  // start time
2165
			}
2166
			if (captiveportal_xmlrpc_sync_get_details($syncip, $syncport, $syncuser, $syncpass, isset($existing_sessionid))) {
2167
				// $existing_sessionid prevent carp loop : only forward
2168
				// the connection to the other node if we generated the sessionid by ourselves
2169
				$rpc_client = new pfsense_xmlrpc_client();
2170
				$rpc_client->setConnectionData($syncip, $syncport, $syncuser, $syncpass);
2171
				$rpc_client->set_noticefile("CaptivePortalUserSync");
2172
				$arguments = array(
2173
					'clientip' => $clientip,
2174
					'clientmac' => $clientmac,
2175
					'username' => $username,
2176
					'password' => $password,
2177
					'attributes' => $attributes,
2178
					'allow_time' => $allow_time,
2179
					'authmethod' => $authmethod,
2180
					'context' => $context,
2181
					'sessionid' => $sessionid
2182
				);
2183

    
2184
				$rpc_client->xmlrpc_method('captive_portal_sync',
2185
					array(
2186
						'op' => 'connect_user',
2187
						'zone' => $cpzone,
2188
						'user' => base64_encode(serialize($arguments))
2189
					)
2190
				);
2191
			}
2192
		}
2193
	} else {
2194
		/* NOTE: #3062-11 If the pipeno has been allocated free it to not DoS the CP */
2195
		if (!is_null($pipeno)) {
2196
			captiveportal_free_dn_rulenos(array($pipeno, $pipeno+1));
2197
		}
2198

    
2199
		unlock($cpdblck);
2200
	}
2201

    
2202
	if ($writecfg == true) {
2203
		write_config(gettext("Captive Portal allowed users configuration changed"));
2204
	}
2205

    
2206
	if ($existing_sessionid !== null) {
2207
		if (!empty($sessionid)) {
2208
			return $sessionid;
2209
		} else {
2210
			return false;
2211
		}
2212
	}
2213
	$cpzone_config = config_get_path("captiveportal/{$cpzone}", []);
2214
	/* redirect user to desired destination */
2215
	if (is_URL($attributes['url_redirection'], true)) {
2216
		$my_redirurl = $attributes['url_redirection'];
2217
	} else if (is_URL($cpzone_config['redirurl'], true)) {
2218
		$my_redirurl = $cpzone_config['redirurl'];
2219
	} else if (is_URL($redirurl, true)) {
2220
		$my_redirurl = $redirurl;
2221
	}
2222

    
2223
	if (isset($cpzone_config['logoutwin_enable']) && !isset($cpzone_config['passthrumacadd'])) {
2224
		$ourhostname = portal_hostname_from_client_ip($clientip);
2225
		$protocol = (isset($cpzone_config['httpslogin'])) ? 'https://' : 'http://';
2226
		$logouturl = "{$protocol}{$ourhostname}/";
2227

    
2228
		if (isset($attributes['reply_message'])) {
2229
			$message = $attributes['reply_message'];
2230
		} else {
2231
			$message = 0;
2232
		}
2233

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

    
2236
	} else {
2237
		portal_reply_page($my_redirurl, "redir", "Just redirect the user.");
2238
	}
2239

    
2240
	return $sessionid;
2241
}
2242

    
2243

    
2244
/*
2245
 * Used for when pass-through credits are enabled.
2246
 * Returns true when there was at least one free login to deduct for the MAC.
2247
 * Expired entries are removed as they are seen.
2248
 * Active entries are updated according to the configuration.
2249
 */
2250
function portal_consume_passthrough_credit($clientmac) {
2251
	global $cpzone;
2252

    
2253
	$cpzone_config = config_get_path("captiveportal/{$cpzone}", []);
2254
	if (!empty($cpzone_config['freelogins_count']) && is_numeric($cpzone_config['freelogins_count'])) {
2255
		$freeloginscount = $cpzone_config['freelogins_count'];
2256
	} else {
2257
		return false;
2258
	}
2259

    
2260
	if (!empty($cpzone_config['freelogins_resettimeout']) && is_numeric($cpzone_config['freelogins_resettimeout'])) {
2261
		$resettimeout = $cpzone_config['freelogins_resettimeout'];
2262
	} else {
2263
		return false;
2264
	}
2265

    
2266
	if ($freeloginscount < 1 || $resettimeout <= 0 || !$clientmac) {
2267
		return false;
2268
	}
2269

    
2270
	$updatetimeouts = isset($cpzone_config['freelogins_updatetimeouts']);
2271

    
2272
	/*
2273
	 * Read database of used MACs.  Lines are a comma-separated list
2274
	 * of the time, MAC, then the count of pass-through credits remaining.
2275
	 */
2276
	$usedmacs = captiveportal_read_usedmacs_db();
2277

    
2278
	$currenttime = time();
2279
	$found = false;
2280
	foreach ($usedmacs as $key => $usedmac) {
2281
		$usedmac = explode(",", $usedmac);
2282

    
2283
		if ($usedmac[1] == $clientmac) {
2284
			if ($usedmac[0] + ($resettimeout * 3600) > $currenttime) {
2285
				if ($usedmac[2] < 1) {
2286
					if ($updatetimeouts) {
2287
						$usedmac[0] = $currenttime;
2288
						unset($usedmacs[$key]);
2289
						$usedmacs[] = implode(",", $usedmac);
2290
						captiveportal_write_usedmacs_db($usedmacs);
2291
						xmlrpc_sync_usedmacs($usedmacs);
2292
					}
2293

    
2294
					return false;
2295
				} else {
2296
					$usedmac[2] -= 1;
2297
					$usedmacs[$key] = implode(",", $usedmac);
2298
				}
2299

    
2300
				$found = true;
2301
			} else {
2302
				unset($usedmacs[$key]);
2303
			}
2304

    
2305
			break;
2306
		} else if ($usedmac[0] + ($resettimeout * 3600) <= $currenttime) {
2307
			unset($usedmacs[$key]);
2308
		}
2309
	}
2310

    
2311
	if (!$found) {
2312
		$usedmac = array($currenttime, $clientmac, $freeloginscount - 1);
2313
		$usedmacs[] = implode(",", $usedmac);
2314
	}
2315

    
2316
	captiveportal_write_usedmacs_db($usedmacs);
2317
	xmlrpc_sync_usedmacs($usedmacs);
2318
	return true;
2319
}
2320

    
2321
function xmlrpc_sync_usedmacs($usedmacs) {
2322
	global $cpzone;
2323

    
2324
	// XMLRPC Call over to the other node
2325
	if (captiveportal_xmlrpc_sync_get_details($syncip, $syncport,
2326
	    $syncuser, $syncpass)) {
2327
		$rpc_client = new pfsense_xmlrpc_client();
2328
		$rpc_client->setConnectionData($syncip, $syncport, $syncuser, $syncpass);
2329
		$rpc_client->set_noticefile("CaptivePortalUsedmacsSync");
2330
		$arguments = array(
2331
			'usedmacs' => $usedmacs
2332
		);
2333

    
2334
		$rpc_client->xmlrpc_method('captive_portal_sync',
2335
			array(
2336
				'op' => 'write_usedmacs',
2337
				'zone' => $cpzone,
2338
				'arguments' => base64_encode(serialize($arguments))
2339
			)
2340
		);
2341
	}
2342
}
2343

    
2344
function captiveportal_read_usedmacs_db() {
2345
	global $g, $cpzone;
2346

    
2347
	$cpumaclck = lock("captiveusedmacs{$cpzone}");
2348
	if (file_exists("{$g['vardb_path']}/captiveportal_usedmacs_{$cpzone}.db")) {
2349
		$usedmacs = file("{$g['vardb_path']}/captiveportal_usedmacs_{$cpzone}.db", FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
2350
		if (!$usedmacs) {
2351
			$usedmacs = array();
2352
		}
2353
	} else {
2354
		$usedmacs = array();
2355
	}
2356

    
2357
	unlock($cpumaclck);
2358
	return $usedmacs;
2359
}
2360

    
2361
function captiveportal_write_usedmacs_db($usedmacs) {
2362
	global $g, $cpzone;
2363

    
2364
	if (!is_array($usedmacs)) {
2365
		$usedmacs = [];
2366
	}
2367
	$cpumaclck = lock("captiveusedmacs{$cpzone}", LOCK_EX);
2368
	@file_put_contents("{$g['vardb_path']}/captiveportal_usedmacs_{$cpzone}.db", implode("\n", $usedmacs));
2369
	unlock($cpumaclck);
2370
}
2371

    
2372
function captiveportal_blocked_mac($mac) {
2373
	global $cpzone;
2374

    
2375
	if (empty($mac) || !is_macaddr($mac)) {
2376
		return false;
2377
	}
2378

    
2379
	$mac = strtolower($mac);
2380
	$action = '';
2381
	$matched = false;
2382
	foreach (config_get_path("captiveportal/{$cpzone}/passthrumac", []) as $passthrumac) {
2383
		// assume the config entry contains a valid lowercase MAC address
2384
		list($mac_entry, $mac_entry_mask) = explode('/', $passthrumac['mac']);
2385
		if ($mac_entry_mask === null) {
2386
			$mac_entry_mask = 48;
2387
		}
2388

    
2389
		// Pad config MAC parts with 0 if needed
2390
		$mac_parts = [];
2391
		foreach (explode(':', $mac_entry) as $macpart) {
2392
			$mac_parts[] = str_pad($macpart, 2, '0', STR_PAD_LEFT);
2393
		}
2394
		$mac_entry_long = hexdec(implode($mac_parts));
2395

    
2396
		// Pad client MAC parts with 0 if needed
2397
		$mac_parts = [];
2398
		foreach (explode(':', $mac) as $macpart) {
2399
			$mac_parts[] = str_pad($macpart, 2, '0', STR_PAD_LEFT);
2400
		}
2401
		$mac_long = hexdec(implode($mac_parts));
2402

    
2403
		// check against the masked MAC address
2404
		if (($mac_long & (-1 << (48 - $mac_entry_mask))) == ($mac_entry_long & (-1 << (48 - $mac_entry_mask)))) {
2405
			$action = $passthrumac['action'];
2406
			$matched = true;
2407
		}
2408

    
2409
		// a specific match takes precedence over a partial match
2410
		if ($mac_entry_mask == 48) {
2411
			break;
2412
		}
2413
	}
2414

    
2415
	if ($matched && $action == 'block') {
2416
		return true;
2417
	}
2418

    
2419
	return false;
2420
}
2421

    
2422
/* Captiveportal Radius Accounting */
2423

    
2424
function gigawords($bytes) {
2425

    
2426
	/*
2427
	 * RFC2866 Specifies a 32bit unsigned integer, which is a max of 4294967295
2428
	 * Currently there is a fault in the PECL radius_put_int function which can handle only 32bit signed integer.
2429
	 */
2430

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

    
2434
	// We need to manually set this to a zero instead of NULL for put_int() safety
2435
	if (is_null($gigawords)) {
2436
		$gigawords = 0;
2437
	}
2438

    
2439
	return $gigawords;
2440
}
2441

    
2442
function remainder($bytes) {
2443
	// Calculate the bytes we are going to send to the radius
2444
	$bytes = bcmod($bytes, GIGAWORDS_RIGHT_OPERAND);
2445

    
2446
	if (is_null($bytes)) {
2447
		$bytes = 0;
2448
	}
2449

    
2450
    return $bytes;
2451
}
2452

    
2453
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) {
2454
	global $cpzone;
2455

    
2456
	$cpcfg = config_get_path("captiveportal/{$cpzone}", []);
2457
	$acctcfg = auth_get_authserver($cpcfg['radacct_server']);
2458

    
2459
	if (!isset($cpcfg['radacct_enable']) || empty($acctcfg) ||
2460
	    captiveportal_ha_is_node_in_backup_mode($cpzone)) {
2461
		return null;
2462
	}
2463

    
2464
	if ($type === 'on') {
2465
		$racct = new Auth_RADIUS_Acct_On;
2466
	} elseif ($type === 'off') {
2467
		$racct = new Auth_RADIUS_Acct_Off;
2468
	} elseif ($type === 'start') {
2469
		$racct = new Auth_RADIUS_Acct_Start;
2470
		if (!is_int($start_time)) {
2471
			$start_time = time();
2472
		}
2473
	} elseif ($type === 'stop') {
2474
		$racct = new Auth_RADIUS_Acct_Stop;
2475
		if (!is_int($stop_time)) {
2476
			$stop_time = time();
2477
		}
2478
	} elseif ($type === 'update') {
2479
        $racct = new Auth_RADIUS_Acct_Update;
2480
		if (!is_int($stop_time)) {
2481
			$stop_time = time(); // "top time" here will be used only for calculating session time.
2482
		}
2483
	} else {
2484
		return null;
2485
	}
2486

    
2487
	$racct->addServer($acctcfg['host'], $acctcfg['radius_acct_port'],
2488
		$acctcfg['radius_secret'], $acctcfg['radius_timeout']);
2489

    
2490
	$racct->authentic = RADIUS_AUTH_RADIUS;
2491
	if ($cpcfg['auth_method'] === 'radmac' && $username === "unauthenticated" && !empty($clientmac)) {
2492
		$racct->username = mac_format($clientmac);
2493
	} elseif (!empty($username)) {
2494
		$racct->username = $username;
2495
	}
2496

    
2497
	if (PEAR::isError($racct->start())) {
2498
		captiveportal_syslog('RADIUS ACCOUNTING FAILED : '.$racct->getError());
2499
		$racct->close();
2500
		return null;
2501
	}
2502

    
2503
	$nasip = nasip_fallback($acctcfg['radius_nasip_attribute']);
2504
	$nasmac = get_interface_mac(find_ip_interface($nasip));
2505
	$racct->putAttribute(RADIUS_NAS_IP_ADDRESS, $nasip, "addr");
2506

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

    
2509
	if (is_int($ruleno)) {
2510
		$racct->putAttribute(RADIUS_NAS_PORT_TYPE, RADIUS_ETHERNET);
2511
		$racct->putAttribute(RADIUS_NAS_PORT, intval($ruleno), 'integer');
2512
	}
2513

    
2514
	if (!empty($sessionid)) {
2515
		$racct->putAttribute(RADIUS_ACCT_SESSION_ID, $sessionid);
2516
	}
2517

    
2518
	if (!empty($clientip) && is_ipaddr($clientip)) {
2519
		$racct->putAttribute(RADIUS_FRAMED_IP_ADDRESS, $clientip, "addr");
2520
	}
2521
	if (!empty($clientmac)) {
2522
		$racct->putAttribute(RADIUS_CALLING_STATION_ID, mac_format($clientmac));
2523
	}
2524
	if (!empty($nasmac)) {
2525
		$racct->putAttribute(RADIUS_CALLED_STATION_ID, mac_format($nasmac).':'.gethostname());
2526
	}
2527

    
2528
	// Accounting request Stop and Update : send the current data volume
2529
	if (($type === 'stop' || $type === 'update') && is_int($start_time)) {
2530
		$volume = getVolume($clientip);
2531
		$session_time = $stop_time - $start_time;
2532
		$volume['input_bytes_radius'] = remainder($volume['input_bytes']);
2533
		$volume['input_gigawords'] = gigawords($volume['input_bytes']);
2534
		$volume['output_bytes_radius'] = remainder($volume['output_bytes']);
2535
		$volume['output_gigawords'] = gigawords($volume['output_bytes']);
2536

    
2537
		// Volume stuff: Ingress
2538
		$racct->putAttribute(RADIUS_ACCT_INPUT_PACKETS, intval($volume['input_pkts']), "integer");
2539
		$racct->putAttribute(RADIUS_ACCT_INPUT_OCTETS, intval($volume['input_bytes_radius']), "integer");
2540
		// Volume stuff: Outgress
2541
		$racct->putAttribute(RADIUS_ACCT_OUTPUT_PACKETS, intval($volume['output_pkts']), "integer");
2542
		$racct->putAttribute(RADIUS_ACCT_OUTPUT_OCTETS, intval($volume['output_bytes_radius']), "integer");
2543
		$racct->putAttribute(RADIUS_ACCT_SESSION_TIME, intval($session_time), "integer");
2544

    
2545
		$racct->putAttribute(CUSTOM_RADIUS_ACCT_OUTPUT_GIGAWORDS, intval($volume['output_gigawords']), "integer");
2546
		$racct->putAttribute(CUSTOM_RADIUS_ACCT_INPUT_GIGAWORDS, intval($volume['input_gigawords']), "integer");
2547
		// Set session_time
2548
		$racct->session_time = $session_time;
2549
	}
2550

    
2551
	if ($type === 'stop') {
2552
		if (empty($term_cause)) {
2553
			$term_cause = 1;
2554
		}
2555
		$racct->putAttribute(RADIUS_ACCT_TERMINATE_CAUSE, $term_cause);
2556
	}
2557

    
2558
	// Send request
2559
	$result = $racct->send();
2560

    
2561
	if (PEAR::isError($result)) {
2562
		 captiveportal_syslog('RADIUS ACCOUNTING FAILED : '.$racct->getError());
2563
		 $result = null;
2564
	} elseif ($result !== true) {
2565
		$result = false;
2566
	}
2567

    
2568
	$racct->close();
2569
	return $result;
2570
}
2571

    
2572
function captiveportal_isip_logged($clientip) {
2573
	global $g, $cpzone;
2574

    
2575
	/* read in client database */
2576
	$query = "WHERE ip = '{$clientip}'";
2577
	$cpdb = captiveportal_read_db($query);
2578
	foreach ($cpdb as $cpentry) {
2579
		return $cpentry;
2580
	}
2581
}
2582

    
2583
function captiveportal_allowedhostname_cleanup() {
2584
	global $g, $cpzone;
2585

    
2586
	$cpzone_config = config_get_path("captiveportal/{$cpzone}", []);
2587
	$cpzoneprefix = CPPREFIX . $cpzone_config['zoneid'];
2588

    
2589
	foreach ($cpzone_config['allowedhostname'] as $id => $hostnameent) {
2590
		$pipes = pfSense_pf_cp_get_eth_pipes("{$cpzoneprefix}_allowedhosts/hostname_{$id}");
2591
		pfSense_pf_cp_flush("{$cpzoneprefix}_allowedhosts/hostname_{$id}", "ether");
2592
		if (!empty($pipes)) {
2593
			captiveportal_pipes_delete($pipes);
2594
		}
2595
	}
2596
}
2597

    
2598
function filter_captiveportal_aliases() {
2599
	/* return all aliases used in captive portal zones,
2600
	 * to prevent it from deletion in filter_configure_sync() as unused aliases */
2601
	global $g;
2602

    
2603
	$aliasesnames = array();
2604

    
2605
	foreach (config_get_path('captiveportal', []) as $cpzone => $cpcfg) {
2606
		if (isset($cpcfg['enable'])) {
2607
			$cpzoneprefix = CPPREFIX . $cpcfg['zoneid'];
2608
			$aliasesnames[] = $cpzoneprefix . '_cpips';
2609
			foreach ($cpcfg['allowedhostname'] as $id => $hostnameent) {
2610
				$aliasesnames[] = $cpzoneprefix . '_hostname_' . $id;
2611
			}
2612
		}
2613
	}
2614

    
2615
	return $aliasesnames;
2616
}
2617

    
2618
function filter_captiveportal_tables() {
2619
	/* return pf rules which defines tables used in captive portal zones */
2620
	global $FilterIflist;
2621

    
2622
	$rules = '';
2623
	foreach (config_get_path('captiveportal', []) as $cpzone => $cpcfg) {
2624
		if (!isset($cpcfg['enable'])) {
2625
			continue;
2626
		}
2627

    
2628
		$cpzoneprefix = CPPREFIX . $cpcfg['zoneid'];
2629
		$cpips = $cpzoneprefix . '_cpips';
2630
		$cpiplist = array();
2631

    
2632
		foreach (explode(",", $cpcfg['interface']) as $cpifgrp) {
2633
			if (isset($FilterIflist[$cpifgrp])) {
2634
				$realif = get_real_interface($cpifgrp);
2635
				if (!empty($realif)) {
2636
					$cpip = get_interface_ip($cpifgrp);
2637
					if (is_ipaddrv4($cpip)) {
2638
						$cpipliststring = $cpip . ' ' . get_interface_vip_ips($cpifgrp);
2639
						$cpiplist = array_filter(array_merge($cpiplist, explode(' ', $cpipliststring)),
2640
												 function ($val) {
2641
													 return (trim($val) != "");
2642
												 });
2643
					}
2644
				}
2645
			}
2646
		}
2647
		if (!empty($cpiplist)) {
2648
			/* captive portal web server IP addresses */
2649
			$rules .= "table <{$cpips}> { " . join(' ', $cpiplist)  . "}\n";
2650
		}
2651
	}
2652

    
2653
	if (!empty($rules)) {
2654
		$rules = "\n# Captive Portal\n" . $rules . "\n";
2655
	}
2656

    
2657
	return $rules;
2658
}
2659

    
2660
function filter_captiveportal_ether() {
2661
	global $g;
2662

    
2663
	$rules = '';
2664
	foreach (config_get_path('captiveportal', []) as $cpzone => $cpcfg) {
2665
		if (!isset($cpcfg['enable'])) {
2666
			continue;
2667
		}
2668

    
2669
		$cpzoneprefix = CPPREFIX . $cpcfg['zoneid'];
2670
		$rdrtag = $cpzoneprefix . '_rdr';
2671
		$interfaces = captiveportal_zone_interfaces($cpcfg);
2672

    
2673
		if (!empty($interfaces)) {
2674
			/* set 'rdr' tag for further captive portal web portal redirection */
2675
			$rules .= "ether pass on { {$interfaces} } tag \"{$rdrtag}\"\n";
2676
			/* anchor to set the PASS tag for authenticated clients */
2677
			$rules .= "ether anchor \"{$cpzoneprefix}_auth/*\" on { {$interfaces} }\n";
2678
			/* anchor for Services / Captive Portal / CPZONE / MACs */
2679
			$rules .= "ether anchor \"{$cpzoneprefix}_passthrumac/*\" on { {$interfaces} }\n";
2680
			/* anchor to set the PASSTHRU tag for Allowed IP/Hostnames */
2681
			$rules .= "ether anchor \"{$cpzoneprefix}_allowedhosts/*\" on { {$interfaces} }\n";
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_rdr() {
2693
	global $g, $FilterIflist;
2694

    
2695
	$rules = '';
2696
	foreach (config_get_path('captiveportal', []) as $cpzone => $cpcfg) {
2697
		if (!isset($cpcfg['enable'])) {
2698
			continue;
2699
		}
2700

    
2701
		$cpzoneprefix = CPPREFIX . $cpcfg['zoneid'];
2702
		$rdrtag = $cpzoneprefix . '_rdr';
2703
		$cpips = $cpzoneprefix . '_cpips';
2704
		$rdr_ports = captiveportal_zone_portalports($cpcfg);
2705
		foreach (explode(",", $cpcfg['interface']) as $cpifgrp) {
2706
			if (isset($FilterIflist[$cpifgrp])) {
2707
				$realif = get_real_interface($cpifgrp);
2708
				if (!empty($realif)) {
2709
					$cpip = get_interface_ip($cpifgrp);
2710
					if (is_ipaddrv4($cpip)) {
2711
						foreach ($rdr_ports as list($portalias, $cprdrport)) {
2712
							$rules .= "rdr on {$realif} inet proto tcp from any to ! <{$cpips}> port {$cprdrport} tagged {$rdrtag} -> {$cpip} port {$portalias}\n";
2713
						}
2714
					}
2715
				}
2716
			}
2717
		}
2718
	}
2719

    
2720
	if (!empty($rules)) {
2721
		$rules = "\n# Captive Portal\n" . $rules . "\n";
2722
	}
2723

    
2724
	return $rules;
2725
}
2726

    
2727
function filter_captiveportal_pass() {
2728
	global $g, $FilterIflist;
2729

    
2730
	$captiveportal_increment = 'filter_captiveportal_tracker';
2731

    
2732
	$rules = '';
2733
	foreach (config_get_path('captiveportal', []) as $cpzone => $cpcfg) {
2734
		if (!isset($cpcfg['enable'])) {
2735
			continue;
2736
		}
2737

    
2738
		$cpzoneprefix = CPPREFIX . $cpcfg['zoneid'];
2739
		$cpips = $cpzoneprefix . '_cpips';
2740
		$authtag = $cpzoneprefix . '_auth';
2741
		$rdr_ports = captiveportal_zone_portalports($cpcfg);
2742

    
2743
		foreach (explode(",", $cpcfg['interface']) as $cpifgrp) {
2744
			if (!isset($FilterIflist[$cpifgrp])) {
2745
				continue;
2746
			}
2747
			$realif = get_real_interface($cpifgrp);
2748
			if (!empty($realif)) {
2749
				$cpip = get_interface_ip($cpifgrp);
2750
				if (is_ipaddrv4($cpip)) {
2751
					foreach ($rdr_ports as list($portalias, $cprdrport)) {						/* pass non-authenticated clients to captive portal */
2752
						$rules .= "pass in quick on {$realif} proto tcp from any to <{$cpips}> port {$portalias} ridentifier {$captiveportal_increment()} keep state(sloppy)\n";
2753
						/* without this rule captive portal doesn't show login page after manual disconnect */
2754
						$rules .= "pass out quick on {$realif} proto tcp from {$cpip} port {$portalias} to any flags any ridentifier {$captiveportal_increment()} keep state(sloppy)\n";
2755
					}
2756
					/* block non-authenticated clients access to internet */
2757
					$rules .= "block in quick on {$realif} from any to ! <{$cpips}> ! tagged {$authtag} ridentifier {$captiveportal_increment()}\n";
2758
				}
2759
			}
2760
		}
2761
	}
2762

    
2763
	if (!empty($rules)) {
2764
		$rules = "\n# Captive Portal\n" . $rules . "\n";
2765
	}
2766

    
2767
	return $rules;
2768
}
2769

    
2770
function captiveportal_zone_interfaces($cpcfg) {
2771
	/* return a list of captive portal zone interfaces */
2772
	global $FilterIflist;
2773

    
2774
	$interfaces = '';
2775
	foreach (explode(",", $cpcfg['interface']) as $cpifgrp) {
2776
		if (isset($FilterIflist[$cpifgrp])) {
2777
			$realif = get_real_interface($cpifgrp);
2778
			if (!empty($realif) && get_interface_ip($realif)) {
2779
				$interfaces .= $realif . ' ';
2780
			}
2781
		}
2782
	}
2783
	return $interfaces;
2784
}
2785

    
2786
/*
2787
 * Returns an array of (alias, rdrport) pairs describing ports to be forwarded for the captive portal
2788
 */
2789
function captiveportal_zone_portalports($cpcfg) {
2790
	$rdr_ports = array();
2791
	if (isset($cpcfg['httpslogin']) && !isset($cpcfg['nohttpsforwards'])) {
2792
		$portalias = $cpcfg['listenporthttps'] ? $cpcfg['listenporthttps'] : 8001 + $cpcfg['zoneid'];
2793
		$cprdrport = '443';
2794
		array_push($rdr_ports, array($portalias, $cprdrport));
2795
	}
2796
	$portalias = $cpcfg['listenporthttp'] ? $cpcfg['listenporthttp'] : 8000 + $cpcfg['zoneid'];
2797
	$cprdrport = '80';
2798
	array_push($rdr_ports, array($portalias, $cprdrport));
2799

    
2800
	return $rdr_ports;
2801
}
2802

    
2803
function captiveportal_pipes_delete($pipes) {
2804
	if (!empty($pipes)) {
2805
		foreach ($pipes as $pipe) {
2806
			mwexec("/sbin/dnctl pipe delete {$pipe}");
2807
		}
2808
		captiveportal_free_dn_rulenos($pipes);
2809
	}
2810
}
2811

    
2812
function captiveportal_ether_configure_entry($hostent, $anchor, $user_auth = false) {
2813
	global $g, $cpzone;
2814

    
2815
	if (($hostent['action'] == 'block') && ($anchor == 'passthrumac')) {
2816
		return;
2817
	}
2818

    
2819
	$cpzoneprefix = CPPREFIX . config_get_path("captiveportal/{$cpzone}/zoneid");
2820
	if ($anchor == 'passthrumac') {
2821
		$tag = $cpzoneprefix . '_auth';
2822
	} else {
2823
		$tag = $cpzoneprefix . '_' . $anchor;
2824
	}
2825

    
2826
	if ($anchor == 'passthrumac') {
2827
		list($pipeup, $pipedown) = captiveportal_pipe_configure($hostent, 'pipe_mac', $user_auth);
2828
		$host = str_replace("/", "_", str_replace(":", "", $hostent['mac']));
2829
		$l3from = '';
2830
		$l3to = '';
2831
		$macfrom = "from {$hostent['mac']}";
2832
		$macto = "to {$hostent['mac']}";
2833
	} else {
2834
		list($pipeup, $pipedown) = captiveportal_pipe_configure($hostent, 'auth', $user_auth);
2835
		$host = $hostent['ip'] . '_32';
2836
		$l3from = "l3 from {$hostent['ip']}";
2837
		$l3to = "l3 to {$hostent['ip']}";
2838
		if (!config_path_enabled("captiveportal/{$cpzone}/nomacfilter")) {
2839
			if (!empty($hostent['mac'])) {
2840
				$macfrom = "from {$hostent['mac']}";
2841
				$macto = "to {$hostent['mac']}";
2842
			} else {
2843
				return;
2844
			}
2845
		} else {
2846
			$macfrom = '';
2847
			$macto = '';
2848
		}
2849
	}
2850

    
2851
	$rules = "ether pass in quick {$macfrom} {$l3from} tag {$tag} dnpipe {$pipeup}\n";
2852
	$rules .= "ether pass out quick {$macto} {$l3to} tag {$tag} dnpipe {$pipedown}\n";
2853

    
2854
	captiveportal_load_pfctl("{$cpzoneprefix}_{$anchor}", $host, $rules);
2855
}
2856

    
2857
function captiveportal_pipe_configure($host, $type, $user_auth = true) {
2858
	global $cpzone;
2859

    
2860
	$cpzone_config = config_get_path("captiveportal/{$cpzone}", []);
2861
	$bwUp = 0;
2862
	if (!empty($host['bw_up'])) {
2863
		$bwUp = $host['bw_up'];
2864
	} elseif ($user_auth &&
2865
		isset($cpzone_config['peruserbw']) &&
2866
	    !empty($cpzone_config['bwdefaultup'])) {
2867
		$bwUp = $cpzone_config['bwdefaultup'];
2868
	}
2869
	$bwDown = 0;
2870
	if (!empty($host['bw_down'])) {
2871
		$bwDown = $host['bw_down'];
2872
	} elseif ($user_auth &&
2873
		isset($cpzone_config['peruserbw']) &&
2874
	    !empty($cpzone_config['bwdefaultdn'])) {
2875
		$bwDown = $cpzone_config['bwdefaultdn'];
2876
	}
2877

    
2878
	if (isset($host['pipeno']) && !empty($host['pipeno'])) {
2879
		$pipeup = $host['pipeno'];
2880
	} else {
2881
		$pipeup = captiveportal_get_next_dn_ruleno($type);
2882
	}
2883

    
2884
	mwexec("/sbin/dnctl pipe {$pipeup} config bw {$bwUp}Kbit/s queue 100 buckets 16");
2885
	$pipedown = $pipeup + 1;
2886
	mwexec("/sbin/dnctl pipe {$pipedown} config bw {$bwDown}Kbit/s queue 100 buckets 16");
2887

    
2888
	return array($pipeup, $pipedown);
2889
}
2890

    
2891
function captiveportal_allowedip_configure_entry($ipent) {
2892
	global $g, $cpzone;
2893

    
2894
	$cpzoneprefix = CPPREFIX . config_get_path("captiveportal/{$cpzone}/zoneid");
2895
	$tag = $cpzoneprefix . '_auth';
2896

    
2897
	if (empty($ipent['sn'])) {
2898
		$ipent['sn'] = '32';
2899
	}
2900

    
2901
	$host = $ipent['ip'] . '_' . $ipent['sn'];
2902
	list($pipeup, $pipedown) = captiveportal_pipe_configure($ipent, 'allowed', false);
2903

    
2904
	$rules = '';
2905
	if (($ipent['dir'] == 'to') || ($ipent['dir'] == 'both')) {
2906
		$rules = "ether pass in quick l3 to {$ipent['ip']}/{$ipent['sn']} tag {$tag} dnpipe {$pipeup}\n";
2907
	}
2908
	if (($ipent['dir'] == 'from') || ($ipent['dir'] == 'both')) {
2909
		$rules .= "ether pass in quick l3 from {$ipent['ip']}/{$ipent['sn']} tag {$tag} dnpipe {$pipedown}\n";
2910
	}
2911

    
2912
	captiveportal_load_pfctl("{$cpzoneprefix}_allowedhosts", $host, $rules);
2913
}
2914

    
2915
function captiveportal_allowedhostname_configure_entry($ipent, $hostnameid = 1) {
2916
	global $cpzone;
2917

    
2918
	if (!isset($ipent['hostname'])) {
2919
		return;
2920
	}
2921

    
2922
	$cpzoneprefix = CPPREFIX . config_get_path("captiveportal/{$cpzone}/zoneid");
2923
	$tag = $cpzoneprefix . '_auth';
2924
	$table = $cpzoneprefix . '_hostname_' . $hostnameid;
2925
	$host = 'hostname_' . $hostnameid;
2926
	list($pipeup, $pipedown) = captiveportal_pipe_configure($ipent, 'allowed', false);
2927

    
2928
	$rules = "table <{$table}> persist\n";
2929
	if (($ipent['dir'] == 'to') || ($ipent['dir'] == 'both')) {
2930
		$rules .= "ether pass in quick l3 to <{$table}> tag {$tag} dnpipe {$pipeup}\n";
2931
	}
2932
	if (($ipent['dir'] == 'from') || ($ipent['dir'] == 'both')) {
2933
		$rules .= "ether pass in quick l3 from <{$table}> tag {$tag} dnpipe {$pipedown}\n";
2934
	}
2935

    
2936
	captiveportal_load_pfctl("{$cpzoneprefix}_allowedhosts", $host, $rules);
2937

    
2938
	/* return filterdns entry */
2939
	return "pf {$ipent['hostname']} {$table} {$cpzoneprefix}_allowedhosts/{$host}\n";
2940
}
2941

    
2942
function captiveportal_load_pfctl($anchor, $host, $rules) {
2943
	global $g, $cpzone;
2944

    
2945
	if (!empty($rules)) {
2946
		mwexec("/usr/bin/printf \"{$rules}\" | /sbin/pfctl -a {$anchor}/{$host} -f-");
2947
	} else {
2948
		log_error("CP zone {$cpzone}: {$anchor} rules are empty for {$host}");
2949
	}
2950
}
2951

    
2952
function captiveportal_anchor_zerocnt($ip, $anchor = 'auth') {
2953
	global $cpzone;
2954
	$cpzoneprefix = CPPREFIX . config_get_path("captiveportal/{$cpzone}/zoneid");
2955

    
2956
	pfSense_pf_cp_zerocnt("{$cpzoneprefix}_{$anchor}/{$ip}_32");
2957
}
2958

    
2959
?>
(6-6/61)