Project

General

Profile

Download (102 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_default_captive_portal_html() {
46
	global $config, $g, $cpzone;
47

    
48
	$translated_text1 = gettext("User");
49
	$translated_text2 = gettext("Password");
50
	$translated_text3 = gettext("First Authentication Method ");
51
	$translated_text4 = gettext("Second Authentication Method ");
52
	// default images to use.
53
	$logo_src = "captiveportal-default-logo.png";
54
	$bg_src = "linear-gradient(135deg, #1475CF, #2B40B5, #1C1275)";
55
	// Check if customlogo is set and if the element exists
56
	// Check if the image is in the directory
57
	if (isset($config['captiveportal'][$cpzone]['customlogo'])) {
58
		foreach ($config['captiveportal'][$cpzone]['element'] as $element) {
59
			if (strpos($element['name'], "captiveportal-logo.") !== false) {
60
				if (file_exists("{$g['captiveportal_path']}/{$element['name']}")) {
61
					$logo_src = $element['name'];
62
					break;
63
				}
64
			}
65
		}
66
	}
67
	// check if custombg is set and if the element exists
68
	if (isset($config['captiveportal'][$cpzone]['custombg'])) {
69
		foreach ($config['captiveportal'][$cpzone]['element'] as $element) {
70
			if (strpos($element['name'],"captiveportal-background.") !== false) {
71
				if( file_exists("{$g['captiveportal_path']}/{$element['name']}")) {
72
					$bg_src = "url(" . $element['name'] . ")" . "center center no-repeat fixed";
73
					break;
74
				}
75
			}
76
		}
77

    
78
	}
79
	// bring in terms and conditions
80
	$termsconditions = base64_decode($config['captiveportal'][$cpzone]['termsconditions']);
81
	// if there is no terms and conditions do not require the checkbox to be selected.
82
	$disabled = "";
83
	if ($termsconditions) {
84
		$disabled = "disabled";
85
	}
86
	$htmltext = <<<EOD
87
<!DOCTYPE html>
88
<html>
89

    
90
<head>
91

    
92
  <meta charset="UTF-8">
93
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
94
  <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
95
  <title>Captive Portal Login Page</title>
96
  <style>
97
	  #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;}
98
  </style>
99
</head>
100

    
101
<body>
102
<div id="content">
103
	<div class="login-card">
104
		<img src="{$logo_src}"/><br>
105
 		<h1></h1>
106
		<div id="error-message">
107
			\$PORTAL_MESSAGE\$
108
		</div>
109
	  <form name="login_form" method="post" action="\$PORTAL_ACTION\$">
110
EOD;
111
	if ($config['captiveportal'][$cpzone]['auth_method'] != "none"){
112
		if ($config['captiveportal'][$cpzone]['auth_method'] === 'authserver' && !empty($config['captiveportal'][$cpzone]['auth_server2'])) {
113
			$htmltext .= <<<EOD
114
			<div class="auth_head_div">
115
				<h6 class="auth_head">{$translated_text3}</h6>
116
			</div>
117
			<div class="auth_source">
118

    
119
EOD;
120
		}
121
		$htmltext .=<<<EOD
122
		<input type="text" name="auth_user" placeholder="{$translated_text1}" id="auth_user">
123
		<input type="password" name="auth_pass" placeholder="{$translated_text2}" id="auth_pass">
124
EOD;
125

    
126
		if ($config['captiveportal'][$cpzone]['auth_method'] === 'authserver' && !empty($config['captiveportal'][$cpzone]['auth_server2'])) {
127
			$htmltext .= <<<EOD
128
			</div>
129
			<div class="auth_head_div">
130
				<h6 class="auth_head">{$translated_text4}</h6>
131
			</div>
132
			<div class="auth_source">
133

    
134
			<input type="text" name="auth_user2" placeholder="{$translated_text1}" id="auth_user2">
135
			<input type="password" name="auth_pass2" placeholder="{$translated_text2}" id="auth_pass2">
136
			</div>
137
EOD;
138
		}
139

    
140

    
141
		if (isset($config['voucher'][$cpzone]['enable'])) {
142
			$translated_text = gettext("Voucher Code");
143
			$htmltext .= <<<EOD
144
				<br  /><br  />
145
				<input name="auth_voucher" type="text" placeholder="{$translated_text}" value="#VOUCHER#">
146
EOD;
147
		}
148
	}
149

    
150
if ($termsconditions) {
151
	$htmltext .= <<<EOD
152
		  <div class="login-help">
153
			<ul class="list">
154
				<li class="list__item">
155
				  <label class="label--checkbox">
156
					<input type="checkbox" class="checkbox" onchange="document.getElementById('login').disabled = !this.checked;">
157
					<span>I agree with the <a  rel="noopener" href="#terms" onclick="document.getElementById('terms').style.display = 'block';">terms & conditions</a></span>
158
				  </label>
159
				</li>
160
			</ul>
161
		  </div>
162
EOD;
163
}
164
	$htmltext .= <<<EOD
165

    
166
		<input name="redirurl" type="hidden" value="\$PORTAL_REDIRURL\$">
167
		<input type="submit" name="accept" class="login login-submit" value="Login" id="login" {$disabled}>
168
	  </form>
169
	  <br  />
170
	  <span> <i>Made with &hearts; by</i> <strong>Netgate</strong></span>
171
	</div>
172
	<div id="terms">
173
		<textarea readonly>{$termsconditions}</textarea>
174
	</div>
175
</div>
176
</body>
177
</html>
178

    
179
EOD;
180

    
181
	return $htmltext;
182
}
183

    
184
function captiveportal_load_modules() {
185
	global $config;
186

    
187
	mute_kernel_msgs();
188
	if (!is_module_loaded("ipfw.ko")) {
189
		mwexec("/sbin/kldload ipfw");
190
		/* make sure ipfw is not on pfil hooks */
191
		set_sysctl(array(
192
		    "net.inet.ip.pfil.inbound" => "pf",
193
		    "net.inet6.ip6.pfil.inbound" => "pf",
194
		    "net.inet.ip.pfil.outbound" => "pf",
195
		    "net.inet6.ip6.pfil.outbound" => "pf"
196
		));
197
	}
198
	/* Activate layer2 filtering */
199
	set_sysctl(array(
200
	    "net.link.ether.ipfw" => "1",
201
	    "net.inet.ip.fw.one_pass" => "1",
202
	    "net.inet.ip.fw.tables_max" => "65534"
203
	));
204

    
205
	/* Always load dummynet now that even allowed ip and mac passthrough use it. */
206
	if (!is_module_loaded("dummynet.ko")) {
207
		mwexec("/sbin/kldload dummynet");
208
		set_sysctl(array(
209
		    "net.inet.ip.dummynet.io_fast" => "1",
210
		    "net.inet.ip.dummynet.hash_size" => "256"
211
		));
212
	}
213
	unmute_kernel_msgs();
214
}
215

    
216
function captiveportal_configure() {
217
	global $config, $cpzone, $cpzoneid;
218

    
219
	if (is_array($config['captiveportal'])) {
220
		foreach ($config['captiveportal'] as $cpkey => $cp) {
221
			$cpzone = $cpkey;
222
			$cpzoneid = $cp['zoneid'];
223
			captiveportal_configure_zone($cp);
224
		}
225
	}
226
}
227

    
228
function captiveportal_configure_zone($cpcfg) {
229
	global $config, $g, $cpzone, $cpzoneid;
230

    
231
	$captiveportallck = lock("captiveportal{$cpzone}", LOCK_EX);
232

    
233
	if (isset($cpcfg['enable'])) {
234

    
235
		if (platform_booting()) {
236
			echo "Starting captive portal({$cpcfg['zone']})... ";
237
		} else {
238
			captiveportal_syslog("Reconfiguring captive portal({$cpcfg['zone']}).");
239
		}
240

    
241
		/* init ipfw rules */
242
		captiveportal_init_rules();
243

    
244
		/* kill any running minicron */
245
		killbypid("{$g['varrun_path']}/cp_prunedb_{$cpzone}.pid");
246

    
247
		/* initialize minicron interval value */
248
		$croninterval = $cpcfg['croninterval'] ? $cpcfg['croninterval'] : 60;
249

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

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

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

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

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

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

    
344
document.location.href="<?=\$my_redirurl;?>";
345
//]]>
346
</script>
347
</body>
348
</html>
349

    
350
EOD;
351
		}
352

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

    
360
		/* write elements */
361
		captiveportal_write_elements();
362

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

    
367
		/* start up the webserving daemon */
368
		captiveportal_init_webgui_zone($cpcfg);
369

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

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

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

    
382
		if (platform_booting()) {
383
			/* send Accounting-On to server */
384
			captiveportal_send_server_accounting('on');
385
			echo "done\n";
386

    
387
			if (isset($cpcfg['preservedb']) ||
388
			    captiveportal_xmlrpc_sync_get_details($syncip, $syncport, $syncuser, $syncpass)) {
389

    
390
				$connected_users = captiveportal_read_db();
391
				if (!empty($connected_users)) {
392
					echo "Reconnecting users to captive portal {$cpcfg['zone']}... ";
393
					foreach ($connected_users as $user) {
394
						captiveportal_reserve_ruleno($user['pipeno']);
395
						$bw_up = intval($user['bw_up']);
396
						$bw_down = intval($user['bw_down']);
397
						$clientip = $user['ip'];
398
						$clientmac = $user['mac'];
399
						$bw_up_pipeno = $user['pipeno'];
400
						$bw_down_pipeno = $user['pipeno'] + 1;
401

    
402
						$rule_entry = "{$clientip}/" . (is_ipaddrv6($clientip) ? "128" : "32");
403
						if (!isset($cpcfg['nomacfilter'])) {
404
							$rule_entry .= ",{$clientmac}";
405
						}
406

    
407
						$_gb = @pfSense_ipfw_pipe("pipe {$bw_up_pipeno} config bw {$bw_up}Kbit/s queue 100 buckets 16");
408
						$_gb = @pfSense_ipfw_pipe("pipe {$bw_down_pipeno} config bw {$bw_down}Kbit/s queue 100 buckets 16");
409
						$_gb = @pfSense_ipfw_table("{$cpzone}_auth_up", IP_FW_TABLE_XADD, "{$rule_entry}", $bw_up_pipeno);
410
						$_gb = @pfSense_ipfw_table("{$cpzone}_auth_down", IP_FW_TABLE_XADD, "{$rule_entry}", $bw_down_pipeno);
411
					}
412
					echo "done\n";
413
				}
414
			}
415
		}
416

    
417
	} else {
418
		killbypid("{$g['varrun_path']}/nginx-{$cpzone}-CaptivePortal.pid");
419
		killbypid("{$g['varrun_path']}/nginx-{$cpzone}-CaptivePortal-SSL.pid");
420
		killbypid("{$g['varrun_path']}/cp_prunedb_{$cpzone}.pid");
421
		@unlink("{$g['varetc_path']}/captiveportal_{$cpzone}.html");
422
		@unlink("{$g['varetc_path']}/captiveportal-{$cpzone}-error.html");
423
		@unlink("{$g['varetc_path']}/captiveportal-{$cpzone}-logout.html");
424

    
425
		captiveportal_radius_stop_all(10); // NAS-Request
426

    
427
		captiveportal_filterdns_configure();
428

    
429
		/* remove old information */
430
		unlink_if_exists("{$g['vardb_path']}/captiveportal{$cpzone}.db");
431
		unlink_if_exists("{$g['vardb_path']}/captiveportal_radius_{$cpzone}.db");
432
		unlink_if_exists("{$g['vardb_path']}/captiveportal_{$cpzone}.rules");
433
		/* Release allocated pipes for this zone */
434
		$pipes_to_remove = captiveportal_free_dnrules();
435

    
436
		captiveportal_delete_rules($pipes_to_remove);
437

    
438
		if (empty($config['captiveportal'])) {
439
			set_single_sysctl("net.link.ether.ipfw", "0");
440
		} else {
441
			/* Deactivate ipfw(4) if not needed */
442
			$cpactive = false;
443
			if (is_array($config['captiveportal'])) {
444
				foreach ($config['captiveportal'] as $cpkey => $cp) {
445
					if (isset($cp['enable'])) {
446
						$cpactive = true;
447
						break;
448
					}
449
				}
450
			}
451
			if ($cpactive === false) {
452
				set_single_sysctl("net.link.ether.ipfw", "0");
453
			}
454
		}
455
	}
456

    
457
	unlock($captiveportallck);
458

    
459
	return 0;
460
}
461

    
462
function captiveportal_init_webgui() {
463
	global $config, $cpzone;
464

    
465
	if (is_array($config['captiveportal'])) {
466
		foreach ($config['captiveportal'] as $cpkey => $cp) {
467
			$cpzone = $cpkey;
468
			captiveportal_init_webgui_zone($cp);
469
		}
470
	}
471
}
472

    
473
function captiveportal_init_webgui_zonename($zone) {
474
	global $config, $cpzone;
475

    
476
	if (isset($config['captiveportal'][$zone])) {
477
		$cpzone = $zone;
478
		captiveportal_init_webgui_zone($config['captiveportal'][$zone]);
479
	}
480
}
481

    
482
function captiveportal_init_webgui_zone($cpcfg) {
483
	global $g, $config, $cpzone;
484

    
485
	if (!isset($cpcfg['enable'])) {
486
		return;
487
	}
488

    
489
	if (isset($cpcfg['httpslogin'])) {
490
		$cert = lookup_cert($cpcfg['certref']);
491
		$crt = base64_decode($cert['crt']);
492
		$key = base64_decode($cert['prv']);
493
		$ca = ca_chain($cert);
494

    
495
		/* generate nginx configuration */
496
		if (!empty($cpcfg['listenporthttps'])) {
497
			$listenporthttps = $cpcfg['listenporthttps'];
498
		} else {
499
			$listenporthttps = 8001 + $cpcfg['zoneid'];
500
		}
501
		system_generate_nginx_config("{$g['varetc_path']}/nginx-{$cpzone}-CaptivePortal-SSL.conf",
502
			$crt, $key, $ca, "nginx-{$cpzone}-CaptivePortal-SSL.pid", $listenporthttps, "/usr/local/captiveportal",
503
			"cert-{$cpzone}-portal.pem", "ca-{$cpzone}-portal.pem", $cpzone, false);
504
	}
505

    
506
	/* generate nginx configuration */
507
	if (!empty($cpcfg['listenporthttp'])) {
508
		$listenporthttp = $cpcfg['listenporthttp'];
509
	} else {
510
		$listenporthttp = 8000 + $cpcfg['zoneid'];
511
	}
512
	system_generate_nginx_config("{$g['varetc_path']}/nginx-{$cpzone}-CaptivePortal.conf",
513
		"", "", "", "nginx-{$cpzone}-CaptivePortal.pid", $listenporthttp, "/usr/local/captiveportal",
514
		"", "", $cpzone, false);
515

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

    
520
	/* fire up https instance */
521
	if (isset($cpcfg['httpslogin'])) {
522
		@unlink("{$g['varrun']}/nginx-{$cpzone}-CaptivePortal-SSL.pid");
523
		$res = mwexec("/usr/local/sbin/nginx -c {$g['varetc_path']}/nginx-{$cpzone}-CaptivePortal-SSL.conf");
524
	}
525
}
526

    
527
function captiveportal_init_rules_byinterface($interface) {
528
	global $cpzone, $cpzoneid, $config;
529

    
530
	if (!is_array($config['captiveportal'])) {
531
		return;
532
	}
533

    
534
	foreach ($config['captiveportal'] as $cpkey => $cp) {
535
		$cpzone = $cpkey;
536
		$cpzoneid = $cp['zoneid'];
537
		$cpinterfaces = explode(",", $cp['interface']);
538
		if (in_array($interface, $cpinterfaces)) {
539
			captiveportal_init_rules();
540
			break;
541
		}
542
	}
543
}
544

    
545
/* Create basic rules used by all zones */
546
function captiveportal_init_general_rules($flush = false) {
547
	global $g;
548

    
549
	$flush_rule = '';
550
	if ($flush) {
551
		$flush_rule = 'flush';
552
	}
553

    
554
	/* Already loaded */
555
	if (!$flush && (mwexec("/sbin/ipfw list 1000", true) == 0)) {
556
		return;
557
	}
558

    
559
	$cprules = <<<EOD
560
{$flush_rule}
561
# Table with interfaces that have CP enabled
562
table cp_ifaces create type iface valtype skipto
563

    
564
# Redirect each CP interface to its specific rule
565
add 1000 skipto tablearg all from any to any via table(cp_ifaces)
566

    
567
# This interface has no cp zone configured
568
add 1100 allow all from any to any
569

    
570
# block everything else
571
add 65534 deny all from any to any
572
EOD;
573

    
574
	/* load rules */
575
	file_put_contents("{$g['tmp_path']}/ipfw.cp.rules", $cprules);
576
	mwexec("/sbin/ipfw -q {$g['tmp_path']}/ipfw.cp.rules", true);
577
	@unlink("{$g['tmp_path']}/ipfw_{$cpzone}.cp.rules");
578
	unset($cprules);
579
}
580

    
581
/* Create a string with ipfw rule and increase rulenum */
582
function captiveportal_create_ipfw_rule($cmd, &$rulenum, $args) {
583
	$rule = "{$cmd} {$rulenum} {$args}\n";
584
	$rulenum++;
585

    
586
	return $rule;
587
}
588

    
589
/* Return first rule number for a cp zone */
590
function captiveportal_ipfw_ruleno($id) {
591
	global $g;
592

    
593
	return 2000 + $id * $g['captiveportal_rules_interval'];
594
}
595

    
596
/* reinit will disconnect all users, be careful! */
597
function captiveportal_init_rules($reinit = false) {
598
	global $config, $g, $cpzone;
599

    
600
	if (!isset($config['captiveportal'][$cpzone]['enable'])) {
601
		return;
602
	}
603

    
604
	captiveportal_load_modules();
605
	captiveportal_init_general_rules();
606

    
607
	/* Cleanup so nothing is leaked */
608
	captiveportal_free_dnrules(2000, 64500, false, $reinit);
609
	unlink_if_exists("{$g['vardb_path']}/captiveportal_{$cpzone}.rules");
610

    
611
	$cpzoneid = $config['captiveportal'][$cpzone]['zoneid'];
612
	$skipto = captiveportal_ipfw_ruleno($cpzoneid);
613

    
614
	$cprules = '';
615

    
616
	$cpips = array();
617
	$ifaces = get_configured_interface_list();
618
	$cpinterfaces = explode(",", $config['captiveportal'][$cpzone]['interface']);
619
	$firsttime = 0;
620
	foreach ($cpinterfaces as $cpifgrp) {
621
		if (!isset($ifaces[$cpifgrp])) {
622
			continue;
623
		}
624
		$tmpif = get_real_interface($cpifgrp);
625
		if (empty($tmpif)) {
626
			continue;
627
		}
628

    
629
		$cpipm = get_interface_ip($cpifgrp);
630

    
631
		if (!is_ipaddr($cpipm)) {
632
			continue;
633
		}
634

    
635
		$cpips[] = $cpipm;
636
		if (is_array($config['virtualip']['vip'])) {
637
			foreach ($config['virtualip']['vip'] as $vip) {
638
				if (($vip['interface'] == $cpifgrp) &&
639
				    (($vip['mode'] == "carp") ||
640
				    ($vip['mode'] == "ipalias"))) {
641
					$cpips[] = $vip['subnet'];
642
				}
643
			}
644
		}
645

    
646
		$cprules .= "table cp_ifaces add {$tmpif} {$skipto}\n";
647
	}
648
	if (count($cpips) > 0) {
649
		$cpactive = true;
650
	} else {
651
		return false;
652
	}
653

    
654
	$captiveportallck = try_lock("captiveportal{$cpzone}", 0);
655

    
656
	$tables = captiveportal_get_ipfw_table_names();
657

    
658
	$rulenum = $skipto;
659
	$cprules .= "table {$cpzone}_pipe_mac create type mac valtype pipe\n";
660
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
661
	    "pipe tablearg MAC table({$cpzone}_pipe_mac)");
