Project

General

Profile

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

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

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

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

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

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

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

    
105
<head>
106

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

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

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

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

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

    
155

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

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

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

    
194
EOD;
195

    
196
	return $htmltext;
197
}
198

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
343
EOD;
344
		}
345

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

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

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

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

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

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

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

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

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

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

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

    
407
		/* 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
	captiveportal_pipes_delete($pipes_to_remove);
550
}
551

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

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

    
565
	$cpcfg = $config['captiveportal'][$cpzone];
566
	$vcpcfg = $config['voucher'][$cpzone];
567

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

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

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

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

    
593

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

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

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

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

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

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

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

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

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

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

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

    
797
	captiveportal_prune_old_automac();
798

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
966
	captiveportal_radius_stop_all($term_cause, $logoutReason);
967

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

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

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

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

    
988
	$cpdb = captiveportal_read_db();
989

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1118
	$cp_filterdns_filename = $g['varetc_path'] .
1119
	    "/filterdns-{$cpzone}-captiveportal.conf";
1120

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

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

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

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

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

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

    
1166
	return $time;
1167
}
1168

    
1169

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1365
	try {
1366
		$DB = new SQLite3($db_path);
1367
		$DB->busyTimeout(60000);
1368
	} catch (Exception $e) {
1369
		captiveportal_syslog("Could not open {$db_path} as an sqlite database for {$cpzone}. Error message: " . $e->getMessage() . " -- Trying again.");
1370
		unlink_if_exists($db_path);
1371
		try {
1372
			$DB = new SQLite3($db_path);
1373
			$DB->busyTimeout(60000);
1374
		} catch (Exception $e) {
1375
			captiveportal_syslog("Still could not open {$db_path} as an sqlite database for {$cpzone}. Error message: " . $e->getMessage() . " -- Remove the database file manually and ensure there is enough free space.");
1376
			return;
1377
		}
1378
	}
1379

    
1380
	if (!$DB) {
1381
		captiveportal_syslog("Could not open {$db_path} as an sqlite database for {$cpzone}. Error message: {$DB->lastErrorMsg()}. Trying again.");
1382
		unlink_if_exists($db_path);
1383
		$DB = new SQLite3($db_path);
1384
		$DB->busyTimeout(60000);
1385
		if (!$DB) {
1386
			captiveportal_syslog("Still could not open {$db_path} as an sqlite database for {$cpzone}. Error message: {$DB->lastErrorMsg()}. Remove the database file manually and ensure there is enough free space.");
1387
			return;
1388
		}
1389
	}
1390

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

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

    
1421
	return $DB;
1422
}
1423

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

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

    
1439
	return $cpdb;
1440
}
1441

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

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

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

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

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

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

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

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

    
1503
	$cpcfg = $config['captiveportal'][$cpzone];
1504

    
1505
	if (!is_dir($g['captiveportal_element_path'])) {
1506
		@mkdir($g['captiveportal_element_path']);
1507
	}
1508

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

    
1528
	return 0;
1529
}
1530

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

    
1535
	$removed_pipes = array();
1536

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

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

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

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

    
1575
	unset($rules);
1576

    
1577
	return $removed_pipes;
1578
}
1579

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

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

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

    
1597
	return $ruleno;
1598
}
1599

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

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

    
1632
	return $ruleno;
1633
}
1634

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

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

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

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

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

    
1662
	return $pipes;
1663
}
1664

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

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

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

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

    
1689
	$result = pfSense_pf_cp_get_eth_rule_counters("{$anchor}/{$ip}");
1690
	if (!empty($result)) {
1691
	   list($output_pkts, $output_bytes, $input_pkts, $input_bytes) = $result;
1692

    
1693
		if ($reverse) {
1694
			$volume['output_pkts'] = $input_pkts;
1695
			$volume['output_bytes'] = $input_bytes;
1696
			$volume['input_pkts'] = $output_pkts;
1697
			$volume['input_bytes'] = $output_bytes;
1698
		} else {
1699
			$volume['output_pkts'] = $output_pkts;
1700
			$volume['output_bytes'] = $output_bytes;
1701
			$volume['input_pkts'] = $input_pkts;
1702
			$volume['input_bytes'] = $input_bytes;
1703
		}
1704
	}
1705

    
1706
	return $volume;
1707
}
1708

    
1709
function portal_ip_from_client_ip($cliip) {
1710
	global $config, $cpzone;
1711

    
1712
	$isipv6 = is_ipaddrv6($cliip);
1713
	$interfaces = explode(",", $config['captiveportal'][$cpzone]['interface']);
1714
	foreach ($interfaces as $cpif) {
1715
		if ($isipv6) {
1716
			$ip = get_interface_ipv6($cpif);
1717
			$sn = get_interface_subnetv6($cpif);
1718
		} else {
1719
			$ip = get_interface_ip($cpif);
1720
			$sn = get_interface_subnet($cpif);
1721
		}
1722
		if (ip_in_subnet($cliip, "{$ip}/{$sn}")) {
1723
			return $ip;
1724
		}
1725
	}
1726

    
1727
	$route = route_get($cliip, 'inet', true);
1728
	if (empty($route)) {
1729
		return false;
1730
	}
1731

    
1732
	$iface = $route[0]['interface-name'];
1733
	if (!empty($iface)) {
1734
		$ip = ($isipv6) ? find_interface_ipv6($iface)
1735
		    : find_interface_ip($iface);
1736
		if (is_ipaddr($ip)) {
1737
			return $ip;
1738
		}
1739
	}
1740

    
1741
	// doesn't match up to any particular interface
1742
	// so let's set the portal IP to what PHP says
1743
	// the server IP issuing the request is.
1744
	// allows same behavior as 1.2.x where IP isn't
1745
	// in the subnet of any CP interface (static routes, etc.)
1746
	// rather than forcing to DNS hostname resolution
1747
	$ip = $_SERVER['SERVER_ADDR'];
1748
	if (is_ipaddr($ip)) {
1749
		return $ip;
1750
	}
1751

    
1752
	return false;
1753
}
1754

    
1755
function portal_hostname_from_client_ip($cliip) {
1756
	global $config, $cpzone;
1757

    
1758
	$cpcfg = $config['captiveportal'][$cpzone];
1759

    
1760
	if (isset($cpcfg['httpslogin'])) {
1761
		$listenporthttps = $cpcfg['listenporthttps'] ? $cpcfg['listenporthttps'] : ($cpcfg['zoneid'] + 8001);
1762
		$ourhostname = $cpcfg['httpsname'];
1763

    
1764
		if ($listenporthttps != 443) {
1765
			$ourhostname .= ":" . $listenporthttps;
1766
		}
1767
	} else {
1768
		$listenporthttp = $cpcfg['listenporthttp'] ? $cpcfg['listenporthttp'] : ($cpcfg['zoneid'] + 8000);
1769
		$ifip = portal_ip_from_client_ip($cliip);
1770
		if (!$ifip) {
1771
			$ourhostname = "{$config['system']['hostname']}.{$config['system']['domain']}";
1772
		} else {
1773
			$ourhostname = (is_ipaddrv6($ifip)) ? "[{$ifip}]" : "{$ifip}";
1774
		}
1775

    
1776
		if ($listenporthttp != 80) {
1777
			$ourhostname .= ":" . $listenporthttp;
1778
		}
1779
	}
1780

    
1781
	return $ourhostname;
1782
}
1783

    
1784
/* functions move from index.php */
1785

    
1786
function portal_reply_page($redirurl, $type = null, $message = null, $clientmac = null, $clientip = null, $username = null, $password = null, $voucher = null) {
1787
	global $g, $config, $cpzone;
1788

    
1789
	$cpcfg = $config['captiveportal'][$cpzone];
1790
	$ourhostname = portal_hostname_from_client_ip($clientip);
1791
	$protocol = (isset($cpcfg['httpslogin'])) ? 'https://' : 'http://';
1792
	$portal_url = "{$protocol}{$ourhostname}/index.php?zone={$cpzone}";
1793

    
1794
	/* Get captive portal layout */
1795
	if ($type == "redir") {
1796
		$redirurl = is_URL($redirurl, true) ? $redirurl : $portal_url;
1797
		header("Location: {$redirurl}");
1798
		return;
1799
	} else if ($type == "login") {
1800
		$htmltext = get_include_contents("{$g['varetc_path']}/captiveportal_{$cpzone}.html");
1801
	} else {
1802
		$htmltext = get_include_contents("{$g['varetc_path']}/captiveportal-{$cpzone}-error.html");
1803
	}
1804

    
1805
	/* substitute the PORTAL_REDIRURL variable */
1806
	if ($cpcfg['preauthurl']) {
1807
		$htmltext = str_replace("\$PORTAL_REDIRURL\$", "{$cpcfg['preauthurl']}", $htmltext);
1808
		$htmltext = str_replace("#PORTAL_REDIRURL#", "{$cpcfg['preauthurl']}", $htmltext);
1809
	}
1810

    
1811
	/* substitute other variables */
1812
	$htmltext = str_replace("\$PORTAL_ACTION\$", $portal_url, $htmltext);
1813
	$htmltext = str_replace("#PORTAL_ACTION#", $portal_url, $htmltext);
1814

    
1815
	$htmltext = str_replace("\$PORTAL_ZONE\$", htmlspecialchars($cpzone), $htmltext);
1816
	$htmltext = str_replace("\$PORTAL_REDIRURL\$", htmlspecialchars($redirurl), $htmltext);
1817
	$htmltext = str_replace("\$PORTAL_MESSAGE\$", htmlspecialchars($message), $htmltext);
1818
	$htmltext = str_replace("\$CLIENT_MAC\$", htmlspecialchars($clientmac), $htmltext);
1819
	$htmltext = str_replace("\$CLIENT_IP\$", htmlspecialchars($clientip), $htmltext);
1820

    
1821
	// Special handling case for captive portal master page so that it can be ran
1822
	// through the PHP interpreter using the include method above.  We convert the
1823
	// $VARIABLE$ case to #VARIABLE# in /etc/inc/captiveportal.inc before writing out.
1824
	$htmltext = str_replace("#PORTAL_ZONE#", htmlspecialchars($cpzone), $htmltext);
1825
	$htmltext = str_replace("#PORTAL_REDIRURL#", htmlspecialchars($redirurl), $htmltext);
1826
	$htmltext = str_replace("#PORTAL_MESSAGE#", htmlspecialchars($message), $htmltext);
1827
	$htmltext = str_replace("#CLIENT_MAC#", htmlspecialchars($clientmac), $htmltext);
1828
	$htmltext = str_replace("#CLIENT_IP#", htmlspecialchars($clientip), $htmltext);
1829
	$htmltext = str_replace("#USERNAME#", htmlspecialchars($username), $htmltext);
1830
	$htmltext = str_replace("#PASSWORD#", htmlspecialchars($password), $htmltext);
1831
	$htmltext = str_replace("#VOUCHER#", htmlspecialchars($voucher), $htmltext);
1832

    
1833
	echo $htmltext;
1834
}
1835

    
1836
function captiveportal_reapply_attributes($cpentry, $attributes) {
1837
	global $config, $cpzone, $g;
1838

    
1839
	if (isset($config['captiveportal'][$cpzone]['peruserbw'])) {
1840
		$dwfaultbw_up = !empty($config['captiveportal'][$cpzone]['bwdefaultup']) ? $config['captiveportal'][$cpzone]['bwdefaultup'] : 0;
1841
		$dwfaultbw_down = !empty($config['captiveportal'][$cpzone]['bwdefaultdn']) ? $config['captiveportal'][$cpzone]['bwdefaultdn'] : 0;
1842
	} else {
1843
		$dwfaultbw_up = $dwfaultbw_down = 0;
1844
	}
1845
	/* pipe throughputs must always be an integer, enforce that restriction again here. */
1846
	if (isset($config['captiveportal'][$cpzone]['radiusperuserbw'])) {
1847
		$bw_up = round(!empty($attributes['bw_up']) ? intval($attributes['bw_up'])/1000 : $dwfaultbw_up, 0);
1848
		$bw_down = round(!empty($attributes['bw_down']) ? intval($attributes['bw_down'])/1000 : $dwfaultbw_down, 0);
1849
	} else {
1850
		$bw_up = round($dwfaultbw_up,0);
1851
		$bw_down = round($dwfaultbw_down,0);
1852
	}
1853

    
1854
	$bw_up_pipeno = $cpentry[1];
1855
	$bw_down_pipeno = $cpentry[1]+1;
1856

    
1857
	if ($cpentry['bw_up'] !== $bw_up) {
1858
		$_gb = mwexec("/sbin/dnctl pipe {$bw_up_pipeno} config bw {$bw_up}Kbit/s queue 100 buckets 16");
1859
		captiveportal_update_entry($cpentry['sessionid'], $bw_up, 'bw_up');
1860
	}
1861
	if ($cpentry['bw_down'] !== $bw_down) {
1862
		$_gb = mwexec("/sbin/dnctl pipe {$bw_down_pipeno} config bw {$bw_down}Kbit/s queue 100 buckets 16");
1863
		captiveportal_update_entry($cpentry['sessionid'], $bw_down, 'bw_down');
1864
	}
1865
	unset($bw_up_pipeno, $bw_down_pipeno, $bw_up, $bw_down);
1866
}
1867

    
1868
function captiveportal_update_entry($sessionid, $new_value, $field_to_update) {
1869
	global $cpzone;
1870

    
1871
	if (!intval($new_value)) {
1872
		$new_value = "'{$new_value}'";
1873
	}
1874
	captiveportal_write_db("UPDATE captiveportal SET {$field_to_update} = {$new_value} WHERE sessionid = '{$sessionid}'");
1875
}
1876

    
1877
function portal_allow($clientip, $clientmac, $username, $password = null, $redirurl = null,
1878
    $attributes = null, $pipeno = null, $authmethod = null, $context = 'first', $existing_sessionid = null) {
1879
	global $g, $config, $cpzone;
1880

    
1881
	// Ensure we create an array if we are missing attributes
1882
	if (!is_array($attributes)) {
1883
		$attributes = array();
1884
	}
1885

    
1886
	unset($sessionid);
1887

    
1888
	/* Do not allow concurrent login execution. */
1889
	$cpdblck = lock("captiveportaldb{$cpzone}", LOCK_EX);
1890

    
1891
	if ($attributes['voucher']) {
1892
		$remaining_time = $attributes['session_timeout'];
1893
		$authmethod = "voucher"; // Set RADIUS-Attribute to Voucher to prevent ReAuth-Request for Vouchers Bug: #2155
1894
		$context = "voucher";
1895
	}
1896

    
1897
	$writecfg = false;
1898
	/* If both "Add MAC addresses of connected users as pass-through MAC" and "Disable concurrent logins" are checked,
1899
	then we need to check if the user was already authenticated using another MAC Address, and if so remove the previous Pass-Through MAC. */
1900
	if ((isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) && ($config['captiveportal'][$cpzone]['noconcurrentlogins'] == 'last') && ($username != 'unauthenticated') && isset($config['captiveportal'][$cpzone]['passthrumacadd'])) {
1901
		$mac = captiveportal_passthrumac_findbyname($username);
1902
		if (!empty($mac)) {
1903
			foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $idx => $macent) {
1904
				if ($macent['mac'] != $mac['mac']) {
1905
					continue;
1906
				}
1907

    
1908
				captiveportal_passthrumac_delete_entry($macent);
1909
				unset($config['captiveportal'][$cpzone]['passthrumac'][$idx]);
1910
			}
1911
		}
1912
	}
