Project

General

Profile

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

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

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

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

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

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

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

    
105
<head>
106

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

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

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

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

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

    
155

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

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

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

    
194
EOD;
195

    
196
	return $htmltext;
197
}
198

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
343
EOD;
344
		}
345

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

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

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

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

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

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

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

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

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

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

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

    
407
		/* remove old information */
408
		unlink_if_exists("{$g['vardb_path']}/captiveportal{$cpzone}.db");
409
		unlink_if_exists("{$g['vardb_path']}/captiveportal_radius_{$cpzone}.db");
410
		unlink_if_exists("{$g['vardb_path']}/captiveportal_{$cpzone}.rules");
411
		/* Release allocated pipes for this zone */
412
		$pipes_to_remove = captiveportal_free_dnrules();
413

    
414
		captiveportal_delete_rules($pipes_to_remove);
415
	}
416

    
417
	unlock($captiveportallck);
418

    
419
	return 0;
420
}
421

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

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

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

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

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

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

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

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

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

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

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

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

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

    
495
	dummynet_load_module('100');
496

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

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

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

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

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

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

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

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

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

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

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

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

    
548
	/* delete pipes */
549
	foreach ($pipes_to_remove as $pipeno) {
550
		captiveportal_pipe_delete($pipeno);
551
	}
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, $config, $cpzone, $cpzoneid;
562

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

    
567
	$cpcfg = $config['captiveportal'][$cpzone];
568
	$vcpcfg = $config['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 (!isset($config['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_list['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_list['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 (isset($config['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(isset($config['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, $config, $cpzone, $cpzoneid;
814

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

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

    
849
	$stop_time = (empty($stop_time)) ? time() : $stop_time;
850

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

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

    
893
	/*
894
	 * These are the pipe numbers we use to control traffic shaping for
895
	 * each logged in user via captive portal
896
	 * We could get an error if the pipe doesn't exist but everything
897
	 * should still be fine
898
	 */
899
	if (!empty($dbent[1])) {
900
		/*
901
		 * Call captiveportal_free_dnrules() in dry_run mode to verify
902
		 * if there are pipes to be removed and prevent the attempt to
903
		 * delete invalid pipes
904
		 */
905
		$removed_pipes = captiveportal_free_dnrules($dbent[1],
906
		    $dbent[1]+1, true);
907

    
908
		if (!empty($removed_pipes)) {
909
			captiveportal_pipe_delete($dbent[1]);
910
		}
911
	}
912

    
913
	// XMLRPC Call over to the backup node if necessary
914
	if (captiveportal_xmlrpc_sync_get_details($syncip, $syncport,
915
	    $syncuser, $syncpass, $carp_loop)) {
916
		$rpc_client = new pfsense_xmlrpc_client();
917
		$rpc_client->setConnectionData($syncip, $syncport, $syncuser, $syncpass);
918
		$rpc_client->set_noticefile("CaptivePortalUserSync");
919
		$arguments = array(
920
			'sessionid' => $dbent[5],
921
			'term_cause' => $term_cause,
922
			'stop_time' => $stop_time
923
		);
924

    
925
		$rpc_client->xmlrpc_method('captive_portal_sync',
926
			array(
927
				'op' => 'disconnect_user',
928
				'zone' => $cpzone,
929
				'session' => base64_encode(serialize($arguments))
930
			)
931
		);
932
	}
933
	return true;
934
}
935

    
936
/* remove a single client by sessionid */
937
function captiveportal_disconnect_client($sessionid, $term_cause = 1, $logoutReason = "LOGOUT") {
938
	global $g, $config;
939

    
940
	$sessionid = SQLite3::escapeString($sessionid);
941
	/* read database */
942
	$result = captiveportal_read_db("WHERE sessionid = '{$sessionid}'");
943

    
944
	/* find entry */
945
	if (!empty($result)) {
946
		foreach ($result as $cpentry) {
947
			captiveportal_disconnect($cpentry, $term_cause);
948
			captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "DISCONNECT");
949
		}
950
		captiveportal_remove_entries(array($sessionid));
951
	}
952
}
953

    
954
/* remove all clients */
955
function captiveportal_disconnect_all($term_cause = 6, $logoutReason = "DISCONNECT", $carp_loop = false) {
956
	global $g, $config, $cpzone, $cpzoneid;
957

    
958
	if (captiveportal_xmlrpc_sync_get_details($syncip, $syncport, $syncuser, $syncpass, $carp_loop)) {
959
		$rpc_client = new pfsense_xmlrpc_client();
960
		$rpc_client->setConnectionData($syncip, $syncport, $syncuser, $syncpass);
961
		$rpc_client->set_noticefile("CaptivePortalUserSync");
962
		$arguments = array(
963
			'term_cause' => $term_cause,
964
			'logout_reason' => $logoutReason
965
		);
966

    
967
		$rpc_client->xmlrpc_method('captive_portal_sync',
968
			array(
969
				'op' => 'disconnect_all',
970
				'zone' => $cpzone,
971
				'arguments' => base64_encode(serialize($arguments))
972
			)
973
		);
974
	}
975
	/* check if we're pruning old entries and eventually wait */
976
	$rcprunelock = try_lock("rcprunecaptiveportal{$cpzone}", 15);
977

    
978
	/* if we still don't have the lock, unlock forcefully and take it */
979
	if (!$rcprunelock) {
980
		log_error("CP zone ${cpzone}: could not obtain the lock for more than 15 seconds, lock taken forcefully to disconnect all users");
981
		unlock_force("rcprunecaptiveportal{$cpzone}");
982
		$rcprunelock = lock("rcprunecaptiveportal{$cpzone}", LOCK_EX);
983
	}
984

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

    
988
	captiveportal_radius_stop_all($term_cause, $logoutReason);
989

    
990
	/* reinit captiveportal pipes and anchors */
991
	captiveportal_init_rules(true);
992

    
993
	/* remove users from the database */
994
	$cpdb = captiveportal_read_db();
995
	$unsetindexes = array_column($cpdb,5);
996
	if (!empty($unsetindexes)) {
997
		// High Availability : do not sync removed entries
998
		captiveportal_remove_entries($unsetindexes, true);
999
	}
1000

    
1001
	unlock($cpdblck);
1002
	unlock($rcprunelock);
1003
	return true;
1004
}
1005

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

    
1010
	$cpdb = captiveportal_read_db();
1011

    
1012
	$radacct = isset($config['captiveportal'][$cpzone]['radacct_enable']) ? true : false;
1013
	foreach ($cpdb as $cpentry) {
1014
		if ($cpentry['authmethod'] === 'radius' && $radacct) {
1015
			if ($config['captiveportal'][$cpzone]['reauthenticateacct'] == "stopstartfreeradius") {
1016
				$session_time = (time() - $cpentry[0]) % 60;
1017
				$start_time = time() - $session_time;
1018
			} else {
1019
				$start_time = $cpentry[0];
1020
			}
1021
			captiveportal_send_server_accounting('stop',
1022
				$cpentry[1], // ruleno
1023
				$cpentry[4], // username
1024
				$cpentry[2], // clientip
1025
				$cpentry[3], // clientmac
1026
				$cpentry[5], // sessionid
1027
				$start_time, // start time
1028
				$stop_time, // stop time
1029
				$term_cause); // Acct-Terminate-Cause
1030
		}
1031
		captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], $logoutReason);
1032
	}
1033
	unset($cpdb);
1034
}
1035

    
1036
function captiveportal_passthrumac_delete_entry($hostent) {
1037
	global $g, $cpzone, $config;
1038

    
1039
	$host = str_replace("/", "_", str_replace(":", "", $hostent['mac']));
1040
	$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
1041

    
1042
	if ($macent['action'] == 'pass') {
1043
		$pipeno = captiveportal_get_dn_passthru_ruleno($mac['mac']);
1044
		if (!empty($pipeno)) {
1045
			captiveportal_pipe_delete($pipeno);
1046
		}
1047
	} else {
1048
		/* no rules on passthru block */
1049
		return;
1050
	}
1051

    
1052
	pfSense_pf_cp_flush("{$cpzoneprefix}_passthrumac/{$host}", "ether");
1053
}
1054

    
1055
function captiveportal_passthrumac_configure($startindex = 0, $stopindex = 0) {
1056
	global $config, $g, $cpzone;
1057

    
1058
	if (is_array($config['captiveportal'][$cpzone]['passthrumac'])) {
1059
		if ($stopindex > 0) {
1060
			for ($idx = $startindex; $idx <= $stopindex; $idx++) {
1061
				if (isset($config['captiveportal'][$cpzone]['passthrumac'][$idx])) {
1062
					captiveportal_ether_configure_entry($config['captiveportal'][$cpzone]['passthrumac'][$idx], 'passthrumac');
1063
				}
1064
			}
1065
		} else {
1066
			$nentries = count($config['captiveportal'][$cpzone]['passthrumac']);
1067
			if ($nentries > 2000) {
1068
				$nloops = $nentries / 1000;
1069
				$remainder= $nentries % 1000;
1070
				for ($i = 0; $i < $nloops; $i++) {
1071
					mwexec_bg("/usr/local/sbin/fcgicli -f /etc/rc.captiveportal_configure_mac -d \"cpzone={$cpzone}&startidx=" . ($i * 1000) . "&stopidx=" . ((($i+1) * 1000) - 1) . "\"");
1072
				}
1073
				if ($remainder > 0) {
1074
					mwexec_bg("/usr/local/sbin/fcgicli -f /etc/rc.captiveportal_configure_mac -d \"cpzone={$cpzone}&startidx=" . ($i * 1000) . "&stopidx=" . (($i* 1000) + $remainder) ."\"");
1075
				}
1076
			} else {
1077
				foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $macent) {
1078
					captiveportal_ether_configure_entry($macent, 'passthrumac');
1079
				}
1080
			}
1081
		}
1082
	}