662
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
663
	    "allow pfsync from any to any");
664
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
665
	    "allow carp from any to any\n");
666
	$cprules .= "# layer 2: pass ARP\n";
667
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
668
	    "pass layer2 mac-type arp,rarp");
669
	$cprules .= "# pfsense requires for WPA\n";
670
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
671
	    "pass layer2 mac-type 0x888e,0x88c7");
672
	$cprules .= "# PPP Over Ethernet Session Stage/Discovery Stage\n";
673
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
674
	    "pass layer2 mac-type 0x8863,0x8864\n");
675
	$cprules .= "# layer 2: block anything else non-IP(v4/v6)\n";
676
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
677
	    "deny layer2 not mac-type ip,ipv6");
678

    
679
	/* These tables contain host ips */
680
	$cprules .= "table {$cpzone}_host_ips create type addr\n";
681
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
682
	    "pass ip from any to table({$cpzone}_host_ips) in");
683
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
684
	    "pass ip from table({$cpzone}_host_ips) to any out");
685
	foreach ($cpips as $cpip) {
686
		$cprules .= "table {$cpzone}_host_ips add {$cpip}\n";
687
	}
688
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
689
	    "pass ip from any to 255.255.255.255 in");
690
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
691
	    "pass ip from 255.255.255.255 to any out");
692

    
693
	/* Allowed ips */
694
	$cprules .= "table {$cpzone}_allowed_up create type addr valtype pipe\n";
695
	$cprules .= "table {$cpzone}_allowed_down create type addr valtype pipe\n";
696
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
697
	    "pipe tablearg ip from table({$cpzone}_allowed_up) to any in");
698
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
699
	    "pipe tablearg ip from any to table({$cpzone}_allowed_down) in");
700
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
701
	    "pipe tablearg ip from table({$cpzone}_allowed_up) to any out");
702
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
703
	    "pipe tablearg ip from any to table({$cpzone}_allowed_down) out");
704

    
705
	/* Authenticated users rules. */
706
	if (!in_array("{$cpzone}_auth_up", $tables)) {
707
		$cprules .= "table {$cpzone}_auth_up create type addr valtype pipe\n";
708
	}
709
	if (!in_array("{$cpzone}_auth_down", $tables)) {
710
		$cprules .= "table {$cpzone}_auth_down create type addr valtype pipe\n";
711
	}
712
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
713
	    "pipe tablearg ip from table({$cpzone}_auth_up) to any layer2 in");
714
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
715
	    "pipe tablearg ip from any to table({$cpzone}_auth_down) layer2 out");
716

    
717
	if (!empty($config['captiveportal'][$cpzone]['listenporthttp'])) {
718
		$listenporthttp = $config['captiveportal'][$cpzone]['listenporthttp'];
719
	} else {
720
		$listenporthttp = 8000 + $cpzoneid;
721
	}
722

    
723
	if (isset($config['captiveportal'][$cpzone]['httpslogin'])) {
724
		if (!empty($config['captiveportal'][$cpzone]['listenporthttps'])) {
725
			$listenporthttps = $config['captiveportal'][$cpzone]['listenporthttps'];
726
		} else {
727
			$listenporthttps = 8001 + $cpzoneid;
728
		}
729
		if (!isset($config['captiveportal'][$cpzone]['nohttpsforwards'])) {
730
			$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
731
			    "fwd 127.0.0.1,{$listenporthttps} tcp from any to any dst-port 443 in");
732
		}
733
	}
734

    
735
	$cprules .= "# redirect non-authenticated clients to captive portal\n";
736
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
737
	    "fwd 127.0.0.1,{$listenporthttp} tcp from any to any dst-port 80 in");
738
	$cprules .= "# let the responses from the captive portal web server back out\n";
739
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
740
	    "pass tcp from any to any out");
741
	$cprules .= "# This CP zone is over, skip to last rule\n";
742
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
743
	    "skipto 65534 all from any to any");
744

    
745
	/* generate passthru mac database */
746
	$cprules .= captiveportal_passthrumac_configure(true);
747
	$cprules .= "\n";
748

    
749
	/* allowed ipfw rules to make allowed ip work */
750
	$cprules .= captiveportal_allowedip_configure();
751

    
752
	/* allowed ipfw rules to make allowed hostnames work */
753
	$cprules .= captiveportal_allowedhostname_configure();
754

    
755
	/* load rules */
756
	captiveportal_delete_rules(array(), $reinit);
757
	file_put_contents("{$g['tmp_path']}/ipfw_{$cpzone}.cp.rules", $cprules);
758
	mwexec("/sbin/ipfw -q {$g['tmp_path']}/ipfw_{$cpzone}.cp.rules", true);
759
	@unlink("{$g['tmp_path']}/ipfw_{$cpzone}.cp.rules");
760
	unset($cprules);
761

    
762
	captiveportal_filterdns_configure();
763

    
764
	if ($captiveportallck) {
765
		unlock($captiveportallck);
766
	}
767
}
768

    
769
/* Delete all rules related to specific cpzone */
770
function captiveportal_delete_rules($pipes_to_remove = array(), $clear_auth_rules = true) {
771
	global $g, $config, $cpzone;
772

    
773
	$cpzoneid = $config['captiveportal'][$cpzone]['zoneid'];
774

    
775
	$skipto1 = captiveportal_ipfw_ruleno($cpzoneid);
776
	$skipto2 = $skipto1 + $g['captiveportal_rules_interval'];
777

    
778
	$cp_ifaces = pfSense_ipfw_table_list("cp_ifaces");
779
	if (is_array($cp_ifaces)) {
780
		foreach ($cp_ifaces as $cp_iface) {
781
			if (empty($cp_iface['skipto']) ||
782
			    $cp_iface['skipto'] != $skipto1) {
783
				continue;
784
			}
785

    
786
			pfSense_ipfw_table("cp_ifaces", IP_FW_TABLE_XDEL,
787
			    $cp_iface['iface']);
788
		}
789
	}
790

    
791
	mwexec("/sbin/ipfw delete {$skipto1}-{$skipto2}", true);
792

    
793
	$tables = captiveportal_get_ipfw_table_names();
794

    
795
	$delrules = "";
796
	foreach ($tables as $table) {
797
		if (!$clear_auth_rules && substr($table, 0, strlen($cpzone . '_auth')) == $cpzone . '_auth') {
798
			continue;
799
		} else {
800
			$delrules .= "table {$table} destroy\n";
801
		}
802
	}
803

    
804
	foreach ($pipes_to_remove as $pipeno) {
805
		$delrules .= "pipe delete {$pipeno}\n";
806
	}
807

    
808
	if (empty($delrules)) {
809
		return;
810
	}
811

    
812
	file_put_contents("{$g['tmp_path']}/ipfw_{$cpzone}.deltable.rules", $delrules);
813
	mwexec("/sbin/ipfw -q {$g['tmp_path']}/ipfw_{$cpzone}.deltable.rules", true);
814
	@unlink("{$g['tmp_path']}/ipfw_{$cpzone}.deltable.rules");
815
}
816

    
817
/*
818
 * Remove clients that have been around for longer than the specified amount of time
819
 * db file structure:
820
 * timestamp,ipfw_rule_no,clientip,clientmac,username,sessionid,password,session_timeout,idle_timeout,session_terminate_time,interim_interval,traffic_quota,auth_method,context
821
 * (password is in Base64 and only saved when reauthentication is enabled)
822
 */
823
function captiveportal_prune_old() {
824
	global $g, $config, $cpzone, $cpzoneid;
825

    
826
	if (empty($cpzone)) {
827
		return;
828
	}
829

    
830
	$cpcfg = $config['captiveportal'][$cpzone];
831
	$vcpcfg = $config['voucher'][$cpzone];
832

    
833
	/* check for expired entries */
834
	$idletimeout = 0;
835
	$timeout = 0;
836
	if (!empty($cpcfg['timeout']) && is_numeric($cpcfg['timeout'])) {
837
		$timeout = $cpcfg['timeout'] * 60;
838
	}
839

    
840
	if (!empty($cpcfg['idletimeout']) && is_numeric($cpcfg['idletimeout'])) {
841
		$idletimeout = $cpcfg['idletimeout'] * 60;
842
	}
843

    
844
	/* check for entries exceeding their traffic quota */
845
	$trafficquota = 0;
846
	if (!empty($cpcfg['trafficquota']) && is_numeric($cpcfg['trafficquota'])) {
847
		$trafficquota = $cpcfg['trafficquota'] * 1048576;
848
	}
849

    
850
	/* Is there any job to do? If we are in High Availability sync, are we in backup mode ? */
851
	if ((!$timeout && !$idletimeout && !$trafficquota && !isset($cpcfg['reauthenticate']) &&
852
	    !isset($cpcfg['radiussession_timeout']) && !isset($cpcfg['radiustraffic_quota']) &&
853
	    !isset($vcpcfg['enable']) && !isset($cpcfg['radacct_enable'])) ||
854
	    captiveportal_ha_is_node_in_backup_mode($cpzone)) {
855
		return;
856
	}
857

    
858

    
859
	/* Read database */
860
	/* NOTE: while this can be simplified in non radius case keep as is for now */
861
	$cpdb = captiveportal_read_db();
862

    
863
	$unsetindexes = array();
864
	$voucher_needs_sync = false;
865
	/*
866
	 * Snapshot the time here to use for calculation to speed up the process.
867
	 * If something is missed next run will catch it!
868
	 */
869
	$pruning_time = time();
870
	foreach ($cpdb as $cpentry) {
871
		$stop_time = $pruning_time;
872

    
873
		$timedout = false;
874
		$term_cause = 1;
875
		/* hard timeout or session_timeout from radius if enabled */
876
		if (isset($cpcfg['radiussession_timeout'])) {
877
			$utimeout = (is_numeric($cpentry[7])) ? $cpentry[7] : $timeout;
878
		} else {
879
			$utimeout = $timeout;
880
		}
881
		if ($utimeout) {
882
			if (($pruning_time - $cpentry[0]) >= $utimeout) {
883
				$timedout = true;
884
				$term_cause = 5; // Session-Timeout
885
				$logout_cause = 'SESSION TIMEOUT';
886
			}
887
		}
888

    
889
		/* Session-Terminate-Time */
890
		if (!$timedout && !empty($cpentry[9])) {
891
			if ($pruning_time >= $cpentry[9]) {
892
				$timedout = true;
893
				$term_cause = 5; // Session-Timeout
894
				$logout_cause = 'SESSION TIMEOUT';
895
			}
896
		}
897

    
898
		/* check if an idle_timeout has been set and if its set change the idletimeout to this value */
899
		$uidletimeout = (is_numeric($cpentry[8])) ? $cpentry[8] : $idletimeout;
900
		/* if an idle timeout is specified, get last activity timestamp from ipfw */
901
		if (!$timedout && $uidletimeout > 0) {
902
			$lastact = captiveportal_get_last_activity($cpentry[2]);
903
			/*	If the user has logged on but not sent any traffic they will never be logged out.
904
			 *	We "fix" this by setting lastact to the login timestamp.
905
			 */
906
			$lastact = $lastact ? $lastact : $cpentry[0];
907
			if ($lastact && (($pruning_time - $lastact) >= $uidletimeout)) {
908
				$timedout = true;
909
				$term_cause = 4; // Idle-Timeout
910
				$logout_cause = 'IDLE TIMEOUT';
911
				if (!isset($config['captiveportal'][$cpzone]['includeidletime'])) {
912
					$stop_time = $lastact;
913
				}
914
			}
915
		}
916

    
917
		/* if vouchers are configured, activate session timeouts */
918
		if (!$timedout && isset($vcpcfg['enable']) && !empty($cpentry[7])) {
919
			if ($pruning_time >= ($cpentry[0] + $cpentry[7])) {
920
				$timedout = true;
921
				$term_cause = 5; // Session-Timeout
922
				$logout_cause = 'SESSION TIMEOUT';
923
				$voucher_needs_sync = true;
924
			}
925
		}
926

    
927
		/* traffic quota, value retrieved from the radius attribute if the option is enabled */
928
		if (isset($cpcfg['radiustraffic_quota'])) {
929
			$utrafficquota = (is_numeric($cpentry[11])) ? $cpentry[11] : $trafficquota;
930
		} else {
931
			$utrafficquota = $trafficquota;
932
		}
933
		if (!$timedout && $utrafficquota > 0) {
934
			$volume = getVolume($cpentry[2], $cpentry[3]);
935
			if (($volume['input_bytes'] + $volume['output_bytes']) > $utrafficquota) {
936
				$timedout = true;
937
				$term_cause = 10; // NAS-Request
938
				$logout_cause = 'QUOTA EXCEEDED';
939
			}
940
		}
941

    
942
		if ($timedout) {
943
			captiveportal_disconnect($cpentry, $term_cause, $stop_time);
944
			captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], $logout_cause);
945
			$unsetindexes[] = $cpentry[5];
946
		}