1913

    
1914
	/* read in client database */
1915
	$query = "WHERE ip = '{$clientip}'";
1916
	$tmpusername = SQLite3::escapeString(strtolower($username));
1917
	if (isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) {
1918
		$query .= " OR (username != 'unauthenticated' AND lower(username) = '{$tmpusername}')";
1919
	}
1920
	$cpdb = captiveportal_read_db($query);
1921

    
1922
	/* Snapshot the timestamp */
1923
	$allow_time = time();
1924

    
1925
	if ($existing_sessionid !== null) {
1926
		// If we recieved this connection through XMLRPC sync :
1927
		// we fetch allow_time from the info given by the other node
1928
		$allow_time = $attributes['allow_time'];
1929
	}
1930
	$unsetindexes = array();
1931

    
1932
	foreach ($cpdb as $cpentry) {
1933
		/* on the same ip */
1934
		if ($cpentry[2] == $clientip) {
1935
			if (isset($config['captiveportal'][$cpzone]['nomacfilter']) || $cpentry[3] == $clientmac) {
1936
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT LOGIN - REUSING OLD SESSION");
1937
			} else {
1938
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT LOGIN - REUSING IP {$cpentry[2]} WITH DIFFERENT MAC ADDRESS {$cpentry[3]}");
1939
			}
1940
			$sessionid = $cpentry[5];
1941
			break;
1942
		} elseif (($attributes['voucher']) && ($username != 'unauthenticated') && ($cpentry[4] == $username)) {
1943
			// user logged in with an active voucher. Check for how long and calculate
1944
			// how much time we can give him (voucher credit - used time)
1945
			$remaining_time = $cpentry[0] + $cpentry[7] - $allow_time;
1946
			if ($remaining_time < 0) { // just in case.
1947
				$remaining_time = 0;
1948
			}
1949

    
1950
			/* This user was already logged in so we disconnect the old one, or 
1951
			keep the old one, refusing the new login, or
1952
			allow the login */
1953

    
1954
			if (!isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) {
1955
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "config['captiveportal'][$cpzone]['noconcurrentlogins'] 2 does not exists = NOT set");		
1956
			} else {
1957
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "config['captiveportal'][$cpzone]['noconcurrentlogins'] 2 exists = set");		
1958
			}
