Project

General

Profile

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

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

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

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

    
105
<head>
106

    
107
  <meta charset="UTF-8">
108
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
109
  <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
110
  <title>Captive Portal Login Page</title>
111
  <style>
112
	  #content,.login,.login-card a,.login-card h1,.login-help{text-align:center}body,html{margin:0;padding:0;width:100%;height:100%;display:table}#content{font-family:'Source Sans Pro',sans-serif;background-color:#1C1275;background:{$bg_src};-webkit-background-size:cover;-moz-background-size:cover;-o-background-size:cover;background-size:cover;display:table-cell;vertical-align:middle}.login-card{padding:40px;width:280px;background-color:#F7F7F7;margin:100px auto 10px;border-radius:2px;box-shadow:0 2px 2px rgba(0,0,0,.3);overflow:hidden}.login-card h1{font-weight:400;font-size:2.3em;color:#1383c6}.login-card h1 span{color:#f26721}.login-card img{width:70%;height:70%}.login-card input[type=submit]{width:100%;display:block;margin-bottom:10px;position:relative}.login-card input[type=text],input[type=password]{height:44px;font-size:16px;width:100%;margin-bottom:10px;-webkit-appearance:none;background:#fff;border:1px solid #d9d9d9;border-top:1px solid silver;padding:0 8px;box-sizing:border-box;-moz-box-sizing:border-box}.login-card input[type=text]:hover,input[type=password]:hover{border:1px solid #b9b9b9;border-top:1px solid #a0a0a0;-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.login{font-size:14px;font-family:Arial,sans-serif;font-weight:700;height:36px;padding:0 8px}.login-submit{-webkit-appearance:none;-moz-appearance:none;appearance:none;border:0;color:#fff;text-shadow:0 1px rgba(0,0,0,.1);background-color:#4d90fe}.login-submit:disabled{opacity:.6}.login-submit:hover{border:0;text-shadow:0 1px rgba(0,0,0,.3);background-color:#357ae8}.login-card a{text-decoration:none;color:#222;font-weight:400;display:inline-block;opacity:.6;transition:opacity ease .5s}.login-card a:hover{opacity:1}.login-help{width:100%;font-size:12px}.list{list-style-type:none;padding:0}.list__item{margin:0 0 .7rem;padding:0}label{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;text-align:left;font-size:14px;}input[type=checkbox]{-webkit-box-flex:0;-webkit-flex:none;-ms-flex:none;flex:none;margin-right:10px;float:left}@media screen and (max-width:450px){.login-card{width:70%!important}.login-card img{width:30%;height:30%}}textarea{width:66%;margin:auto;height:120px;max-height:120px;background-color:#f7f7f7;padding:20px}#terms{display:none;padding-top:100px;padding-bottom:300px;}.auth_source{border: 1px solid lightgray; padding:20px 8px 0px 8px; margin-top: -2em; border-radius: 2px; }.auth_head{background-color:#f7f7f7;display:inline-block;}.auth_head_div{text-align:left;}#error-message{text-align:left;color:#ff3e3e;font-style:italic;}
113
  </style>
114
</head>
115

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

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

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

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

    
155

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

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

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

    
194
EOD;
195

    
196
	return $htmltext;
197
}
198

    
199
function captiveportal_configure() {
200
	global $config, $g, $cpzone, $cpzoneid;
201

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
343
EOD;
344
		}
345

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

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

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

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

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

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

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

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

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

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

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

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

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

    
417
	unlock($captiveportallck);
418

    
419
	return 0;
420
}
421

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

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

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

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

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

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

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

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

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

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

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

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

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

    
495
	dummynet_load_module('100');
496

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
593

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

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

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

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

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

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

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

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

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

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

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

    
797
	captiveportal_prune_old_automac();
798

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
963
	captiveportal_radius_stop_all($term_cause, $logoutReason);
964

    
965
	/* reinit captiveportal pipes and anchors */
966
	captiveportal_init_rules(true);
967

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

    
976
	unlock($cpdblck);
977
	unlock($rcprunelock);
978
	return true;
979
}
980

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

    
985
	$cpdb = captiveportal_read_db();