947

    
948
		/* do periodic reauthentication? For Radius servers, send accounting updates? */
949
		if (!$timedout) {
950
			//Radius servers : send accounting
951
			if (isset($cpcfg['radacct_enable']) && $cpentry['authmethod'] === 'radius') {
952
				if (substr($cpcfg['reauthenticateacct'], 0, 9) === "stopstart") {
953
					/* stop and restart accounting */
954
					if ($cpcfg['reauthenticateacct'] === "stopstartfreeradius") {
955
						$rastart_time = 0;
956
						$rastop_time = 60;
957
					} else {
958
						$rastart_time = $cpentry[0];
959
						$rastop_time = time();
960
					}
961
					captiveportal_send_server_accounting('stop',
962
						$cpentry[1], // ruleno
963
						$cpentry[4], // username
964
						$cpentry[2], // clientip
965
						$cpentry[3], // clientmac
966
						$cpentry[5], // sessionid
967
						$rastart_time, // start time
968
						$rastop_time, // Stop Time
969
						10); // NAS Request
970
					$clientsn = (is_ipaddrv6($cpentry[2])) ? 128 : 32;
971
					pfSense_ipfw_table_zerocnt("{$cpzone}_auth_up", "{$cpentry[2]}/{$clientsn}");
972
					pfSense_ipfw_table_zerocnt("{$cpzone}_auth_down", "{$cpentry[2]}/{$clientsn}");
973
					if ($cpcfg['reauthenticateacct'] == "stopstartfreeradius") {
974
						/* Need to pause here or the FreeRADIUS server gets confused about packet ordering. */
975
						sleep(1);
976
					}
977
					captiveportal_send_server_accounting('start',
978
						$cpentry[1], // ruleno
979
						$cpentry[4], // username
980
						$cpentry[2], // clientip
981
						$cpentry[3], // clientmac
982
						$cpentry[5]); // sessionid
983
				} else if ($cpcfg['reauthenticateacct'] == "interimupdate") {
984
					$session_time = $pruning_time - $cpentry[0];
985
					if (!empty($cpentry[10]) && $cpentry[10] > 60) {
986
						$interval = $cpentry[10];
987
					} else {
988
						$interval = 0;
989
					}
990
					$past_interval_min = ($session_time > $interval);
991
					if ($interval != 0) {
992
						$within_interval = ($session_time % $interval >= 0 && $session_time % $interval <= 59);
993
					}
994
					if ($interval === 0 || ($interval > 0 && $past_interval_min && $within_interval)) {
995
					captiveportal_send_server_accounting('update',
996
						$cpentry[1], // ruleno
997
						$cpentry[4], // username
998
						$cpentry[2], // clientip
999
						$cpentry[3], // clientmac
1000
						$cpentry[5], // sessionid
1001
						$cpentry[0]); // start time
1002
					}
1003
				}
1004
			}
1005

    
1006
			/* check this user again */
1007
			if (isset($cpcfg['reauthenticate']) && $cpentry['context'] !== 'voucher') {
1008
				$auth_result = captiveportal_authenticate_user(
1009
					$cpentry[4], // username
1010
					base64_decode($cpentry[6]), // password
1011
					$cpentry[3], // clientmac
1012
					$cpentry[2], // clientip
1013
					$cpentry[1], // ruleno
1014
					$cpentry['context']); // context
1015
				if ($auth_result['result'] === false) {
1016
					captiveportal_disconnect($cpentry, 17);
1017
					captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "DISCONNECT - REAUTHENTICATION FAILED", $auth_list['reply_message']);
1018
					$unsetindexes[] = $cpentry[5];
1019
				} else if ($auth_result['result'] === true) {
1020
					if ($cpentry['authmethod'] !== $auth_result['auth_method']) {
1021
						// if the user got authenticated against another server type:  we update the database
1022
						if (!empty($cpentry[5])) {
1023
							captiveportal_update_entry($cpentry['sessionid'], $auth_result['auth_method'], 'authmethod');
1024
							captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CHANGED AUTHENTICATION SERVER", $auth_list['reply_message']);
1025
						}
1026
						// User was logged on a RADIUS server, but is now logged in by another server type : we send an accounting Stop
1027
						if (isset($config['captiveportal'][$cpzone]['radacct_enable']) && $cpentry['authmethod'] == 'radius') {
1028
							if ($cpcfg['reauthenticateacct'] === "stopstartfreeradius") {
1029
								$rastart_time = 0;
1030
								$rastop_time = 60;
1031
							} else {
1032
								$rastart_time = $cpentry[0];
1033
								$rastop_time = time();
1034
							}
1035
							captiveportal_send_server_accounting('stop',
1036
								$cpentry[1], // ruleno
1037
								$cpentry[4], // username
1038
								$cpentry[2], // clientip
1039
								$cpentry[3], // clientmac
1040
								$cpentry[5], // sessionid
1041
								$rastart_time, // start time
1042
								$rastop_time, // Stop Time
1043
								3); // Lost Service
1044
						// User was logged on a non-RADIUS Server but is now logged in by a RADIUS server : we send an accounting Start
1045
						} else if(isset($config['captiveportal'][$cpzone]['radacct_enable']) && $auth_result['auth_method'] === 'radius') {
1046
							captiveportal_send_server_accounting('start',
1047
								$cpentry[1], // ruleno
1048
								$cpentry[4], // username
1049
								$cpentry[2], // clientip
1050
								$cpentry[3], // clientmac
1051
								$cpentry[5], // sessionid
1052
								$cpentry[0]); // start_time
1053
						}
1054
					}
1055
					captiveportal_reapply_attributes($cpentry, $auth_result['attributes']);
1056
				}
1057
			}
1058
		}
1059
	}
1060
	unset($cpdb);
1061

    
1062
	captiveportal_prune_old_automac();
1063

    
1064
	if ($voucher_needs_sync == true) {
1065
		/* perform in-use vouchers expiration using check_reload_status */
1066
		send_event("service sync vouchers");
1067
	}
1068

    
1069
	/* write database */
1070
	if (!empty($unsetindexes)) {
1071
		captiveportal_remove_entries($unsetindexes);
1072
	}
1073
}
1074

    
1075
function captiveportal_prune_old_automac() {
1076
	global $g, $config, $cpzone, $cpzoneid;
1077

    
1078
	if (is_array($config['captiveportal'][$cpzone]['passthrumac']) &&
1079
	    isset($config['captiveportal'][$cpzone]['passthrumacadd'])) {
1080
		$tmpvoucherdb = array();
1081
		$macrules = "";
1082
		$writecfg = false;
1083
		foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $eid => $emac) {
1084
			if ($emac['logintype'] != "voucher") {
1085
				continue;
1086
			}
1087
			if (isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) {
1088
				if (isset($tmpvoucherdb[$emac['username']])) {
1089
					$temac = $config['captiveportal'][$cpzone]['passthrumac'][$tmpvoucherdb[$emac['username']]];
1090
					$pipeno = captiveportal_get_dn_passthru_ruleno($temac['mac']);
1091
					if ($pipeno) {
1092
						captiveportal_free_dn_ruleno($pipeno);
1093
						$macrules .= "table {$cpzone}_pipe_mac delete any,{$temac['mac']}\n";
1094
						$macrules .= "table {$cpzone}_pipe_mac delete {$temac['mac']},any\n";
1095
						$macrules .= "pipe delete {$pipeno}\n";
1096
						++$pipeno;
1097
						$macrules .= "pipe delete {$pipeno}\n";
1098
					}
1099
					$writecfg = true;
1100
					captiveportal_logportalauth($temac['username'], $temac['mac'],
1101
					    $temac['ip'], "DUPLICATE {$temac['username']} LOGIN - TERMINATING OLD SESSION");
1102
					unset($config['captiveportal'][$cpzone]['passthrumac'][$tmpvoucherdb[$emac['username']]]);
1103
				}
1104
				$tmpvoucherdb[$emac['username']] = $eid;
1105
			}
1106
		}
1107
		unset($tmpvoucherdb);
1108
		if (!empty($macrules)) {
1109
			@file_put_contents("{$g['tmp_path']}/macentry.prunerules.tmp", $macrules);
1110
			unset($macrules);
1111
			mwexec("/sbin/ipfw -q {$g['tmp_path']}/macentry.prunerules.tmp");
1112
		}
1113
		if ($writecfg === true) {
1114
			write_config("Prune session for auto-added macs");
1115
		}
1116
	}
1117
}
1118

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

    
1123
	$stop_time = (empty($stop_time)) ? time() : $stop_time;
1124

    
1125
	/* this client needs to be deleted - remove ipfw rules */
1126
	if (isset($config['captiveportal'][$cpzone]['radacct_enable']) && $dbent['authmethod'] == 'radius') {
1127
		if ($config['captiveportal'][$cpzone]['reauthenticateacct'] == "stopstartfreeradius") {
1128
			/*
1129
			 * Interim updates are on so the session time must be
1130
			 * reported as the elapsed time since the previous
1131
			 * interim update.
1132
			 */
1133
			$session_time = ($stop_time - $dbent[0]) % 60;
1134
			$start_time = $stop_time - $session_time;
1135
		} else {
1136
			$start_time = $dbent[0];
1137
		}
1138
		captiveportal_send_server_accounting('stop',
1139
			$dbent[1], // ruleno
1140
			$dbent[4], // username
1141
			$dbent[2], // clientip
1142
			$dbent[3], // clientmac
1143
			$dbent[5], // sessionid
1144
			$start_time, // start time
1145
			$stop_time, // stop time
1146
			$term_cause); // Acct-Terminate-Cause
1147
	}
1148

    
1149
	if (is_ipaddr($dbent[2])) {
1150
		/*
1151
		 * Delete client's ip entry from tables auth_up and auth_down.
1152
		 * It's not necessary to explicit specify mac address here
1153
		 */
1154
		$cpsession = captiveportal_isip_logged($dbent[2]);
1155
		if (!empty($cpsession)) {
1156
			$clientsn = (is_ipaddrv6($dbent[2])) ? 128 : 32;
1157
			pfSense_ipfw_table("{$cpzone}_auth_up",
1158
			    IP_FW_TABLE_XDEL, "{$dbent[2]}/{$clientsn}");
1159
			pfSense_ipfw_table("{$cpzone}_auth_down",
1160
			    IP_FW_TABLE_XDEL, "{$dbent[2]}/{$clientsn}");
1161
		}
1162
		/* XXX: Redundant?! Ensure all pf(4) states are killed. */
1163
		$_gb = @pfSense_kill_states(utf8_encode($dbent[2]));
1164
		$_gb = @pfSense_kill_srcstates(utf8_encode($dbent[2]));
1165
	}
1166

    
1167
	/*
1168
	 * These are the pipe numbers we use to control traffic shaping for
1169
	 * each logged in user via captive portal
1170
	 * We could get an error if the pipe doesn't exist but everything
1171
	 * should still be fine
1172
	 */
1173
	if (!empty($dbent[1])) {
1174
		/*
1175
		 * Call captiveportal_free_dnrules() in dry_run mode to verify
1176
		 * if there are pipes to be removed and prevent the attempt to
1177
		 * delete invalid pipes
1178
		 */
1179
		$removed_pipes = captiveportal_free_dnrules($dbent[1],
1180
		    $dbent[1]+1, true);
1181

    
1182
		if (!empty($removed_pipes)) {
1183
			$_gb = @pfSense_ipfw_pipe("pipe delete {$dbent[1]}");
1184
			$_gb = @pfSense_ipfw_pipe("pipe delete " .
1185
			    ($dbent[1]+1));
1186

    
1187
			/*
1188
			 * Release the ruleno so it can be reallocated to new
1189
			 * clients
1190
			 */
1191
			captiveportal_free_dn_ruleno($dbent[1]);
1192
		}
1193
	}
1194

    
1195
	// XMLRPC Call over to the backup node if necessary
1196
	if (captiveportal_xmlrpc_sync_get_details($syncip, $syncport,
1197
	    $syncuser, $syncpass, $carp_loop)) {
1198
		$rpc_client = new pfsense_xmlrpc_client();
1199
		$rpc_client->setConnectionData($syncip, $syncport, $syncuser, $syncpass);
1200
		$rpc_client->set_noticefile("CaptivePortalUserSync");
1201
		$arguments = array(
1202
			'sessionid' => $dbent[5],
1203
			'term_cause' => $term_cause,
1204
			'stop_time' => $stop_time
1205
		);
1206

    
1207
		$rpc_client->xmlrpc_method('captive_portal_sync',
1208
			array(
1209
				'op' => 'disconnect_user',
1210
				'zone' => $cpzone,
1211
				'session' => base64_encode(serialize($arguments))
1212
			)
1213
		);
1214
	}
1215
	return true;
1216
}
1217

    
1218
/* remove a single client by sessionid */
1219
function captiveportal_disconnect_client($sessionid, $term_cause = 1, $logoutReason = "LOGOUT") {
1220
	global $g, $config;
1221

    
1222
	$sessionid = SQLite3::escapeString($sessionid);
1223
	/* read database */
1224
	$result = captiveportal_read_db("WHERE sessionid = '{$sessionid}'");
1225

    
1226
	/* find entry */
1227
	if (!empty($result)) {
1228

    
1229
		foreach ($result as $cpentry) {
1230
			captiveportal_disconnect($cpentry, $term_cause);
1231
			captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "DISCONNECT");
1232
		}
1233
		captiveportal_remove_entries(array($sessionid));
1234
		unset($result);
1235
	}