1083
}
1084

    
1085
function captiveportal_passthrumac_findbyname($username) {
1086
	global $config, $cpzone;
1087

    
1088
	if (is_array($config['captiveportal'][$cpzone]['passthrumac'])) {
1089
		foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $macent) {
1090
			if ($macent['username'] == $username) {
1091
				return $macent;
1092
			}
1093
		}
1094
	}
1095
	return NULL;
1096
}
1097

    
1098
function captiveportal_ether_delete_entry($hostent, $anchor = 'allowedhosts') {
1099
	global $g, $cpzone, $config;
1100

    
1101
	$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
1102

    
1103
	if (!empty($hostent['sn'])) {
1104
		$host = $hostent['ip'] . '_' . $hostent['sn'];
1105
	} else {
1106
		$host = $hostent['ip'] . '_32';
1107
	}
1108

    
1109
	$result = pfSense_pf_cp_get_eth_pipes("{$cpzoneprefix}_{$anchor}/$host");
1110
	if (!empty($result)) {
1111
		list($pipeup, $pipedown) = $result;
1112
		captiveportal_free_dn_ruleno($pipeup);
1113
	}
1114
	/* flush anchor rules */
1115
	pfSense_pf_cp_flush("{$cpzoneprefix}_{$anchor}/{$host}", "ether");
1116
}
1117

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

    
1121
	if (!is_array($config['captiveportal'][$cpzone]['allowedhostname'])) {
1122
		return false;
1123
	}
1124

    
1125
	$cp_filterdns_conf = "";
1126
	foreach ($config['captiveportal'][$cpzone]['allowedhostname'] as $id => $hostnameent) {
1127
		$cp_filterdns_conf .= captiveportal_allowedhostname_configure_entry($hostnameent, $id);
1128
	}
1129
	$cp_filterdns_filename = "{$g['varetc_path']}/filterdns-{$cpzone}-captiveportal.conf";
1130
	if ((!file_exists($cp_filterdns_filename) && !empty($cp_filterdns_conf)) ||
1131
	    (file_exists($cp_filterdns_filename) && ($cp_filterdns_conf != file_get_contents($cp_filterdns_filename)))) {
1132
		@file_put_contents($cp_filterdns_filename, $cp_filterdns_conf);
1133
		filter_configure();
1134
		captiveportal_filterdns_configure();
1135
	}