1959

    
1960
			if ($config['captiveportal'][$cpzone]['noconcurrentlogins'] == "last") {
1961
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "Found last");
1962
			} else {
1963
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "Found NOT last");
1964
			}
1965
				
1966
			if (!isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) {
1967
				/* 'noconcurrentlogins' not set : accept login 'username' creating multiple sessions. */
1968
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "config['captiveportal'][$cpzone]['noconcurrentlogins'] 3 does not exists = NOT set");
1969
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT LOGIN - NOT TERMINATING EXISTING SESSION(S)");			
1970
			} elseif ($config['captiveportal'][$cpzone]['noconcurrentlogins'] == "last") {
1971
				/* Classic situation : accept the new login, disconnect the old - present - connection */
1972
				if (isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) {
1973
					captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "config['captiveportal'][$cpzone]['noconcurrentlogins'] 4 exists = set");		
1974
				} else {
1975
					captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "config['captiveportal'][$cpzone]['noconcurrentlogins'] 4 does not exists = NOT set");		
1976
				}
1977
				
1978
				captiveportal_disconnect($cpentry, 13);
1979
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT LOGIN - TERMINATING OLD SESSION");
1980
				$unsetindexes[] = $cpentry[5];
1981
				break;
1982
			} else {
1983
				/* Implicit 'first' : refuse the new login - 'username' is already logged in */
1984
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT VOUCHER LOGIN - NOT ALLOWED KEEPING OLD SESSION ");
1985
				unlock($cpdblck);
1986
				return 2;
1987
			}
1988
		} elseif ((isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) && ($username != 'unauthenticated')) {
1989
			if ($config['captiveportal'][$cpzone]['noconcurrentlogins'] == "last") {
1990
				/* on the same username */
1991
				if (strcasecmp($cpentry[4], $username) == 0) {
1992
					/* This user was already logged in so we disconnect the old one */
1993
					captiveportal_disconnect($cpentry, 13);
1994
					captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT USER LOGIN - TERMINATING OLD SESSION");
1995
					$unsetindexes[] = $cpentry[5];
1996
					break;
1997
				}
1998
			} else {
1999
				/* Implicit 'first' : refuse the new login - 'username' is already logged in */
2000
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT USER LOGIN - NOT ALLOWED KEEPING OLD SESSION ");
2001
				unlock($cpdblck);
2002
				return 2;				
2003
			}
2004
		}
2005
	}
2006
	unset($cpdb);
2007

    
2008
	if (!empty($unsetindexes)) {
2009
		captiveportal_remove_entries($unsetindexes);
2010
	}
2011

    
2012
	if ($attributes['voucher'] && $remaining_time <= 0) {
2013
		return 0;       // voucher already used and no time left
2014
	}