1236
}
1237

    
1238
/* remove all clients */
1239
function captiveportal_disconnect_all($term_cause = 6, $logoutReason = "DISCONNECT", $carp_loop = false) {
1240
	global $g, $config, $cpzone, $cpzoneid;
1241

    
1242
	if (captiveportal_xmlrpc_sync_get_details($syncip, $syncport, $syncuser, $syncpass, $carp_loop)) {
1243
		$rpc_client = new pfsense_xmlrpc_client();
1244
		$rpc_client->setConnectionData($syncip, $syncport, $syncuser, $syncpass);
1245
		$rpc_client->set_noticefile("CaptivePortalUserSync");
1246
		$arguments = array(
1247
			'term_cause' => $term_cause,
1248
			'logout_reason' => $logoutReason
1249
		);
1250

    
1251
		$rpc_client->xmlrpc_method('captive_portal_sync',
1252
			array(
1253
				'op' => 'disconnect_all',
1254
				'zone' => $cpzone,
1255
				'arguments' => base64_encode(serialize($arguments))
1256
			)
1257
		);
1258
	}
1259
	/* check if we're pruning old entries and eventually wait */
1260
	$rcprunelock = try_lock("rcprunecaptiveportal{$cpzone}", 15);
1261

    
1262
	/* if we still don't have the lock, unlock forcefully and take it */
1263
	if (!$rcprunelock) {
1264
		log_error("CP zone ${cpzone}: could not obtain the lock for more than 15 seconds, lock taken forcefully to disconnect all users");
1265
		unlock_force("rcprunecaptiveportal{$cpzone}");
1266
		$rcprunelock = lock("rcprunecaptiveportal{$cpzone}", LOCK_EX);
1267
	}
1268

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

    
1272
	captiveportal_radius_stop_all($term_cause, $logoutReason);
1273

    
1274
	/* remove users from the database */
1275
	$cpdb = captiveportal_read_db();
1276
	$unsetindexes = array_column($cpdb,5);
1277
	if (!empty($unsetindexes)) {
1278
		// High Availability : do not sync removed entries
1279
		captiveportal_remove_entries($unsetindexes, true);
1280
	}
1281

    
1282
	/* reinit ipfw rules */
1283
	captiveportal_init_rules(true);
1284

    
1285
	unlock($cpdblck);
1286
	unlock($rcprunelock);
1287
	return true;
1288
}
1289

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

    
1294
	$cpdb = captiveportal_read_db();
1295

    
1296
	$radacct = isset($config['captiveportal'][$cpzone]['radacct_enable']) ? true : false;
1297
	foreach ($cpdb as $cpentry) {
1298
		if ($cpentry['authmethod'] === 'radius' && $radacct) {
1299
			if ($config['captiveportal'][$cpzone]['reauthenticateacct'] == "stopstartfreeradius") {
1300
				$session_time = (time() - $cpentry[0]) % 60;
1301
				$start_time = time() - $session_time;
1302
			} else {
1303
				$start_time = $cpentry[0];
1304
			}
1305
			captiveportal_send_server_accounting('stop',
1306
				$cpentry[1], // ruleno
1307
				$cpentry[4], // username
1308
				$cpentry[2], // clientip
1309
				$cpentry[3], // clientmac
1310
				$cpentry[5], // sessionid
1311
				$start_time, // start time
1312
				$stop_time, // stop time
1313
				$term_cause); // Acct-Terminate-Cause
1314
		}
1315
		captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], $logoutReason);
1316
	}
1317
	unset($cpdb);
1318
}
1319

    
1320
function captiveportal_passthrumac_configure_entry($macent, $pipeinrule = false) {
1321
	global $config, $g, $cpzone;
1322

    
1323
	$bwUp = 0;
1324
	if (!empty($macent['bw_up'])) {
1325
		$bwUp = $macent['bw_up'];
1326
	} elseif (isset($config['captiveportal'][$cpzone]['peruserbw']) &&
1327
	    !empty($config['captiveportal'][$cpzone]['bwdefaultup'])) {
1328
		$bwUp = $config['captiveportal'][$cpzone]['bwdefaultup'];
1329
	}
1330
	$bwDown = 0;
1331
	if (!empty($macent['bw_down'])) {
1332
		$bwDown = $macent['bw_down'];
1333
	} elseif (isset($config['captiveportal'][$cpzone]['peruserbw']) &&
1334
	    !empty($config['captiveportal'][$cpzone]['bwdefaultdn'])) {
1335
		$bwDown = $config['captiveportal'][$cpzone]['bwdefaultdn'];
1336
	}
1337

    
1338
	if ($macent['action'] == 'pass') {
1339
		$rules = "";
1340

    
1341
		$pipeno = captiveportal_get_next_dn_ruleno('pipe_mac');
1342

    
1343
		$pipeup = $pipeno;
1344
		if ($pipeinrule == true) {
1345
			$_gb = @pfSense_ipfw_pipe("pipe {$pipeno} config bw {$bwUp}Kbit/s queue 100 buckets 16");
1346
		} else {
1347
			$rules .= "pipe {$pipeno} config bw {$bwUp}Kbit/s queue 100 buckets 16\n";
1348
		}
1349

    
1350
		$pipedown = $pipeno + 1;
1351
		if ($pipeinrule == true) {
1352
			$_gb = @pfSense_ipfw_pipe("pipe {$pipedown} config bw {$bwDown}Kbit/s queue 100 buckets 16");
1353
		} else {
1354
			$rules .= "pipe {$pipedown} config bw {$bwDown}Kbit/s queue 100 buckets 16\n";
1355
		}
1356

    
1357
		$rules .= "table {$cpzone}_pipe_mac add any,{$macent['mac']} {$pipeup}\n";
1358
		$rules .= "table {$cpzone}_pipe_mac add {$macent['mac']},any {$pipedown}\n";
1359
	}
1360

    
1361
	return $rules;
1362
}
1363

    
1364
function captiveportal_passthrumac_delete_entry($macent) {
1365
	global $cpzone;
1366
	$rules = "";
1367

    
1368
	if ($macent['action'] == 'pass') {
1369
		$pipeno = captiveportal_get_dn_passthru_ruleno($macent['mac']);
1370

    
1371
		if (!empty($pipeno)) {
1372
			captiveportal_free_dn_ruleno($pipeno);
1373
			$rules .= "table {$cpzone}_pipe_mac delete any,{$macent['mac']}\n";
1374
			$rules .= "table {$cpzone}_pipe_mac delete {$macent['mac']},any\n";
1375
			$rules .= "pipe delete " . $pipeno . "\n";
1376
			$rules .= "pipe delete " . ++$pipeno . "\n";
1377
		}
1378
	}
1379

    
1380
	return $rules;
1381
}
1382

    
1383
function captiveportal_passthrumac_configure($filename = false, $startindex = 0, $stopindex = 0) {
1384
	global $config, $g, $cpzone;
1385

    
1386
	$rules = "";
1387

    
1388
	if (is_array($config['captiveportal'][$cpzone]['passthrumac'])) {
1389
		if ($stopindex > 0) {
1390
			$fd = fopen($filename, "w");
1391
			for ($idx = $startindex; $idx <= $stopindex; $idx++) {
1392
				if (isset($config['captiveportal'][$cpzone]['passthrumac'][$idx])) {
1393
					$rules = captiveportal_passthrumac_configure_entry($config['captiveportal'][$cpzone]['passthrumac'][$idx]);
1394
					fwrite($fd, $rules);
1395
				}
1396
			}
1397
			fclose($fd);
1398

    
1399
			return;
1400
		} else {
1401
			$nentries = count($config['captiveportal'][$cpzone]['passthrumac']);
1402
			if ($nentries > 2000) {
1403
				$nloops = $nentries / 1000;
1404
				$remainder= $nentries % 1000;
1405
				for ($i = 0; $i < $nloops; $i++) {
1406
					mwexec_bg("/usr/local/sbin/fcgicli -f /etc/rc.captiveportal_configure_mac -d \"cpzone={$cpzone}&startidx=" . ($i * 1000) . "&stopidx=" . ((($i+1) * 1000) - 1) . "\"");
1407
				}
1408
				if ($remainder > 0) {
1409
					mwexec_bg("/usr/local/sbin/fcgicli -f /etc/rc.captiveportal_configure_mac -d \"cpzone={$cpzone}&startidx=" . ($i * 1000) . "&stopidx=" . (($i* 1000) + $remainder) ."\"");
1410
				}
1411
			} else {
1412
				foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $macent) {
1413
					$rules .= captiveportal_passthrumac_configure_entry($macent, true);
1414
				}
1415
			}
1416
		}
1417
	}
1418

    
1419
	return $rules;
1420
}
1421

    
1422
function captiveportal_passthrumac_findbyname($username) {
1423
	global $config, $cpzone;
1424

    
1425
	if (is_array($config['captiveportal'][$cpzone]['passthrumac'])) {
1426
		foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $macent) {
1427
			if ($macent['username'] == $username) {
1428
				return $macent;
1429
			}
1430
		}
1431
	}
1432
	return NULL;
1433
}
1434

    
1435
/*
1436
 * table (3=IN)/(4=OUT) hold allowed ip's without bw limits
1437
 */