1136
}
1137

    
1138
function captiveportal_filterdns_configure() {
1139
	global $config, $g, $cpzone, $cpzoneid;
1140

    
1141
	$cp_filterdns_filename = $g['varetc_path'] .
1142
	    "/filterdns-{$cpzone}-captiveportal.conf";
1143

    
1144
	if (isset($config['captiveportal'][$cpzone]['enable']) &&
1145
	    is_array($config['captiveportal'][$cpzone]['allowedhostname']) &&
1146
	    file_exists($cp_filterdns_filename) &&
1147
	    !empty(file_get_contents($cp_filterdns_filename))) {
1148
		if (isvalidpid($g['varrun_path'] .
1149
		    "/filterdns-{$cpzone}-cpah.pid")) {
1150
			sigkillbypid($g['varrun_path'] .
1151
			    "/filterdns-{$cpzone}-cpah.pid", "HUP");
1152
		} else {
1153
			mwexec("/usr/local/sbin/filterdns -p " .
1154
			    "{$g['varrun_path']}/filterdns-{$cpzone}-cpah.pid" .
1155
			    " -i 300 -c {$cp_filterdns_filename} -d 1");
1156
		}
1157
	} else {
1158
		killbypid("{$g['varrun_path']}/filterdns-{$cpzone}-cpah.pid");
1159
		@unlink("{$g['varrun_path']}/filterdns-{$cpzone}-cpah.pid");
1160
	}
1161
}
1162

    
1163
function captiveportal_allowedip_configure() {
1164
	global $config, $g, $cpzone;
1165

    
1166
	if (is_array($config['captiveportal'][$cpzone]['allowedip'])) {
1167
		foreach ($config['captiveportal'][$cpzone]['allowedip'] as $ipent) {
1168
			captiveportal_allowedip_configure_entry($ipent);
1169
		}
1170
	}
1171
}
1172

    
1173
/* get last activity timestamp given client IP address */
1174
function captiveportal_get_last_activity($ip) {
1175
	global $cpzone;
1176

    
1177
	$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
1178
	$anchor = $cpzoneprefix . '_auth';
1179

    
1180
	$active_times = pfSense_pf_cp_get_eth_last_active("{$anchor}/{$ip}_32");
1181
	$time = 0;
1182
	if (!empty($active_times)) {
1183
		foreach ($active_times as $active_time) {
1184
			if ( $active_time > $time)
1185
				$time = $active_time;
1186
	   }
1187
	}
1188

    
1189
	return $time;
1190
}
1191

    
1192

    
1193
/* log successful captive portal authentication to syslog */
1194
/* part of this code from php.net */
1195
function captiveportal_logportalauth($user, $mac, $ip, $status, $message = null) {
1196
	// Log it
1197
	if (!$message) {
1198
		$message = "{$status}: {$user}, {$mac}, {$ip}";
1199
	} else {
1200
		$message = trim($message);
1201
		$message = "{$status}: {$user}, {$mac}, {$ip}, {$message}";
1202
	}
1203
	captiveportal_syslog($message);
1204
}
1205

    
1206
/* log simple messages to syslog */
1207
function captiveportal_syslog($message) {
1208
	global $cpzone;
1209

    
1210
	$message = trim($message);
1211
	$message = "Zone: {$cpzone} - {$message}";
1212
	openlog("logportalauth", LOG_PID, LOG_LOCAL4);
1213
	// Log it
1214
	syslog(LOG_INFO, $message);
1215
	closelog();
1216
}
1217

    
1218
/* Authenticate users using Authentication Backend */
1219
function captiveportal_authenticate_user(&$login = '', &$password = '', $clientmac = '', $clientip = '', $pipeno = 'null', $context = 'first') {
1220
	global $g, $config, $cpzone;
1221
	$cpcfg = $config['captiveportal'][$cpzone];
1222

    
1223
	$login_status = 'FAILURE';
1224
	$login_msg = gettext('Invalid credentials specified');
1225
	$reply_attributes = array();
1226
	$auth_method = '';
1227
	$auth_result = null;
1228

    
1229
	/*
1230
	Management of the reply Message (reason why the authentication failed) :
1231
	multiple authentication servers can be used, so multiple reply messages could theoretically be returned.
1232
	But only one message is returned (the most important one).
1233
	The return value of authenticate_user() define how important messages are :
1234
		- Reply message of a successful auth is more important than reply message of
1235
		a user failed auth(invalid credentials/authorization)
1236

    
1237
		- Reply message of a user failed auth is more important than reply message of
1238
		a server failed auth (unable to contact server)
1239

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

    
1243
	The $authlevel variable is a flag indicating the status of authentication
1244
	0 = failed server auth
1245
	1 = failed user auth
1246
	2 = failed user auth with custom server reply recieved
1247
	3 = successful auth
1248
	*/
1249
	$authlevel = 0;
1250

    
1251
	/* Getting authentication servers from captiveportal configuration */
1252
	$auth_servers = array();
1253

    
1254
	if ($cpcfg['auth_method'] === 'none') {
1255
		$auth_servers[] = array('type' => 'none');
1256
	} else {
1257
		if ($context === 'second') {
1258
			$fullauthservers = explode(",", $cpcfg['auth_server2']);
1259
		} else {
1260
			$fullauthservers = explode(",", $cpcfg['auth_server']);
1261
		}
1262

    
1263
		foreach ($fullauthservers as $authserver) {
1264
			if (strpos($authserver, ' - ') !== false) {
1265
				$authserver = explode(' - ', $authserver);
1266
				array_shift($authserver);
1267
				$authserver = implode(' - ', $authserver);
1268

    
1269
				if (auth_get_authserver($authserver) !== null) {
1270
					$auth_servers[] = auth_get_authserver($authserver);
1271
				} else {
1272
					log_error("Zone: {$cpzone} - Captive portal was unable to find the settings of the server '{$authserver}' used for authentication !");
1273
				}
1274
			}
1275
		}
1276
	}
1277

    
1278
	/* Unable to find the any authentication server config - shouldn't happen! - bail out */
1279
	if (count($auth_servers) === 0) {
1280
		log_error("Zone: {$cpzone} - No valid server could be used for authentication.");
1281
		$login_msg = gettext("Internal Error");
1282
	} else {
1283
		foreach ($auth_servers as $authcfg) {
1284
			if ($authlevel < 3) {
1285
				$radmac_error = false;
1286
				$attributes = array("nas_identifier" => empty($cpcfg["radiusnasid"]) ? "CaptivePortal-{$cpzone}" : $cpcfg["radiusnasid"],
1287
					"nas_port_type" => RADIUS_ETHERNET,
1288
					"nas_port" => $pipeno,
1289
					"framed_ip" => $clientip);
1290
				if (mac_format($clientmac) !== null) {
1291
					$attributes["calling_station_id"] = mac_format($clientmac);
1292
				}
1293

    
1294
				$result = null;
1295
				$status = null;
1296
				$msg = null;
1297

    
1298
				/* Radius MAC authentication */
1299
				if ($context === 'radmac' && $clientmac) {
1300
					if ($authcfg['type'] === 'radius') {
1301
						$login = mac_format($clientmac);
1302
						$status = "MACHINE LOGIN";
1303
					} else {
1304
						/* Trying to perform a Radius MAC authentication on a non-radius server - shouldn't happen! - bail out */
1305
						$msg = gettext("Internal Error");
1306
						log_error("Zone: {$cpzone} - Trying to perform RADIUS MAC authentication on a non-RADIUS server !");
1307
						$radmac_error = true;
1308
						$result = null;
1309
					}
1310
				}
1311

    
1312
				if (!$radmac_error) {
1313
					if ($authcfg['type'] === 'none') {
1314
						$result = true;
1315
					} else {
1316
						$result = authenticate_user($login, $password, $authcfg, $attributes);
1317
					}
1318

    
1319
					if (!empty($attributes['error_message'])) {
1320
						$msg = $attributes['error_message'];
1321
					}
1322

    
1323
					if ($authcfg['type'] == 'Local Auth' && $result && isset($cpcfg['localauth_priv'])) {
1324
						if (!userHasPrivilege(getUserEntry($login), "user-services-captiveportal-login")) {
1325
							$result = false;
1326
							$msg = gettext("Access Denied");
1327
						}
1328
					}
1329
					if ($context === 'radmac' && $result === null && empty($attributes['reply_message'])) {
1330
						$msg = gettext("RADIUS MAC Authentication Failed.");
1331
					}
1332

    
1333
					if (empty($status)) {
1334
						if ($result === true) {
1335
							$status = "ACCEPT";
1336
						} elseif ($result === null) {
1337
							$status = "ERROR";
1338
						} else {
1339
							$status = "FAILURE";
1340
						}
1341
					}
1342

    
1343
					if ($context === 'radmac' && $login == mac_format($clientmac) || $authcfg['type'] === 'none' && empty($login)) {
1344
						$login = "unauthenticated";
1345
					}
1346
				}
1347
				// We determine a flag
1348
				if ($result === true) {
1349
					$val = 3;
1350
				} elseif ($result === false && !empty($attributes['reply_message'])) {
1351
					$val = 2;
1352
					$msg = $attributes['reply_message'];
1353
				} elseif ($result === false) {
1354
					$val = 1;
1355
				} elseif ($result === null) {
1356
					$val = 0;
1357
				}
1358

    
1359
				if ($val >= $authlevel) {
1360
					$authlevel = $val;
1361
					$auth_method = $authcfg['type'];
1362
					$login_status = $status;
1363
					$login_msg = $msg;
1364
					$reply_attributes = $attributes;
1365
					$auth_result = $result;
1366
				}
1367
			}
1368
		}
1369
	}
1370

    
1371
	return array('result'=>$auth_result, 'attributes'=>$reply_attributes, 'auth_method' =>$auth_method, 'login_status'=> $login_status, 'login_message' => $login_msg);
1372
}
1373

    
1374
function captiveportal_opendb() {
1375
	global $g, $config, $cpzone, $cpzoneid;
1376

    
1377
	$db_path = "{$g['vardb_path']}/captiveportal{$cpzone}.db";
1378
	$createquery = "CREATE TABLE IF NOT EXISTS captiveportal (" .
1379
				"allow_time INTEGER, pipeno INTEGER, ip TEXT, mac TEXT, username TEXT, " .
1380
				"sessionid TEXT, bpassword TEXT, session_timeout INTEGER, idle_timeout INTEGER, " .
1381
				"session_terminate_time INTEGER, interim_interval INTEGER, traffic_quota INTEGER, " .
1382
				"bw_up INTEGER, bw_down INTEGER, authmethod TEXT, context TEXT); " .
1383
			"CREATE UNIQUE INDEX IF NOT EXISTS idx_active ON captiveportal (sessionid, username); " .
1384
			"CREATE INDEX IF NOT EXISTS user ON captiveportal (username); " .
1385
			"CREATE INDEX IF NOT EXISTS ip ON captiveportal (ip); " .
1386
			"CREATE INDEX IF NOT EXISTS starttime ON captiveportal (allow_time)";
1387

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

    
1403
	if (!$DB) {
1404
		captiveportal_syslog("Could not open {$db_path} as an sqlite database for {$cpzone}. Error message: {$DB->lastErrorMsg()}. Trying again.");
1405
		unlink_if_exists($db_path);
1406
		$DB = new SQLite3($db_path);
1407
		$DB->busyTimeout(60000);
1408
		if (!$DB) {
1409
			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.");
1410
			return;
1411
		}
1412
	}
1413

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

    
1417
		/* If unable to initialize the database, reset and try again. */
1418
		$DB->close();
1419
		unset($DB);
1420
		unlink_if_exists($db_path);
1421
		$DB = new SQLite3($db_path);
1422
		$DB->busyTimeout(60000);
1423
		if ($DB->exec($createquery)) {
1424
			captiveportal_syslog("Successfully reinitialized tables for {$cpzone} -- database has been reset.");
1425
			if (!is_numericint($cpzoneid)) {
1426
				if (is_array($config['captiveportal'])) {
1427
					foreach ($config['captiveportal'] as $cpkey => $cp) {
1428
						if ($cpzone == $cpkey) {
1429
							$cpzoneid = $cp['zoneid'];
1430
						}
1431
					}
1432
				}
1433
			}
1434
			if (is_numericint($cpzoneid)) {
1435
				captiveportal_delete_rules(array(), true);
1436
				filter_configure();
1437
				captiveportal_syslog("Flushed tables for {$cpzone} after database reset.");
1438
			}
1439
		} else {
1440
			captiveportal_syslog("Still unable to create tables for {$cpzone}. Error message: {$DB->lastErrorMsg()}. Remove the database file manually and try again.");
1441
		}
1442
	}
1443

    
1444
	return $DB;