2015

    
2016
	if (!isset($sessionid)) {
2017
		if ($existing_sessionid != null) { // existing_sessionid should only be set during XMLRPC sync
2018
			$sessionid = $existing_sessionid;
2019
		} else {
2020
			/* generate unique session ID */
2021
			$tod = gettimeofday();
2022
			$sessionid = substr(md5(mt_rand() . $tod['sec'] . $tod['usec'] . $clientip . $clientmac), 0, 16);
2023
		}
2024

    
2025
		if (isset($config['captiveportal'][$cpzone]['peruserbw'])) {
2026
			$dwfaultbw_up = !empty($config['captiveportal'][$cpzone]['bwdefaultup']) ? $config['captiveportal'][$cpzone]['bwdefaultup'] : 0;
2027
			$dwfaultbw_down = !empty($config['captiveportal'][$cpzone]['bwdefaultdn']) ? $config['captiveportal'][$cpzone]['bwdefaultdn'] : 0;
2028
		} else {
2029
			$dwfaultbw_up = $dwfaultbw_down = 0;
2030
		}
2031
		/* pipe throughputs must always be an integer, enforce that restriction again here. */
2032
		if (isset($config['captiveportal'][$cpzone]['radiusperuserbw'])) {
2033
			$bw_up = round(!empty($attributes['bw_up']) ? intval($attributes['bw_up'])/1000 : $dwfaultbw_up, 0);
2034
			$bw_down = round(!empty($attributes['bw_down']) ? intval($attributes['bw_down'])/1000 : $dwfaultbw_down, 0);
2035
		} else {
2036
			$bw_up = round($dwfaultbw_up,0);
2037
			$bw_down = round($dwfaultbw_down,0);
2038
		}
2039

    
2040
		$mac = array();
2041
		$mac['action'] = 'pass';
2042
		$mac['ip'] = $clientip;
2043
		$mac['username'] = $username;
2044
		if (isset($config['captiveportal'][$cpzone]['passthrumacadd'])) {
2045
			$mac['mac'] = $clientmac;
2046
			if ($attributes['voucher']) {
2047
				$mac['logintype'] = "voucher";
2048
			}
2049
			if ($username == "unauthenticated") {
2050
				$mac['descr'] = "Auto-added";
2051
			} else if ($authmethod == "voucher") {
2052
				$mac['descr'] = "Auto-added for voucher {$username}";
2053
			} else {
2054
				$mac['descr'] = "Auto-added for user {$username}";
2055
			}
2056
			if (!empty($bw_up)) {
2057
				$mac['bw_up'] = $bw_up;
2058
			}
2059
			if (!empty($bw_down)) {
2060
				$mac['bw_down'] = $bw_down;
2061
			}
2062
			if (!is_array($config['captiveportal'][$cpzone]['passthrumac'])) {
2063
				$config['captiveportal'][$cpzone]['passthrumac'] = array();
2064
			}
2065
			//check for mac duplicates before adding it to config.
2066
			$mac_duplicate = false;
2067
			foreach($config['captiveportal'][$cpzone]['passthrumac'] as $mac_check){
2068
				if($mac_check['mac'] == $mac['mac']){
2069
					$mac_duplicate = true;
2070
				}
2071
			}
2072
			if(!$mac_duplicate){
2073
				$config['captiveportal'][$cpzone]['passthrumac'][] = $mac;
2074
			}
2075
			unlock($cpdblck);
2076
			captiveportal_ether_configure_entry($mac, 'passthrumac', true);
2077
			$writecfg = true;
2078
		} else {
2079
			/* See if a pipeno is passed, if not start sessions because this means there isn't one atm */
2080
			if (is_null($pipeno)) {
2081
				$pipeno = captiveportal_get_next_dn_ruleno('auth');
2082
			}
2083
			/* if the pool is empty, return appropriate message and exit */
2084
			if (is_null($pipeno)) {
2085
				captiveportal_syslog("Zone: {$cpzone} - WARNING!  Captive portal has reached maximum login capacity");
2086
				unlock($cpdblck);
2087
				return false;
2088
			}
2089

    
2090
			$mac['pipeno'] = $pipeno;
2091
			$mac['ip'] = $clientip;
2092
			if (!isset($config['captiveportal'][$cpzone]['nomacfilter'])) {
2093
				$mac['mac'] = $clientmac;
2094
			}
2095
			captiveportal_ether_configure_entry($mac, 'auth', true);
2096

    
2097
			if ($attributes['voucher']) {
2098
				$attributes['session_timeout'] = $remaining_time;
2099
			}
2100

    
2101
			/* handle empty attributes */
2102
			$session_timeout = (!empty($attributes['session_timeout'])) ? $attributes['session_timeout'] : 'NULL';
2103
			$idle_timeout = (!empty($attributes['idle_timeout'])) ? $attributes['idle_timeout'] : 'NULL';
2104
			$session_terminate_time = (!empty($attributes['session_terminate_time'])) ? $attributes['session_terminate_time'] : 'NULL';
2105
			$interim_interval = (!empty($attributes['interim_interval'])) ? $attributes['interim_interval'] : 'NULL';
2106
			$traffic_quota = (!empty($attributes['maxbytes'])) ? $attributes['maxbytes'] : 'NULL';
2107

    
2108
			/* escape username */
2109
			$safe_username = SQLite3::escapeString($username);
2110

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

    
2117
			/* store information to database */
2118
			captiveportal_write_db($insertquery);
2119
			unlock($cpdblck);
2120
			unset($insertquery, $bpassword);
2121

    
2122
			$radacct = isset($config['captiveportal'][$cpzone]['radacct_enable']) ? true : false;
2123
			if ($authmethod === 'radius' && $radacct) {
2124
				captiveportal_send_server_accounting('start',
2125
					$pipeno, // ruleno
2126
					$username, // username
2127
					$clientip, // clientip
2128
					$clientmac, // clientmac
2129
					$sessionid, // sessionid
2130
					time());  // start time
2131
			}
2132
			if (captiveportal_xmlrpc_sync_get_details($syncip, $syncport, $syncuser, $syncpass, isset($existing_sessionid))) {
2133
				// $existing_sessionid prevent carp loop : only forward
2134
				// the connection to the other node if we generated the sessionid by ourselves
2135
				$rpc_client = new pfsense_xmlrpc_client();
2136
				$rpc_client->setConnectionData($syncip, $syncport, $syncuser, $syncpass);
2137
				$rpc_client->set_noticefile("CaptivePortalUserSync");
2138
				$arguments = array(
2139
					'clientip' => $clientip,
2140
					'clientmac' => $clientmac,
2141
					'username' => $username,
2142
					'password' => $password,
2143
					'attributes' => $attributes,
2144
					'allow_time' => $allow_time,
2145
					'authmethod' => $authmethod,
2146
					'context' => $context,
2147
					'sessionid' => $sessionid
2148
				);
2149

    
2150
				$rpc_client->xmlrpc_method('captive_portal_sync',
2151
					array(
2152
						'op' => 'connect_user',
2153
						'zone' => $cpzone,
2154
						'user' => base64_encode(serialize($arguments))
2155
					)
2156
				);
2157
			}
2158
		}
2159
	} else {
2160
		/* NOTE: #3062-11 If the pipeno has been allocated free it to not DoS the CP */
2161
		if (!is_null($pipeno)) {
2162
			captiveportal_free_dn_rulenos(array($pipeno, $pipeno+1));
2163
		}
2164

    
2165
		unlock($cpdblck);
2166
	}
2167

    
2168
	if ($writecfg == true) {
2169
		write_config(gettext("Captive Portal allowed users configuration changed"));
2170
	}
2171

    
2172
	if ($existing_sessionid !== null) {
2173
		if (!empty($sessionid)) {
2174
			return $sessionid;
2175
		} else {
2176
			return false;
2177
		}
2178
	}
2179
	/* redirect user to desired destination */
2180
	if (is_URL($attributes['url_redirection'], true)) {
2181
		$my_redirurl = $attributes['url_redirection'];
2182
	} else if (is_URL($config['captiveportal'][$cpzone]['redirurl'], true)) {
2183
		$my_redirurl = $config['captiveportal'][$cpzone]['redirurl'];
2184
	} else if (is_URL($redirurl, true)) {
2185
		$my_redirurl = $redirurl;
2186
	}
2187

    
2188
	if (isset($config['captiveportal'][$cpzone]['logoutwin_enable']) && !isset($config['captiveportal'][$cpzone]['passthrumacadd'])) {
2189
		$ourhostname = portal_hostname_from_client_ip($clientip);
2190
		$protocol = (isset($config['captiveportal'][$cpzone]['httpslogin'])) ? 'https://' : 'http://';
2191
		$logouturl = "{$protocol}{$ourhostname}/";
2192

    
2193
		if (isset($attributes['reply_message'])) {
2194
			$message = $attributes['reply_message'];
2195
		} else {
2196
			$message = 0;
2197
		}
2198

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

    
2201
	} else {
2202
		portal_reply_page($my_redirurl, "redir", "Just redirect the user.");
2203
	}
2204

    
2205
	return $sessionid;
2206
}
2207

    
2208

    
2209
/*
2210
 * Used for when pass-through credits are enabled.
2211
 * Returns true when there was at least one free login to deduct for the MAC.
2212
 * Expired entries are removed as they are seen.
2213
 * Active entries are updated according to the configuration.
2214
 */