1438
function captiveportal_allowedip_configure_entry($ipent, $ishostname = false) {
1439
	global $g, $config, $cpzone;
1440

    
1441
	/*  Instead of copying this entire function for something
1442
	 *  easy such as hostname vs ip address add this check
1443
	 */
1444
	if ($ishostname === true) {
1445
		if (!platform_booting()) {
1446
			$ipaddress = resolve_host_addresses($ipent['hostname'], array(DNS_A), false);
1447
			if (empty($ipaddress)) {
1448
				return;
1449
			}
1450
		} else {
1451
			$ipaddress = array();
1452
		}
1453
	} else {
1454
		$ipaddress = array($ipent['ip']);
1455
	}
1456

    
1457
	$rules = "";
1458
	$cp_filterdns_conf = "";
1459
	$enBwup = 0;
1460
	if (!empty($ipent['bw_up'])) {
1461
		$enBwup = intval($ipent['bw_up']);
1462
	} elseif (isset($config['captiveportal'][$cpzone]['peruserbw']) &&
1463
	    !empty($config['captiveportal'][$cpzone]['bwdefaultup'])) {
1464
		$enBwup = $config['captiveportal'][$cpzone]['bwdefaultup'];
1465
	}
1466
	$enBwdown = 0;
1467
	if (!empty($ipent['bw_down'])) {
1468
		$enBwdown = intval($ipent['bw_down']);
1469
	} elseif (isset($config['captiveportal'][$cpzone]['peruserbw']) &&
1470
	    !empty($config['captiveportal'][$cpzone]['bwdefaultdn'])) {
1471
		$enBwdown = $config['captiveportal'][$cpzone]['bwdefaultdn'];
1472
	}
1473

    
1474
	$pipeup = captiveportal_get_next_dn_ruleno('allowed');
1475
	$_gb = @pfSense_ipfw_pipe("pipe {$pipeup} config bw {$enBwup}Kbit/s queue 100 buckets 16");
1476
	$pipedown = $pipeup + 1;
1477
	$_gb = @pfSense_ipfw_pipe("pipe {$pipedown} config bw {$enBwdown}Kbit/s queue 100 buckets 16");
1478

    
1479
	if ($ishostname === true) {
1480
		$cp_filterdns_conf .= "ipfw {$ipent['hostname']} {$cpzone}_allowed_up pipe {$pipeup}\n";
1481
		$cp_filterdns_conf .= "ipfw {$ipent['hostname']} {$cpzone}_allowed_down pipe {$pipedown}\n";
1482
		if (!is_ipaddr($ipaddress[0])) {
1483
			return array("", $cp_filterdns_conf);
1484
		}
1485
	}
1486

    
1487
	$subnet = "";
1488
	if (!empty($ipent['sn'])) {
1489
		$subnet = "/{$ipent['sn']}";
1490
	}
1491
	foreach ($ipaddress as $ip) {
1492
		$rules .= "table {$cpzone}_allowed_up add {$ip}{$subnet} {$pipeup}\n";
1493
		$rules .= "table {$cpzone}_allowed_down add {$ip}{$subnet} {$pipedown}\n";
1494
	}
1495

    
1496
	if ($ishostname === true) {
1497
		return array($rules, $cp_filterdns_conf);
1498
	} else {
1499
		return $rules;
1500
	}
1501
}
1502

    
1503
function captiveportal_allowedhostname_configure() {
1504
	global $config, $g, $cpzone, $cpzoneid;
1505

    
1506
	$rules = "";
1507
	if (!is_array($config['captiveportal'][$cpzone]['allowedhostname'])) {
1508
		return $rules;
1509
	}
1510

    
1511
	$rules = "\n# captiveportal_allowedhostname_configure()\n";
1512
	$cp_filterdns_conf = "";
1513
	foreach ($config['captiveportal'][$cpzone]['allowedhostname'] as $hostnameent) {
1514
		$tmprules = captiveportal_allowedip_configure_entry($hostnameent, true);
1515
		$rules .= $tmprules[0];
1516
		$cp_filterdns_conf .= $tmprules[1];
1517
	}
1518
	$cp_filterdns_filename = "{$g['varetc_path']}/filterdns-{$cpzone}-captiveportal.conf";
1519
	@file_put_contents($cp_filterdns_filename, $cp_filterdns_conf);
1520
	unset($cp_filterdns_conf);
1521

    
1522
	return $rules;
1523
}
1524

    
1525
function captiveportal_filterdns_configure() {
1526
	global $config, $g, $cpzone, $cpzoneid;
1527

    
1528
	$cp_filterdns_filename = $g['varetc_path'] .
1529
	    "/filterdns-{$cpzone}-captiveportal.conf";
1530

    
1531
	if (isset($config['captiveportal'][$cpzone]['enable']) &&
1532
	    is_array($config['captiveportal'][$cpzone]['allowedhostname']) &&
1533
	    file_exists($cp_filterdns_filename)) {
1534
		if (isvalidpid($g['varrun_path'] .
1535
		    "/filterdns-{$cpzone}-cpah.pid")) {
1536
			sigkillbypid($g['varrun_path'] .
1537
			    "/filterdns-{$cpzone}-cpah.pid", "HUP");
1538
		} else {
1539
			mwexec("/usr/local/sbin/filterdns -p " .
1540
			    "{$g['varrun_path']}/filterdns-{$cpzone}-cpah.pid" .
1541
			    " -i 300 -c {$cp_filterdns_filename} -d 1");
1542
		}
1543
	} else {
1544
		killbypid("{$g['varrun_path']}/filterdns-{$cpzone}-cpah.pid");
1545
		@unlink("{$g['varrun_path']}/filterdns-{$cpzone}-cpah.pid");
1546
	}
1547

    
1548
	return $rules;
1549
}
1550

    
1551
function captiveportal_allowedip_configure() {
1552
	global $config, $g, $cpzone;
1553

    
1554
	$rules = "";
1555
	if (is_array($config['captiveportal'][$cpzone]['allowedip'])) {
1556
		foreach ($config['captiveportal'][$cpzone]['allowedip'] as $ipent) {
1557
			$rules .= captiveportal_allowedip_configure_entry($ipent);
1558
		}
1559
	}
1560

    
1561
	return $rules;
1562
}
1563

    
1564
/* get last activity timestamp given client IP address */
1565
function captiveportal_get_last_activity($ip) {
1566
	global $cpzone;
1567

    
1568
	/* Reading only from one of the tables is enough of approximation. */
1569
	$tables = array("{$cpzone}_allowed_up", "{$cpzone}_auth_up");
1570

    
1571
	foreach ($tables as $table) {
1572
		$ipfw = pfSense_ipfw_table_lookup($table, $ip);
1573
		if (is_array($ipfw)) {
1574
			/* Workaround for #46652 */
1575
			if ($ipfw['packets'] > 0) {
1576
				return $ipfw['timestamp'];
1577
			} else {
1578
				return 0;
1579
			}
1580
		}
1581
	}
1582

    
1583
	return 0;
1584
}
1585

    
1586

    
1587
/* log successful captive portal authentication to syslog */
1588
/* part of this code from php.net */
1589
function captiveportal_logportalauth($user, $mac, $ip, $status, $message = null) {
1590
	// Log it
1591
	if (!$message) {
1592
		$message = "{$status}: {$user}, {$mac}, {$ip}";
1593
	} else {
1594
		$message = trim($message);
1595
		$message = "{$status}: {$user}, {$mac}, {$ip}, {$message}";
1596
	}
1597
	captiveportal_syslog($message);
1598
}
1599

    
1600
/* log simple messages to syslog */
1601
function captiveportal_syslog($message) {
1602
	global $cpzone;
1603

    
1604
	$message = trim($message);
1605
	$message = "Zone: {$cpzone} - {$message}";
1606
	openlog("logportalauth", LOG_PID, LOG_LOCAL4);
1607
	// Log it
1608
	syslog(LOG_INFO, $message);
1609
	closelog();
1610
}
1611

    
1612
/* Authenticate users using Authentication Backend */
1613
function captiveportal_authenticate_user(&$login = '', &$password = '', $clientmac = '', $clientip = '', $pipeno = 'null', $context = 'first') {
1614
	global $g, $config, $cpzone;
1615
	$cpcfg = $config['captiveportal'][$cpzone];
1616

    
1617
	$login_status = 'FAILURE';
1618
	$login_msg = gettext('Invalid credentials specified');
1619
	$reply_attributes = array();
1620
	$auth_method = '';
1621
	$auth_result = null;
1622

    
1623
	/*
1624
	Management of the reply Message (reason why the authentication failed) :
1625
	multiple authentication servers can be used, so multiple reply messages could theoretically be returned.
1626
	But only one message is returned (the most important one).
1627
	The return value of authenticate_user() define how important messages are :
1628
		- Reply message of a successful auth is more important than reply message of
1629
		a user failed auth(invalid credentials/authorization)
1630

    
1631
		- Reply message of a user failed auth is more important than reply message of
1632
		a server failed auth (unable to contact server)
1633

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

    
1637
	The $authlevel variable is a flag indicating the status of authentication
1638
	0 = failed server auth
1639
	1 = failed user auth
1640
	2 = failed user auth with custom server reply recieved
1641
	3 = successful auth
1642
	*/
1643
	$authlevel = 0;
1644

    
1645
	/* Getting authentication servers from captiveportal configuration */
1646
	$auth_servers = array();
1647

    
1648
	if ($cpcfg['auth_method'] === 'none') {
1649
		$auth_servers[] = array('type' => 'none');
1650
	} else {
1651
		if ($context === 'second') {
1652
			$fullauthservers = explode(",", $cpcfg['auth_server2']);
1653
		} else {
1654
			$fullauthservers = explode(",", $cpcfg['auth_server']);
1655
		}
1656

    
1657
		foreach ($fullauthservers as $authserver) {
1658
			if (strpos($authserver, ' - ') !== false) {
1659
				$authserver = explode(' - ', $authserver);
1660
				array_shift($authserver);
1661
				$authserver = implode(' - ', $authserver);
1662

    
1663
				if (auth_get_authserver($authserver) !== null) {
1664
					$auth_servers[] = auth_get_authserver($authserver);
1665
				} else {
1666
					log_error("Zone: {$cpzone} - Captive portal was unable to find the settings of the server '{$authserver}' used for authentication !");
1667
				}
1668
			}
1669
		}
1670
	}
1671

    
1672
	/* Unable to find the any authentication server config - shouldn't happen! - bail out */
1673
	if (count($auth_servers) === 0) {
1674
		log_error("Zone: {$cpzone} - No valid server could be used for authentication.");
1675
		$login_msg = gettext("Internal Error");
1676
	} else {
1677
		foreach ($auth_servers as $authcfg) {
1678
			if ($authlevel < 3) {
1679
				$radmac_error = false;
1680
				$attributes = array("nas_identifier" => empty($cpcfg["radiusnasid"]) ? "CaptivePortal-{$cpzone}" : $cpcfg["radiusnasid"],
1681
					"nas_port_type" => RADIUS_ETHERNET,
1682
					"nas_port" => $pipeno,
1683
					"framed_ip" => $clientip);
1684
				if (mac_format($clientmac) !== null) {
1685
					$attributes["calling_station_id"] = mac_format($clientmac);
1686
				}
1687

    
1688
				$result = null;
1689
				$status = null;
1690
				$msg = null;
1691

    
1692
				/* Radius MAC authentication */
1693
				if ($context === 'radmac' && $clientmac) {
1694
					if ($authcfg['type'] === 'radius') {
1695
						$login = mac_format($clientmac);
1696
						$status = "MACHINE LOGIN";
1697
					} else {
1698
						/* Trying to perform a Radius MAC authentication on a non-radius server - shouldn't happen! - bail out */
1699
						$msg = gettext("Internal Error");
1700
						log_error("Zone: {$cpzone} - Trying to perform RADIUS MAC authentication on a non-RADIUS server !");
1701
						$radmac_error = true;
1702
						$result = null;
1703
					}
1704
				}
1705

    
1706
				if (!$radmac_error) {
1707
					if ($authcfg['type'] === 'none') {
1708
						$result = true;
1709
					} else {
1710
						$result = authenticate_user($login, $password, $authcfg, $attributes);
1711
					}
1712

    
1713
					if (!empty($attributes['error_message'])) {
1714
						$msg = $attributes['error_message'];
1715
					}
1716

    
1717
					if ($authcfg['type'] == 'Local Auth' && $result && isset($cpcfg['localauth_priv'])) {
1718
						if (!userHasPrivilege(getUserEntry($login), "user-services-captiveportal-login")) {
1719
							$result = false;
1720
							$msg = gettext("Access Denied");
1721
						}
1722
					}
1723
					if ($context === 'radmac' && $result === null && empty($attributes['reply_message'])) {
1724
						$msg = gettext("RADIUS MAC Authentication Failed.");
1725
					}
1726

    
1727
					if (empty($status)) {
1728
						if ($result === true) {
1729
							$status = "ACCEPT";
1730
						} elseif ($result === null) {
1731
							$status = "ERROR";
1732
						} else {
1733
							$status = "FAILURE";
1734
						}
1735
					}
1736

    
1737
					if ($context === 'radmac' && $login == mac_format($clientmac) || $authcfg['type'] === 'none' && empty($login)) {
1738
						$login = "unauthenticated";
1739
					}
1740
				}
1741
				// We determine a flag
1742
				if ($result === true) {
1743
					$val = 3;
1744
				} elseif ($result === false && !empty($attributes['reply_message'])) {
1745
					$val = 2;
1746
					$msg = $attributes['reply_message'];
1747
				} elseif ($result === false) {
1748
					$val = 1;
1749
				} elseif ($result === null) {
1750
					$val = 0;
1751
				}
1752

    
1753
				if ($val >= $authlevel) {
1754
					$authlevel = $val;
1755
					$auth_method = $authcfg['type'];
1756
					$login_status = $status;
1757
					$login_msg = $msg;
1758
					$reply_attributes = $attributes;
1759
					$auth_result = $result;
1760
				}
1761
			}
1762
		}
1763
	}
1764

    
1765
	return array('result'=>$auth_result, 'attributes'=>$reply_attributes, 'auth_method' =>$auth_method, 'login_status'=> $login_status, 'login_message' => $login_msg);
1766
}
1767

    
1768
function captiveportal_opendb() {
1769
	global $g, $config, $cpzone, $cpzoneid;
1770

    
1771
	$db_path = "{$g['vardb_path']}/captiveportal{$cpzone}.db";
1772
	$createquery = "CREATE TABLE IF NOT EXISTS captiveportal (" .
1773
				"allow_time INTEGER, pipeno INTEGER, ip TEXT, mac TEXT, username TEXT, " .
1774
				"sessionid TEXT, bpassword TEXT, session_timeout INTEGER, idle_timeout INTEGER, " .
1775
				"session_terminate_time INTEGER, interim_interval INTEGER, traffic_quota INTEGER, " .
1776
				"bw_up INTEGER, bw_down INTEGER, authmethod TEXT, context TEXT); " .
1777
			"CREATE UNIQUE INDEX IF NOT EXISTS idx_active ON captiveportal (sessionid, username); " .
1778
			"CREATE INDEX IF NOT EXISTS user ON captiveportal (username); " .
1779
			"CREATE INDEX IF NOT EXISTS ip ON captiveportal (ip); " .
1780
			"CREATE INDEX IF NOT EXISTS starttime ON captiveportal (allow_time)";
1781

    
1782
	try {
1783
		$DB = new SQLite3($db_path);
1784
		$DB->busyTimeout(60000);
1785
	} catch (Exception $e) {
1786
		captiveportal_syslog("Could not open {$db_path} as an sqlite database for {$cpzone}. Error message: " . $e->getMessage() . " -- Trying again.");
1787
		unlink_if_exists($db_path);
1788
		try {
1789
			$DB = new SQLite3($db_path);
1790
			$DB->busyTimeout(60000);
1791
		} catch (Exception $e) {
1792
			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.");
1793
			return;
1794
		}
1795
	}
1796

    
1797
	if (!$DB) {
1798
		captiveportal_syslog("Could not open {$db_path} as an sqlite database for {$cpzone}. Error message: {$DB->lastErrorMsg()}. Trying again.");
1799
		unlink_if_exists($db_path);
1800
		$DB = new SQLite3($db_path);
1801
		$DB->busyTimeout(60000);
1802
		if (!$DB) {
1803
			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.");
1804
			return;
1805
		}
1806
	}
1807

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

    
1811
		/* If unable to initialize the database, reset and try again. */
1812
		$DB->close();
1813
		unset($DB);
1814
		unlink_if_exists($db_path);
1815
		$DB = new SQLite3($db_path);
1816
		$DB->busyTimeout(60000);
1817
		if ($DB->exec($createquery)) {
1818
			captiveportal_syslog("Successfully reinitialized tables for {$cpzone} -- database has been reset.");
1819
			if (!is_numericint($cpzoneid)) {
1820
				if (is_array($config['captiveportal'])) {
1821
					foreach ($config['captiveportal'] as $cpkey => $cp) {
1822
						if ($cpzone == $cpkey) {
1823
							$cpzoneid = $cp['zoneid'];
1824
						}
1825
					}
1826
				}
1827
			}
1828
			if (is_numericint($cpzoneid)) {
1829
				$table_names = captiveportal_get_ipfw_table_names();
1830
				foreach ($table_names as $table_name) {
1831
					mwexec("/sbin/ipfw table {$table_name} flush");
1832
				}
1833
				captiveportal_syslog("Flushed tables for {$cpzone} after database reset.");
1834
			}
1835
		} else {
1836
			captiveportal_syslog("Still unable to create tables for {$cpzone}. Error message: {$DB->lastErrorMsg()}. Remove the database file manually and try again.");
1837
		}
1838
	}
1839

    
1840
	return $DB;
1841
}
1842

    
1843
/* Get all tables for specific cpzone */
1844
function captiveportal_get_ipfw_table_names() {
1845
	global $cpzone;
1846

    
1847
	$result = array();
1848
	$tables = pfSense_ipfw_tables_list();
1849

    
1850
	if (!is_array($tables)) {
1851
		return $result;
1852
	}
1853

    
1854
	$len = strlen($cpzone) + 1;
1855
	foreach ($tables as $table) {
1856
		if (substr($table['name'], 0, $len) != $cpzone . '_') {
1857
			continue;
1858
		}
1859

    
1860
		$result[] = $table['name'];
1861
	}
1862

    
1863
	return $result;
1864
}
1865

    
1866
/* read captive portal DB into array */
1867
function captiveportal_read_db($query = "") {
1868
	$cpdb = array();
1869

    
1870
	$DB = captiveportal_opendb();
1871
	if ($DB) {
1872
		$response = $DB->query("SELECT * FROM captiveportal {$query}");
1873
		if ($response != FALSE) {
1874
			while ($row = $response->fetchArray()) {
1875
				$cpdb[] = $row;
1876
			}
1877
		}
1878
		$DB->close();
1879
	}
1880

    
1881
	return $cpdb;
1882
}
1883

    
1884
function captiveportal_remove_entries($remove, $carp_loop = false) {
1885
	global $cpzone;
1886

    
1887
	if (!is_array($remove) || empty($remove)) {
1888
		return;
1889
	}
1890

    
1891
	$query = "DELETE FROM captiveportal WHERE sessionid in (";
1892
	foreach ($remove as $idx => $unindex) {
1893
		$query .= "'{$unindex}'";
1894
		if ($idx < (count($remove) - 1)) {
1895
			$query .= ",";
1896
		}
1897
	}
1898
	$query .= ")";
1899
	captiveportal_write_db($query);
1900

    
1901
	if (captiveportal_xmlrpc_sync_get_details($syncip, $syncport, $syncuser, $syncpass, $carp_loop)) {
1902
		$rpc_client = new pfsense_xmlrpc_client();
1903
		$rpc_client->setConnectionData($syncip, $syncport, $syncuser, $syncpass);
1904
		$rpc_client->set_noticefile("CaptivePortalUserSync");
1905
		$rpc_client->xmlrpc_method('captive_portal_sync',
1906
			array(
1907
				'op' => 'remove_entries',
1908
				'zone' => $cpzone,
1909
				'entries' => base64_encode(serialize($remove))
1910
			)
1911
		);
1912
	}
1913
	return true;
1914
}
1915

    
1916
/* write captive portal DB */
1917
function captiveportal_write_db($queries) {
1918
	global $g;
1919

    
1920
	if (is_array($queries)) {
1921
		$query = implode(";", $queries);
1922
	} else {
1923
		$query = $queries;
1924
	}
1925

    
1926
	$DB = captiveportal_opendb();
1927
	if ($DB) {
1928
		$DB->exec("BEGIN TRANSACTION");
1929
		$result = $DB->exec($query);
1930
		if (!$result) {
1931
			captiveportal_syslog("Trying to modify DB returned error: {$DB->lastErrorMsg()}");
1932
		} else {
1933
			$DB->exec("END TRANSACTION");
1934
		}
1935
		$DB->close();
1936
		return $result;
1937
	} else {
1938
		return true;
1939
	}
1940
}
1941

    
1942
function captiveportal_write_elements() {
1943
	global $g, $config, $cpzone;
1944

    
1945
	$cpcfg = $config['captiveportal'][$cpzone];
1946

    
1947
	if (!is_dir($g['captiveportal_element_path'])) {
1948
		@mkdir($g['captiveportal_element_path']);
1949
	}
1950

    
1951
	if (is_array($cpcfg['element'])) {
1952
		foreach ($cpcfg['element'] as $data) {
1953
			/* Do not attempt to decode or write out empty files. */
1954
			if (isset($data['nocontent'])) {
1955
					continue;
1956
			}
1957
			if (empty($data['content']) || empty(base64_decode($data['content']))) {
1958
				unlink_if_exists("{$g['captiveportal_element_path']}/{$data['name']}");
1959
				touch("{$g['captiveportal_element_path']}/{$data['name']}");
1960
			} elseif (!@file_put_contents("{$g['captiveportal_element_path']}/{$data['name']}", base64_decode($data['content']))) {
1961
				printf(gettext('Error: cannot open \'%1$s\' in captiveportal_write_elements()%2$s'), $data['name'], "\n");
1962
				return 1;
1963
			}
1964
			if (!file_exists("{$g['captiveportal_path']}/{$data['name']}")) {
1965
				@symlink("{$g['captiveportal_element_path']}/{$data['name']}", "{$g['captiveportal_path']}/{$data['name']}");
1966
			}
1967
		}
1968
	}
1969

    
1970
	return 0;
1971
}
1972

    
1973
function captiveportal_free_dnrules($rulenos_start = 2000,
1974
    $rulenos_range_max = 64500, $dry_run = false, $clear_auth_pipes = true) {
1975
	global $g, $cpzone;
1976

    
1977
	$removed_pipes = array();
1978

    
1979
	if (!file_exists("{$g['vardb_path']}/captiveportaldn.rules")) {
1980
		return $removed_pipes;
1981
	}
1982

    
1983
	if (!$dry_run) {
1984
		$cpruleslck = lock("captiveportalrulesdn", LOCK_EX);
1985
	}
1986

    
1987
	$rules = unserialize(file_get_contents(
1988
	    "{$g['vardb_path']}/captiveportaldn.rules"));
1989
	$ridx = $rulenos_start;
1990
	while ($ridx < $rulenos_range_max) {
1991
		if (substr($rules[$ridx], 0, strlen($cpzone . '_')) == $cpzone . '_') {
1992
			if (!$clear_auth_pipes && substr($rules[$ridx], 0, strlen($cpzone . '_auth')) == $cpzone . '_auth') {
1993
				$ridx += 2;
1994
			} else {
1995
				if (!$dry_run) {
1996
					$rules[$ridx] = false;
1997
				}
1998
				$removed_pipes[] = $ridx;
1999
				$ridx++;
2000
				if (!$dry_run) {
2001
					$rules[$ridx] = false;
2002
				}
2003
				$removed_pipes[] = $ridx;
2004
				$ridx++;
2005
			}
2006
		} else {
2007
			$ridx += 2;
2008
		}
2009
	}
2010

    
2011
	if (!$dry_run) {
2012
		file_put_contents("{$g['vardb_path']}/captiveportaldn.rules",
2013
		    serialize($rules));
2014
		unlock($cpruleslck);
2015
	}
2016

    
2017
	unset($rules);
2018

    
2019
	return $removed_pipes;
2020
}
2021

    
2022
function captiveportal_reserve_ruleno($ruleno) {
2023
	global $g, $cpzone;
2024

    
2025
	$cpruleslck = lock("captiveportalrulesdn", LOCK_EX);
2026
	if (file_exists("{$g['vardb_path']}/captiveportaldn.rules")) {
2027
		$rules = unserialize(file_get_contents("{$g['vardb_path']}/captiveportaldn.rules"));
2028
	} else {
2029
		$rules = array_pad(array(), 64500, false);
2030
	}
2031
	$rules[$ruleno] = $cpzone . '_auth';
2032
	$ruleno++;
2033
	$rules[$ruleno] = $cpzone . '_auth';
2034

    
2035
	file_put_contents("{$g['vardb_path']}/captiveportaldn.rules", serialize($rules));
2036
	unlock($cpruleslck);
2037
	unset($rules);
2038

    
2039
	return $ruleno;
2040
}
2041

    
2042
function captiveportal_get_next_dn_ruleno($rule_type = 'default', $rulenos_start = 2000, $rulenos_range_max = 64500) {
2043
	global $config, $g, $cpzone;
2044

    
2045
	$cpruleslck = lock("captiveportalrulesdn", LOCK_EX);
2046
	$ruleno = 0;
2047
	if (file_exists("{$g['vardb_path']}/captiveportaldn.rules")) {
2048
		$rules = unserialize(file_get_contents("{$g['vardb_path']}/captiveportaldn.rules"));
2049
		$ridx = $rulenos_start;
2050
		while ($ridx < $rulenos_range_max) {
2051
			if (empty($rules[$ridx])) {
2052
				$ruleno = $ridx;
2053
				$rules[$ridx] = $cpzone . '_' . $rule_type;
2054
				$ridx++;
2055
				$rules[$ridx] = $cpzone . '_' . $rule_type;
2056
				break;
2057
			} else {
2058
				$ridx += 2;
2059
			}
2060
		}
2061
	} else {
2062
		$rules = array_pad(array(), $rulenos_range_max, false);
2063
		$ruleno = $rulenos_start;
2064
		$rules[$rulenos_start] = $cpzone . '_' . $rule_type;
2065
		$rulenos_start++;
2066
		$rules[$rulenos_start] = $cpzone . '_' . $rule_type;
2067
	}
2068
	file_put_contents("{$g['vardb_path']}/captiveportaldn.rules", serialize($rules));
2069
	unlock($cpruleslck);
2070
	unset($rules);
2071

    
2072
	return $ruleno;
2073
}
2074

    
2075
function captiveportal_free_dn_ruleno($ruleno) {
2076
	global $config, $g;
2077

    
2078
	$cpruleslck = lock("captiveportalrulesdn", LOCK_EX);
2079
	if (file_exists("{$g['vardb_path']}/captiveportaldn.rules")) {
2080
		$rules = unserialize(file_get_contents("{$g['vardb_path']}/captiveportaldn.rules"));
2081
		$rules[$ruleno] = false;
2082
		$ruleno++;
2083
		$rules[$ruleno] = false;
2084
		file_put_contents("{$g['vardb_path']}/captiveportaldn.rules", serialize($rules));
2085
		unset($rules);
2086
	}
2087
	unlock($cpruleslck);
2088
}
2089

    
2090
function captiveportal_get_dn_passthru_ruleno($value) {
2091
	global $config, $g, $cpzone, $cpzoneid;
2092

    
2093
	$cpcfg = $config['captiveportal'][$cpzone];
2094
	if (!isset($cpcfg['enable'])) {
2095
		return NULL;
2096
	}
2097

    
2098
	$cpruleslck = lock("captiveportalrulesdn", LOCK_EX);
2099
	$ruleno = NULL;
2100
	if (file_exists("{$g['vardb_path']}/captiveportaldn.rules")) {
2101
		unset($output);
2102
		$item = pfSense_ipfw_table_lookup("{$cpzone}_pipe_mac",
2103
		    "any,{$value}");
2104
		if (!is_array($item) || empty($item['pipe'])) {
2105
			unlock($cpruleslck);
2106
			return NULL;
2107
		}
2108

    
2109
		$ruleno = intval($item['pipe']);
2110
		$rules = unserialize(file_get_contents("{$g['vardb_path']}/captiveportaldn.rules"));
2111
		if (!$rules[$ruleno]) {
2112
			$ruleno = NULL;
2113
		}
2114
		unset($rules);
2115
	}
2116
	unlock($cpruleslck);
2117

    
2118
	return $ruleno;
2119
}
2120

    
2121
/**
2122
 * This function will calculate the traffic produced by a client
2123
 * based on its firewall rule
2124
 *
2125
 * Point of view: NAS
2126
 *
2127
 * Input means: from the client
2128
 * Output means: to the client
2129
 *
2130
 */