1445
}
1446

    
1447
/* read captive portal DB into array */
1448
function captiveportal_read_db($query = "") {
1449
	$cpdb = array();
1450

    
1451
	$DB = captiveportal_opendb();
1452
	if ($DB) {
1453
		$response = $DB->query("SELECT * FROM captiveportal {$query}");
1454
		if ($response != FALSE) {
1455
			while ($row = $response->fetchArray()) {
1456
				$cpdb[] = $row;
1457
			}
1458
		}
1459
		$DB->close();
1460
	}
1461

    
1462
	return $cpdb;
1463
}
1464

    
1465
function captiveportal_remove_entries($remove, $carp_loop = false) {
1466
	global $cpzone;
1467

    
1468
	if (!is_array($remove) || empty($remove)) {
1469
		return;
1470
	}
1471

    
1472
	$query = "DELETE FROM captiveportal WHERE sessionid in (";
1473
	foreach ($remove as $idx => $unindex) {
1474
		$query .= "'{$unindex}'";
1475
		if ($idx < (count($remove) - 1)) {
1476
			$query .= ",";
1477
		}
1478
	}
1479
	$query .= ")";
1480
	captiveportal_write_db($query);
1481

    
1482
	if (captiveportal_xmlrpc_sync_get_details($syncip, $syncport, $syncuser, $syncpass, $carp_loop)) {
1483
		$rpc_client = new pfsense_xmlrpc_client();
1484
		$rpc_client->setConnectionData($syncip, $syncport, $syncuser, $syncpass);
1485
		$rpc_client->set_noticefile("CaptivePortalUserSync");
1486
		$rpc_client->xmlrpc_method('captive_portal_sync',
1487
			array(
1488
				'op' => 'remove_entries',
1489
				'zone' => $cpzone,
1490
				'entries' => base64_encode(serialize($remove))
1491
			)
1492
		);
1493
	}
1494
	return true;
1495
}
1496

    
1497
/* write captive portal DB */
1498
function captiveportal_write_db($queries) {
1499
	global $g;
1500

    
1501
	if (is_array($queries)) {
1502
		$query = implode(";", $queries);
1503
	} else {
1504
		$query = $queries;
1505
	}
1506

    
1507
	$DB = captiveportal_opendb();
1508
	if ($DB) {
1509
		$DB->exec("BEGIN TRANSACTION");
1510
		$result = $DB->exec($query);
1511
		if (!$result) {
1512
			captiveportal_syslog("Trying to modify DB returned error: {$DB->lastErrorMsg()}");
1513
		} else {
1514
			$DB->exec("END TRANSACTION");
1515
		}
1516
		$DB->close();
1517
		return $result;
1518
	} else {
1519
		return true;
1520
	}
1521
}
1522

    
1523
function captiveportal_write_elements() {
1524
	global $g, $config, $cpzone;
1525

    
1526
	$cpcfg = $config['captiveportal'][$cpzone];
1527

    
1528
	if (!is_dir($g['captiveportal_element_path'])) {
1529
		@mkdir($g['captiveportal_element_path']);
1530
	}
1531

    
1532
	if (is_array($cpcfg['element'])) {
1533
		foreach ($cpcfg['element'] as $data) {
1534
			/* Do not attempt to decode or write out empty files. */
1535
			if (isset($data['nocontent'])) {
1536
					continue;
1537
			}
1538
			if (empty($data['content']) || empty(base64_decode($data['content']))) {
1539
				unlink_if_exists("{$g['captiveportal_element_path']}/{$data['name']}");
1540
				touch("{$g['captiveportal_element_path']}/{$data['name']}");
1541
			} elseif (!@file_put_contents("{$g['captiveportal_element_path']}/{$data['name']}", base64_decode($data['content']))) {
1542
				printf(gettext('Error: cannot open \'%1$s\' in captiveportal_write_elements()%2$s'), $data['name'], "\n");
1543
				return 1;
1544
			}
1545
			if (!file_exists("{$g['captiveportal_path']}/{$data['name']}")) {
1546
				@symlink("{$g['captiveportal_element_path']}/{$data['name']}", "{$g['captiveportal_path']}/{$data['name']}");
1547
			}
1548
		}
1549
	}
1550

    
1551
	return 0;
1552
}
1553

    
1554
function captiveportal_free_dnrules($rulenos_start = 2000,
1555
    $rulenos_range_max = 64500, $dry_run = false, $clear_auth_pipes = true) {
1556
	global $g, $cpzone;
1557

    
1558
	$removed_pipes = array();
1559

    
1560
	if (!file_exists("{$g['vardb_path']}/captiveportaldn.rules")) {
1561
		return $removed_pipes;
1562
	}
1563

    
1564
	if (!$dry_run) {
1565
		$cpruleslck = lock("captiveportalrulesdn", LOCK_EX);
1566
	}
1567

    
1568
	$rules = unserialize(file_get_contents(
1569
	    "{$g['vardb_path']}/captiveportaldn.rules"));
1570
	$ridx = $rulenos_start;
1571
	while ($ridx < $rulenos_range_max) {
1572
		if (substr($rules[$ridx], 0, strlen($cpzone . '_')) == $cpzone . '_') {
1573
			if (!$clear_auth_pipes && substr($rules[$ridx], 0, strlen($cpzone . '_auth')) == $cpzone . '_auth') {
1574
				$ridx += 2;
1575
			} else {
1576
				if (!$dry_run) {
1577
					$rules[$ridx] = false;
1578
				}
1579
				$removed_pipes[] = $ridx;
1580
				$ridx++;
1581
				if (!$dry_run) {
1582
					$rules[$ridx] = false;
1583
				}
1584
				$removed_pipes[] = $ridx;
1585
				$ridx++;
1586
			}
1587
		} else {
1588
			$ridx += 2;
1589
		}
1590
	}
1591

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

    
1598
	unset($rules);
1599

    
1600
	return $removed_pipes;
1601
}
1602

    
1603
function captiveportal_reserve_ruleno($ruleno) {
1604
	global $g, $cpzone;
1605

    
1606
	$cpruleslck = lock("captiveportalrulesdn", LOCK_EX);
1607
	if (file_exists("{$g['vardb_path']}/captiveportaldn.rules")) {
1608
		$rules = unserialize(file_get_contents("{$g['vardb_path']}/captiveportaldn.rules"));
1609
	} else {
1610
		$rules = array_pad(array(), 64500, false);
1611
	}
1612
	$rules[$ruleno] = $cpzone . '_auth';
1613
	$ruleno++;
1614
	$rules[$ruleno] = $cpzone . '_auth';
1615

    
1616
	file_put_contents("{$g['vardb_path']}/captiveportaldn.rules", serialize($rules));
1617
	unlock($cpruleslck);
1618
	unset($rules);
1619

    
1620
	return $ruleno;
1621
}
1622

    
1623
function captiveportal_get_next_dn_ruleno($rule_type = 'default', $rulenos_start = 2000, $rulenos_range_max = 64500) {
1624
	global $config, $g, $cpzone;
1625

    
1626
	$cpruleslck = lock("captiveportalrulesdn", LOCK_EX);
1627
	$ruleno = 0;
1628
	if (file_exists("{$g['vardb_path']}/captiveportaldn.rules")) {
1629
		$rules = unserialize(file_get_contents("{$g['vardb_path']}/captiveportaldn.rules"));
1630
		$ridx = $rulenos_start;
1631
		while ($ridx < $rulenos_range_max) {
1632
			if (empty($rules[$ridx])) {
1633
				$ruleno = $ridx;
1634
				$rules[$ridx] = $cpzone . '_' . $rule_type;
1635
				$ridx++;
1636
				$rules[$ridx] = $cpzone . '_' . $rule_type;
1637
				break;
1638
			} else {
1639
				$ridx += 2;
1640
			}
1641
		}
1642
	} else {
1643
		$rules = array_pad(array(), $rulenos_range_max, false);
1644
		$ruleno = $rulenos_start;
1645
		$rules[$rulenos_start] = $cpzone . '_' . $rule_type;
1646
		$rulenos_start++;
1647
		$rules[$rulenos_start] = $cpzone . '_' . $rule_type;
1648
	}
1649
	file_put_contents("{$g['vardb_path']}/captiveportaldn.rules", serialize($rules));
1650
	unlock($cpruleslck);
1651
	unset($rules);
1652

    
1653
	return $ruleno;
1654
}
1655

    
1656
function captiveportal_free_dn_ruleno($ruleno) {
1657
	global $config, $g;
1658

    
1659
	$cpruleslck = lock("captiveportalrulesdn", LOCK_EX);
1660
	if (file_exists("{$g['vardb_path']}/captiveportaldn.rules")) {
1661
		$rules = unserialize(file_get_contents("{$g['vardb_path']}/captiveportaldn.rules"));
1662
		$rules[$ruleno] = false;
1663
		$ruleno++;
1664
		$rules[$ruleno] = false;
1665
		file_put_contents("{$g['vardb_path']}/captiveportaldn.rules", serialize($rules));
1666
		unset($rules);
1667
	}
1668
	unlock($cpruleslck);
1669
}
1670

    
1671
function captiveportal_get_dn_passthru_ruleno($mac, $anchor = 'passthrumac') {
1672
	global $config, $g, $cpzone, $cpzoneid;
1673

    
1674
	$cpcfg = $config['captiveportal'][$cpzone];
1675
	$cpzoneprefix = CPPREFIX . $cpcfg['zoneid'];
1676
	if (!isset($cpcfg['enable'])) {
1677
		return NULL;
1678
	}
1679

    
1680
	$cpruleslck = lock("captiveportalrulesdn", LOCK_EX);
1681
	$ruleno = NULL;
1682
	if (file_exists("{$g['vardb_path']}/captiveportaldn.rules")) {
1683
		$host = str_replace(":", "", $mac);
1684

    
1685
		$result = pfSense_pf_cp_get_eth_pipes("{$cpzoneprefix}_{$anchor}mac/$host");
1686
		if (!empty($result)) {
1687
			list($pipeup, $pipedown) = $result;
1688
			$rules = unserialize(file_get_contents("{$g['vardb_path']}/captiveportaldn.rules"));
1689
			if (!$rules[$pipeup]) {
1690
				$ruleno = NULL;
1691
			}
1692
		}
1693
	}
1694
	unlock($cpruleslck);
1695

    
1696
	return $ruleno;
1697
}
1698

    
1699
/**
1700
 * This function will calculate the traffic produced by a client
1701
 * based on its firewall rule
1702
 *
1703
 * Point of view: NAS
1704
 *
1705
 * Input means: from the client
1706
 * Output means: to the client
1707
 *
1708
 */
