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-2020 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}">
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($dbent[2]);
1164
		$_gb = @pfSense_kill_srcstates($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) {
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

    
2297
	echo $htmltext;
2298
}
2299

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

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

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

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

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

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

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

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

    
2350
	unset($sessionid);
2351

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

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

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

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

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

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

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

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

    
2427
			/* This user was already logged in so we disconnect the old one, or 
2428
			keep the old one, refusing the new login, or
2429
			allow the login */
2430

    
2431
			if (!isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) {
2432
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "config['captiveportal'][$cpzone]['noconcurrentlogins'] 2 does not exists = NOT set");		
2433
			} else {
2434
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "config['captiveportal'][$cpzone]['noconcurrentlogins'] 2 exists = set");		
2435
			}
2436

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

    
2485
	if (!empty($unsetindexes)) {
2486
		captiveportal_remove_entries($unsetindexes);
2487
	}
2488

    
2489
	if ($attributes['voucher'] && $remaining_time <= 0) {
2490
		return 0;       // voucher already used and no time left
2491
	}
2492

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

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

    
2517
		if (isset($config['captiveportal'][$cpzone]['passthrumacadd'])) {
2518

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

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

    
2571
			$bw_up_pipeno = $pipeno;
2572
			$bw_down_pipeno = $pipeno + 1;
2573
			//$bw_up /= 1000; // Scale to Kbit/s
2574
			$_gb = @pfSense_ipfw_pipe("pipe {$bw_up_pipeno} config bw {$bw_up}Kbit/s queue 100 buckets 16");
2575
			$_gb = @pfSense_ipfw_pipe("pipe {$bw_down_pipeno} config bw {$bw_down}Kbit/s queue 100 buckets 16");
2576

    
2577
			$rule_entry = "{$clientip}/" . (is_ipaddrv6($clientip) ? "128" : "32");
2578
			if (!isset($config['captiveportal'][$cpzone]['nomacfilter'])) {
2579
				$rule_entry .= ",{$clientmac}";
2580
			}
2581
			$_gb = @pfSense_ipfw_table("{$cpzone}_auth_up", IP_FW_TABLE_XADD, "{$rule_entry}", $bw_up_pipeno);
2582
			$_gb = @pfSense_ipfw_table("{$cpzone}_auth_down", IP_FW_TABLE_XADD, "{$rule_entry}", $bw_down_pipeno);
2583

    
2584
			if ($attributes['voucher']) {
2585
				$attributes['session_timeout'] = $remaining_time;
2586
			}
2587

    
2588
			/* handle empty attributes */
2589
			$session_timeout = (!empty($attributes['session_timeout'])) ? $attributes['session_timeout'] : 'NULL';
2590
			$idle_timeout = (!empty($attributes['idle_timeout'])) ? $attributes['idle_timeout'] : 'NULL';
2591
			$session_terminate_time = (!empty($attributes['session_terminate_time'])) ? $attributes['session_terminate_time'] : 'NULL';
2592
			$interim_interval = (!empty($attributes['interim_interval'])) ? $attributes['interim_interval'] : 'NULL';
2593
			$traffic_quota = (!empty($attributes['maxbytes'])) ? $attributes['maxbytes'] : 'NULL';
2594

    
2595
			/* escape username */
2596
			$safe_username = SQLite3::escapeString($username);
2597

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

    
2604
			/* store information to database */
2605
			captiveportal_write_db($insertquery);
2606
			unlock($cpdblck);
2607
			unset($insertquery, $bpassword);
2608

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

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

    
2652
		unlock($cpdblck);
2653
	}
2654

    
2655
	if ($writecfg == true) {
2656
		write_config(gettext("Captive Portal allowed users configuration changed"));
2657
	}
2658

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

    
2675
	if (isset($config['captiveportal'][$cpzone]['logoutwin_enable']) && !isset($config['captiveportal'][$cpzone]['passthrumacadd'])) {
2676
		$ourhostname = portal_hostname_from_client_ip($clientip);
2677
		$protocol = (isset($config['captiveportal'][$cpzone]['httpslogin'])) ? 'https://' : 'http://';
2678
		$logouturl = "{$protocol}{$ourhostname}/";
2679

    
2680
		if (isset($attributes['reply_message'])) {
2681
			$message = $attributes['reply_message'];
2682
		} else {
2683
			$message = 0;
2684
		}
2685

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

    
2688
	} else {
2689
		portal_reply_page($my_redirurl, "redir", "Just redirect the user.");
2690
	}
2691

    
2692
	return $sessionid;
2693
}
2694

    
2695

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

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

    
2711
	if (!empty($config['captiveportal'][$cpzone]['freelogins_resettimeout']) && is_numeric($config['captiveportal'][$cpzone]['freelogins_resettimeout'])) {
2712
		$resettimeout = $config['captiveportal'][$cpzone]['freelogins_resettimeout'];
2713
	} else {
2714
		return false;
2715
	}