2131

    
2132
function getVolume($ip) {
2133
	global $config, $cpzone;
2134

    
2135
	$reverse = isset($config['captiveportal'][$cpzone]['reverseacct'])
2136
	    ? true : false;
2137
	$volume = array();
2138
	// Initialize vars properly, since we don't want NULL vars
2139
	$volume['input_pkts'] = $volume['input_bytes'] = 0;
2140
	$volume['output_pkts'] = $volume['output_bytes'] = 0;
2141

    
2142
	$tables = array("allowed", "auth");
2143

    
2144
	foreach($tables as $table) {
2145
		$ipfw = pfSense_ipfw_table_lookup("{$cpzone}_{$table}_up", $ip);
2146
		if (!is_array($ipfw)) {
2147
			continue;
2148
		}
2149
		if ($reverse) {
2150
			$volume['output_pkts'] = $ipfw['packets'];
2151
			$volume['output_bytes'] = $ipfw['bytes'];
2152
		} else {
2153
			$volume['input_pkts'] = $ipfw['packets'];
2154
			$volume['input_bytes'] = $ipfw['bytes'];
2155
		}
2156
	}
2157

    
2158
	foreach($tables as $table) {
2159
		$ipfw = pfSense_ipfw_table_lookup("{$cpzone}_{$table}_down",
2160
		    $ip);
2161
		if (!is_array($ipfw)) {
2162
			continue;
2163
		}
2164
		if ($reverse) {
2165
			$volume['input_pkts'] = $ipfw['packets'];
2166
			$volume['input_bytes'] = $ipfw['bytes'];
2167
		} else {
2168
			$volume['output_pkts'] = $ipfw['packets'];
2169
			$volume['output_bytes'] = $ipfw['bytes'];
2170
		}
2171
	}
2172

    
2173
	return $volume;
2174
}
2175

    
2176
function portal_ip_from_client_ip($cliip) {
2177
	global $config, $cpzone;
2178

    
2179
	$isipv6 = is_ipaddrv6($cliip);
2180
	$interfaces = explode(",", $config['captiveportal'][$cpzone]['interface']);
2181
	foreach ($interfaces as $cpif) {
2182
		if ($isipv6) {
2183
			$ip = get_interface_ipv6($cpif);
2184
			$sn = get_interface_subnetv6($cpif);
2185
		} else {
2186
			$ip = get_interface_ip($cpif);
2187
			$sn = get_interface_subnet($cpif);
2188
		}
2189
		if (ip_in_subnet($cliip, "{$ip}/{$sn}")) {
2190
			return $ip;
2191
		}
2192
	}
2193

    
2194
	$route = route_get($cliip);
2195
	if (empty($route)) {
2196
		return false;
2197
	}
2198

    
2199
	$iface = $route[0]['interface-name'];
2200
	if (!empty($iface)) {
2201
		$ip = ($isipv6) ? find_interface_ipv6($iface)
2202
		    : find_interface_ip($iface);
2203
		if (is_ipaddr($ip)) {
2204
			return $ip;
2205
		}
2206
	}
2207

    
2208
	// doesn't match up to any particular interface
2209
	// so let's set the portal IP to what PHP says
2210
	// the server IP issuing the request is.
2211
	// allows same behavior as 1.2.x where IP isn't
2212
	// in the subnet of any CP interface (static routes, etc.)
2213
	// rather than forcing to DNS hostname resolution
2214
	$ip = $_SERVER['SERVER_ADDR'];
2215
	if (is_ipaddr($ip)) {
2216
		return $ip;
2217
	}
2218

    
2219
	return false;
2220
}
2221

    
2222
function portal_hostname_from_client_ip($cliip) {
2223
	global $config, $cpzone;
2224

    
2225
	$cpcfg = $config['captiveportal'][$cpzone];
2226

    
2227
	if (isset($cpcfg['httpslogin'])) {
2228
		$listenporthttps = $cpcfg['listenporthttps'] ? $cpcfg['listenporthttps'] : ($cpcfg['zoneid'] + 8001);
2229
		$ourhostname = $cpcfg['httpsname'];
2230

    
2231
		if ($listenporthttps != 443) {
2232
			$ourhostname .= ":" . $listenporthttps;
2233
		}
2234
	} else {
2235
		$listenporthttp = $cpcfg['listenporthttp'] ? $cpcfg['listenporthttp'] : ($cpcfg['zoneid'] + 8000);
2236
		$ifip = portal_ip_from_client_ip($cliip);
2237
		if (!$ifip) {
2238
			$ourhostname = "{$config['system']['hostname']}.{$config['system']['domain']}";
2239
		} else {
2240
			$ourhostname = (is_ipaddrv6($ifip)) ? "[{$ifip}]" : "{$ifip}";
2241
		}
2242

    
2243
		if ($listenporthttp != 80) {
2244
			$ourhostname .= ":" . $listenporthttp;
2245
		}
2246
	}
2247

    
2248
	return $ourhostname;
2249
}
2250

    
2251
/* functions move from index.php */
2252

    
2253
function portal_reply_page($redirurl, $type = null, $message = null, $clientmac = null, $clientip = null, $username = null, $password = null, $voucher = null) {
2254
	global $g, $config, $cpzone;
2255

    
2256
	/* Get captive portal layout */
2257
	if ($type == "redir") {
2258
		header("Location: {$redirurl}");
2259
		return;
2260
	} else if ($type == "login") {
2261
		$htmltext = get_include_contents("{$g['varetc_path']}/captiveportal_{$cpzone}.html");
2262
	} else {
2263
		$htmltext = get_include_contents("{$g['varetc_path']}/captiveportal-{$cpzone}-error.html");
2264
	}
2265

    
2266
	$cpcfg = $config['captiveportal'][$cpzone];
2267

    
2268
	/* substitute the PORTAL_REDIRURL variable */
2269
	if ($cpcfg['preauthurl']) {
2270
		$htmltext = str_replace("\$PORTAL_REDIRURL\$", "{$cpcfg['preauthurl']}", $htmltext);
2271
		$htmltext = str_replace("#PORTAL_REDIRURL#", "{$cpcfg['preauthurl']}", $htmltext);
2272
	}
2273

    
2274
	/* substitute other variables */
2275
	$ourhostname = portal_hostname_from_client_ip($clientip);
2276
	$protocol = (isset($cpcfg['httpslogin'])) ? 'https://' : 'http://';
2277
	$htmltext = str_replace("\$PORTAL_ACTION\$", "{$protocol}{$ourhostname}/index.php?zone={$cpzone}", $htmltext);
2278
	$htmltext = str_replace("#PORTAL_ACTION#", "{$protocol}{$ourhostname}/index.php?zone={$cpzone}", $htmltext);
2279

    
2280
	$htmltext = str_replace("\$PORTAL_ZONE\$", htmlspecialchars($cpzone), $htmltext);
2281
	$htmltext = str_replace("\$PORTAL_REDIRURL\$", htmlspecialchars($redirurl), $htmltext);
2282
	$htmltext = str_replace("\$PORTAL_MESSAGE\$", htmlspecialchars($message), $htmltext);
2283
	$htmltext = str_replace("\$CLIENT_MAC\$", htmlspecialchars($clientmac), $htmltext);
2284
	$htmltext = str_replace("\$CLIENT_IP\$", htmlspecialchars($clientip), $htmltext);
2285

    
2286
	// Special handling case for captive portal master page so that it can be ran
2287
	// through the PHP interpreter using the include method above.  We convert the
2288
	// $VARIABLE$ case to #VARIABLE# in /etc/inc/captiveportal.inc before writing out.
2289
	$htmltext = str_replace("#PORTAL_ZONE#", htmlspecialchars($cpzone), $htmltext);
2290
	$htmltext = str_replace("#PORTAL_REDIRURL#", htmlspecialchars($redirurl), $htmltext);
2291
	$htmltext = str_replace("#PORTAL_MESSAGE#", htmlspecialchars($message), $htmltext);
2292
	$htmltext = str_replace("#CLIENT_MAC#", htmlspecialchars($clientmac), $htmltext);
2293
	$htmltext = str_replace("#CLIENT_IP#", htmlspecialchars($clientip), $htmltext);
2294
	$htmltext = str_replace("#USERNAME#", htmlspecialchars($username), $htmltext);
2295
	$htmltext = str_replace("#PASSWORD#", htmlspecialchars($password), $htmltext);
2296
	$htmltext = str_replace("#VOUCHER#", htmlspecialchars($voucher), $htmltext);
2297

    
2298
	echo $htmltext;
2299
}
2300

    
2301
function captiveportal_reapply_attributes($cpentry, $attributes) {
2302
	global $config, $cpzone, $g;
2303

    
2304
	if (isset($config['captiveportal'][$cpzone]['peruserbw'])) {
2305
		$dwfaultbw_up = !empty($config['captiveportal'][$cpzone]['bwdefaultup']) ? $config['captiveportal'][$cpzone]['bwdefaultup'] : 0;
2306
		$dwfaultbw_down = !empty($config['captiveportal'][$cpzone]['bwdefaultdn']) ? $config['captiveportal'][$cpzone]['bwdefaultdn'] : 0;
2307
	} else {
2308
		$dwfaultbw_up = $dwfaultbw_down = 0;
2309
	}
2310
	/* pipe throughputs must always be an integer, enforce that restriction again here. */
2311
	if (isset($config['captiveportal'][$cpzone]['radiusperuserbw'])) {
2312
		$bw_up = round(!empty($attributes['bw_up']) ? intval($attributes['bw_up'])/1000 : $dwfaultbw_up, 0);
2313
		$bw_down = round(!empty($attributes['bw_down']) ? intval($attributes['bw_down'])/1000 : $dwfaultbw_down, 0);
2314
	} else {
2315
		$bw_up = round($dwfaultbw_up,0);
2316
		$bw_down = round($dwfaultbw_down,0);
2317
	}
2318

    
2319
	$bw_up_pipeno = $cpentry[1];
2320
	$bw_down_pipeno = $cpentry[1]+1;
2321

    
2322
	if ($cpentry['bw_up'] !== $bw_up) {
2323
		$_gb = @pfSense_ipfw_pipe("pipe {$bw_up_pipeno} config bw {$bw_up}Kbit/s queue 100 buckets 16");
2324
		captiveportal_update_entry($cpentry['sessionid'], $bw_up, 'bw_up');
2325
	}
2326
	if ($cpentry['bw_down'] !== $bw_down) {
2327
		$_gb = @pfSense_ipfw_pipe("pipe {$bw_down_pipeno} config bw {$bw_down}Kbit/s queue 100 buckets 16");
2328
		captiveportal_update_entry($cpentry['sessionid'], $bw_down, 'bw_down');
2329
	}
2330
	unset($bw_up_pipeno, $bw_down_pipeno, $bw_up, $bw_down);
2331
}
2332

    
2333
function captiveportal_update_entry($sessionid, $new_value, $field_to_update) {
2334
	global $cpzone;
2335

    
2336
	if (!intval($new_value)) {
2337
		$new_value = "'{$new_value}'";
2338
	}
2339
	captiveportal_write_db("UPDATE captiveportal SET {$field_to_update} = {$new_value} WHERE sessionid = '{$sessionid}'");
2340
}
2341

    
2342
function portal_allow($clientip, $clientmac, $username, $password = null, $redirurl = null,
2343
    $attributes = null, $pipeno = null, $authmethod = null, $context = 'first', $existing_sessionid = null) {
2344
	global $g, $config, $cpzone;
2345

    
2346
	// Ensure we create an array if we are missing attributes
2347
	if (!is_array($attributes)) {
2348
		$attributes = array();
2349
	}
2350

    
2351
	unset($sessionid);
2352

    
2353
	/* Do not allow concurrent login execution. */
2354
	$cpdblck = lock("captiveportaldb{$cpzone}", LOCK_EX);
2355

    
2356
	if ($attributes['voucher']) {
2357
		$remaining_time = $attributes['session_timeout'];
2358
		$authmethod = "voucher"; // Set RADIUS-Attribute to Voucher to prevent ReAuth-Request for Vouchers Bug: #2155
2359
		$context = "voucher";
2360
	}
2361

    
2362
	$writecfg = false;
2363
	/* If both "Add MAC addresses of connected users as pass-through MAC" and "Disable concurrent logins" are checked,
2364
	then we need to check if the user was already authenticated using another MAC Address, and if so remove the previous Pass-Through MAC. */
2365
	if ((isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) && ($config['captiveportal'][$cpzone]['noconcurrentlogins'] == 'last') && ($username != 'unauthenticated') && isset($config['captiveportal'][$cpzone]['passthrumacadd'])) {
2366
		$mac = captiveportal_passthrumac_findbyname($username);
2367
		if (!empty($mac)) {
2368
			foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $idx => $macent) {
2369
				if ($macent['mac'] != $mac['mac']) {
2370
					continue;
2371
				}
2372

    
2373
				$pipeno = captiveportal_get_dn_passthru_ruleno($mac['mac']);
2374
				if ($pipeno) {
2375
					captiveportal_free_dn_ruleno($pipeno);
2376
					@pfSense_ipfw_table("{$cpzone}_pipe_mac", IP_FW_TABLE_XDEL, "any,{$mac['mac']}");
2377
					@pfSense_ipfw_table("{$cpzone}_pipe_mac", IP_FW_TABLE_XDEL, "{$mac['mac']},any");
2378
					@pfSense_ipfw_pipe("pipe delete " . ($pipeno+1));
2379
					@pfSense_ipfw_pipe("pipe delete " . $pipeno);
2380
				}
2381
				unset($config['captiveportal'][$cpzone]['passthrumac'][$idx]);
2382
			}
2383
		}
2384
	}