1709

    
1710
function getVolume($ip) {
1711
	global $g, $config, $cpzone;
1712

    
1713
	$reverse = isset($config['captiveportal'][$cpzone]['reverseacct']) ? true : false;
1714
	$volume = array();
1715
	// Initialize vars properly, since we don't want NULL vars
1716
	$volume['input_pkts'] = $volume['input_bytes'] = 0;
1717
	$volume['output_pkts'] = $volume['output_bytes'] = 0;
1718

    
1719
	/* no needs to check allowedip */
1720
	$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
1721
	$anchor = $cpzoneprefix . '_auth';
1722

    
1723
	$result = pfSense_pf_cp_get_eth_rule_counters("{$anchor}/{$ip}");
1724
	if (!empty($result)) {
1725
	   list($output_pkts, $output_bytes, $input_pkts, $input_bytes) = $result;
1726

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

    
1740
	return $volume;
1741
}
1742

    
1743
function portal_ip_from_client_ip($cliip) {
1744
	global $config, $cpzone;
1745

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

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

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

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

    
1786
	return false;
1787
}
1788

    
1789
function portal_hostname_from_client_ip($cliip) {
1790
	global $config, $cpzone;
1791

    
1792
	$cpcfg = $config['captiveportal'][$cpzone];
1793

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

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

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

    
1815
	return $ourhostname;
1816
}
1817

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

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

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

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

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

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

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

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

    
1867
	echo $htmltext;
1868
}
1869

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

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

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

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

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

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

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

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

    
1920
	unset($sessionid);
1921

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

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

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

    
1942
				captiveportal_passthrumac_delete_entry($macent);
1943
				unset($config['captiveportal'][$cpzone]['passthrumac'][$idx]);
1944
			}
1945
		}
1946
	}
1947

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

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

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

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

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

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

    
1994
			if ($config['captiveportal'][$cpzone]['noconcurrentlogins'] == "last") {
1995
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "Found last");
1996
			} else {
1997
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "Found NOT last");
1998
			}
1999
				
2000
			if (!isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) {
2001
				/* 'noconcurrentlogins' not set : accept login 'username' creating multiple sessions. */
2002
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "config['captiveportal'][$cpzone]['noconcurrentlogins'] 3 does not exists = NOT set");
2003
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT LOGIN - NOT TERMINATING EXISTING SESSION(S)");			
2004
			} elseif ($config['captiveportal'][$cpzone]['noconcurrentlogins'] == "last") {
2005
				/* Classic situation : accept the new login, disconnect the old - present - connection */
2006
				if (isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) {
2007
					captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "config['captiveportal'][$cpzone]['noconcurrentlogins'] 4 exists = set");		
2008
				} else {
2009
					captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "config['captiveportal'][$cpzone]['noconcurrentlogins'] 4 does not exists = NOT set");		
2010
				}
2011
				
2012
				captiveportal_disconnect($cpentry, 13);
2013
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT LOGIN - TERMINATING OLD SESSION");
2014
				$unsetindexes[] = $cpentry[5];
2015
				break;
2016
			} else {
2017
				/* Implicit 'first' : refuse the new login - 'username' is already logged in */
2018
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT VOUCHER LOGIN - NOT ALLOWED KEEPING OLD SESSION ");
2019
				unlock($cpdblck);
2020
				return 2;
2021
			}
2022
		} elseif ((isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) && ($username != 'unauthenticated')) {
2023
			if ($config['captiveportal'][$cpzone]['noconcurrentlogins'] == "last") {
2024
				/* on the same username */
2025
				if (strcasecmp($cpentry[4], $username) == 0) {
2026
					/* This user was already logged in so we disconnect the old one */
2027
					captiveportal_disconnect($cpentry, 13);
2028
					captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT USER LOGIN - TERMINATING OLD SESSION");
2029
					$unsetindexes[] = $cpentry[5];
2030
					break;
2031
				}
2032
			} else {
2033
				/* Implicit 'first' : refuse the new login - 'username' is already logged in */
2034
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT USER LOGIN - NOT ALLOWED KEEPING OLD SESSION ");
2035
				unlock($cpdblck);
2036
				return 2;				
2037
			}
2038
		}
2039
	}
2040
	unset($cpdb);
2041

    
2042
	if (!empty($unsetindexes)) {
2043
		captiveportal_remove_entries($unsetindexes);
2044
	}
2045

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

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

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

    
2074
		$mac = array();
2075
		$mac['action'] = 'pass';
2076
		$mac['ip'] = $clientip;
2077
		$mac['username'] = $username;
2078
		if (isset($config['captiveportal'][$cpzone]['passthrumacadd'])) {
2079
			$mac['mac'] = $clientmac;
2080
			if ($attributes['voucher']) {
2081
				$mac['logintype'] = "voucher";
2082
			}
2083
			if ($username == "unauthenticated") {
2084
				$mac['descr'] = "Auto-added";
2085
			} else if ($authmethod == "voucher") {
2086
				$mac['descr'] = "Auto-added for voucher {$username}";
2087
			} else {
2088
				$mac['descr'] = "Auto-added for user {$username}";
2089
			}
2090
			if (!empty($bw_up)) {
2091
				$mac['bw_up'] = $bw_up;
2092
			}
2093
			if (!empty($bw_down)) {
2094
				$mac['bw_down'] = $bw_down;
2095
			}
2096
			if (!is_array($config['captiveportal'][$cpzone]['passthrumac'])) {
2097
				$config['captiveportal'][$cpzone]['passthrumac'] = array();
2098
			}
2099
			//check for mac duplicates before adding it to config.
2100
			$mac_duplicate = false;
2101
			foreach($config['captiveportal'][$cpzone]['passthrumac'] as $mac_check){
2102
				if($mac_check['mac'] == $mac['mac']){
2103
					$mac_duplicate = true;
2104
				}
2105
			}
2106
			if(!$mac_duplicate){
2107
				$config['captiveportal'][$cpzone]['passthrumac'][] = $mac;
2108
			}
2109
			unlock($cpdblck);
2110
			captiveportal_passthrumac_configure_entry($mac);
2111
			$writecfg = true;
2112
		} else {
2113
			/* See if a pipeno is passed, if not start sessions because this means there isn't one atm */
2114
			if (is_null($mac['pipeno'])) {
2115
				$mac['pipeno'] = captiveportal_get_next_dn_ruleno('auth');
2116
			}
2117

    
2118
			/* if the pool is empty, return appropriate message and exit */
2119
			if (is_null($mac['pipeno'])) {
2120
				captiveportal_syslog("Zone: {$cpzone} - WARNING!  Captive portal has reached maximum login capacity");
2121
				unlock($cpdblck);
2122
				return false;
2123
			}
2124

    
2125
			$mac['ip'] = $clientip;
2126
			if (!isset($config['captiveportal'][$cpzone]['nomacfilter'])) {
2127
				$mac['mac'] = $clientmac;
2128
			}
2129
			captiveportal_ether_configure_entry($mac, 'auth');
2130

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

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

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

    
2145
			/* encode password in Base64 just in case it contains commas */
2146
			$bpassword = (isset($config['captiveportal'][$cpzone]['reauthenticate'])) ? base64_encode($password) : '';
2147
			$insertquery = "INSERT INTO captiveportal (allow_time, pipeno, ip, mac, username, sessionid, bpassword, session_timeout, idle_timeout, session_terminate_time, interim_interval, traffic_quota, bw_up, bw_down, authmethod, context) ";
2148
			$insertquery .= "VALUES ({$allow_time}, {$pipeno}, '{$clientip}', '{$clientmac}', '{$safe_username}', '{$sessionid}', '{$bpassword}', ";
2149
			$insertquery .= "{$session_timeout}, {$idle_timeout}, {$session_terminate_time}, {$interim_interval}, {$traffic_quota}, {$bw_up}, {$bw_down}, '{$authmethod}', '{$context}')";
2150

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

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

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

    
2199
		unlock($cpdblck);
2200
	}