2716

    
2717
	if ($freeloginscount < 1 || $resettimeout <= 0 || !$clientmac) {
2718
		return false;
2719
	}
2720

    
2721
	$updatetimeouts = isset($config['captiveportal'][$cpzone]['freelogins_updatetimeouts']);
2722

    
2723
	/*
2724
	 * Read database of used MACs.  Lines are a comma-separated list
2725
	 * of the time, MAC, then the count of pass-through credits remaining.
2726
	 */
2727
	$usedmacs = captiveportal_read_usedmacs_db();
2728

    
2729
	$currenttime = time();
2730
	$found = false;
2731
	foreach ($usedmacs as $key => $usedmac) {
2732
		$usedmac = explode(",", $usedmac);
2733

    
2734
		if ($usedmac[1] == $clientmac) {
2735
			if ($usedmac[0] + ($resettimeout * 3600) > $currenttime) {
2736
				if ($usedmac[2] < 1) {
2737
					if ($updatetimeouts) {
2738
						$usedmac[0] = $currenttime;
2739
						unset($usedmacs[$key]);
2740
						$usedmacs[] = implode(",", $usedmac);
2741
						captiveportal_write_usedmacs_db($usedmacs);
2742
						xmlrpc_sync_usedmacs($usedmacs);
2743
					}
2744

    
2745
					return false;
2746
				} else {
2747
					$usedmac[2] -= 1;
2748
					$usedmacs[$key] = implode(",", $usedmac);
2749
				}
2750

    
2751
				$found = true;
2752
			} else {
2753
				unset($usedmacs[$key]);
2754
			}
2755

    
2756
			break;
2757
		} else if ($usedmac[0] + ($resettimeout * 3600) <= $currenttime) {
2758
			unset($usedmacs[$key]);
2759
		}
2760
	}
2761

    
2762
	if (!$found) {
2763
		$usedmac = array($currenttime, $clientmac, $freeloginscount - 1);
2764
		$usedmacs[] = implode(",", $usedmac);
2765
	}
2766

    
2767
	captiveportal_write_usedmacs_db($usedmacs);
2768
	xmlrpc_sync_usedmacs($usedmacs);
2769
	return true;
2770
}
2771

    
2772
function xmlrpc_sync_usedmacs($usedmacs) { 
2773
	global $config, $cpzone;
2774

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

    
2785
		$rpc_client->xmlrpc_method('captive_portal_sync',
2786
			array(
2787
				'op' => 'write_usedmacs',
2788
				'zone' => $cpzone,
2789
				'arguments' => base64_encode(serialize($arguments))
2790
			)
2791
		);
2792
	}