2385

    
2386
	/* read in client database */
2387
	$query = "WHERE ip = '{$clientip}'";
2388
	$tmpusername = SQLite3::escapeString(strtolower($username));
2389
	if (isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) {
2390
		$query .= " OR (username != 'unauthenticated' AND lower(username) = '{$tmpusername}')";
2391
	}
2392
	$cpdb = captiveportal_read_db($query);
2393

    
2394
	/* Snapshot the timestamp */
2395
	$allow_time = time();
2396

    
2397
	if ($existing_sessionid !== null) {
2398
		// If we recieved this connection through XMLRPC sync :
2399
		// we fetch allow_time from the info given by the other node
2400
		$allow_time = $attributes['allow_time'];
2401
	}
2402
	$unsetindexes = array();
2403

    
2404
	foreach ($cpdb as $cpentry) {
2405
		/* on the same ip */
2406
		if ($cpentry[2] == $clientip) {
2407
			if (isset($config['captiveportal'][$cpzone]['nomacfilter']) || $cpentry[3] == $clientmac) {
2408
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT LOGIN - REUSING OLD SESSION");
2409
			} else {
2410
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT LOGIN - REUSING IP {$cpentry[2]} WITH DIFFERENT MAC ADDRESS {$cpentry[3]}");
2411
			}
2412
			$sessionid = $cpentry[5];
2413
			break;
2414
		} elseif (($attributes['voucher']) && ($username != 'unauthenticated') && ($cpentry[4] == $username)) {
2415
			// user logged in with an active voucher. Check for how long and calculate
2416
			// how much time we can give him (voucher credit - used time)
2417
			$remaining_time = $cpentry[0] + $cpentry[7] - $allow_time;
2418
			if ($remaining_time < 0) { // just in case.
2419
				$remaining_time = 0;
2420
			}
2421

    
2422
			/* This user was already logged in so we disconnect the old one, or 
2423
			keep the old one, refusing the new login, or
2424
			allow the login */
2425

    
2426
			if (!isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) {
2427
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "config['captiveportal'][$cpzone]['noconcurrentlogins'] 2 does not exists = NOT set");		
2428
			} else {
2429
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "config['captiveportal'][$cpzone]['noconcurrentlogins'] 2 exists = set");		
2430
			}
2431

    
2432
			if ($config['captiveportal'][$cpzone]['noconcurrentlogins'] == "last") {
2433
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "Found last");
2434
			} else {
2435
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "Found NOT last");
2436
			}
2437
				
2438
			if (!isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) {
2439
				/* 'noconcurrentlogins' not set : accept login 'username' creating multiple sessions. */
2440
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "config['captiveportal'][$cpzone]['noconcurrentlogins'] 3 does not exists = NOT set");
2441
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT LOGIN - NOT TERMINATING EXISTING SESSION(S)");			
2442
			} elseif ($config['captiveportal'][$cpzone]['noconcurrentlogins'] == "last") {
2443
				/* Classic situation : accept the new login, disconnect the old - present - connection */
2444
				if (isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) {
2445
					captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "config['captiveportal'][$cpzone]['noconcurrentlogins'] 4 exists = set");		
2446
				} else {
2447
					captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "config['captiveportal'][$cpzone]['noconcurrentlogins'] 4 does not exists = NOT set");		
2448
				}
2449
				
2450
				captiveportal_disconnect($cpentry, 13);
2451
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT LOGIN - TERMINATING OLD SESSION");
2452
				$unsetindexes[] = $cpentry[5];
2453
				break;
2454
			} else {
2455
				/* Implicit 'first' : refuse the new login - 'username' is already logged in */
2456
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT VOUCHER LOGIN - NOT ALLOWED KEEPING OLD SESSION ");
2457
				unlock($cpdblck);
2458
				return 2;
2459
			}
2460
		} elseif ((isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) && ($username != 'unauthenticated')) {
2461
			if ($config['captiveportal'][$cpzone]['noconcurrentlogins'] == "last") {
2462
				/* on the same username */
2463
				if (strcasecmp($cpentry[4], $username) == 0) {
2464
					/* This user was already logged in so we disconnect the old one */
2465
					captiveportal_disconnect($cpentry, 13);
2466
					captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT USER LOGIN - TERMINATING OLD SESSION");
2467
					$unsetindexes[] = $cpentry[5];
2468
					break;
2469
				}
2470
			} else {
2471
				/* Implicit 'first' : refuse the new login - 'username' is already logged in */
2472
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT USER LOGIN - NOT ALLOWED KEEPING OLD SESSION ");
2473
				unlock($cpdblck);
2474
				return 2;				
2475
			}
2476
		}
2477
	}
2478
	unset($cpdb);
2479

    
2480
	if (!empty($unsetindexes)) {
2481
		captiveportal_remove_entries($unsetindexes);
2482
	}
2483

    
2484
	if ($attributes['voucher'] && $remaining_time <= 0) {
2485
		return 0;       // voucher already used and no time left
2486
	}
2487

    
2488
	if (!isset($sessionid)) {
2489
		if ($existing_sessionid != null) { // existing_sessionid should only be set during XMLRPC sync
2490
			$sessionid = $existing_sessionid;
2491
		} else {
2492
			/* generate unique session ID */
2493
			$tod = gettimeofday();
2494
			$sessionid = substr(md5(mt_rand() . $tod['sec'] . $tod['usec'] . $clientip . $clientmac), 0, 16);
2495
		}
2496

    
2497
		if (isset($config['captiveportal'][$cpzone]['peruserbw'])) {
2498
			$dwfaultbw_up = !empty($config['captiveportal'][$cpzone]['bwdefaultup']) ? $config['captiveportal'][$cpzone]['bwdefaultup'] : 0;
2499
			$dwfaultbw_down = !empty($config['captiveportal'][$cpzone]['bwdefaultdn']) ? $config['captiveportal'][$cpzone]['bwdefaultdn'] : 0;
2500
		} else {
2501
			$dwfaultbw_up = $dwfaultbw_down = 0;
2502
		}
2503
		/* pipe throughputs must always be an integer, enforce that restriction again here. */
2504
		if (isset($config['captiveportal'][$cpzone]['radiusperuserbw'])) {
2505
			$bw_up = round(!empty($attributes['bw_up']) ? intval($attributes['bw_up'])/1000 : $dwfaultbw_up, 0);
2506
			$bw_down = round(!empty($attributes['bw_down']) ? intval($attributes['bw_down'])/1000 : $dwfaultbw_down, 0);
2507
		} else {
2508
			$bw_up = round($dwfaultbw_up,0);
2509
			$bw_down = round($dwfaultbw_down,0);
2510
		}
2511

    
2512
		if (isset($config['captiveportal'][$cpzone]['passthrumacadd'])) {
2513

    
2514
			$mac = array();
2515
			$mac['action'] = 'pass';
2516
			$mac['mac'] = $clientmac;
2517
			$mac['ip'] = $clientip; /* Used only for logging */
2518
			$mac['username'] = $username;
2519
			if ($attributes['voucher']) {
2520
				$mac['logintype'] = "voucher";
2521
			}
2522
			if ($username == "unauthenticated") {
2523
				$mac['descr'] = "Auto-added";
2524
			} else if ($authmethod == "voucher") {
2525
				$mac['descr'] = "Auto-added for voucher {$username}";
2526
			} else {
2527
				$mac['descr'] = "Auto-added for user {$username}";
2528
			}
2529
			if (!empty($bw_up)) {
2530
				$mac['bw_up'] = $bw_up;
2531
			}
2532
			if (!empty($bw_down)) {
2533
				$mac['bw_down'] = $bw_down;
2534
			}
2535
			if (!is_array($config['captiveportal'][$cpzone]['passthrumac'])) {
2536
				$config['captiveportal'][$cpzone]['passthrumac'] = array();
2537
			}
2538
			//check for mac duplicates before adding it to config.
2539
			$mac_duplicate = false;
2540
			foreach($config['captiveportal'][$cpzone]['passthrumac'] as $mac_check){
2541
				if($mac_check['mac'] == $mac['mac']){
2542
					$mac_duplicate = true;
2543
				}
2544
			}
2545
			if(!$mac_duplicate){
2546
				$config['captiveportal'][$cpzone]['passthrumac'][] = $mac;
2547
			}
2548
			unlock($cpdblck);
2549
			$macrules = captiveportal_passthrumac_configure_entry($mac);
2550
			file_put_contents("{$g['tmp_path']}/macentry_{$cpzone}.rules.tmp", $macrules);
2551
			mwexec("/sbin/ipfw -q {$g['tmp_path']}/macentry_{$cpzone}.rules.tmp");
2552
			$writecfg = true;
2553
		} else {
2554
			/* See if a pipeno is passed, if not start sessions because this means there isn't one atm */
2555
			if (is_null($pipeno)) {
2556
				$pipeno = captiveportal_get_next_dn_ruleno('auth');
2557
			}
2558

    
2559
			/* if the pool is empty, return appropriate message and exit */
2560
			if (is_null($pipeno)) {
2561
				captiveportal_syslog("Zone: {$cpzone} - WARNING!  Captive portal has reached maximum login capacity");
2562
				unlock($cpdblck);
2563
				return false;
2564
			}
2565

    
2566
			$bw_up_pipeno = $pipeno;
2567
			$bw_down_pipeno = $pipeno + 1;
2568
			//$bw_up /= 1000; // Scale to Kbit/s
2569
			$_gb = @pfSense_ipfw_pipe("pipe {$bw_up_pipeno} config bw {$bw_up}Kbit/s queue 100 buckets 16");
2570
			$_gb = @pfSense_ipfw_pipe("pipe {$bw_down_pipeno} config bw {$bw_down}Kbit/s queue 100 buckets 16");
2571

    
2572
			$rule_entry = "{$clientip}/" . (is_ipaddrv6($clientip) ? "128" : "32");
2573
			if (!isset($config['captiveportal'][$cpzone]['nomacfilter'])) {
2574
				$rule_entry .= ",{$clientmac}";
2575
			}
2576
			$_gb = @pfSense_ipfw_table("{$cpzone}_auth_up", IP_FW_TABLE_XADD, "{$rule_entry}", $bw_up_pipeno);
2577
			$_gb = @pfSense_ipfw_table("{$cpzone}_auth_down", IP_FW_TABLE_XADD, "{$rule_entry}", $bw_down_pipeno);
2578

    
2579
			if ($attributes['voucher']) {
2580
				$attributes['session_timeout'] = $remaining_time;
2581
			}
2582

    
2583
			/* handle empty attributes */
2584
			$session_timeout = (!empty($attributes['session_timeout'])) ? $attributes['session_timeout'] : 'NULL';
2585
			$idle_timeout = (!empty($attributes['idle_timeout'])) ? $attributes['idle_timeout'] : 'NULL';
2586
			$session_terminate_time = (!empty($attributes['session_terminate_time'])) ? $attributes['session_terminate_time'] : 'NULL';
2587
			$interim_interval = (!empty($attributes['interim_interval'])) ? $attributes['interim_interval'] : 'NULL';
2588
			$traffic_quota = (!empty($attributes['maxbytes'])) ? $attributes['maxbytes'] : 'NULL';
2589

    
2590
			/* escape username */
2591
			$safe_username = SQLite3::escapeString($username);
2592

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

    
2599
			/* store information to database */
2600
			captiveportal_write_db($insertquery);
2601
			unlock($cpdblck);
2602
			unset($insertquery, $bpassword);
2603

    
2604
			$radacct = isset($config['captiveportal'][$cpzone]['radacct_enable']) ? true : false;
2605
			if ($authmethod === 'radius' && $radacct) {
2606
				captiveportal_send_server_accounting('start',
2607
					$pipeno, // ruleno
2608
					$username, // username
2609
					$clientip, // clientip
2610
					$clientmac, // clientmac
2611
					$sessionid, // sessionid
2612
					time());  // start time
2613
			}
2614
			if (captiveportal_xmlrpc_sync_get_details($syncip, $syncport, $syncuser, $syncpass, isset($existing_sessionid))) {
2615
				// $existing_sessionid prevent carp loop : only forward
2616
				// the connection to the other node if we generated the sessionid by ourselves
2617
				$rpc_client = new pfsense_xmlrpc_client();
2618
				$rpc_client->setConnectionData($syncip, $syncport, $syncuser, $syncpass);
2619
				$rpc_client->set_noticefile("CaptivePortalUserSync");
2620
				$arguments = array(
2621
					'clientip' => $clientip,
2622
					'clientmac' => $clientmac,
2623
					'username' => $username,
2624
					'password' => $password,
2625
					'attributes' => $attributes,
2626
					'allow_time' => $allow_time,
2627
					'authmethod' => $authmethod,
2628
					'context' => $context,
2629
					'sessionid' => $sessionid
2630
				);
2631

    
2632
				$rpc_client->xmlrpc_method('captive_portal_sync',
2633
					array(
2634
						'op' => 'connect_user',
2635
						'zone' => $cpzone,
2636
						'user' => base64_encode(serialize($arguments))
2637
					)
2638
				);
2639
			}
2640
		}
2641
	} else {
2642
		/* NOTE: #3062-11 If the pipeno has been allocated free it to not DoS the CP */
2643
		if (!is_null($pipeno)) {
2644
			captiveportal_free_dn_ruleno($pipeno);
2645
		}
2646

    
2647
		unlock($cpdblck);
2648
	}