2201

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

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

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

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

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

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

    
2239
	return $sessionid;
2240
}
2241

    
2242

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

    
2252
	if (!empty($config['captiveportal'][$cpzone]['freelogins_count']) && is_numeric($config['captiveportal'][$cpzone]['freelogins_count'])) {
2253
		$freeloginscount = $config['captiveportal'][$cpzone]['freelogins_count'];
2254
	} else {
2255
		return false;
2256
	}
2257

    
2258
	if (!empty($config['captiveportal'][$cpzone]['freelogins_resettimeout']) && is_numeric($config['captiveportal'][$cpzone]['freelogins_resettimeout'])) {
2259
		$resettimeout = $config['captiveportal'][$cpzone]['freelogins_resettimeout'];
2260
	} else {
2261
		return false;
2262
	}
2263

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

    
2268
	$updatetimeouts = isset($config['captiveportal'][$cpzone]['freelogins_updatetimeouts']);
2269

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

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

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

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

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

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

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

    
2314
	captiveportal_write_usedmacs_db($usedmacs);
2315
	xmlrpc_sync_usedmacs($usedmacs);
2316
	return true;
2317
}
2318

    
2319
function xmlrpc_sync_usedmacs($usedmacs) { 
2320
	global $config, $cpzone;
2321

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

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

    
2342
function captiveportal_read_usedmacs_db() {
2343
	global $g, $cpzone;
2344

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

    
2355
	unlock($cpumaclck);
2356
	return $usedmacs;
2357
}
2358

    
2359
function captiveportal_write_usedmacs_db($usedmacs) {
2360
	global $g, $cpzone;
2361

    
2362
	$cpumaclck = lock("captiveusedmacs{$cpzone}", LOCK_EX);
2363
	@file_put_contents("{$g['vardb_path']}/captiveportal_usedmacs_{$cpzone}.db", implode("\n", $usedmacs));
2364
	unlock($cpumaclck);
2365
}
2366

    
2367
function captiveportal_blocked_mac($mac) {
2368
	global $config, $g, $cpzone;
2369

    
2370
	if (empty($mac) || !is_macaddr($mac)) {
2371
		return false;
2372
	}
2373

    
2374
	if (!is_array($config['captiveportal'][$cpzone]['passthrumac'])) {
2375
		return false;
2376
	}
2377

    
2378
	foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $passthrumac) {
2379
		if (($passthrumac['action'] == 'block') &&
2380
		    ($passthrumac['mac'] == strtolower($mac))) {
2381
			return true;
2382
		}
2383
	}
2384

    
2385
	return false;
2386

    
2387
}
2388

    
2389
/* Captiveportal Radius Accounting */
2390

    
2391
function gigawords($bytes) {
2392

    
2393
	/*
2394
	 * RFC2866 Specifies a 32bit unsigned integer, which is a max of 4294967295
2395
	 * Currently there is a fault in the PECL radius_put_int function which can handle only 32bit signed integer.
2396
	 */
2397

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

    
2401
	// We need to manually set this to a zero instead of NULL for put_int() safety
2402
	if (is_null($gigawords)) {
2403
		$gigawords = 0;
2404
	}
2405

    
2406
	return $gigawords;
2407
}
2408

    
2409
function remainder($bytes) {
2410
	// Calculate the bytes we are going to send to the radius
2411
	$bytes = bcmod($bytes, GIGAWORDS_RIGHT_OPERAND);
2412

    
2413
	if (is_null($bytes)) {
2414
		$bytes = 0;
2415
	}
2416

    
2417
    return $bytes;
2418
}
2419

    
2420
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) {
2421
	global $cpzone, $config;
2422

    
2423
	$cpcfg = $config['captiveportal'][$cpzone];
2424
	$acctcfg = auth_get_authserver($cpcfg['radacct_server']);
2425

    
2426
	if (!isset($cpcfg['radacct_enable']) || empty($acctcfg) ||
2427
	    captiveportal_ha_is_node_in_backup_mode($cpzone)) {
2428
		return null;
2429
	}
2430

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

    
2454
	$racct->addServer($acctcfg['host'], $acctcfg['radius_acct_port'],
2455
		$acctcfg['radius_secret'], $acctcfg['radius_timeout']);
2456

    
2457
	$racct->authentic = RADIUS_AUTH_RADIUS;
2458
	if ($cpcfg['auth_method'] === 'radmac' && $username === "unauthenticated" && !empty($clientmac)) {
2459
		$racct->username = mac_format($clientmac);
2460
	} elseif (!empty($username)) {
2461
		$racct->username = $username;
2462
	}
2463

    
2464
	if (PEAR::isError($racct->start())) {
2465
		captiveportal_syslog('RADIUS ACCOUNTING FAILED : '.$racct->getError());
2466
		$racct->close();
2467
		return null;
2468
	}
2469

    
2470
	$nasip = nasip_fallback($acctcfg['radius_nasip_attribute']);
2471
	$nasmac = get_interface_mac(find_ip_interface($nasip));
2472
	$racct->putAttribute(RADIUS_NAS_IP_ADDRESS, $nasip, "addr");
2473

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

    
2476
	if (is_int($ruleno)) {
2477
		$racct->putAttribute(RADIUS_NAS_PORT_TYPE, RADIUS_ETHERNET);
2478
		$racct->putAttribute(RADIUS_NAS_PORT, intval($ruleno), 'integer');
2479
	}
2480

    
2481
	if (!empty($sessionid)) {
2482
		$racct->putAttribute(RADIUS_ACCT_SESSION_ID, $sessionid);
2483
	}
2484

    
2485
	if (!empty($clientip) && is_ipaddr($clientip)) {
2486
		$racct->putAttribute(RADIUS_FRAMED_IP_ADDRESS, $clientip, "addr");
2487
	}
2488
	if (!empty($clientmac)) {
2489
		$racct->putAttribute(RADIUS_CALLING_STATION_ID, mac_format($clientmac));
2490
	}
2491
	if (!empty($nasmac)) {
2492
		$racct->putAttribute(RADIUS_CALLED_STATION_ID, mac_format($nasmac).':'.gethostname());
2493
	}
2494

    
2495
	// Accounting request Stop and Update : send the current data volume
2496
	if (($type === 'stop' || $type === 'update') && is_int($start_time)) {
2497
		$volume = getVolume($clientip);
2498
		$session_time = $stop_time - $start_time;
2499
		$volume['input_bytes_radius'] = remainder($volume['input_bytes']);
2500
		$volume['input_gigawords'] = gigawords($volume['input_bytes']);
2501
		$volume['output_bytes_radius'] = remainder($volume['output_bytes']);
2502
		$volume['output_gigawords'] = gigawords($volume['output_bytes']);
2503

    
2504
		// Volume stuff: Ingress
2505
		$racct->putAttribute(RADIUS_ACCT_INPUT_PACKETS, intval($volume['input_pkts']), "integer");
2506
		$racct->putAttribute(RADIUS_ACCT_INPUT_OCTETS, intval($volume['input_bytes_radius']), "integer");
2507
		// Volume stuff: Outgress
2508
		$racct->putAttribute(RADIUS_ACCT_OUTPUT_PACKETS, intval($volume['output_pkts']), "integer");
2509
		$racct->putAttribute(RADIUS_ACCT_OUTPUT_OCTETS, intval($volume['output_bytes_radius']), "integer");
2510
		$racct->putAttribute(RADIUS_ACCT_SESSION_TIME, intval($session_time), "integer");
2511

    
2512
		$racct->putAttribute(CUSTOM_RADIUS_ACCT_OUTPUT_GIGAWORDS, intval($volume['output_gigawords']), "integer");
2513
		$racct->putAttribute(CUSTOM_RADIUS_ACCT_INPUT_GIGAWORDS, intval($volume['input_gigawords']), "integer");
2514
		// Set session_time
2515
		$racct->session_time = $session_time;
2516
	}
2517

    
2518
	if ($type === 'stop') {
2519
		if (empty($term_cause)) {
2520
			$term_cause = 1;
2521
		}
2522
		$racct->putAttribute(RADIUS_ACCT_TERMINATE_CAUSE, $term_cause);
2523
	}
2524

    
2525
	// Send request
2526
	$result = $racct->send();
2527

    
2528
	if (PEAR::isError($result)) {
2529
		 captiveportal_syslog('RADIUS ACCOUNTING FAILED : '.$racct->getError());
2530
		 $result = null;
2531
	} elseif ($result !== true) {
2532
		$result = false;
2533
	}
2534

    
2535
	$racct->close();
2536
	return $result;
2537
}
2538

    
2539
function captiveportal_isip_logged($clientip) {
2540
	global $g, $cpzone;
2541

    
2542
	/* read in client database */
2543
	$query = "WHERE ip = '{$clientip}'";
2544
	$cpdb = captiveportal_read_db($query);
2545
	foreach ($cpdb as $cpentry) {
2546
		return $cpentry;
2547
	}
2548
}
2549

    
2550
function captiveportal_allowedhostname_cleanup() {
2551
	global $g, $config, $cpzone;
2552

    
2553
	$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
2554

    
2555
	init_config_arr(array('captiveportal', $cpzone, 'allowedhostname'));
2556
	foreach ($config['captiveportal'][$cpzone]['allowedhostname'] as $id => $hostnameent) {
2557
		$result = array();
2558
		list($pipeup, $pipedown) = pfSense_pf_cp_get_eth_pipes("{$cpzoneprefix}__allowedhosts/hostname_{$id}");
2559
		pfSense_pf_cp_flush("{$cpzoneprefix}_allowedhosts/hostname_{$id}", "ether");
2560
		if (!empty($pipeup)) {
2561
			captiveportal_pipe_delete($pipeup);
2562
		}
2563
	}
2564
}
2565

    
2566
function filter_captiveportal_aliases() {
2567
	/* return all aliases used in captive portal zones, 
2568
	 * to prevent it from deletion in filter_configure_sync() as unused aliases */
2569
	global $g, $config;
2570

    
2571
	$aliasesnames = array();
2572

    
2573
	init_config_arr(array('captiveportal'));
2574
	foreach ($config['captiveportal'] as $cpzone => $cpcfg) {
2575
		if (isset($cpcfg['enable'])) {
2576
			$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
2577
			$aliasesnames[] = $cpzoneprefix . '_cpips';
2578
			init_config_arr(array('captiveportal', $cpzone, 'allowedhostname'));
2579
			foreach ($config['captiveportal'][$cpzone]['allowedhostname'] as $id => $hostnameent) {
2580
				$aliasesnames[] = $cpzoneprefix . '_hostname_' . $id;
2581
			}
2582
		}
2583
	}
2584

    
2585
	return $aliasesnames;
2586
}
2587

    
2588
function filter_captiveportal_tables() {
2589
	/* return pf rules which defines tables used in captive portal zones */
2590
	global $g, $config, $FilterIflist;
2591

    
2592
	$rules = '';
2593
	init_config_arr(array('captiveportal'));
2594
	foreach ($config['captiveportal'] as $cpzone => $cpcfg) {
2595
		if (!isset($cpcfg['enable'])) {
2596
			continue;
2597
		}
2598

    
2599
		$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
2600
		$cpips = $cpzoneprefix . '_cpips';
2601
		$hosttable = $cpzoneprefix . '_host_';
2602
		$cpiplist = '';
2603

    
2604
		foreach (explode(",", $cpcfg['interface']) as $cpifgrp) {
2605
			if (isset($FilterIflist[$cpifgrp])) {
2606
				$realif = get_real_interface($cpifgrp);
2607
				if (!empty($realif)) {
2608
					$cpip = get_interface_ip($cpifgrp);
2609
					if (is_ipaddrv4($cpip)) {
2610
						$cpiplist = $cpip . ' ';
2611
						$cpiplist .= get_interface_vip_ips($cpifgrp);
2612
					}
2613
				}
2614
			}
2615
		}
2616
		if (!empty($cpiplist)) {
2617
			/* captive portal web server IP addresses */
2618
			$rules .= "table <{$cpips}> { {$cpiplist} }\n";
2619
		}
2620
	}
2621

    
2622
	if (!empty($rules)) {
2623
		$rules = "\n# Captive Portal\n" . $rules . "\n";
2624
	}
2625

    
2626
	return $rules;
2627
}
2628

    
2629
function filter_captiveportal_ether() {
2630
	global $g, $config;
2631

    
2632
	$rules = '';
2633
	init_config_arr(array('captiveportal'));
2634
	foreach ($config['captiveportal'] as $cpzone => $cpcfg) {
2635
		if (!isset($cpcfg['enable'])) {
2636
			continue;
2637
		}
2638

    
2639
		$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
2640
		$rdrtag = $cpzoneprefix . '_rdr';
2641
		$interfaces = captiveportal_zone_interfaces($cpcfg);
2642

    
2643
		if (!empty($interfaces)) {
2644
			/* set 'rdr' tag for futher captive portal web portal redirection */
2645
			$rules .= "ether pass on { {$interfaces} } tag \"{$rdrtag}\"\n";
2646
			if (!isset($cpcfg['nomacfilter'])) {
2647
				/* anchor to set MAC-specified tag (cpzoneid_N_001122334455)
2648
				 * for futher IP spoofing check */
2649
				$rules .= "ether anchor \"{$cpzoneprefix}_authmac/*\" on { {$interfaces} }\n";
2650
			}
2651
			/* anchor for Services / Captive Portal / CPZONE / MACs */
2652
			$rules .= "ether anchor \"{$cpzoneprefix}_passthrumac/*\" on { {$interfaces} }\n";
2653
			/* anchor to set the PASSTHRU tag for Allowed IP/Hostnames */
2654
			$rules .= "ether anchor \"{$cpzoneprefix}_allowedhosts/*\" on { {$interfaces} }\n";
2655
			/* anchor to set the PASS tag for authenticated clients */
2656
			$rules .= "ether anchor \"{$cpzoneprefix}_auth/*\" on { {$interfaces} }\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_rdr() {
2668
	global $g, $config, $FilterIflist;
2669

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

    
2676
		$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
2677
		$rdrtag = $cpzoneprefix . '_rdr';
2678
		$cpips = $cpzoneprefix . '_cpips';
2679
		$interfaces = captiveportal_zone_interfaces($cpcfg);
2680
		list($portalias, $cprdrport) = captiveportal_zone_portalports($cpcfg);
2681

    
2682
		foreach (explode(",", $cpcfg['interface']) as $cpifgrp) {
2683
			if (isset($FilterIflist[$cpifgrp])) {
2684
				$realif = get_real_interface($cpifgrp);
2685
				if (!empty($realif)) {
2686
					$cpip = get_interface_ip($cpifgrp);
2687
					if (is_ipaddrv4($cpip)) {
2688
						$rules .= "rdr on {$realif} inet proto tcp from any to ! <{$cpips}> port {$cprdrport}  tagged {$rdrtag} -> {$cpip} port {$portalias}\n";
2689
					}
2690
				}
2691
			}
2692
		}
2693
	}
2694

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

    
2699
	return $rules;
2700
}
2701

    
2702
function filter_captiveportal_pass() {
2703
	global $g, $config, $FilterIflist;
2704

    
2705
	$captiveportal_increment = 'filter_captiveportal_tracker';
2706

    
2707
	$rules = '';
2708
	foreach ($config['captiveportal'] as $cpzone => $cpcfg) {
2709
		if (!isset($cpcfg['enable'])) {
2710
			continue;
2711
		}
2712

    
2713
		$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
2714
		$cpips = $cpzoneprefix . '_cpips';
2715
		$authtag = $cpzoneprefix . '_auth';
2716
		$passthrutag = $cpzoneprefix . '_passthru';
2717
		list($portalias, $cprdrport) = captiveportal_zone_portalports($cpcfg);
2718

    
2719
		foreach (explode(",", $cpcfg['interface']) as $cpifgrp) {
2720
			if (!isset($FilterIflist[$cpifgrp])) {
2721
				continue;
2722
			}
2723
			$realif = get_real_interface($cpifgrp);
2724
			if (!empty($realif)) {
2725
				$cpip = get_interface_ip($cpifgrp);
2726
				if (is_ipaddrv4($cpip)) {
2727
					/* pass non-authenticated clients to captive portal */
2728
					$rules .= "pass in quick on {$realif} proto tcp from any to <{$cpips}> port {$portalias} ridentifier {$captiveportal_increment()} keep state(sloppy)\n";
2729
					/* without this rule captive portal doesn't show login page after manual disconnect */
2730
					$rules .= "pass out quick on {$realif} proto tcp from {$cpip} port {$portalias} to any flags any ridentifier {$captiveportal_increment()} keep state(sloppy)\n";
2731
					/* Allowed IP/MAC passthrough */
2732
					$rules .= "pass in quick from any to any tagged {$passthrutag} ridentifier {$captiveportal_increment()} keep state\n";
2733
					/* block non-authenticated clients access to internet */
2734
					$rules .= "block in quick on {$realif} from any to ! <{$cpips}> ! tagged {$authtag} ridentifier {$captiveportal_increment()}\n";
2735
				}
2736
			}
2737
		}
2738
	}
2739

    
2740
	if (!empty($rules)) {
2741
		$rules = "\n# Captive Portal\n" . $rules . "\n";
2742
	}
2743

    
2744
	return $rules;
2745
}
2746

    
2747
function captiveportal_zone_interfaces($cpcfg) {
2748
	/* return a list of captive portal zone interfaces */
2749
	global $FilterIflist;
2750

    
2751
	$interfaces = '';
2752
	foreach (explode(",", $cpcfg['interface']) as $cpifgrp) {
2753
		if (isset($FilterIflist[$cpifgrp])) {
2754
			$realif = get_real_interface($cpifgrp);
2755
			if (!empty($realif) && get_interface_ip($realif)) {
2756
				$interfaces .= $realif . ' '; 
2757
			}
2758
		}
2759
	}
2760
	return $interfaces;
2761
}
2762

    
2763
function captiveportal_zone_portalports($cpcfg) {
2764
	if (isset($cpcfg['httpslogin']) && !isset($cpcfg['nohttpsforwards'])) {
2765
		$portalias = $cpcfg['listenporthttps'] ? $cpcfg['listenporthttps'] : 8001 + $cpcfg['zoneid'];
2766
		$cprdrport = '443';
2767
	} else {
2768
		$portalias = $cpcfg['listenporthttp'] ? $cpcfg['listenporthttp'] : 8000 + $cpcfg['zoneid'];
2769
		$cprdrport = '80';
2770
	}
2771
	return array($portalias, $cprdrport);
2772
}
2773

    
2774
function captiveportal_pipe_delete($pipeup) {
2775
	if (!empty($pipeup)) {
2776
		mwexec("/sbin/dnctl pipe delete {$pipeup}");
2777
		mwexec("/sbin/dnctl pipe delete " . ($pipeup+1));
2778
		captiveportal_free_dn_ruleno($pipeup);
2779
	}
2780
}
2781

    
2782
function captiveportal_ether_configure_entry($hostent, $anchor='passthrumac') {
2783
	global $config, $g, $cpzone;
2784

    
2785
	if (($hostent['action'] == 'block') && ($anchor == 'passthrumac')) {
2786
		return;
2787
	}
2788

    
2789
	$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
2790
	if ($anchor == 'passthrumac') {
2791
		$tag = $cpzoneprefix . '_passthru';
2792
	} else {
2793
		$tag = $cpzoneprefix . '_' . $anchor;
2794
	}
2795

    
2796
	list($pipeup, $pipedown) = captiveportal_pipe_configure($hostent);
2797

    
2798
	if ($anchor == 'passthrumac') {
2799
		$host = str_replace("/", "_", str_replace(":", "", $hostent['mac']));
2800
		$l3from = '';
2801
		$l3to = '';
2802
		$macfrom = "from {$hostent['mac']}";
2803
		$macto = "to {$hostent['mac']}";
2804
	} else {
2805
		$host = $hostent['ip'] . '_32';
2806
		$l3from = "l3 from {$hostent['ip']}";
2807
		$l3to = "l3 to {$hostent['ip']}";
2808
		if (!isset($config['captiveportal'][$cpzone]['nomacfilter'])) { 
2809
			if (!empty($hostent['mac'])) {
2810
				$macfrom = "from {$hostent['mac']}";
2811
				$macto = "to {$hostent['mac']}";
2812
			} else {
2813
				return;
2814
			}
2815
		} else {
2816
			$macfrom = '';
2817
			$macto = '';
2818
		}
2819
	}
2820

    
2821
	$rules = "ether pass in quick {$macfrom} {$l3from} tag {$tag} dnpipe {$pipeup}\n";
2822
	$rules .= "ether pass out quick {$macto} {$l3to} tag {$tag} dnpipe {$pipedown}\n";
2823

    
2824
	captiveportal_load_pfctl("{$cpzoneprefix}_{$anchor}", $host, $rules);
2825
}
2826

    
2827
function captiveportal_pipe_configure($host) {
2828
	global $config, $cpzone;
2829

    
2830
	$bwUp = 0;
2831
	if (!empty($host['bw_up'])) {
2832
		$bwUp = $host['bw_up'];
2833
	} elseif (isset($config['captiveportal'][$cpzone]['peruserbw']) &&
2834
	    !empty($config['captiveportal'][$cpzone]['bwdefaultup'])) {
2835
		$bwUp = $config['captiveportal'][$cpzone]['bwdefaultup'];
2836
	}
2837
	$bwDown = 0;
2838
	if (!empty($host['bw_down'])) {
2839
		$bwDown = $host['bw_down'];
2840
	} elseif (isset($config['captiveportal'][$cpzone]['peruserbw']) &&
2841
	    !empty($config['captiveportal'][$cpzone]['bwdefaultdn'])) {
2842
		$bwDown = $config['captiveportal'][$cpzone]['bwdefaultdn'];
2843
	}
2844

    
2845
	if (isset($host['pipeno']) && !empty($host['pipeno'])) {
2846
		$pipeup = $host['pipeno'];
2847
	} else {
2848
		$pipeup = captiveportal_get_next_dn_ruleno('pipe_mac');
2849
	}
2850

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

    
2855
	return array($pipeup, $pipedown);
2856
}
2857

    
2858
function captiveportal_allowedip_configure_entry($ipent) {
2859
	global $g, $config, $cpzone;
2860

    
2861
	$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
2862
	$tag = $cpzoneprefix . '_passthru';
2863

    
2864
	if (empty($ipent['sn'])) {
2865
		$ipent['sn'] = '32';
2866
	}
2867

    
2868
	$host = $ipent['ip'] . '_' . $ipent['sn'];
2869
	list($pipeup, $pipedown) = captiveportal_pipe_configure($ipent);
2870

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

    
2879
	captiveportal_load_pfctl("{$cpzoneprefix}_allowedhosts", $host, $rules);
2880
}
2881

    
2882
function captiveportal_allowedhostname_configure_entry($ipent, $hostnameid = 1) {
2883
	global $config, $cpzone;
2884

    
2885
	if (!isset($ipent['hostname'])) {
2886
		return;
2887
	}
2888

    
2889
	$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
2890
	$tag = $cpzoneprefix . '_passthru';
2891
	$table = $cpzoneprefix . '_hostname_' . $hostnameid;
2892
	$host = 'hostname_' . $hostnameid;
2893
	list($pipeup, $pipedown) = captiveportal_pipe_configure($ipent);
2894

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

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

    
2905
	/* return filterdns entry */
2906
	return "pf {$ipent['hostname']} {$table} {$cpzoneprefix}_allowedhosts/{$host}\n";
2907
}
2908

    
2909
function captiveportal_load_pfctl($anchor, $host, $rules) {
2910
	global $g, $cpzone;
2911

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

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

    
2923
	pfSense_pf_cp_zerocnt("{$cpzoneprefix}_{$anchor}/{$ip}");
2924
}
2925

    
2926
?>
(6-6/61)