2215
function portal_consume_passthrough_credit($clientmac) {
2216
	global $config, $cpzone;
2217

    
2218
	if (!empty($config['captiveportal'][$cpzone]['freelogins_count']) && is_numeric($config['captiveportal'][$cpzone]['freelogins_count'])) {
2219
		$freeloginscount = $config['captiveportal'][$cpzone]['freelogins_count'];
2220
	} else {
2221
		return false;
2222
	}
2223

    
2224
	if (!empty($config['captiveportal'][$cpzone]['freelogins_resettimeout']) && is_numeric($config['captiveportal'][$cpzone]['freelogins_resettimeout'])) {
2225
		$resettimeout = $config['captiveportal'][$cpzone]['freelogins_resettimeout'];
2226
	} else {
2227
		return false;
2228
	}
2229

    
2230
	if ($freeloginscount < 1 || $resettimeout <= 0 || !$clientmac) {
2231
		return false;
2232
	}
2233

    
2234
	$updatetimeouts = isset($config['captiveportal'][$cpzone]['freelogins_updatetimeouts']);
2235

    
2236
	/*
2237
	 * Read database of used MACs.  Lines are a comma-separated list
2238
	 * of the time, MAC, then the count of pass-through credits remaining.
2239
	 */
2240
	$usedmacs = captiveportal_read_usedmacs_db();
2241

    
2242
	$currenttime = time();
2243
	$found = false;
2244
	foreach ($usedmacs as $key => $usedmac) {
2245
		$usedmac = explode(",", $usedmac);
2246

    
2247
		if ($usedmac[1] == $clientmac) {
2248
			if ($usedmac[0] + ($resettimeout * 3600) > $currenttime) {
2249
				if ($usedmac[2] < 1) {
2250
					if ($updatetimeouts) {
2251
						$usedmac[0] = $currenttime;
2252
						unset($usedmacs[$key]);
2253
						$usedmacs[] = implode(",", $usedmac);
2254
						captiveportal_write_usedmacs_db($usedmacs);
2255
						xmlrpc_sync_usedmacs($usedmacs);
2256
					}
2257

    
2258
					return false;
2259
				} else {
2260
					$usedmac[2] -= 1;
2261
					$usedmacs[$key] = implode(",", $usedmac);
2262
				}
2263

    
2264
				$found = true;
2265
			} else {
2266
				unset($usedmacs[$key]);
2267
			}
2268

    
2269
			break;
2270
		} else if ($usedmac[0] + ($resettimeout * 3600) <= $currenttime) {
2271
			unset($usedmacs[$key]);
2272
		}
2273
	}
2274

    
2275
	if (!$found) {
2276
		$usedmac = array($currenttime, $clientmac, $freeloginscount - 1);
2277
		$usedmacs[] = implode(",", $usedmac);
2278
	}
2279

    
2280
	captiveportal_write_usedmacs_db($usedmacs);
2281
	xmlrpc_sync_usedmacs($usedmacs);
2282
	return true;
2283
}
2284

    
2285
function xmlrpc_sync_usedmacs($usedmacs) { 
2286
	global $config, $cpzone;
2287

    
2288
	// XMLRPC Call over to the other node
2289
	if (captiveportal_xmlrpc_sync_get_details($syncip, $syncport,
2290
	    $syncuser, $syncpass, $carp_loop)) {
2291
		$rpc_client = new pfsense_xmlrpc_client();
2292
		$rpc_client->setConnectionData($syncip, $syncport, $syncuser, $syncpass);
2293
		$rpc_client->set_noticefile("CaptivePortalUsedmacsSync");
2294
		$arguments = array(
2295
			'usedmacs' => $usedmacs
2296
		);
2297

    
2298
		$rpc_client->xmlrpc_method('captive_portal_sync',
2299
			array(
2300
				'op' => 'write_usedmacs',
2301
				'zone' => $cpzone,
2302
				'arguments' => base64_encode(serialize($arguments))
2303
			)
2304
		);
2305
	}
2306
}
2307

    
2308
function captiveportal_read_usedmacs_db() {
2309
	global $g, $cpzone;
2310

    
2311
	$cpumaclck = lock("captiveusedmacs{$cpzone}");
2312
	if (file_exists("{$g['vardb_path']}/captiveportal_usedmacs_{$cpzone}.db")) {
2313
		$usedmacs = file("{$g['vardb_path']}/captiveportal_usedmacs_{$cpzone}.db", FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
2314
		if (!$usedmacs) {
2315
			$usedmacs = array();
2316
		}
2317
	} else {
2318
		$usedmacs = array();
2319
	}
2320

    
2321
	unlock($cpumaclck);
2322
	return $usedmacs;
2323
}
2324

    
2325
function captiveportal_write_usedmacs_db($usedmacs) {
2326
	global $g, $cpzone;
2327

    
2328
	$cpumaclck = lock("captiveusedmacs{$cpzone}", LOCK_EX);
2329
	@file_put_contents("{$g['vardb_path']}/captiveportal_usedmacs_{$cpzone}.db", implode("\n", $usedmacs));
2330
	unlock($cpumaclck);
2331
}
2332

    
2333
function captiveportal_blocked_mac($mac) {
2334
	global $config, $g, $cpzone;
2335

    
2336
	if (empty($mac) || !is_macaddr($mac)) {
2337
		return false;
2338
	}
2339

    
2340
	if (!is_array($config['captiveportal'][$cpzone]['passthrumac'])) {
2341
		return false;
2342
	}
2343

    
2344
	foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $passthrumac) {
2345
		if (($passthrumac['action'] == 'block') &&
2346
		    ($passthrumac['mac'] == strtolower($mac))) {
2347
			return true;
2348
		}
2349
	}
2350

    
2351
	return false;
2352

    
2353
}
2354

    
2355
/* Captiveportal Radius Accounting */
2356

    
2357
function gigawords($bytes) {
2358

    
2359
	/*
2360
	 * RFC2866 Specifies a 32bit unsigned integer, which is a max of 4294967295
2361
	 * Currently there is a fault in the PECL radius_put_int function which can handle only 32bit signed integer.
2362
	 */
2363

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

    
2367
	// We need to manually set this to a zero instead of NULL for put_int() safety
2368
	if (is_null($gigawords)) {
2369
		$gigawords = 0;
2370
	}
2371

    
2372
	return $gigawords;
2373
}
2374

    
2375
function remainder($bytes) {
2376
	// Calculate the bytes we are going to send to the radius
2377
	$bytes = bcmod($bytes, GIGAWORDS_RIGHT_OPERAND);
2378

    
2379
	if (is_null($bytes)) {
2380
		$bytes = 0;
2381
	}
2382

    
2383
    return $bytes;
2384
}
2385

    
2386
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) {
2387
	global $cpzone, $config;
2388

    
2389
	$cpcfg = $config['captiveportal'][$cpzone];
2390
	$acctcfg = auth_get_authserver($cpcfg['radacct_server']);
2391

    
2392
	if (!isset($cpcfg['radacct_enable']) || empty($acctcfg) ||
2393
	    captiveportal_ha_is_node_in_backup_mode($cpzone)) {
2394
		return null;
2395
	}
2396

    
2397
	if ($type === 'on') {
2398
		$racct = new Auth_RADIUS_Acct_On;
2399
	} elseif ($type === 'off') {
2400
		$racct = new Auth_RADIUS_Acct_Off;
2401
	} elseif ($type === 'start') {
2402
		$racct = new Auth_RADIUS_Acct_Start;
2403
		if (!is_int($start_time)) {
2404
			$start_time = time();
2405
		}
2406
	} elseif ($type === 'stop') {
2407
		$racct = new Auth_RADIUS_Acct_Stop;
2408
		if (!is_int($stop_time)) {
2409
			$stop_time = time();
2410
		}
2411
	} elseif ($type === 'update') {
2412
        $racct = new Auth_RADIUS_Acct_Update;
2413
		if (!is_int($stop_time)) {
2414
			$stop_time = time(); // "top time" here will be used only for calculating session time.
2415
		}
2416
	} else {
2417
		return null;
2418
	}
2419

    
2420
	$racct->addServer($acctcfg['host'], $acctcfg['radius_acct_port'],
2421
		$acctcfg['radius_secret'], $acctcfg['radius_timeout']);
2422

    
2423
	$racct->authentic = RADIUS_AUTH_RADIUS;
2424
	if ($cpcfg['auth_method'] === 'radmac' && $username === "unauthenticated" && !empty($clientmac)) {
2425
		$racct->username = mac_format($clientmac);
2426
	} elseif (!empty($username)) {
2427
		$racct->username = $username;
2428
	}
2429

    
2430
	if (PEAR::isError($racct->start())) {
2431
		captiveportal_syslog('RADIUS ACCOUNTING FAILED : '.$racct->getError());
2432
		$racct->close();
2433
		return null;
2434
	}
2435

    
2436
	$nasip = nasip_fallback($acctcfg['radius_nasip_attribute']);
2437
	$nasmac = get_interface_mac(find_ip_interface($nasip));
2438
	$racct->putAttribute(RADIUS_NAS_IP_ADDRESS, $nasip, "addr");
2439

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

    
2442
	if (is_int($ruleno)) {
2443
		$racct->putAttribute(RADIUS_NAS_PORT_TYPE, RADIUS_ETHERNET);
2444
		$racct->putAttribute(RADIUS_NAS_PORT, intval($ruleno), 'integer');
2445
	}
2446

    
2447
	if (!empty($sessionid)) {
2448
		$racct->putAttribute(RADIUS_ACCT_SESSION_ID, $sessionid);
2449
	}
2450

    
2451
	if (!empty($clientip) && is_ipaddr($clientip)) {
2452
		$racct->putAttribute(RADIUS_FRAMED_IP_ADDRESS, $clientip, "addr");
2453
	}
2454
	if (!empty($clientmac)) {
2455
		$racct->putAttribute(RADIUS_CALLING_STATION_ID, mac_format($clientmac));
2456
	}
2457
	if (!empty($nasmac)) {
2458
		$racct->putAttribute(RADIUS_CALLED_STATION_ID, mac_format($nasmac).':'.gethostname());
2459
	}
2460

    
2461
	// Accounting request Stop and Update : send the current data volume
2462
	if (($type === 'stop' || $type === 'update') && is_int($start_time)) {
2463
		$volume = getVolume($clientip);
2464
		$session_time = $stop_time - $start_time;
2465
		$volume['input_bytes_radius'] = remainder($volume['input_bytes']);
2466
		$volume['input_gigawords'] = gigawords($volume['input_bytes']);
2467
		$volume['output_bytes_radius'] = remainder($volume['output_bytes']);
2468
		$volume['output_gigawords'] = gigawords($volume['output_bytes']);
2469

    
2470
		// Volume stuff: Ingress
2471
		$racct->putAttribute(RADIUS_ACCT_INPUT_PACKETS, intval($volume['input_pkts']), "integer");
2472
		$racct->putAttribute(RADIUS_ACCT_INPUT_OCTETS, intval($volume['input_bytes_radius']), "integer");
2473
		// Volume stuff: Outgress
2474
		$racct->putAttribute(RADIUS_ACCT_OUTPUT_PACKETS, intval($volume['output_pkts']), "integer");
2475
		$racct->putAttribute(RADIUS_ACCT_OUTPUT_OCTETS, intval($volume['output_bytes_radius']), "integer");
2476
		$racct->putAttribute(RADIUS_ACCT_SESSION_TIME, intval($session_time), "integer");
2477

    
2478
		$racct->putAttribute(CUSTOM_RADIUS_ACCT_OUTPUT_GIGAWORDS, intval($volume['output_gigawords']), "integer");
2479
		$racct->putAttribute(CUSTOM_RADIUS_ACCT_INPUT_GIGAWORDS, intval($volume['input_gigawords']), "integer");
2480
		// Set session_time
2481
		$racct->session_time = $session_time;
2482
	}
2483

    
2484
	if ($type === 'stop') {
2485
		if (empty($term_cause)) {
2486
			$term_cause = 1;
2487
		}
2488
		$racct->putAttribute(RADIUS_ACCT_TERMINATE_CAUSE, $term_cause);
2489
	}
2490

    
2491
	// Send request
2492
	$result = $racct->send();
2493

    
2494
	if (PEAR::isError($result)) {
2495
		 captiveportal_syslog('RADIUS ACCOUNTING FAILED : '.$racct->getError());
2496
		 $result = null;
2497
	} elseif ($result !== true) {
2498
		$result = false;
2499
	}
2500

    
2501
	$racct->close();
2502
	return $result;
2503
}
2504

    
2505
function captiveportal_isip_logged($clientip) {
2506
	global $g, $cpzone;
2507

    
2508
	/* read in client database */
2509
	$query = "WHERE ip = '{$clientip}'";
2510
	$cpdb = captiveportal_read_db($query);
2511
	foreach ($cpdb as $cpentry) {
2512
		return $cpentry;
2513
	}
2514
}
2515

    
2516
function captiveportal_allowedhostname_cleanup() {
2517
	global $g, $config, $cpzone;
2518

    
2519
	$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
2520

    
2521
	init_config_arr(array('captiveportal', $cpzone, 'allowedhostname'));
2522
	foreach ($config['captiveportal'][$cpzone]['allowedhostname'] as $id => $hostnameent) {
2523
		$pipes = pfSense_pf_cp_get_eth_pipes("{$cpzoneprefix}_allowedhosts/hostname_{$id}");
2524
		pfSense_pf_cp_flush("{$cpzoneprefix}_allowedhosts/hostname_{$id}", "ether");
2525
		if (!empty($pipes)) {
2526
			captiveportal_pipes_delete($pipes);
2527
		}
2528
	}
2529
}
2530

    
2531
function filter_captiveportal_aliases() {
2532
	/* return all aliases used in captive portal zones, 
2533
	 * to prevent it from deletion in filter_configure_sync() as unused aliases */
2534
	global $g, $config;
2535

    
2536
	$aliasesnames = array();
2537

    
2538
	init_config_arr(array('captiveportal'));
2539
	foreach ($config['captiveportal'] as $cpzone => $cpcfg) {
2540
		if (isset($cpcfg['enable'])) {
2541
			$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
2542
			$aliasesnames[] = $cpzoneprefix . '_cpips';
2543
			init_config_arr(array('captiveportal', $cpzone, 'allowedhostname'));
2544
			foreach ($config['captiveportal'][$cpzone]['allowedhostname'] as $id => $hostnameent) {
2545
				$aliasesnames[] = $cpzoneprefix . '_hostname_' . $id;
2546
			}
2547
		}
2548
	}
2549

    
2550
	return $aliasesnames;
2551
}
2552

    
2553
function filter_captiveportal_tables() {
2554
	/* return pf rules which defines tables used in captive portal zones */
2555
	global $g, $config, $FilterIflist;
2556

    
2557
	$rules = '';
2558
	init_config_arr(array('captiveportal'));
2559
	foreach ($config['captiveportal'] as $cpzone => $cpcfg) {
2560
		if (!isset($cpcfg['enable'])) {
2561
			continue;
2562
		}
2563

    
2564
		$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
2565
		$cpips = $cpzoneprefix . '_cpips';
2566
		$hosttable = $cpzoneprefix . '_host_';
2567
		$cpiplist = '';
2568

    
2569
		foreach (explode(",", $cpcfg['interface']) as $cpifgrp) {
2570
			if (isset($FilterIflist[$cpifgrp])) {
2571
				$realif = get_real_interface($cpifgrp);
2572
				if (!empty($realif)) {
2573
					$cpip = get_interface_ip($cpifgrp);
2574
					if (is_ipaddrv4($cpip)) {
2575
						$cpiplist = $cpip . ' ';
2576
						$cpiplist .= get_interface_vip_ips($cpifgrp);
2577
					}
2578
				}
2579
			}
2580
		}
2581
		if (!empty($cpiplist)) {
2582
			/* captive portal web server IP addresses */
2583
			$rules .= "table <{$cpips}> { {$cpiplist} }\n";
2584
		}
2585
	}
2586

    
2587
	if (!empty($rules)) {
2588
		$rules = "\n# Captive Portal\n" . $rules . "\n";
2589
	}
2590

    
2591
	return $rules;
2592
}
2593

    
2594
function filter_captiveportal_ether() {
2595
	global $g, $config;
2596

    
2597
	$rules = '';
2598
	init_config_arr(array('captiveportal'));
2599
	foreach ($config['captiveportal'] as $cpzone => $cpcfg) {
2600
		if (!isset($cpcfg['enable'])) {
2601
			continue;
2602
		}
2603

    
2604
		$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
2605
		$rdrtag = $cpzoneprefix . '_rdr';
2606
		$interfaces = captiveportal_zone_interfaces($cpcfg);
2607

    
2608
		if (!empty($interfaces)) {
2609
			/* set 'rdr' tag for futher captive portal web portal redirection */
2610
			$rules .= "ether pass on { {$interfaces} } tag \"{$rdrtag}\"\n";
2611
			/* anchor to set the PASS tag for authenticated clients */
2612
			$rules .= "ether anchor \"{$cpzoneprefix}_auth/*\" on { {$interfaces} }\n";
2613
			/* anchor for Services / Captive Portal / CPZONE / MACs */
2614
			$rules .= "ether anchor \"{$cpzoneprefix}_passthrumac/*\" on { {$interfaces} }\n";
2615
			/* anchor to set the PASSTHRU tag for Allowed IP/Hostnames */
2616
			$rules .= "ether anchor \"{$cpzoneprefix}_allowedhosts/*\" on { {$interfaces} }\n";
2617
		}
2618
	}
2619

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

    
2624
	return $rules;
2625
}
2626

    
2627
function filter_captiveportal_rdr() {
2628
	global $g, $config, $FilterIflist;
2629

    
2630
	$rules = '';
2631
	foreach ($config['captiveportal'] as $cpzone => $cpcfg) {
2632
		if (!isset($cpcfg['enable'])) {
2633
			continue;
2634
		}
2635

    
2636
		$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
2637
		$rdrtag = $cpzoneprefix . '_rdr';
2638
		$cpips = $cpzoneprefix . '_cpips';
2639
		$rdr_ports = captiveportal_zone_portalports($cpcfg);
2640
		foreach (explode(",", $cpcfg['interface']) as $cpifgrp) {
2641
			if (isset($FilterIflist[$cpifgrp])) {
2642
				$realif = get_real_interface($cpifgrp);
2643
				if (!empty($realif)) {
2644
					$cpip = get_interface_ip($cpifgrp);
2645
					if (is_ipaddrv4($cpip)) {
2646
						foreach ($rdr_ports as list($portalias, $cprdrport)) {
2647
							$rules .= "rdr on {$realif} inet proto tcp from any to ! <{$cpips}> port {$cprdrport} tagged {$rdrtag} -> {$cpip} port {$portalias}\n";
2648
						}
2649
					}
2650
				}
2651
			}
2652
		}
2653
	}
2654

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

    
2659
	return $rules;
2660
}
2661

    
2662
function filter_captiveportal_pass() {
2663
	global $g, $config, $FilterIflist;
2664

    
2665
	$captiveportal_increment = 'filter_captiveportal_tracker';
2666

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

    
2673
		$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
2674
		$cpips = $cpzoneprefix . '_cpips';
2675
		$authtag = $cpzoneprefix . '_auth';
2676
		$rdr_ports = captiveportal_zone_portalports($cpcfg);
2677

    
2678
		foreach (explode(",", $cpcfg['interface']) as $cpifgrp) {
2679
			if (!isset($FilterIflist[$cpifgrp])) {
2680
				continue;
2681
			}
2682
			$realif = get_real_interface($cpifgrp);
2683
			if (!empty($realif)) {
2684
				$cpip = get_interface_ip($cpifgrp);
2685
				if (is_ipaddrv4($cpip)) {
2686
					foreach ($rdr_ports as list($portalias, $cprdrport)) {						/* pass non-authenticated clients to captive portal */
2687
						$rules .= "pass in quick on {$realif} proto tcp from any to <{$cpips}> port {$portalias} ridentifier {$captiveportal_increment()} keep state(sloppy)\n";
2688
						/* without this rule captive portal doesn't show login page after manual disconnect */
2689
						$rules .= "pass out quick on {$realif} proto tcp from {$cpip} port {$portalias} to any flags any ridentifier {$captiveportal_increment()} keep state(sloppy)\n";
2690
					}
2691
					/* block non-authenticated clients access to internet */
2692
					$rules .= "block in quick on {$realif} from any to ! <{$cpips}> ! tagged {$authtag} ridentifier {$captiveportal_increment()}\n";
2693
				}
2694
			}
2695
		}
2696
	}
