Project

General

Profile

Download (100 KB) Statistics
| Branch: | Tag: | Revision:
1
<?php
2
/*
3
 * captiveportal.inc
4
 *
5
 * part of pfSense (https://www.pfsense.org)
6
 * Copyright (c) 2004-2013 BSD Perimeter
7
 * Copyright (c) 2013-2016 Electric Sheep Fencing
8
 * Copyright (c) 2014-2024 Rubicon Communications, LLC (Netgate)
9
 * All rights reserved.
10
 *
11
 * originally part of m0n0wall (http://m0n0.ch/wall)
12
 * Copyright (c) 2003-2006 Manuel Kasper <mk@neon1.net>.
13
 * All rights reserved.
14
 *
15
 * Licensed under the Apache License, Version 2.0 (the "License");
16
 * you may not use this file except in compliance with the License.
17
 * You may obtain a copy of the License at
18
 *
19
 * http://www.apache.org/licenses/LICENSE-2.0
20
 *
21
 * Unless required by applicable law or agreed to in writing, software
22
 * distributed under the License is distributed on an "AS IS" BASIS,
23
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
24
 * See the License for the specific language governing permissions and
25
 * limitations under the License.
26
 */
27

    
28
/* include all configuration functions */
29
require_once("auth.inc");
30
require_once("PEAR.php"); // required for bcmath
31
require_once("Auth/RADIUS.php"); // required for radius accounting
32
require_once("config.inc");
33
require_once("functions.inc");
34
require_once("filter.inc");
35
require_once("voucher.inc");
36
require_once("xmlrpc_client.inc");
37

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

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

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

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

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

    
108
<head>
109

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

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

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

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

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

    
158

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

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

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

    
197
EOD;
198

    
199
	return $htmltext;
200
}
201

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
347
EOD;
348
		}
349

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

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

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

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

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

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

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

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

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

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

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

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

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

    
421
	unlock($captiveportallck);
422

    
423
	return 0;
424
}
425

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

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

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

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

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

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

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

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

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

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

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

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

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

    
499
	dummynet_load_module('100');
500

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

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

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

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

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

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

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

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

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

    
533
	/* delete Allowed IP entries */
534
	config_init_path("captiveportal/{$cpzone}/allowedip");
535
	foreach (config_get_path("captiveportal/{$cpzone}/allowedip", []) as $ipent) {
536
		captiveportal_ether_delete_entry($ipent, 'allowedhosts');
537
	}
538

    
539
	/* delete Allowed Hostnames entries */
540
	captiveportal_allowedhostname_cleanup();
541

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

    
550
	/* delete pipes */
551
	captiveportal_pipes_delete($pipes_to_remove);
552
}
553

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

    
563
	if (empty($cpzone)) {
564
		return;
565
	}
566

    
567
	$cpcfg = config_get_path("captiveportal/{$cpzone}", []);
568
	$vcpcfg = config_get_path("voucher/{$cpzone}", []);
569

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

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

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

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

    
595

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

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

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

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

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

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

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

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

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

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

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

    
799
	captiveportal_prune_old_automac();