2649

    
2650
	if ($writecfg == true) {
2651
		write_config(gettext("Captive Portal allowed users configuration changed"));
2652
	}
2653

    
2654
	if ($existing_sessionid !== null) {
2655
		if (!empty($sessionid)) {
2656
			return $sessionid;
2657
		} else {
2658
			return false;
2659
		}
2660
	}
2661
	/* redirect user to desired destination */
2662
	if (!empty($attributes['url_redirection'])) {
2663
		$my_redirurl = $attributes['url_redirection'];
2664
	} else if (!empty($redirurl)) {
2665
		$my_redirurl = $redirurl;
2666
	} else if (!empty($config['captiveportal'][$cpzone]['redirurl'])) {
2667
		$my_redirurl = $config['captiveportal'][$cpzone]['redirurl'];
2668
	}
2669

    
2670
	if (isset($config['captiveportal'][$cpzone]['logoutwin_enable']) && !isset($config['captiveportal'][$cpzone]['passthrumacadd'])) {
2671
		$ourhostname = portal_hostname_from_client_ip($clientip);
2672
		$protocol = (isset($config['captiveportal'][$cpzone]['httpslogin'])) ? 'https://' : 'http://';
2673
		$logouturl = "{$protocol}{$ourhostname}/";
2674

    
2675
		if (isset($attributes['reply_message'])) {
2676
			$message = $attributes['reply_message'];
2677
		} else {
2678
			$message = 0;
2679
		}
2680

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

    
2683
	} else {
2684
		portal_reply_page($my_redirurl, "redir", "Just redirect the user.");
2685
	}
2686

    
2687
	return $sessionid;
2688
}
2689

    
2690

    
2691
/*
2692
 * Used for when pass-through credits are enabled.
2693
 * Returns true when there was at least one free login to deduct for the MAC.
2694
 * Expired entries are removed as they are seen.
2695
 * Active entries are updated according to the configuration.
2696
 */
2697
function portal_consume_passthrough_credit($clientmac) {
2698
	global $config, $cpzone;
2699

    
2700
	if (!empty($config['captiveportal'][$cpzone]['freelogins_count']) && is_numeric($config['captiveportal'][$cpzone]['freelogins_count'])) {
2701
		$freeloginscount = $config['captiveportal'][$cpzone]['freelogins_count'];
2702
	} else {
2703
		return false;
2704
	}
2705

    
2706
	if (!empty($config['captiveportal'][$cpzone]['freelogins_resettimeout']) && is_numeric($config['captiveportal'][$cpzone]['freelogins_resettimeout'])) {
2707
		$resettimeout = $config['captiveportal'][$cpzone]['freelogins_resettimeout'];
2708
	} else {
2709
		return false;
2710
	}
2711

    
2712
	if ($freeloginscount < 1 || $resettimeout <= 0 || !$clientmac) {
2713
		return false;
2714
	}
2715

    
2716
	$updatetimeouts = isset($config['captiveportal'][$cpzone]['freelogins_updatetimeouts']);
2717

    
2718
	/*
2719
	 * Read database of used MACs.  Lines are a comma-separated list
2720
	 * of the time, MAC, then the count of pass-through credits remaining.
2721
	 */
2722
	$usedmacs = captiveportal_read_usedmacs_db();
2723

    
2724
	$currenttime = time();
2725
	$found = false;
2726
	foreach ($usedmacs as $key => $usedmac) {
2727
		$usedmac = explode(",", $usedmac);
2728

    
2729
		if ($usedmac[1] == $clientmac) {
2730
			if ($usedmac[0] + ($resettimeout * 3600) > $currenttime) {
2731
				if ($usedmac[2] < 1) {
2732
					if ($updatetimeouts) {
2733
						$usedmac[0] = $currenttime;
2734
						unset($usedmacs[$key]);
2735
						$usedmacs[] = implode(",", $usedmac);
2736
						captiveportal_write_usedmacs_db($usedmacs);
2737
						xmlrpc_sync_usedmacs($usedmacs);
2738
					}
2739

    
2740
					return false;
2741
				} else {
2742
					$usedmac[2] -= 1;
2743
					$usedmacs[$key] = implode(",", $usedmac);
2744
				}
2745

    
2746
				$found = true;
2747
			} else {
2748
				unset($usedmacs[$key]);
2749
			}
2750

    
2751
			break;
2752
		} else if ($usedmac[0] + ($resettimeout * 3600) <= $currenttime) {
2753
			unset($usedmacs[$key]);
2754
		}
2755
	}
2756

    
2757
	if (!$found) {
2758
		$usedmac = array($currenttime, $clientmac, $freeloginscount - 1);
2759
		$usedmacs[] = implode(",", $usedmac);
2760
	}
2761

    
2762
	captiveportal_write_usedmacs_db($usedmacs);
2763
	xmlrpc_sync_usedmacs($usedmacs);
2764
	return true;
2765
}
2766

    
2767
function xmlrpc_sync_usedmacs($usedmacs) { 
2768
	global $config, $cpzone;
2769

    
2770
	// XMLRPC Call over to the other node
2771
	if (captiveportal_xmlrpc_sync_get_details($syncip, $syncport,
2772
	    $syncuser, $syncpass, $carp_loop)) {
2773
		$rpc_client = new pfsense_xmlrpc_client();
2774
		$rpc_client->setConnectionData($syncip, $syncport, $syncuser, $syncpass);
2775
		$rpc_client->set_noticefile("CaptivePortalUsedmacsSync");
2776
		$arguments = array(
2777
			'usedmacs' => $usedmacs
2778
		);
2779

    
2780
		$rpc_client->xmlrpc_method('captive_portal_sync',
2781
			array(
2782
				'op' => 'write_usedmacs',
2783
				'zone' => $cpzone,
2784
				'arguments' => base64_encode(serialize($arguments))
2785
			)
2786
		);
2787
	}
2788
}
2789

    
2790
function captiveportal_read_usedmacs_db() {
2791
	global $g, $cpzone;
2792

    
2793
	$cpumaclck = lock("captiveusedmacs{$cpzone}");
2794
	if (file_exists("{$g['vardb_path']}/captiveportal_usedmacs_{$cpzone}.db")) {
2795
		$usedmacs = file("{$g['vardb_path']}/captiveportal_usedmacs_{$cpzone}.db", FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
2796
		if (!$usedmacs) {
2797
			$usedmacs = array();
2798
		}
2799
	} else {
2800
		$usedmacs = array();
2801
	}
2802

    
2803
	unlock($cpumaclck);
2804
	return $usedmacs;
2805
}
2806

    
2807
function captiveportal_write_usedmacs_db($usedmacs) {
2808
	global $g, $cpzone;
2809

    
2810
	$cpumaclck = lock("captiveusedmacs{$cpzone}", LOCK_EX);
2811
	@file_put_contents("{$g['vardb_path']}/captiveportal_usedmacs_{$cpzone}.db", implode("\n", $usedmacs));
2812
	unlock($cpumaclck);
2813
}
2814

    
2815
function captiveportal_blocked_mac($mac) {
2816
	global $config, $g, $cpzone;
2817

    
2818
	if (empty($mac) || !is_macaddr($mac)) {
2819
		return false;
2820
	}
2821

    
2822
	if (!is_array($config['captiveportal'][$cpzone]['passthrumac'])) {
2823
		return false;
2824
	}
2825

    
2826
	foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $passthrumac) {
2827
		if (($passthrumac['action'] == 'block') &&
2828
		    ($passthrumac['mac'] == strtolower($mac))) {
2829
			return true;
2830
		}
2831
	}
2832

    
2833
	return false;
2834

    
2835
}
2836

    
2837
/* Captiveportal Radius Accounting */
2838

    
2839
function gigawords($bytes) {
2840

    
2841
	/*
2842
	 * RFC2866 Specifies a 32bit unsigned integer, which is a max of 4294967295
2843
	 * Currently there is a fault in the PECL radius_put_int function which can handle only 32bit signed integer.
2844
	 */
2845

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

    
2849
	// We need to manually set this to a zero instead of NULL for put_int() safety
2850
	if (is_null($gigawords)) {
2851
		$gigawords = 0;
2852
	}
2853

    
2854
	return $gigawords;
2855
}
2856

    
2857
function remainder($bytes) {
2858
	// Calculate the bytes we are going to send to the radius
2859
	$bytes = bcmod($bytes, GIGAWORDS_RIGHT_OPERAND);
2860

    
2861
	if (is_null($bytes)) {
2862
		$bytes = 0;
2863
	}
2864

    
2865
    return $bytes;
2866
}
2867

    
2868
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) {
2869
	global $cpzone, $config;
2870

    
2871
	$cpcfg = $config['captiveportal'][$cpzone];
2872
	$acctcfg = auth_get_authserver($cpcfg['radacct_server']);
2873

    
2874
	if (!isset($cpcfg['radacct_enable']) || empty($acctcfg) ||
2875
	    captiveportal_ha_is_node_in_backup_mode($cpzone)) {
2876
		return null;
2877
	}
2878

    
2879
	if ($type === 'on') {
2880
		$racct = new Auth_RADIUS_Acct_On;
2881
	} elseif ($type === 'off') {
2882
		$racct = new Auth_RADIUS_Acct_Off;
2883
	} elseif ($type === 'start') {
2884
		$racct = new Auth_RADIUS_Acct_Start;
2885
		if (!is_int($start_time)) {
2886
			$start_time = time();
2887
		}
2888
	} elseif ($type === 'stop') {
2889
		$racct = new Auth_RADIUS_Acct_Stop;
2890
		if (!is_int($stop_time)) {
2891
			$stop_time = time();
2892
		}
2893
	} elseif ($type === 'update') {
2894
        $racct = new Auth_RADIUS_Acct_Update;
2895
		if (!is_int($stop_time)) {
2896
			$stop_time = time(); // "top time" here will be used only for calculating session time.
2897
		}
2898
	} else {
2899
		return null;
2900
	}
2901

    
2902
	$racct->addServer($acctcfg['host'], $acctcfg['radius_acct_port'],
2903
		$acctcfg['radius_secret'], $acctcfg['radius_timeout']);
2904

    
2905
	$racct->authentic = RADIUS_AUTH_RADIUS;
2906
	if ($cpcfg['auth_method'] === 'radmac' && $username === "unauthenticated" && !empty($clientmac)) {
2907
		$racct->username = mac_format($clientmac);
2908
	} elseif (!empty($username)) {
2909
		$racct->username = $username;
2910
	}
2911

    
2912
	if (PEAR::isError($racct->start())) {
2913
		captiveportal_syslog('RADIUS ACCOUNTING FAILED : '.$racct->getError());
2914
		$racct->close();
2915
		return null;
2916
	}
2917

    
2918
	$nasip = $acctcfg['radius_nasip_attribute'];
2919
	if (!is_ipaddr($nasip)) {
2920
		$nasip = get_interface_ip($nasip);
2921
		if (!is_ipaddr($nasip)) {
2922
			$nasip = get_interface_ip();//We use WAN interface IP as fallback for NAS-IP-Address
2923
		}
2924
	}
2925
	$nasmac = get_interface_mac(find_ip_interface($nasip));
2926
	$racct->putAttribute(RADIUS_NAS_IP_ADDRESS, $nasip, "addr");
2927

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

    
2930
	if (is_int($ruleno)) {
2931
		$racct->putAttribute(RADIUS_NAS_PORT_TYPE, RADIUS_ETHERNET);
2932
		$racct->putAttribute(RADIUS_NAS_PORT, intval($ruleno), 'integer');
2933
	}
2934

    
2935
	if (!empty($sessionid)) {
2936
		$racct->putAttribute(RADIUS_ACCT_SESSION_ID, $sessionid);
2937
	}
2938

    
2939
	if (!empty($clientip) && is_ipaddr($clientip)) {
2940
		$racct->putAttribute(RADIUS_FRAMED_IP_ADDRESS, $clientip, "addr");
2941
	}
2942
	if (!empty($clientmac)) {
2943
		$racct->putAttribute(RADIUS_CALLING_STATION_ID, mac_format($clientmac));
2944
	}
2945
	if (!empty($nasmac)) {
2946
		$racct->putAttribute(RADIUS_CALLED_STATION_ID, mac_format($nasmac).':'.gethostname());
2947
	}
2948

    
2949
	// Accounting request Stop and Update : send the current data volume
2950
	if (($type === 'stop' || $type === 'update') && is_int($start_time)) {
2951
		$volume = getVolume($clientip);
2952
		$session_time = $stop_time - $start_time;
2953
		$volume['input_bytes_radius'] = remainder($volume['input_bytes']);
2954
		$volume['input_gigawords'] = gigawords($volume['input_bytes']);
2955
		$volume['output_bytes_radius'] = remainder($volume['output_bytes']);
2956
		$volume['output_gigawords'] = gigawords($volume['output_bytes']);
2957

    
2958
		// Volume stuff: Ingress
2959
		$racct->putAttribute(RADIUS_ACCT_INPUT_PACKETS, intval($volume['input_pkts']), "integer");
2960
		$racct->putAttribute(RADIUS_ACCT_INPUT_OCTETS, intval($volume['input_bytes_radius']), "integer");
2961
		// Volume stuff: Outgress
2962
		$racct->putAttribute(RADIUS_ACCT_OUTPUT_PACKETS, intval($volume['output_pkts']), "integer");
2963
		$racct->putAttribute(RADIUS_ACCT_OUTPUT_OCTETS, intval($volume['output_bytes_radius']), "integer");
2964
		$racct->putAttribute(RADIUS_ACCT_SESSION_TIME, intval($session_time), "integer");
2965

    
2966
		$racct->putAttribute(CUSTOM_RADIUS_ACCT_OUTPUT_GIGAWORDS, intval($volume['output_gigawords']), "integer");
2967
		$racct->putAttribute(CUSTOM_RADIUS_ACCT_INPUT_GIGAWORDS, intval($volume['input_gigawords']), "integer");
2968
		// Set session_time
2969
		$racct->session_time = $session_time;
2970
	}
2971

    
2972
	if ($type === 'stop') {
2973
		if (empty($term_cause)) {
2974
			$term_cause = 1;
2975
		}
2976
		$racct->putAttribute(RADIUS_ACCT_TERMINATE_CAUSE, $term_cause);
2977
	}
2978

    
2979
	// Send request
2980
	$result = $racct->send();
2981

    
2982
	if (PEAR::isError($result)) {
2983
		 captiveportal_syslog('RADIUS ACCOUNTING FAILED : '.$racct->getError());
2984
		 $result = null;
2985
	} elseif ($result !== true) {
2986
		$result = false;
2987
	}
2988

    
2989
	$racct->close();
2990
	return $result;
2991
}
2992

    
2993
function captiveportal_isip_logged($clientip) {
2994
	global $g, $cpzone;
2995

    
2996
	/* read in client database */
2997
	$query = "WHERE ip = '{$clientip}'";
2998
	$cpdb = captiveportal_read_db($query);
2999
	foreach ($cpdb as $cpentry) {
3000
		return $cpentry;
3001
	}
3002
}
3003

    
3004
?>
(6-6/61)