2697

    
2698
	if (!empty($rules)) {
2699
		$rules = "\n# Captive Portal\n" . $rules . "\n";
2700
	}
2701

    
2702
	return $rules;
2703
}
2704

    
2705
function captiveportal_zone_interfaces($cpcfg) {
2706
	/* return a list of captive portal zone interfaces */
2707
	global $FilterIflist;
2708

    
2709
	$interfaces = '';
2710
	foreach (explode(",", $cpcfg['interface']) as $cpifgrp) {
2711
		if (isset($FilterIflist[$cpifgrp])) {
2712
			$realif = get_real_interface($cpifgrp);
2713
			if (!empty($realif) && get_interface_ip($realif)) {
2714
				$interfaces .= $realif . ' '; 
2715
			}
2716
		}
2717
	}
2718
	return $interfaces;
2719
}
2720

    
2721
/*
2722
 * Returns an array of (alias, rdrport) pairs describing ports to be forwarded for the captive portal
2723
 */
2724
function captiveportal_zone_portalports($cpcfg) {
2725
	$rdr_ports = array();
2726
	if (isset($cpcfg['httpslogin']) && !isset($cpcfg['nohttpsforwards'])) {
2727
		$portalias = $cpcfg['listenporthttps'] ? $cpcfg['listenporthttps'] : 8001 + $cpcfg['zoneid'];
2728
		$cprdrport = '443';
2729
		array_push($rdr_ports, array($portalias, $cprdrport));
2730
	}
2731
	$portalias = $cpcfg['listenporthttp'] ? $cpcfg['listenporthttp'] : 8000 + $cpcfg['zoneid'];
2732
	$cprdrport = '80';
2733
	array_push($rdr_ports, array($portalias, $cprdrport));
2734

    
2735
	return $rdr_ports;
2736
}
2737

    
2738
function captiveportal_pipes_delete($pipes) {
2739
	if (!empty($pipes)) {
2740
		foreach ($pipes as $pipe) {
2741
			mwexec("/sbin/dnctl pipe delete {$pipe}");
2742
		}
2743
		captiveportal_free_dn_rulenos($pipes);
2744
	}
2745
}
2746

    
2747
function captiveportal_ether_configure_entry($hostent, $anchor, $user_auth = false) {
2748
	global $config, $g, $cpzone;
2749

    
2750
	if (($hostent['action'] == 'block') && ($anchor == 'passthrumac')) {
2751
		return;
2752
	}
2753

    
2754
	$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
2755
	if ($anchor == 'passthrumac') {
2756
		$tag = $cpzoneprefix . '_auth';
2757
	} else {
2758
		$tag = $cpzoneprefix . '_' . $anchor;
2759
	}
2760

    
2761
	if ($anchor == 'passthrumac') {
2762
		list($pipeup, $pipedown) = captiveportal_pipe_configure($hostent, 'pipe_mac', $user_auth);
2763
		$host = str_replace("/", "_", str_replace(":", "", $hostent['mac']));
2764
		$l3from = '';
2765
		$l3to = '';
2766
		$macfrom = "from {$hostent['mac']}";
2767
		$macto = "to {$hostent['mac']}";
2768
	} else {
2769
		list($pipeup, $pipedown) = captiveportal_pipe_configure($hostent, 'auth', $user_auth);
2770
		$host = $hostent['ip'] . '_32';
2771
		$l3from = "l3 from {$hostent['ip']}";
2772
		$l3to = "l3 to {$hostent['ip']}";
2773
		if (!isset($config['captiveportal'][$cpzone]['nomacfilter'])) { 
2774
			if (!empty($hostent['mac'])) {
2775
				$macfrom = "from {$hostent['mac']}";
2776
				$macto = "to {$hostent['mac']}";
2777
			} else {
2778
				return;
2779
			}
2780
		} else {
2781
			$macfrom = '';
2782
			$macto = '';
2783
		}
2784
	}
2785

    
2786
	$rules = "ether pass in quick {$macfrom} {$l3from} tag {$tag} dnpipe {$pipeup}\n";
2787
	$rules .= "ether pass out quick {$macto} {$l3to} tag {$tag} dnpipe {$pipedown}\n";
2788

    
2789
	captiveportal_load_pfctl("{$cpzoneprefix}_{$anchor}", $host, $rules);
2790
}
2791

    
2792
function captiveportal_pipe_configure($host, $type, $user_auth = true) {
2793
	global $config, $cpzone;
2794

    
2795
	$bwUp = 0;
2796
	if (!empty($host['bw_up'])) {
2797
		$bwUp = $host['bw_up'];
2798
	} elseif ($user_auth &&
2799
		isset($config['captiveportal'][$cpzone]['peruserbw']) &&
2800
	    !empty($config['captiveportal'][$cpzone]['bwdefaultup'])) {
2801
		$bwUp = $config['captiveportal'][$cpzone]['bwdefaultup'];
2802
	}
2803
	$bwDown = 0;
2804
	if (!empty($host['bw_down'])) {
2805
		$bwDown = $host['bw_down'];
2806
	} elseif ($user_auth &&
2807
		isset($config['captiveportal'][$cpzone]['peruserbw']) &&
2808
	    !empty($config['captiveportal'][$cpzone]['bwdefaultdn'])) {
2809
		$bwDown = $config['captiveportal'][$cpzone]['bwdefaultdn'];
2810
	}
2811

    
2812
	if (isset($host['pipeno']) && !empty($host['pipeno'])) {
2813
		$pipeup = $host['pipeno'];
2814
	} else {
2815
		$pipeup = captiveportal_get_next_dn_ruleno($type);
2816
	}
2817

    
2818
	mwexec("/sbin/dnctl pipe {$pipeup} config bw {$bwUp}Kbit/s queue 100 buckets 16");
2819
	$pipedown = $pipeup + 1;
2820
	mwexec("/sbin/dnctl pipe {$pipedown} config bw {$bwDown}Kbit/s queue 100 buckets 16");
2821

    
2822
	return array($pipeup, $pipedown);
2823
}
2824

    
2825
function captiveportal_allowedip_configure_entry($ipent) {
2826
	global $g, $config, $cpzone;
2827

    
2828
	$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
2829
	$tag = $cpzoneprefix . '_auth';
2830

    
2831
	if (empty($ipent['sn'])) {
2832
		$ipent['sn'] = '32';
2833
	}
2834

    
2835
	$host = $ipent['ip'] . '_' . $ipent['sn'];
2836
	list($pipeup, $pipedown) = captiveportal_pipe_configure($ipent, 'allowed', false);
2837

    
2838
	$rules = '';
2839
	if (($ipent['dir'] == 'to') || ($ipent['dir'] == 'both')) {
2840
		$rules = "ether pass in quick l3 to {$ipent['ip']}/{$ipent['sn']} tag {$tag} dnpipe {$pipeup}\n";
2841
	}
2842
	if (($ipent['dir'] == 'from') || ($ipent['dir'] == 'both')) {
2843
		$rules .= "ether pass in quick l3 from {$ipent['ip']}/{$ipent['sn']} tag {$tag} dnpipe {$pipedown}\n";
2844
	}
2845

    
2846
	captiveportal_load_pfctl("{$cpzoneprefix}_allowedhosts", $host, $rules);
2847
}
2848

    
2849
function captiveportal_allowedhostname_configure_entry($ipent, $hostnameid = 1) {
2850
	global $config, $cpzone;
2851

    
2852
	if (!isset($ipent['hostname'])) {
2853
		return;
2854
	}
2855

    
2856
	$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
2857
	$tag = $cpzoneprefix . '_auth';
2858
	$table = $cpzoneprefix . '_hostname_' . $hostnameid;
2859
	$host = 'hostname_' . $hostnameid;
2860
	list($pipeup, $pipedown) = captiveportal_pipe_configure($ipent, 'allowed', false);
2861

    
2862
	$rules = "table <{$table}> persist\n";
2863
	if (($ipent['dir'] == 'to') || ($ipent['dir'] == 'both')) {
2864
		$rules .= "ether pass in quick l3 to <{$table}> tag {$tag} dnpipe {$pipeup}\n";
2865
	}
2866
	if (($ipent['dir'] == 'from') || ($ipent['dir'] == 'both')) {
2867
		$rules .= "ether pass in quick l3 from <{$table}> tag {$tag} dnpipe {$pipedown}\n";
2868
	}
2869

    
2870
	captiveportal_load_pfctl("{$cpzoneprefix}_allowedhosts", $host, $rules);
2871

    
2872
	/* return filterdns entry */
2873
	return "pf {$ipent['hostname']} {$table} {$cpzoneprefix}_allowedhosts/{$host}\n";
2874
}
2875

    
2876
function captiveportal_load_pfctl($anchor, $host, $rules) {
2877
	global $g, $cpzone;
2878

    
2879
	if (!empty($rules)) {
2880
		mwexec("/usr/bin/printf \"{$rules}\" | /sbin/pfctl -a {$anchor}/{$host} -f-");
2881
	} else {
2882
		log_error("CP zone ${cpzone}: {$anchor} rules are empty for {$host}");
2883
	}
2884
}
2885

    
2886
function captiveportal_anchor_zerocnt($ip, $anchor = 'auth') {
2887
	global $config, $cpzone;
2888
	$cpzoneprefix = CPPREFIX . $config['captiveportal'][$cpzone]['zoneid'];
2889

    
2890
	pfSense_pf_cp_zerocnt("{$cpzoneprefix}_{$anchor}/{$ip}");
2891
}
2892

    
2893
?>
(7-7/62)