986

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

    
1011
function captiveportal_passthrumac_delete_entry($macent) {
1012
	global $g, $cpzone, $config;
1013

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

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

    
1027
	pfSense_pf_cp_flush("{$cpzoneprefix}_passthrumac/{$host}", "ether");
1028
}
1029

    
1030
function captiveportal_passthrumac_configure($startindex = 0, $stopindex = 0) {
1031
	global $config, $g, $cpzone;
1032

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

    
1060
function captiveportal_passthrumac_findbyname($username) {
1061
	global $config, $cpzone;
1062

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

    
1073
function captiveportal_ether_delete_entry($hostent, $anchor = 'allowedhosts') {
1074
	global $g, $cpzone, $config;
1075

    
1076
	$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
1077

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

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

    
1092
function captiveportal_allowedhostname_configure() {
1093
	global $config, $g, $cpzone, $cpzoneid;
1094

    
1095
	if (!is_array($config['captiveportal'][$cpzone]['allowedhostname'])) {
1096
		return false;
1097
	}
1098

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

    
1112
function captiveportal_filterdns_configure() {
1113
	global $config, $g, $cpzone, $cpzoneid;
1114

    
1115
	$cp_filterdns_filename = g_get('varetc_path') .
1116
	    "/filterdns-{$cpzone}-captiveportal.conf";
1117

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

    
1137
function captiveportal_allowedip_configure() {
1138
	global $config, $g, $cpzone;
1139

    
1140
	if (is_array($config['captiveportal'][$cpzone]['allowedip'])) {
1141
		foreach ($config['captiveportal'][$cpzone]['allowedip'] as $ipent) {
1142
			captiveportal_allowedip_configure_entry($ipent);
1143
		}
1144
	}
1145
}
1146

    
1147
/* get last activity timestamp given client IP address */
1148
function captiveportal_get_last_activity($ip) {
1149
	global $config, $cpzone;
1150

    
1151
	$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
1152
	$anchor = $cpzoneprefix . '_auth';
1153

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

    
1163
	return $time;
1164
}
1165

    
1166

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

    
1180
/* log simple messages to syslog */
1181
function captiveportal_syslog($message) {
1182
	global $cpzone;
1183

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

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

    
1197
	$login_status = 'FAILURE';
1198
	$login_msg = gettext('Invalid credentials specified');
1199
	$reply_attributes = array();
1200
	$auth_method = '';
1201
	$auth_result = null;
1202

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

    
1211
		- Reply message of a user failed auth is more important than reply message of
1212
		a server failed auth (unable to contact server)
1213

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

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

    
1225
	/* Getting authentication servers from captiveportal configuration */
1226
	$auth_servers = array();
1227

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

    
1237
		foreach ($fullauthservers as $authserver) {
1238
			if (strpos($authserver, ' - ') !== false) {
1239
				$authserver = explode(' - ', $authserver);
1240
				array_shift($authserver);
1241
				$authserver = implode(' - ', $authserver);
1242

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

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

    
1268
				$result = null;
1269
				$status = null;
1270
				$msg = null;
1271

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

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

    
1293
					if (!empty($attributes['error_message'])) {
1294
						$msg = $attributes['error_message'];
1295
					}
1296

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

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

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

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

    
1345
	return array('result'=>$auth_result, 'attributes'=>$reply_attributes, 'auth_method' =>$auth_method, 'login_status'=> $login_status, 'login_message' => $login_msg);
1346
}
1347

    
1348
function captiveportal_opendb() {
1349
	global $g, $config, $cpzone, $cpzoneid;
1350

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

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

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

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

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

    
1418
	return $DB;
1419
}
1420

    
1421
/* read captive portal DB into array */
1422
function captiveportal_read_db($query = "") {
1423
	$cpdb = array();
1424

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

    
1436
	return $cpdb;
1437
}
1438

    
1439
function captiveportal_remove_entries($remove, $carp_loop = false) {
1440
	global $cpzone;
1441

    
1442
	if (!is_array($remove) || empty($remove)) {
1443
		return;
1444
	}
1445

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

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

    
1471
/* write captive portal DB */
1472
function captiveportal_write_db($queries) {
1473
	global $g;
1474

    
1475
	if (is_array($queries)) {
1476
		$query = implode(";", $queries);
1477
	} else {
1478
		$query = $queries;
1479
	}
1480

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

    
1497
function captiveportal_write_elements() {
1498
	global $g, $config, $cpzone;
1499

    
1500
	$cpcfg = config_get_path("captiveportal/{$cpzone}");
1501

    
1502
	if (!is_dir(g_get('captiveportal_element_path'))) {
1503
		@mkdir(g_get('captiveportal_element_path'));
1504
	}
1505

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

    
1525
	return 0;
1526
}
1527

    
1528
function captiveportal_free_dnrules($rulenos_start = 2000,
1529
    $rulenos_range_max = 64500, $dry_run = false, $clear_auth_pipes = true) {
1530
	global $g, $cpzone;
1531

    
1532
	$removed_pipes = array();
1533

    
1534
	if (!file_exists("{$g['vardb_path']}/captiveportaldn.rules")) {
1535
		return $removed_pipes;
1536
	}
1537

    
1538
	if (!$dry_run) {
1539
		$cpruleslck = lock("captiveportalrulesdn", LOCK_EX);
1540
	}
1541

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

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

    
1572
	unset($rules);
1573

    
1574
	return $removed_pipes;
1575
}
1576

    
1577
function captiveportal_reserve_ruleno($ruleno) {
1578
	global $g, $cpzone;
1579

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

    
1590
	file_put_contents("{$g['vardb_path']}/captiveportaldn.rules", serialize($rules));
1591
	unlock($cpruleslck);
1592
	unset($rules);
1593

    
1594
	return $ruleno;
1595
}
1596

    
1597
function captiveportal_get_next_dn_ruleno($rule_type = 'default', $rulenos_start = 2000, $rulenos_range_max = 64500, $check_only = false) {
1598
	global $config, $g, $cpzone;
1599

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

    
1629
	return $ruleno;
1630
}
1631

    
1632
function captiveportal_free_dn_rulenos($rulenos) {
1633
	global $config, $g;
1634

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

    
1647
function captiveportal_get_dn_passthru_pipes($mac, $anchor = 'passthrumac') {
1648
	global $config, $g, $cpzone, $cpzoneid;
1649

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

    
1656
	$host = str_replace(":", "", $mac);
1657
	$pipes = pfSense_pf_cp_get_eth_pipes("{$cpzoneprefix}_{$anchor}/$host");
1658

    
1659
	return $pipes;
1660
}
1661

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

    
1673
function getVolume($ip) {
1674
	global $g, $config, $cpzone;
1675

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

    
1682
	/* no needs to check allowedip */
1683
	$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
1684
	$anchor = $cpzoneprefix . '_auth';
1685

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

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

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

    
1728
	return $volume;
1729
}
1730

    
1731
function portal_ip_from_client_ip($cliip) {
1732
	global $config, $cpzone;
1733

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

    
1749
	$route = route_get($cliip, 'inet', true);
1750
	if (empty($route)) {
1751
		return false;
1752
	}
1753

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

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

    
1774
	return false;
1775
}
1776

    
1777
function portal_hostname_from_client_ip($cliip) {
1778
	global $config, $cpzone;
1779

    
1780
	$cpcfg = config_get_path("captiveportal/{$cpzone}");
1781

    
1782
	if (isset($cpcfg['httpslogin'])) {
1783
		$listenporthttps = $cpcfg['listenporthttps'] ? $cpcfg['listenporthttps'] : ($cpcfg['zoneid'] + 8001);
1784
		$ourhostname = $cpcfg['httpsname'];
1785

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

    
1798
		if ($listenporthttp != 80) {
1799
			$ourhostname .= ":" . $listenporthttp;
1800
		}
1801
	}
1802

    
1803
	return $ourhostname;
1804
}
1805

    
1806
/* functions move from index.php */
1807

    
1808
function portal_reply_page($redirurl, $type = null, $message = null, $clientmac = null, $clientip = null, $username = null, $password = null, $voucher = null) {
1809
	global $g, $config, $cpzone;
1810

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

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

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

    
1833
	/* substitute other variables */
1834
	$htmltext = str_replace("\$PORTAL_ACTION\$", $portal_url, $htmltext);
1835
	$htmltext = str_replace("#PORTAL_ACTION#", $portal_url, $htmltext);
1836

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

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

    
1855
	echo $htmltext;
1856
}
1857

    
1858
function captiveportal_reapply_attributes($cpentry, $attributes) {
1859
	global $config, $cpzone, $g;
1860

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

    
1876
	$bw_up_pipeno = $cpentry[1];
1877
	$bw_down_pipeno = $cpentry[1]+1;
1878

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

    
1890
function captiveportal_update_entry($sessionid, $new_value, $field_to_update) {
1891
	global $cpzone;
1892

    
1893
	if (!intval($new_value)) {
1894
		$new_value = "'{$new_value}'";
1895
	}
1896
	captiveportal_write_db("UPDATE captiveportal SET {$field_to_update} = {$new_value} WHERE sessionid = '{$sessionid}'");
1897
}
1898

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

    
1903
	// Ensure we create an array if we are missing attributes
1904
	if (!is_array($attributes)) {
1905
		$attributes = array();
1906
	}
1907

    
1908
	unset($sessionid);
1909

    
1910
	/* Do not allow concurrent login execution. */
1911
	$cpdblck = lock("captiveportaldb{$cpzone}", LOCK_EX);
1912

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

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

    
1930
				captiveportal_passthrumac_delete_entry($macent);
1931
				config_del_path("captiveportal/{$cpzone}/passthrumac/{$idx}");
1932
			}
1933
		}
1934
	}
1935

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

    
1944
	/* Snapshot the timestamp */
1945
	$allow_time = time();
1946

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

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

    
1972
			/* This user was already logged in so we disconnect the old one, or
1973
			keep the old one, refusing the new login, or
1974
			allow the login */
1975

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

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

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

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

    
2030
	if (!empty($unsetindexes)) {
2031
		captiveportal_remove_entries($unsetindexes);
2032
	}
2033

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

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

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

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

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

    
2119
			if ($attributes['voucher']) {
2120
				$attributes['session_timeout'] = $remaining_time;
2121
			}
2122

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

    
2130
			/* escape username */
2131
			$safe_username = SQLite3::escapeString($username);
2132

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

    
2139
			/* store information to database */
2140
			captiveportal_write_db($insertquery);
2141
			unlock($cpdblck);
2142
			unset($insertquery, $bpassword);
2143

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

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

    
2187
		unlock($cpdblck);
2188
	}
2189

    
2190
	if ($writecfg == true) {
2191
		write_config(gettext("Captive Portal allowed users configuration changed"));
2192
	}
2193

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

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

    
2215
		if (isset($attributes['reply_message'])) {
2216
			$message = $attributes['reply_message'];
2217
		} else {
2218
			$message = 0;
2219
		}
2220

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

    
2223
	} else {
2224
		portal_reply_page($my_redirurl, "redir", "Just redirect the user.");
2225
	}
2226

    
2227
	return $sessionid;
2228
}
2229

    
2230

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

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

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

    
2252
	if ($freeloginscount < 1 || $resettimeout <= 0 || !$clientmac) {
2253
		return false;
2254
	}
2255

    
2256
	$updatetimeouts = isset($config['captiveportal'][$cpzone]['freelogins_updatetimeouts']);
2257

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

    
2264
	$currenttime = time();
2265
	$found = false;
2266
	foreach ($usedmacs as $key => $usedmac) {
2267
		$usedmac = explode(",", $usedmac);
2268

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

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

    
2286
				$found = true;
2287
			} else {
2288
				unset($usedmacs[$key]);
2289
			}
2290

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

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

    
2302
	captiveportal_write_usedmacs_db($usedmacs);
2303
	xmlrpc_sync_usedmacs($usedmacs);
2304
	return true;
2305
}
2306

    
2307
function xmlrpc_sync_usedmacs($usedmacs) {
2308
	global $config, $cpzone;
2309

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

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

    
2330
function captiveportal_read_usedmacs_db() {
2331
	global $g, $cpzone;
2332

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

    
2343
	unlock($cpumaclck);
2344
	return $usedmacs;
2345
}
2346

    
2347
function captiveportal_write_usedmacs_db($usedmacs) {
2348
	global $g, $cpzone;
2349

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

    
2358
function captiveportal_blocked_mac($mac) {
2359
	global $cpzone;
2360

    
2361
	if (empty($mac) || !is_macaddr($mac)) {
2362
		return false;
2363
	}
2364

    
2365
	$mac = strtolower($mac);
2366
	$action = '';
2367
	$matched = false;
2368
	foreach (config_get_path("captiveportal/{$cpzone}/passthrumac", []) as $passthrumac) {
2369
		// assume the config entry contains a valid lowercase MAC address
2370
		list($mac_entry, $mac_entry_mask) = explode('/', $passthrumac['mac']);
2371
		if ($mac_entry_mask === null) {
2372
			$mac_entry_mask = 48;
2373
		}
2374

    
2375
		// Pad config MAC parts with 0 if needed
2376
		$mac_parts = [];
2377
		foreach (explode(':', $mac_entry) as $macpart) {
2378
			$mac_parts[] = str_pad($macpart, 2, '0', STR_PAD_LEFT);
2379
		}
2380
		$mac_entry_long = hexdec(implode($mac_parts));
2381

    
2382
		// Pad client MAC parts with 0 if needed
2383
		$mac_parts = [];
2384
		foreach (explode(':', $mac) as $macpart) {
2385
			$mac_parts[] = str_pad($macpart, 2, '0', STR_PAD_LEFT);
2386
		}
2387
		$mac_long = hexdec(implode($mac_parts));
2388

    
2389
		// check against the masked MAC address
2390
		if (($mac_long & (-1 << (48 - $mac_entry_mask))) == ($mac_entry_long & (-1 << (48 - $mac_entry_mask)))) {
2391
			$action = $passthrumac['action'];
2392
			$matched = true;
2393
		}
2394

    
2395
		// a specific match takes precedence over a partial match
2396
		if ($mac_entry_mask == 48) {
2397
			break;
2398
		}
2399
	}
2400

    
2401
	if ($matched && $action == 'block') {
2402
		return true;
2403
	}
2404

    
2405
	return false;
2406
}
2407

    
2408
/* Captiveportal Radius Accounting */
2409

    
2410
function gigawords($bytes) {
2411

    
2412
	/*
2413
	 * RFC2866 Specifies a 32bit unsigned integer, which is a max of 4294967295
2414
	 * Currently there is a fault in the PECL radius_put_int function which can handle only 32bit signed integer.
2415
	 */
2416

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

    
2420
	// We need to manually set this to a zero instead of NULL for put_int() safety
2421
	if (is_null($gigawords)) {
2422
		$gigawords = 0;
2423
	}
2424

    
2425
	return $gigawords;
2426
}
2427

    
2428
function remainder($bytes) {
2429
	// Calculate the bytes we are going to send to the radius
2430
	$bytes = bcmod($bytes, GIGAWORDS_RIGHT_OPERAND);
2431

    
2432
	if (is_null($bytes)) {
2433
		$bytes = 0;
2434
	}
2435

    
2436
    return $bytes;
2437
}
2438

    
2439
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) {
2440
	global $cpzone, $config;
2441

    
2442
	$cpcfg = config_get_path("captiveportal/{$cpzone}");
2443
	$acctcfg = auth_get_authserver($cpcfg['radacct_server']);
2444

    
2445
	if (!isset($cpcfg['radacct_enable']) || empty($acctcfg) ||
2446
	    captiveportal_ha_is_node_in_backup_mode($cpzone)) {
2447
		return null;
2448
	}
2449

    
2450
	if ($type === 'on') {
2451
		$racct = new Auth_RADIUS_Acct_On;
2452
	} elseif ($type === 'off') {
2453
		$racct = new Auth_RADIUS_Acct_Off;
2454
	} elseif ($type === 'start') {
2455
		$racct = new Auth_RADIUS_Acct_Start;
2456
		if (!is_int($start_time)) {
2457
			$start_time = time();
2458
		}
2459
	} elseif ($type === 'stop') {
2460
		$racct = new Auth_RADIUS_Acct_Stop;
2461
		if (!is_int($stop_time)) {
2462
			$stop_time = time();
2463
		}
2464
	} elseif ($type === 'update') {
2465
        $racct = new Auth_RADIUS_Acct_Update;
2466
		if (!is_int($stop_time)) {
2467
			$stop_time = time(); // "top time" here will be used only for calculating session time.
2468
		}
2469
	} else {
2470
		return null;
2471
	}
2472

    
2473
	$racct->addServer($acctcfg['host'], $acctcfg['radius_acct_port'],
2474
		$acctcfg['radius_secret'], $acctcfg['radius_timeout']);
2475

    
2476
	$racct->authentic = RADIUS_AUTH_RADIUS;
2477
	if ($cpcfg['auth_method'] === 'radmac' && $username === "unauthenticated" && !empty($clientmac)) {
2478
		$racct->username = mac_format($clientmac);
2479
	} elseif (!empty($username)) {
2480
		$racct->username = $username;
2481
	}
2482

    
2483
	if (PEAR::isError($racct->start())) {
2484
		captiveportal_syslog('RADIUS ACCOUNTING FAILED : '.$racct->getError());
2485
		$racct->close();
2486
		return null;
2487
	}
2488

    
2489
	$nasip = nasip_fallback($acctcfg['radius_nasip_attribute']);
2490
	$nasmac = get_interface_mac(find_ip_interface($nasip));
2491
	$racct->putAttribute(RADIUS_NAS_IP_ADDRESS, $nasip, "addr");
2492

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

    
2495
	if (is_int($ruleno)) {
2496
		$racct->putAttribute(RADIUS_NAS_PORT_TYPE, RADIUS_ETHERNET);
2497
		$racct->putAttribute(RADIUS_NAS_PORT, intval($ruleno), 'integer');
2498
	}
2499

    
2500
	if (!empty($sessionid)) {
2501
		$racct->putAttribute(RADIUS_ACCT_SESSION_ID, $sessionid);
2502
	}
2503

    
2504
	if (!empty($clientip) && is_ipaddr($clientip)) {
2505
		$racct->putAttribute(RADIUS_FRAMED_IP_ADDRESS, $clientip, "addr");
2506
	}
2507
	if (!empty($clientmac)) {
2508
		$racct->putAttribute(RADIUS_CALLING_STATION_ID, mac_format($clientmac));
2509
	}
2510
	if (!empty($nasmac)) {
2511
		$racct->putAttribute(RADIUS_CALLED_STATION_ID, mac_format($nasmac).':'.gethostname());
2512
	}
2513

    
2514
	// Accounting request Stop and Update : send the current data volume
2515
	if (($type === 'stop' || $type === 'update') && is_int($start_time)) {
2516
		$volume = getVolume($clientip);
2517
		$session_time = $stop_time - $start_time;
2518
		$volume['input_bytes_radius'] = remainder($volume['input_bytes']);
2519
		$volume['input_gigawords'] = gigawords($volume['input_bytes']);
2520
		$volume['output_bytes_radius'] = remainder($volume['output_bytes']);
2521
		$volume['output_gigawords'] = gigawords($volume['output_bytes']);
2522

    
2523
		// Volume stuff: Ingress
2524
		$racct->putAttribute(RADIUS_ACCT_INPUT_PACKETS, intval($volume['input_pkts']), "integer");
2525
		$racct->putAttribute(RADIUS_ACCT_INPUT_OCTETS, intval($volume['input_bytes_radius']), "integer");
2526
		// Volume stuff: Outgress
2527
		$racct->putAttribute(RADIUS_ACCT_OUTPUT_PACKETS, intval($volume['output_pkts']), "integer");
2528
		$racct->putAttribute(RADIUS_ACCT_OUTPUT_OCTETS, intval($volume['output_bytes_radius']), "integer");
2529
		$racct->putAttribute(RADIUS_ACCT_SESSION_TIME, intval($session_time), "integer");
2530

    
2531
		$racct->putAttribute(CUSTOM_RADIUS_ACCT_OUTPUT_GIGAWORDS, intval($volume['output_gigawords']), "integer");
2532
		$racct->putAttribute(CUSTOM_RADIUS_ACCT_INPUT_GIGAWORDS, intval($volume['input_gigawords']), "integer");
2533
		// Set session_time
2534
		$racct->session_time = $session_time;
2535
	}
2536

    
2537
	if ($type === 'stop') {
2538
		if (empty($term_cause)) {
2539
			$term_cause = 1;
2540
		}
2541
		$racct->putAttribute(RADIUS_ACCT_TERMINATE_CAUSE, $term_cause);
2542
	}
2543

    
2544
	// Send request
2545
	$result = $racct->send();
2546

    
2547
	if (PEAR::isError($result)) {
2548
		 captiveportal_syslog('RADIUS ACCOUNTING FAILED : '.$racct->getError());
2549
		 $result = null;
2550
	} elseif ($result !== true) {
2551
		$result = false;
2552
	}
2553

    
2554
	$racct->close();
2555
	return $result;
2556
}
2557

    
2558
function captiveportal_isip_logged($clientip) {
2559
	global $g, $cpzone;
2560

    
2561
	/* read in client database */
2562
	$query = "WHERE ip = '{$clientip}'";
2563
	$cpdb = captiveportal_read_db($query);
2564
	foreach ($cpdb as $cpentry) {
2565
		return $cpentry;
2566
	}
2567
}
2568

    
2569
function captiveportal_allowedhostname_cleanup() {
2570
	global $g, $config, $cpzone;
2571

    
2572
	$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
2573

    
2574
	init_config_arr(array('captiveportal', $cpzone, 'allowedhostname'));
2575
	foreach ($config['captiveportal'][$cpzone]['allowedhostname'] as $id => $hostnameent) {
2576
		$pipes = pfSense_pf_cp_get_eth_pipes("{$cpzoneprefix}_allowedhosts/hostname_{$id}");
2577
		pfSense_pf_cp_flush("{$cpzoneprefix}_allowedhosts/hostname_{$id}", "ether");
2578
		if (!empty($pipes)) {
2579
			captiveportal_pipes_delete($pipes);
2580
		}
2581
	}
2582
}
2583

    
2584
function filter_captiveportal_aliases() {
2585
	/* return all aliases used in captive portal zones,
2586
	 * to prevent it from deletion in filter_configure_sync() as unused aliases */
2587
	global $g, $config;
2588

    
2589
	$aliasesnames = array();
2590

    
2591
	init_config_arr(array('captiveportal'));
2592
	foreach (config_get_path('captiveportal', []) as $cpzone => $cpcfg) {
2593
		if (isset($cpcfg['enable'])) {
2594
			$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
2595
			$aliasesnames[] = $cpzoneprefix . '_cpips';
2596
			init_config_arr(array('captiveportal', $cpzone, 'allowedhostname'));
2597
			foreach ($config['captiveportal'][$cpzone]['allowedhostname'] as $id => $hostnameent) {
2598
				$aliasesnames[] = $cpzoneprefix . '_hostname_' . $id;
2599
			}
2600
		}
2601
	}
2602

    
2603
	return $aliasesnames;
2604
}
2605

    
2606
function filter_captiveportal_tables() {
2607
	/* return pf rules which defines tables used in captive portal zones */
2608
	global $config, $FilterIflist;
2609

    
2610
	$rules = '';
2611
	init_config_arr(array('captiveportal'));
2612
	foreach (config_get_path('captiveportal', []) as $cpzone => $cpcfg) {
2613
		if (!isset($cpcfg['enable'])) {
2614
			continue;
2615
		}
2616

    
2617
		$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
2618
		$cpips = $cpzoneprefix . '_cpips';
2619
		$cpiplist = array();
2620

    
2621
		foreach (explode(",", $cpcfg['interface']) as $cpifgrp) {
2622
			if (isset($FilterIflist[$cpifgrp])) {
2623
				$realif = get_real_interface($cpifgrp);
2624
				if (!empty($realif)) {
2625
					$cpip = get_interface_ip($cpifgrp);
2626
					if (is_ipaddrv4($cpip)) {
2627
						$cpipliststring = $cpip . ' ' . get_interface_vip_ips($cpifgrp);
2628
						$cpiplist = array_filter(array_merge($cpiplist, explode(' ', $cpipliststring)),
2629
												 function ($val) {
2630
													 return (trim($val) != "");
2631
												 });
2632
					}
2633
				}
2634
			}
2635
		}
2636
		if (!empty($cpiplist)) {
2637
			/* captive portal web server IP addresses */
2638
			$rules .= "table <{$cpips}> { " . join(' ', $cpiplist)  . "}\n";
2639
		}
2640
	}
2641

    
2642
	if (!empty($rules)) {
2643
		$rules = "\n# Captive Portal\n" . $rules . "\n";
2644
	}
2645

    
2646
	return $rules;
2647
}
2648

    
2649
function filter_captiveportal_ether() {
2650
	global $g, $config;
2651

    
2652
	$rules = '';
2653
	init_config_arr(array('captiveportal'));
2654
	foreach (config_get_path('captiveportal', []) as $cpzone => $cpcfg) {
2655
		if (!isset($cpcfg['enable'])) {
2656
			continue;
2657
		}
2658

    
2659
		$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
2660
		$rdrtag = $cpzoneprefix . '_rdr';
2661
		$interfaces = captiveportal_zone_interfaces($cpcfg);
2662

    
2663
		if (!empty($interfaces)) {
2664
			/* set 'rdr' tag for further captive portal web portal redirection */
2665
			$rules .= "ether pass on { {$interfaces} } tag \"{$rdrtag}\"\n";
2666
			/* anchor to set the PASS tag for authenticated clients */
2667
			$rules .= "ether anchor \"{$cpzoneprefix}_auth/*\" on { {$interfaces} }\n";
2668
			/* anchor for Services / Captive Portal / CPZONE / MACs */
2669
			$rules .= "ether anchor \"{$cpzoneprefix}_passthrumac/*\" on { {$interfaces} }\n";
2670
			/* anchor to set the PASSTHRU tag for Allowed IP/Hostnames */
2671
			$rules .= "ether anchor \"{$cpzoneprefix}_allowedhosts/*\" on { {$interfaces} }\n";
2672
		}
2673
	}
2674

    
2675
	if (!empty($rules)) {
2676
		$rules = "\n# Captive Portal\n" . $rules . "\n";
2677
	}
2678

    
2679
	return $rules;
2680
}
2681

    
2682
function filter_captiveportal_rdr() {
2683
	global $g, $config, $FilterIflist;
2684

    
2685
	$rules = '';
2686
	foreach (config_get_path('captiveportal', []) as $cpzone => $cpcfg) {
2687
		if (!isset($cpcfg['enable'])) {
2688
			continue;
2689
		}
2690

    
2691
		$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
2692
		$rdrtag = $cpzoneprefix . '_rdr';
2693
		$cpips = $cpzoneprefix . '_cpips';
2694
		$rdr_ports = captiveportal_zone_portalports($cpcfg);
2695
		foreach (explode(",", $cpcfg['interface']) as $cpifgrp) {
2696
			if (isset($FilterIflist[$cpifgrp])) {
2697
				$realif = get_real_interface($cpifgrp);
2698
				if (!empty($realif)) {
2699
					$cpip = get_interface_ip($cpifgrp);
2700
					if (is_ipaddrv4($cpip)) {
2701
						foreach ($rdr_ports as list($portalias, $cprdrport)) {
2702
							$rules .= "rdr on {$realif} inet proto tcp from any to ! <{$cpips}> port {$cprdrport} tagged {$rdrtag} -> {$cpip} port {$portalias}\n";
2703
						}
2704
					}
2705
				}
2706
			}
2707
		}
2708
	}
2709

    
2710
	if (!empty($rules)) {
2711
		$rules = "\n# Captive Portal\n" . $rules . "\n";
2712
	}
2713

    
2714
	return $rules;
2715
}
2716

    
2717
function filter_captiveportal_pass() {
2718
	global $g, $config, $FilterIflist;
2719

    
2720
	$captiveportal_increment = 'filter_captiveportal_tracker';
2721

    
2722
	$rules = '';
2723
	foreach (config_get_path('captiveportal', []) as $cpzone => $cpcfg) {
2724
		if (!isset($cpcfg['enable'])) {
2725
			continue;
2726
		}
2727

    
2728
		$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
2729
		$cpips = $cpzoneprefix . '_cpips';
2730
		$authtag = $cpzoneprefix . '_auth';
2731
		$rdr_ports = captiveportal_zone_portalports($cpcfg);
2732

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

    
2753
	if (!empty($rules)) {
2754
		$rules = "\n# Captive Portal\n" . $rules . "\n";
2755
	}
2756

    
2757
	return $rules;
2758
}
2759

    
2760
function captiveportal_zone_interfaces($cpcfg) {
2761
	/* return a list of captive portal zone interfaces */
2762
	global $FilterIflist;
2763

    
2764
	$interfaces = '';
2765
	foreach (explode(",", $cpcfg['interface']) as $cpifgrp) {
2766
		if (isset($FilterIflist[$cpifgrp])) {
2767
			$realif = get_real_interface($cpifgrp);
2768
			if (!empty($realif) && get_interface_ip($realif)) {
2769
				$interfaces .= $realif . ' ';
2770
			}
2771
		}
2772
	}
2773
	return $interfaces;
2774
}
2775

    
2776
/*
2777
 * Returns an array of (alias, rdrport) pairs describing ports to be forwarded for the captive portal
2778
 */
2779
function captiveportal_zone_portalports($cpcfg) {
2780
	$rdr_ports = array();
2781
	if (isset($cpcfg['httpslogin']) && !isset($cpcfg['nohttpsforwards'])) {
2782
		$portalias = $cpcfg['listenporthttps'] ? $cpcfg['listenporthttps'] : 8001 + $cpcfg['zoneid'];
2783
		$cprdrport = '443';
2784
		array_push($rdr_ports, array($portalias, $cprdrport));
2785
	}
2786
	$portalias = $cpcfg['listenporthttp'] ? $cpcfg['listenporthttp'] : 8000 + $cpcfg['zoneid'];
2787
	$cprdrport = '80';
2788
	array_push($rdr_ports, array($portalias, $cprdrport));
2789

    
2790
	return $rdr_ports;
2791
}
2792

    
2793
function captiveportal_pipes_delete($pipes) {
2794
	if (!empty($pipes)) {
2795
		foreach ($pipes as $pipe) {
2796
			mwexec("/sbin/dnctl pipe delete {$pipe}");
2797
		}
2798
		captiveportal_free_dn_rulenos($pipes);
2799
	}
2800
}
2801

    
2802
function captiveportal_ether_configure_entry($hostent, $anchor, $user_auth = false) {
2803
	global $config, $g, $cpzone;
2804

    
2805
	if (($hostent['action'] == 'block') && ($anchor == 'passthrumac')) {
2806
		return;
2807
	}
2808

    
2809
	$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
2810
	if ($anchor == 'passthrumac') {
2811
		$tag = $cpzoneprefix . '_auth';
2812
	} else {
2813
		$tag = $cpzoneprefix . '_' . $anchor;
2814
	}
2815

    
2816
	if ($anchor == 'passthrumac') {
2817
		list($pipeup, $pipedown) = captiveportal_pipe_configure($hostent, 'pipe_mac', $user_auth);
2818
		$host = str_replace("/", "_", str_replace(":", "", $hostent['mac']));
2819
		$l3from = '';
2820
		$l3to = '';
2821
		$macfrom = "from {$hostent['mac']}";
2822
		$macto = "to {$hostent['mac']}";
2823
	} else {
2824
		list($pipeup, $pipedown) = captiveportal_pipe_configure($hostent, 'auth', $user_auth);
2825
		$host = $hostent['ip'] . '_32';
2826
		$l3from = "l3 from {$hostent['ip']}";
2827
		$l3to = "l3 to {$hostent['ip']}";
2828
		if (!isset($config['captiveportal'][$cpzone]['nomacfilter'])) {
2829
			if (!empty($hostent['mac'])) {
2830
				$macfrom = "from {$hostent['mac']}";
2831
				$macto = "to {$hostent['mac']}";
2832
			} else {
2833
				return;
2834
			}
2835
		} else {
2836
			$macfrom = '';
2837
			$macto = '';
2838
		}
2839
	}
2840

    
2841
	$rules = "ether pass in quick {$macfrom} {$l3from} tag {$tag} dnpipe {$pipeup}\n";
2842
	$rules .= "ether pass out quick {$macto} {$l3to} tag {$tag} dnpipe {$pipedown}\n";
2843

    
2844
	captiveportal_load_pfctl("{$cpzoneprefix}_{$anchor}", $host, $rules);
2845
}
2846

    
2847
function captiveportal_pipe_configure($host, $type, $user_auth = true) {
2848
	global $config, $cpzone;
2849

    
2850
	$bwUp = 0;
2851
	if (!empty($host['bw_up'])) {
2852
		$bwUp = $host['bw_up'];
2853
	} elseif ($user_auth &&
2854
		isset($config['captiveportal'][$cpzone]['peruserbw']) &&
2855
	    !empty($config['captiveportal'][$cpzone]['bwdefaultup'])) {
2856
		$bwUp = config_get_path("captiveportal/{$cpzone}/bwdefaultup");
2857
	}
2858
	$bwDown = 0;
2859
	if (!empty($host['bw_down'])) {
2860
		$bwDown = $host['bw_down'];
2861
	} elseif ($user_auth &&
2862
		isset($config['captiveportal'][$cpzone]['peruserbw']) &&
2863
	    !empty($config['captiveportal'][$cpzone]['bwdefaultdn'])) {
2864
		$bwDown = config_get_path("captiveportal/{$cpzone}/bwdefaultdn");
2865
	}
2866

    
2867
	if (isset($host['pipeno']) && !empty($host['pipeno'])) {
2868
		$pipeup = $host['pipeno'];
2869
	} else {
2870
		$pipeup = captiveportal_get_next_dn_ruleno($type);
2871
	}
2872

    
2873
	mwexec("/sbin/dnctl pipe {$pipeup} config bw {$bwUp}Kbit/s queue 100 buckets 16");
2874
	$pipedown = $pipeup + 1;
2875
	mwexec("/sbin/dnctl pipe {$pipedown} config bw {$bwDown}Kbit/s queue 100 buckets 16");
2876

    
2877
	return array($pipeup, $pipedown);
2878
}
2879

    
2880
function captiveportal_allowedip_configure_entry($ipent) {
2881
	global $g, $config, $cpzone;
2882

    
2883
	$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
2884
	$tag = $cpzoneprefix . '_auth';
2885

    
2886
	if (empty($ipent['sn'])) {
2887
		$ipent['sn'] = '32';
2888
	}
2889

    
2890
	$host = $ipent['ip'] . '_' . $ipent['sn'];
2891
	list($pipeup, $pipedown) = captiveportal_pipe_configure($ipent, 'allowed', false);
2892

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

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

    
2904
function captiveportal_allowedhostname_configure_entry($ipent, $hostnameid = 1) {
2905
	global $config, $cpzone;
2906

    
2907
	if (!isset($ipent['hostname'])) {
2908
		return;
2909
	}
2910

    
2911
	$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
2912
	$tag = $cpzoneprefix . '_auth';
2913
	$table = $cpzoneprefix . '_hostname_' . $hostnameid;
2914
	$host = 'hostname_' . $hostnameid;
2915
	list($pipeup, $pipedown) = captiveportal_pipe_configure($ipent, 'allowed', false);
2916

    
2917
	$rules = "table <{$table}> persist\n";
2918
	if (($ipent['dir'] == 'to') || ($ipent['dir'] == 'both')) {
2919
		$rules .= "ether pass in quick l3 to <{$table}> tag {$tag} dnpipe {$pipeup}\n";
2920
	}
2921
	if (($ipent['dir'] == 'from') || ($ipent['dir'] == 'both')) {
2922
		$rules .= "ether pass in quick l3 from <{$table}> tag {$tag} dnpipe {$pipedown}\n";
2923
	}
2924

    
2925
	captiveportal_load_pfctl("{$cpzoneprefix}_allowedhosts", $host, $rules);
2926

    
2927
	/* return filterdns entry */
2928
	return "pf {$ipent['hostname']} {$table} {$cpzoneprefix}_allowedhosts/{$host}\n";
2929
}
2930

    
2931
function captiveportal_load_pfctl($anchor, $host, $rules) {
2932
	global $g, $cpzone;
2933

    
2934
	if (!empty($rules)) {
2935
		mwexec("/usr/bin/printf \"{$rules}\" | /sbin/pfctl -a {$anchor}/{$host} -f-");
2936
	} else {
2937
		log_error("CP zone ${cpzone}: {$anchor} rules are empty for {$host}");
2938
	}
2939
}
2940

    
2941
function captiveportal_anchor_zerocnt($ip, $anchor = 'auth') {
2942
	global $config, $cpzone;
2943
	$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
2944

    
2945
	pfSense_pf_cp_zerocnt("{$cpzoneprefix}_{$anchor}/{$ip}_32");
2946
}
2947

    
2948
?>
(6-6/61)