2793
}
2794

    
2795
function captiveportal_read_usedmacs_db() {
2796
	global $g, $cpzone;
2797

    
2798
	$cpumaclck = lock("captiveusedmacs{$cpzone}");
2799
	if (file_exists("{$g['vardb_path']}/captiveportal_usedmacs_{$cpzone}.db")) {
2800
		$usedmacs = file("{$g['vardb_path']}/captiveportal_usedmacs_{$cpzone}.db", FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
2801
		if (!$usedmacs) {
2802
			$usedmacs = array();
2803
		}
2804
	} else {
2805
		$usedmacs = array();
2806
	}
2807

    
2808
	unlock($cpumaclck);
2809
	return $usedmacs;
2810
}
2811

    
2812
function captiveportal_write_usedmacs_db($usedmacs) {
2813
	global $g, $cpzone;
2814

    
2815
	$cpumaclck = lock("captiveusedmacs{$cpzone}", LOCK_EX);
2816
	@file_put_contents("{$g['vardb_path']}/captiveportal_usedmacs_{$cpzone}.db", implode("\n", $usedmacs));
2817
	unlock($cpumaclck);
2818
}
2819

    
2820
function captiveportal_blocked_mac($mac) {
2821
	global $config, $g, $cpzone;
2822

    
2823
	if (empty($mac) || !is_macaddr($mac)) {
2824
		return false;
2825
	}
2826

    
2827
	if (!is_array($config['captiveportal'][$cpzone]['passthrumac'])) {
2828
		return false;
2829
	}
2830

    
2831
	foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $passthrumac) {
2832
		if (($passthrumac['action'] == 'block') &&
2833
		    ($passthrumac['mac'] == strtolower($mac))) {
2834
			return true;
2835
		}
2836
	}
2837

    
2838
	return false;
2839

    
2840
}
2841

    
2842
/* Captiveportal Radius Accounting */
2843

    
2844
function gigawords($bytes) {
2845

    
2846
	/*
2847
	 * RFC2866 Specifies a 32bit unsigned integer, which is a max of 4294967295
2848
	 * Currently there is a fault in the PECL radius_put_int function which can handle only 32bit signed integer.
2849
	 */
2850

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

    
2854
	// We need to manually set this to a zero instead of NULL for put_int() safety
2855
	if (is_null($gigawords)) {
2856
		$gigawords = 0;
2857
	}
2858

    
2859
	return $gigawords;
2860
}
2861

    
2862
function remainder($bytes) {
2863
	// Calculate the bytes we are going to send to the radius
2864
	$bytes = bcmod($bytes, GIGAWORDS_RIGHT_OPERAND);
2865

    
2866
	if (is_null($bytes)) {
2867
		$bytes = 0;
2868
	}
2869

    
2870
    return $bytes;
2871
}
2872

    
2873
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) {
2874
	global $cpzone, $config;
2875

    
2876
	$cpcfg = $config['captiveportal'][$cpzone];
2877
	$acctcfg = auth_get_authserver($cpcfg['radacct_server']);
2878

    
2879
	if (!isset($cpcfg['radacct_enable']) || empty($acctcfg) ||
2880
	    captiveportal_ha_is_node_in_backup_mode($cpzone)) {
2881
		return null;
2882
	}
2883

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

    
2907
	$racct->addServer($acctcfg['host'], $acctcfg['radius_acct_port'],
2908
		$acctcfg['radius_secret'], $acctcfg['radius_timeout']);
2909

    
2910
	$racct->authentic = RADIUS_AUTH_RADIUS;
2911
	if ($cpcfg['auth_method'] === 'radmac' && $username === "unauthenticated" && !empty($clientmac)) {
2912
		$racct->username = mac_format($clientmac);
2913
	} elseif (!empty($username)) {
2914
		$racct->username = $username;
2915
	}
2916

    
2917
	if (PEAR::isError($racct->start())) {
2918
		captiveportal_syslog('RADIUS ACCOUNTING FAILED : '.$racct->getError());
2919
		$racct->close();
2920
		return null;
2921
	}
2922

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

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

    
2935
	if (is_int($ruleno)) {
2936
		$racct->putAttribute(RADIUS_NAS_PORT_TYPE, RADIUS_ETHERNET);
2937
		$racct->putAttribute(RADIUS_NAS_PORT, intval($ruleno), 'integer');
2938
	}
2939

    
2940
	if (!empty($sessionid)) {
2941
		$racct->putAttribute(RADIUS_ACCT_SESSION_ID, $sessionid);
2942
	}
2943

    
2944
	if (!empty($clientip) && is_ipaddr($clientip)) {
2945
		$racct->putAttribute(RADIUS_FRAMED_IP_ADDRESS, $clientip, "addr");
2946
	}
2947
	if (!empty($clientmac)) {
2948
		$racct->putAttribute(RADIUS_CALLING_STATION_ID, mac_format($clientmac));
2949
	}
2950
	if (!empty($nasmac)) {
2951
		$racct->putAttribute(RADIUS_CALLED_STATION_ID, mac_format($nasmac).':'.gethostname());
2952
	}
2953

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

    
2963
		// Volume stuff: Ingress
2964
		$racct->putAttribute(RADIUS_ACCT_INPUT_PACKETS, intval($volume['input_pkts']), "integer");
2965
		$racct->putAttribute(RADIUS_ACCT_INPUT_OCTETS, intval($volume['input_bytes_radius']), "integer");
2966
		// Volume stuff: Outgress
2967
		$racct->putAttribute(RADIUS_ACCT_OUTPUT_PACKETS, intval($volume['output_pkts']), "integer");
2968
		$racct->putAttribute(RADIUS_ACCT_OUTPUT_OCTETS, intval($volume['output_bytes_radius']), "integer");
2969
		$racct->putAttribute(RADIUS_ACCT_SESSION_TIME, intval($session_time), "integer");
2970

    
2971
		$racct->putAttribute(CUSTOM_RADIUS_ACCT_OUTPUT_GIGAWORDS, intval($volume['output_gigawords']), "integer");
2972
		$racct->putAttribute(CUSTOM_RADIUS_ACCT_INPUT_GIGAWORDS, intval($volume['input_gigawords']), "integer");
2973
		// Set session_time
2974
		$racct->session_time = $session_time;
2975
	}
2976

    
2977
	if ($type === 'stop') {
2978
		if (empty($term_cause)) {
2979
			$term_cause = 1;
2980
		}
2981
		$racct->putAttribute(RADIUS_ACCT_TERMINATE_CAUSE, $term_cause);
2982
	}
2983

    
2984
	// Send request
2985
	$result = $racct->send();
2986

    
2987
	if (PEAR::isError($result)) {
2988
		 captiveportal_syslog('RADIUS ACCOUNTING FAILED : '.$racct->getError());
2989
		 $result = null;
2990
	} elseif ($result !== true) {
2991
		$result = false;
2992
	}
2993

    
2994
	$racct->close();
2995
	return $result;
2996
}
2997

    
2998
function captiveportal_isip_logged($clientip) {
2999
	global $g, $cpzone;
3000

    
3001
	/* read in client database */
3002
	$query = "WHERE ip = '{$clientip}'";
3003
	$cpdb = captiveportal_read_db($query);
3004
	foreach ($cpdb as $cpentry) {
3005
		return $cpentry;
3006
	}
3007
}
3008

    
3009
?>
(6-6/61)