800

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

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

    
812
function captiveportal_prune_old_automac() {
813
	global $g, $cpzone, $cpzoneid;
814
	$cpzone_config = config_get_path("captiveportal/{$cpzone}", []);
815

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
967
	captiveportal_radius_stop_all($term_cause, $logoutReason);
968

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

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

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

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

    
989
	$cpdb = captiveportal_read_db();
990

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

    
1016
function captiveportal_passthrumac_delete_entry($macent) {
1017
	global $g, $cpzone;
1018

    
1019
	$host = str_replace("/", "_", str_replace(":", "", $macent['mac']));
1020
	$cpzoneprefix = CPPREFIX . config_get_path("captiveportal/{$cpzone}/zoneid");
1021

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

    
1032
	pfSense_pf_cp_flush("{$cpzoneprefix}_passthrumac/{$host}", "ether");
1033
}
1034

    
1035
function captiveportal_passthrumac_configure($startindex = 0, $stopindex = 0) {
1036
	global $g, $cpzone;
1037

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

    
1066
function captiveportal_passthrumac_findbyname($username) {
1067
	global $cpzone;
1068

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

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

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

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

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

    
1099
function captiveportal_allowedhostname_configure() {
1100
	global $g, $cpzone, $cpzoneid;
1101

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

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

    
1120
function captiveportal_filterdns_configure() {
1121
	global $g, $cpzone, $cpzoneid;
1122

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

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

    
1146
function captiveportal_allowedip_configure() {
1147
	global $g, $cpzone;
1148

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

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

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

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

    
1173
	return $time;
1174
}
1175

    
1176

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1278
				$result = null;
1279
				$status = null;
1280
				$msg = null;
1281

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

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

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

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

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

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

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

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

    
1359
function captiveportal_opendb() {
1360
	global $g, $cpzone, $cpzoneid;
1361

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

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

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

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

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

    
1430
	return $DB;
1431
}
1432

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

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

    
1448
	return $cpdb;
1449
}
1450

    
1451
function captiveportal_remove_entries($remove, $carp_loop = false) {
1452
	global $cpzone;
1453

    
1454
	if (!is_array($remove) || empty($remove)) {
1455
		return;
1456
	}
1457

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

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

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

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

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

    
1509
function captiveportal_write_elements() {
1510
	global $g, $cpzone;
1511

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

    
1514
	if (!is_dir(g_get('captiveportal_element_path'))) {
1515
		@mkdir(g_get('captiveportal_element_path'));
1516
	}
1517

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

    
1537
	return 0;
1538
}
1539

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

    
1544
	$removed_pipes = array();
1545

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

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

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

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

    
1584
	unset($rules);
1585

    
1586
	return $removed_pipes;
1587
}
1588

    
1589
function captiveportal_reserve_ruleno($ruleno) {
1590
	global $g, $cpzone;
1591

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

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

    
1606
	return $ruleno;
1607
}
1608

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

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

    
1641
	return $ruleno;
1642
}
1643

    
1644
function captiveportal_free_dn_rulenos($rulenos) {
1645
	global $g;
1646

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

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

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

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

    
1671
	return $pipes;
1672
}
1673

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

    
1685
function getVolume($ip) {
1686
	global $g, $cpzone;
1687

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

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

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

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

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

    
1741
	return $volume;
1742
}
1743

    
1744
function portal_ip_from_client_ip($cliip) {
1745
	global $cpzone;
1746

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

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

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

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

    
1787
	return false;
1788
}
1789

    
1790
function portal_hostname_from_client_ip($cliip) {
1791
	global $cpzone;
1792

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

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

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

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

    
1816
	return $ourhostname;
1817
}
1818

    
1819
/* functions move from index.php */
1820

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

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

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

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

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

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

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

    
1868
	echo $htmltext;
1869
}
1870

    
1871
function captiveportal_reapply_attributes($cpentry, $attributes) {
1872
	global $cpzone, $g;
1873

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

    
1890
	$bw_up_pipeno = $cpentry[1];
1891
	$bw_down_pipeno = $cpentry[1]+1;
1892

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

    
1904
function captiveportal_update_entry($sessionid, $new_value, $field_to_update) {
1905
	global $cpzone;
1906

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

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

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

    
1922
	unset($sessionid);
1923

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

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

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

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

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

    
1959
	/* Snapshot the timestamp */
1960
	$allow_time = time();
1961

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

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

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

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

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

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

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

    
2046
	if (!empty($unsetindexes)) {
2047
		captiveportal_remove_entries($unsetindexes);
2048
	}
2049

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

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

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

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

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

    
2134
			if ($attributes['voucher']) {
2135
				$attributes['session_timeout'] = $remaining_time;
2136
			}
2137

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

    
2145
			/* escape username */
2146
			$safe_username = SQLite3::escapeString($username);
2147

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

    
2154
			/* store information to database */
2155
			captiveportal_write_db($insertquery);
2156
			unlock($cpdblck);
2157
			unset($insertquery, $bpassword);
2158

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

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

    
2202
		unlock($cpdblck);
2203
	}
2204

    
2205
	if ($writecfg == true) {
2206
		write_config(gettext("Captive Portal allowed users configuration changed"));
2207
	}
2208

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

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

    
2231
		if (isset($attributes['reply_message'])) {
2232
			$message = $attributes['reply_message'];
2233
		} else {
2234
			$message = 0;
2235
		}
2236

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

    
2239
	} else {
2240
		portal_reply_page($my_redirurl, "redir", "Just redirect the user.");
2241
	}
2242

    
2243
	return $sessionid;
2244
}
2245

    
2246

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

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

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

    
2269
	if ($freeloginscount < 1 || $resettimeout <= 0 || !$clientmac) {
2270
		return false;
2271
	}
2272

    
2273
	$updatetimeouts = isset($cpzone_config['freelogins_updatetimeouts']);
2274

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

    
2281
	$currenttime = time();
2282
	$found = false;
2283
	foreach ($usedmacs as $key => $usedmac) {
2284
		$usedmac = explode(",", $usedmac);
2285

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

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

    
2303
				$found = true;
2304
			} else {
2305
				unset($usedmacs[$key]);
2306
			}
2307

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

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

    
2319
	captiveportal_write_usedmacs_db($usedmacs);
2320
	xmlrpc_sync_usedmacs($usedmacs);
2321
	return true;
2322
}
2323

    
2324
function xmlrpc_sync_usedmacs($usedmacs) {
2325
	global $cpzone;
2326

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

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

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

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

    
2360
	unlock($cpumaclck);
2361
	return $usedmacs;
2362
}
2363

    
2364
function captiveportal_write_usedmacs_db($usedmacs) {
2365
	global $g, $cpzone;
2366

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

    
2375
function captiveportal_blocked_mac($mac) {
2376
	global $cpzone;
2377

    
2378
	if (empty($mac) || !is_macaddr($mac)) {
2379
		return false;
2380
	}
2381

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

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

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

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

    
2412
		// a specific match takes precedence over a partial match
2413
		if ($mac_entry_mask == 48) {
2414
			break;
2415
		}
2416
	}
2417

    
2418
	if ($matched && $action == 'block') {
2419
		return true;
2420
	}
2421

    
2422
	return false;
2423
}
2424

    
2425
/* Captiveportal Radius Accounting */
2426

    
2427
function gigawords($bytes) {
2428

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

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

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

    
2442
	return $gigawords;
2443
}
2444

    
2445
function remainder($bytes) {
2446
	// Calculate the bytes we are going to send to the radius
2447
	$bytes = bcmod($bytes, GIGAWORDS_RIGHT_OPERAND);
2448

    
2449
	if (is_null($bytes)) {
2450
		$bytes = 0;
2451
	}
2452

    
2453
    return $bytes;
2454
}
2455

    
2456
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) {
2457
	global $cpzone;
2458

    
2459
	$cpcfg = config_get_path("captiveportal/{$cpzone}", []);
2460
	$acctcfg = auth_get_authserver($cpcfg['radacct_server']);
2461

    
2462
	if (!isset($cpcfg['radacct_enable']) || empty($acctcfg) ||
2463
	    captiveportal_ha_is_node_in_backup_mode($cpzone)) {
2464
		return null;
2465
	}
2466

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

    
2490
	$racct->addServer($acctcfg['host'], $acctcfg['radius_acct_port'],
2491
		$acctcfg['radius_secret'], $acctcfg['radius_timeout']);
2492

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

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

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

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

    
2512
	if (is_int($ruleno)) {
2513
		$racct->putAttribute(RADIUS_NAS_PORT_TYPE, RADIUS_ETHERNET);
2514
		$racct->putAttribute(RADIUS_NAS_PORT, intval($ruleno), 'integer');
2515
	}
2516

    
2517
	if (!empty($sessionid)) {
2518
		$racct->putAttribute(RADIUS_ACCT_SESSION_ID, $sessionid);
2519
	}
2520

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

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

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

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

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

    
2561
	// Send request
2562
	$result = $racct->send();
2563

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

    
2571
	$racct->close();
2572
	return $result;
2573
}
2574

    
2575
function captiveportal_isip_logged($clientip) {
2576
	global $g, $cpzone;
2577

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

    
2586
function captiveportal_allowedhostname_cleanup() {
2587
	global $g, $cpzone;
2588

    
2589
	config_init_path("captiveportal/{$cpzone}/allowedhostname");
2590
	$cpzone_config = config_get_path("captiveportal/{$cpzone}", []);
2591
	$cpzoneprefix = CPPREFIX . $cpzone_config['zoneid'];
2592

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

    
2602
function filter_captiveportal_aliases() {
2603
	/* return all aliases used in captive portal zones,
2604
	 * to prevent it from deletion in filter_configure_sync() as unused aliases */
2605
	global $g;
2606

    
2607
	$aliasesnames = array();
2608

    
2609
	config_init_path('captiveportal');
2610
	foreach (config_get_path('captiveportal', []) as $cpzone => $cpcfg) {
2611
		if (isset($cpcfg['enable'])) {
2612
			$cpzoneprefix = CPPREFIX . $cpcfg['zoneid'];
2613
			$aliasesnames[] = $cpzoneprefix . '_cpips';
2614
			config_init_path("captiveportal/{$cpzone}/allowedhostname");
2615
			foreach ($cpcfg['allowedhostname'] as $id => $hostnameent) {
2616
				$aliasesnames[] = $cpzoneprefix . '_hostname_' . $id;
2617
			}
2618
		}
2619
	}
2620

    
2621
	return $aliasesnames;
2622
}
2623

    
2624
function filter_captiveportal_tables() {
2625
	/* return pf rules which defines tables used in captive portal zones */
2626
	global $FilterIflist;
2627

    
2628
	$rules = '';
2629
	config_init_path('captiveportal');
2630
	foreach (config_get_path('captiveportal', []) as $cpzone => $cpcfg) {
2631
		if (!isset($cpcfg['enable'])) {
2632
			continue;
2633
		}
2634

    
2635
		$cpzoneprefix = CPPREFIX . $cpcfg['zoneid'];
2636
		$cpips = $cpzoneprefix . '_cpips';
2637
		$cpiplist = array();
2638

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

    
2660
	if (!empty($rules)) {
2661
		$rules = "\n# Captive Portal\n" . $rules . "\n";
2662
	}
2663

    
2664
	return $rules;
2665
}
2666

    
2667
function filter_captiveportal_ether() {
2668
	global $g;
2669

    
2670
	$rules = '';
2671
	config_init_path('captiveportal');
2672
	foreach (config_get_path('captiveportal', []) as $cpzone => $cpcfg) {
2673
		if (!isset($cpcfg['enable'])) {
2674
			continue;
2675
		}
2676

    
2677
		$cpzoneprefix = CPPREFIX . $cpcfg['zoneid'];
2678
		$rdrtag = $cpzoneprefix . '_rdr';
2679
		$interfaces = captiveportal_zone_interfaces($cpcfg);
2680

    
2681
		if (!empty($interfaces)) {
2682
			/* set 'rdr' tag for further captive portal web portal redirection */
2683
			$rules .= "ether pass on { {$interfaces} } tag \"{$rdrtag}\"\n";
2684
			/* anchor to set the PASS tag for authenticated clients */
2685
			$rules .= "ether anchor \"{$cpzoneprefix}_auth/*\" on { {$interfaces} }\n";
2686
			/* anchor for Services / Captive Portal / CPZONE / MACs */
2687
			$rules .= "ether anchor \"{$cpzoneprefix}_passthrumac/*\" on { {$interfaces} }\n";
2688
			/* anchor to set the PASSTHRU tag for Allowed IP/Hostnames */
2689
			$rules .= "ether anchor \"{$cpzoneprefix}_allowedhosts/*\" on { {$interfaces} }\n";
2690
		}
2691
	}
2692

    
2693
	if (!empty($rules)) {
2694
		$rules = "\n# Captive Portal\n" . $rules . "\n";
2695
	}
2696

    
2697
	return $rules;
2698
}
2699

    
2700
function filter_captiveportal_rdr() {
2701
	global $g, $FilterIflist;
2702

    
2703
	$rules = '';
2704
	foreach (config_get_path('captiveportal', []) as $cpzone => $cpcfg) {
2705
		if (!isset($cpcfg['enable'])) {
2706
			continue;
2707
		}
2708

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

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

    
2732
	return $rules;
2733
}
2734

    
2735
function filter_captiveportal_pass() {
2736
	global $g, $FilterIflist;
2737

    
2738
	$captiveportal_increment = 'filter_captiveportal_tracker';
2739

    
2740
	$rules = '';
2741
	foreach (config_get_path('captiveportal', []) as $cpzone => $cpcfg) {
2742
		if (!isset($cpcfg['enable'])) {
2743
			continue;
2744
		}
2745

    
2746
		$cpzoneprefix = CPPREFIX . $cpcfg['zoneid'];
2747
		$cpips = $cpzoneprefix . '_cpips';
2748
		$authtag = $cpzoneprefix . '_auth';
2749
		$rdr_ports = captiveportal_zone_portalports($cpcfg);
2750

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

    
2771
	if (!empty($rules)) {
2772
		$rules = "\n# Captive Portal\n" . $rules . "\n";
2773
	}
2774

    
2775
	return $rules;
2776
}
2777

    
2778
function captiveportal_zone_interfaces($cpcfg) {
2779
	/* return a list of captive portal zone interfaces */
2780
	global $FilterIflist;
2781

    
2782
	$interfaces = '';
2783
	foreach (explode(",", $cpcfg['interface']) as $cpifgrp) {
2784
		if (isset($FilterIflist[$cpifgrp])) {
2785
			$realif = get_real_interface($cpifgrp);
2786
			if (!empty($realif) && get_interface_ip($realif)) {
2787
				$interfaces .= $realif . ' ';
2788
			}
2789
		}
2790
	}
2791
	return $interfaces;
2792
}
2793

    
2794
/*
2795
 * Returns an array of (alias, rdrport) pairs describing ports to be forwarded for the captive portal
2796
 */
2797
function captiveportal_zone_portalports($cpcfg) {
2798
	$rdr_ports = array();
2799
	if (isset($cpcfg['httpslogin']) && !isset($cpcfg['nohttpsforwards'])) {
2800
		$portalias = $cpcfg['listenporthttps'] ? $cpcfg['listenporthttps'] : 8001 + $cpcfg['zoneid'];
2801
		$cprdrport = '443';
2802
		array_push($rdr_ports, array($portalias, $cprdrport));
2803
	}
2804
	$portalias = $cpcfg['listenporthttp'] ? $cpcfg['listenporthttp'] : 8000 + $cpcfg['zoneid'];
2805
	$cprdrport = '80';
2806
	array_push($rdr_ports, array($portalias, $cprdrport));
2807

    
2808
	return $rdr_ports;
2809
}
2810

    
2811
function captiveportal_pipes_delete($pipes) {
2812
	if (!empty($pipes)) {
2813
		foreach ($pipes as $pipe) {
2814
			mwexec("/sbin/dnctl pipe delete {$pipe}");
2815
		}
2816
		captiveportal_free_dn_rulenos($pipes);
2817
	}
2818
}
2819

    
2820
function captiveportal_ether_configure_entry($hostent, $anchor, $user_auth = false) {
2821
	global $g, $cpzone;
2822

    
2823
	if (($hostent['action'] == 'block') && ($anchor == 'passthrumac')) {
2824
		return;
2825
	}
2826

    
2827
	$cpzoneprefix = CPPREFIX . config_get_path("captiveportal/{$cpzone}/zoneid");
2828
	if ($anchor == 'passthrumac') {
2829
		$tag = $cpzoneprefix . '_auth';
2830
	} else {
2831
		$tag = $cpzoneprefix . '_' . $anchor;
2832
	}
2833

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

    
2859
	$rules = "ether pass in quick {$macfrom} {$l3from} tag {$tag} dnpipe {$pipeup}\n";
2860
	$rules .= "ether pass out quick {$macto} {$l3to} tag {$tag} dnpipe {$pipedown}\n";
2861

    
2862
	captiveportal_load_pfctl("{$cpzoneprefix}_{$anchor}", $host, $rules);
2863
}
2864

    
2865
function captiveportal_pipe_configure($host, $type, $user_auth = true) {
2866
	global $cpzone;
2867

    
2868
	$cpzone_config = config_get_path("captiveportal/{$cpzone}", []);
2869
	$bwUp = 0;
2870
	if (!empty($host['bw_up'])) {
2871
		$bwUp = $host['bw_up'];
2872
	} elseif ($user_auth &&
2873
		isset($cpzone_config['peruserbw']) &&
2874
	    !empty($cpzone_config['bwdefaultup'])) {
2875
		$bwUp = $cpzone_config['bwdefaultup'];
2876
	}
2877
	$bwDown = 0;
2878
	if (!empty($host['bw_down'])) {
2879
		$bwDown = $host['bw_down'];
2880
	} elseif ($user_auth &&
2881
		isset($cpzone_config['peruserbw']) &&
2882
	    !empty($cpzone_config['bwdefaultdn'])) {
2883
		$bwDown = $cpzone_config['bwdefaultdn'];
2884
	}
2885

    
2886
	if (isset($host['pipeno']) && !empty($host['pipeno'])) {
2887
		$pipeup = $host['pipeno'];
2888
	} else {
2889
		$pipeup = captiveportal_get_next_dn_ruleno($type);
2890
	}
2891

    
2892
	mwexec("/sbin/dnctl pipe {$pipeup} config bw {$bwUp}Kbit/s queue 100 buckets 16");
2893
	$pipedown = $pipeup + 1;
2894
	mwexec("/sbin/dnctl pipe {$pipedown} config bw {$bwDown}Kbit/s queue 100 buckets 16");
2895

    
2896
	return array($pipeup, $pipedown);
2897
}
2898

    
2899
function captiveportal_allowedip_configure_entry($ipent) {
2900
	global $g, $cpzone;
2901

    
2902
	$cpzoneprefix = CPPREFIX . config_get_path("captiveportal/{$cpzone}/zoneid");
2903
	$tag = $cpzoneprefix . '_auth';
2904

    
2905
	if (empty($ipent['sn'])) {
2906
		$ipent['sn'] = '32';
2907
	}
2908

    
2909
	$host = $ipent['ip'] . '_' . $ipent['sn'];
2910
	list($pipeup, $pipedown) = captiveportal_pipe_configure($ipent, 'allowed', false);
2911

    
2912
	$rules = '';
2913
	if (($ipent['dir'] == 'to') || ($ipent['dir'] == 'both')) {
2914
		$rules = "ether pass in quick l3 to {$ipent['ip']}/{$ipent['sn']} tag {$tag} dnpipe {$pipeup}\n";
2915
	}
2916
	if (($ipent['dir'] == 'from') || ($ipent['dir'] == 'both')) {
2917
		$rules .= "ether pass in quick l3 from {$ipent['ip']}/{$ipent['sn']} tag {$tag} dnpipe {$pipedown}\n";
2918
	}
2919

    
2920
	captiveportal_load_pfctl("{$cpzoneprefix}_allowedhosts", $host, $rules);
2921
}
2922

    
2923
function captiveportal_allowedhostname_configure_entry($ipent, $hostnameid = 1) {
2924
	global $cpzone;
2925

    
2926
	if (!isset($ipent['hostname'])) {
2927
		return;
2928
	}
2929

    
2930
	$cpzoneprefix = CPPREFIX . config_get_path("captiveportal/{$cpzone}/zoneid");
2931
	$tag = $cpzoneprefix . '_auth';
2932
	$table = $cpzoneprefix . '_hostname_' . $hostnameid;
2933
	$host = 'hostname_' . $hostnameid;
2934
	list($pipeup, $pipedown) = captiveportal_pipe_configure($ipent, 'allowed', false);
2935

    
2936
	$rules = "table <{$table}> persist\n";
2937
	if (($ipent['dir'] == 'to') || ($ipent['dir'] == 'both')) {
2938
		$rules .= "ether pass in quick l3 to <{$table}> tag {$tag} dnpipe {$pipeup}\n";
2939
	}
2940
	if (($ipent['dir'] == 'from') || ($ipent['dir'] == 'both')) {
2941
		$rules .= "ether pass in quick l3 from <{$table}> tag {$tag} dnpipe {$pipedown}\n";
2942
	}
2943

    
2944
	captiveportal_load_pfctl("{$cpzoneprefix}_allowedhosts", $host, $rules);
2945

    
2946
	/* return filterdns entry */
2947
	return "pf {$ipent['hostname']} {$table} {$cpzoneprefix}_allowedhosts/{$host}\n";
2948
}
2949

    
2950
function captiveportal_load_pfctl($anchor, $host, $rules) {
2951
	global $g, $cpzone;
2952

    
2953
	if (!empty($rules)) {
2954
		mwexec("/usr/bin/printf \"{$rules}\" | /sbin/pfctl -a {$anchor}/{$host} -f-");
2955
	} else {
2956
		log_error("CP zone {$cpzone}: {$anchor} rules are empty for {$host}");
2957
	}
2958
}
2959

    
2960
function captiveportal_anchor_zerocnt($ip, $anchor = 'auth') {
2961
	global $cpzone;
2962
	$cpzoneprefix = CPPREFIX . config_get_path("captiveportal/{$cpzone}/zoneid");
2963

    
2964
	pfSense_pf_cp_zerocnt("{$cpzoneprefix}_{$anchor}/{$ip}_32");
2965
}
2966

    
2967
?>
(6-6/61)