Project

General

Profile

Download (95.8 KB) Statistics
| Branch: | Tag: | Revision:
1
<?php
2
/*
3
 * captiveportal.inc
4
 *
5
 * part of pfSense (https://www.pfsense.org)
6
 * Copyright (c) 2004-2013 BSD Perimeter
7
 * Copyright (c) 2013-2016 Electric Sheep Fencing
8
 * Copyright (c) 2014-2019 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

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

    
44
function get_default_captive_portal_html() {
45
	global $config, $g, $cpzone;
46

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

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

    
89
<head>
90

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

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

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

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

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

    
139

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

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

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

    
178
EOD;
179

    
180
	return $htmltext;
181
}
182

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
349
EOD;
350
		}
351

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

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

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

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

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

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

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

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

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

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

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

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

    
422
		captiveportal_radius_stop_all(10); // NAS-Request
423

    
424
		captiveportal_filterdns_configure();
425

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

    
433
		captiveportal_delete_rules($pipes_to_remove);
434

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

    
454
	unlock($captiveportallck);
455

    
456
	return 0;
457
}
458

    
459
function captiveportal_init_webgui() {
460
	global $config, $cpzone;
461

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

    
470
function captiveportal_init_webgui_zonename($zone) {
471
	global $config, $cpzone;
472

    
473
	if (isset($config['captiveportal'][$zone])) {
474
		$cpzone = $zone;
475
		captiveportal_init_webgui_zone($config['captiveportal'][$zone]);
476
	}
477
}
478

    
479
function captiveportal_init_webgui_zone($cpcfg) {
480
	global $g, $config, $cpzone;
481

    
482
	if (!isset($cpcfg['enable'])) {
483
		return;
484
	}
485

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

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

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

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

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

    
524
function captiveportal_init_rules_byinterface($interface) {
525
	global $cpzone, $cpzoneid, $config;
526

    
527
	if (!is_array($config['captiveportal'])) {
528
		return;
529
	}
530

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

    
542
/* Create basic rules used by all zones */
543
function captiveportal_init_general_rules($flush = false) {
544
	global $g;
545

    
546
	$flush_rule = '';
547
	if ($flush) {
548
		$flush_rule = 'flush';
549
	}
550

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

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

    
561
# Redirect each CP interface to its specific rule
562
add 1000 skipto tablearg all from any to any via table(cp_ifaces)
563

    
564
# This interface has no cp zone configured
565
add 1100 allow all from any to any
566

    
567
# block everything else
568
add 65534 deny all from any to any
569
EOD;
570

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

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

    
583
	return $rule;
584
}
585

    
586
/* Return first rule number for a cp zone */
587
function captiveportal_ipfw_ruleno($id) {
588
	global $g;
589

    
590
	return 2000 + $id * $g['captiveportal_rules_interval'];
591
}
592

    
593
/* reinit will disconnect all users, be careful! */
594
function captiveportal_init_rules($reinit = false) {
595
	global $config, $g, $cpzone, $cpzoneid;
596

    
597
	if (!isset($config['captiveportal'][$cpzone]['enable'])) {
598
		return;
599
	}
600

    
601
	captiveportal_load_modules();
602
	captiveportal_init_general_rules();
603

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

    
608
	$skipto = captiveportal_ipfw_ruleno($cpzoneid);
609

    
610
	$cprules = '';
611

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

    
625
		$cpipm = get_interface_ip($cpifgrp);
626

    
627
		if (!is_ipaddr($cpipm)) {
628
			continue;
629
		}
630

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

    
642
		$cprules .= "table cp_ifaces add {$tmpif} {$skipto}\n";
643
	}
644
	if (count($cpips) > 0) {
645
		$cpactive = true;
646
	} else {
647
		return false;
648
	}
649

    
650
	$captiveportallck = try_lock("captiveportal{$cpzone}", 0);
651

    
652
	$tables = captiveportal_get_ipfw_table_names();
653

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

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

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

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

    
713
	if (!empty($config['captiveportal'][$cpzone]['listenporthttp'])) {
714
		$listenporthttp = $config['captiveportal'][$cpzone]['listenporthttp'];
715
	} else {
716
		$listenporthttp = 8000 + $cpzoneid;
717
	}
718

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

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

    
741
	/* generate passthru mac database */
742
	$cprules .= captiveportal_passthrumac_configure(true);
743
	$cprules .= "\n";
744

    
745
	/* allowed ipfw rules to make allowed ip work */
746
	$cprules .= captiveportal_allowedip_configure();
747

    
748
	/* allowed ipfw rules to make allowed hostnames work */
749
	$cprules .= captiveportal_allowedhostname_configure();
750

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

    
758
	captiveportal_filterdns_configure();
759

    
760
	if ($captiveportallck) {
761
		unlock($captiveportallck);
762
	}
763
}
764

    
765
/* Delete all rules related to specific cpzone */
766
function captiveportal_delete_rules($pipes_to_remove = array(), $clear_auth_rules = true) {
767
	global $g, $cpzoneid, $cpzone;
768

    
769
	$skipto1 = captiveportal_ipfw_ruleno($cpzoneid);
770
	$skipto2 = $skipto1 + $g['captiveportal_rules_interval'];
771

    
772
	$cp_ifaces = pfSense_ipfw_table_list("cp_ifaces");
773
	if (is_array($cp_ifaces)) {
774
		foreach ($cp_ifaces as $cp_iface) {
775
			if (empty($cp_iface['skipto']) ||
776
			    $cp_iface['skipto'] != $skipto1) {
777
				continue;
778
			}
779

    
780
			pfSense_ipfw_table("cp_ifaces", IP_FW_TABLE_XDEL,
781
			    $cp_iface['iface']);
782
		}
783
	}
784

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

    
787
	$tables = captiveportal_get_ipfw_table_names();
788

    
789
	$delrules = "";
790
	foreach ($tables as $table) {
791
		if (!$clear_auth_rules && substr($table, 0, strlen($cpzone . '_auth')) == $cpzone . '_auth') {
792
			continue;
793
		} else {
794
			$delrules .= "table {$table} destroy\n";
795
		}
796
	}
797

    
798
	foreach ($pipes_to_remove as $pipeno) {
799
		$delrules .= "pipe delete {$pipeno}\n";
800
	}
801

    
802
	if (empty($delrules)) {
803
		return;
804
	}
805

    
806
	file_put_contents("{$g['tmp_path']}/ipfw_{$cpzone}.deltable.rules", $delrules);
807
	mwexec("/sbin/ipfw -q {$g['tmp_path']}/ipfw_{$cpzone}.deltable.rules", true);
808
	@unlink("{$g['tmp_path']}/ipfw_{$cpzone}.deltable.rules");
809
}
810

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

    
820
	if (empty($cpzone)) {
821
		return;
822
	}
823

    
824
	$cpcfg = $config['captiveportal'][$cpzone];
825
	$vcpcfg = $config['voucher'][$cpzone];
826

    
827
	/* check for expired entries */
828
	$idletimeout = 0;
829
	$timeout = 0;
830
	if (!empty($cpcfg['timeout']) && is_numeric($cpcfg['timeout'])) {
831
		$timeout = $cpcfg['timeout'] * 60;
832
	}
833

    
834
	if (!empty($cpcfg['idletimeout']) && is_numeric($cpcfg['idletimeout'])) {
835
		$idletimeout = $cpcfg['idletimeout'] * 60;
836
	}
837

    
838
	/* check for entries exceeding their traffic quota */
839
	$trafficquota = 0;
840
	if (!empty($cpcfg['trafficquota']) && is_numeric($cpcfg['trafficquota'])) {
841
		$trafficquota = $cpcfg['trafficquota'] * 1048576;
842
	}
843

    
844
	/* Is there any job to do? */
845
	if (!$timeout && !$idletimeout && !$trafficquota && !isset($cpcfg['reauthenticate']) &&
846
	    !isset($cpcfg['radiussession_timeout']) && !isset($cpcfg['radiustraffic_quota']) &&
847
	    !isset($vcpcfg['enable']) && !isset($cpcfg['radacct_enable'])) {
848
		return;
849
	}
850

    
851

    
852
	/* Read database */
853
	/* NOTE: while this can be simplified in non radius case keep as is for now */
854
	$cpdb = captiveportal_read_db();
855

    
856
	$unsetindexes = array();
857
	$voucher_needs_sync = false;
858
	/*
859
	 * Snapshot the time here to use for calculation to speed up the process.
860
	 * If something is missed next run will catch it!
861
	 */
862
	$pruning_time = time();
863
	foreach ($cpdb as $cpentry) {
864
		$stop_time = $pruning_time;
865

    
866
		$timedout = false;
867
		$term_cause = 1;
868
		/* hard timeout or session_timeout from radius if enabled */
869
		if (isset($cpcfg['radiussession_timeout'])) {
870
			$utimeout = (is_numeric($cpentry[7])) ? $cpentry[7] : $timeout;
871
		} else {
872
			$utimeout = $timeout;
873
		}
874
		if ($utimeout) {
875
			if (($pruning_time - $cpentry[0]) >= $utimeout) {
876
				$timedout = true;
877
				$term_cause = 5; // Session-Timeout
878
				$logout_cause = 'SESSION TIMEOUT';
879
			}
880
		}
881

    
882
		/* Session-Terminate-Time */
883
		if (!$timedout && !empty($cpentry[9])) {
884
			if ($pruning_time >= $cpentry[9]) {
885
				$timedout = true;
886
				$term_cause = 5; // Session-Timeout
887
				$logout_cause = 'SESSION TIMEOUT';
888
			}
889
		}
890

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

    
910
		/* if vouchers are configured, activate session timeouts */
911
		if (!$timedout && isset($vcpcfg['enable']) && !empty($cpentry[7])) {
912
			if ($pruning_time >= ($cpentry[0] + $cpentry[7])) {
913
				$timedout = true;
914
				$term_cause = 5; // Session-Timeout
915
				$logout_cause = 'SESSION TIMEOUT';
916
				$voucher_needs_sync = true;
917
			}
918
		}
919

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

    
935
		if ($timedout) {
936
			captiveportal_disconnect($cpentry, $term_cause, $stop_time);
937
			captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], $logout_cause);
938
			$unsetindexes[] = $cpentry[5];
939
		}
940

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

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

    
1055
	captiveportal_prune_old_automac();
1056

    
1057
	if ($voucher_needs_sync == true) {
1058
		/* Trigger a sync of the vouchers on config */
1059
		send_event("service sync vouchers");
1060
	}
1061

    
1062
	/* write database */
1063
	if (!empty($unsetindexes)) {
1064
		captiveportal_remove_entries($unsetindexes);
1065
	}
1066
}
1067

    
1068
function captiveportal_prune_old_automac() {
1069
	global $g, $config, $cpzone, $cpzoneid;
1070

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

    
1127
/* remove a single client according to the DB entry */
1128
function captiveportal_disconnect($dbent, $term_cause = 1, $stop_time = null) {
1129
	global $g, $config, $cpzone, $cpzoneid;
1130

    
1131
	$stop_time = (empty($stop_time)) ? time() : $stop_time;
1132

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

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

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

    
1190
		if (!empty($removed_pipes)) {
1191
			$_gb = @pfSense_ipfw_pipe("pipe delete {$dbent[1]}");
1192
			$_gb = @pfSense_ipfw_pipe("pipe delete " .
1193
			    ($dbent[1]+1));
1194

    
1195
			/*
1196
			 * Release the ruleno so it can be reallocated to new
1197
			 * clients
1198
			 */
1199
			captiveportal_free_dn_ruleno($dbent[1]);
1200
		}
1201
	}
1202

    
1203
	// XMLRPC Call over to the master Voucher node
1204
	if (xmlrpc_sync_voucher_details($syncip, $syncport,
1205
	    $vouchersyncusername, $syncpass)) {
1206
		$remote_status = xmlrpc_sync_voucher_disconnect($dbent, $syncip,
1207
		    $syncport, $syncpass, $vouchersyncusername, $term_cause,
1208
		    $stop_time);
1209
	}
1210

    
1211
}
1212

    
1213
/* remove a single client by sessionid */
1214
function captiveportal_disconnect_client($sessionid, $term_cause = 1, $logoutReason = "LOGOUT") {
1215
	global $g, $config;
1216

    
1217
	$sessionid = SQLite3::escapeString($sessionid);
1218
	/* read database */
1219
	$result = captiveportal_read_db("WHERE sessionid = '{$sessionid}'");
1220

    
1221
	/* find entry */
1222
	if (!empty($result)) {
1223

    
1224
		foreach ($result as $cpentry) {
1225
			captiveportal_disconnect($cpentry, $term_cause);
1226
			captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "DISCONNECT");
1227
		}
1228
		captiveportal_remove_entries(array($sessionid));
1229
		unset($result);
1230
	}
1231
}
1232

    
1233
/* remove all clients */
1234
function captiveportal_disconnect_all($term_cause = 6, $logoutReason = "DISCONNECT") {
1235
	global $g, $config, $cpzone, $cpzoneid;
1236

    
1237
	/* check if we're pruning old entries and eventually wait */
1238
	$rcprunelock = try_lock("rcprunecaptiveportal{$cpzone}", 15);
1239

    
1240
	/* if we still don't have the lock, unlock forcefully and take it */
1241
	if (!$rcprunelock) {
1242
		log_error("CP zone ${cpzone}: could not obtain the lock for more than 15 seconds, lock taken forcefully to disconnect all users");
1243
		unlock_force("rcprunecaptiveportal{$cpzone}");
1244
		$rcprunelock = lock("rcprunecaptiveportal{$cpzone}", LOCK_EX);
1245
	}
1246

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

    
1250
	captiveportal_radius_stop_all($term_cause, $logoutReason);
1251

    
1252
	/* remove users from the database */
1253
	$cpdb = captiveportal_read_db();
1254
	$unsetindexes = array_column($cpdb,5);
1255
	if (!empty($unsetindexes)) {
1256
		captiveportal_remove_entries($unsetindexes);
1257
	}
1258

    
1259
	/* reinit ipfw rules */
1260
	captiveportal_init_rules(true);
1261

    
1262
	unlock($cpdblck);
1263
	unlock($rcprunelock);
1264
}
1265

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

    
1270
	$cpdb = captiveportal_read_db();
1271

    
1272
	$radacct = isset($config['captiveportal'][$cpzone]['radacct_enable']) ? true : false;
1273
	foreach ($cpdb as $cpentry) {
1274
		if ($cpentry['authmethod'] === 'radius' && $radacct) {
1275
			if ($config['captiveportal'][$cpzone]['reauthenticateacct'] == "stopstartfreeradius") {
1276
				$session_time = (time() - $cpentry[0]) % 60;
1277
				$start_time = time() - $session_time;
1278
			} else {
1279
				$start_time = $cpentry[0];
1280
			}
1281
			captiveportal_send_server_accounting('stop',
1282
				$cpentry[1], // ruleno
1283
				$cpentry[4], // username
1284
				$cpentry[2], // clientip
1285
				$cpentry[3], // clientmac
1286
				$cpentry[5], // sessionid
1287
				$start_time, // start time
1288
				$stop_time, // stop time
1289
				$term_cause); // Acct-Terminate-Cause
1290
		}
1291
		captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], $logoutReason);
1292
	}
1293
	unset($cpdb);
1294
}
1295

    
1296
function captiveportal_passthrumac_configure_entry($macent, $pipeinrule = false) {
1297
	global $config, $g, $cpzone;
1298

    
1299
	$bwUp = 0;
1300
	if (!empty($macent['bw_up'])) {
1301
		$bwUp = $macent['bw_up'];
1302
	} else if (!empty($config['captiveportal'][$cpzone]['bwdefaultup'])) {
1303
		$bwUp = $config['captiveportal'][$cpzone]['bwdefaultup'];
1304
	}
1305
	$bwDown = 0;
1306
	if (!empty($macent['bw_down'])) {
1307
		$bwDown = $macent['bw_down'];
1308
	} else if (!empty($config['captiveportal'][$cpzone]['bwdefaultdn'])) {
1309
		$bwDown = $config['captiveportal'][$cpzone]['bwdefaultdn'];
1310
	}
1311

    
1312
	if ($macent['action'] == 'pass') {
1313
		$rules = "";
1314

    
1315
		$pipeno = captiveportal_get_next_dn_ruleno('pipe_mac');
1316

    
1317
		$pipeup = $pipeno;
1318
		if ($pipeinrule == true) {
1319
			$_gb = @pfSense_ipfw_pipe("pipe {$pipeno} config bw {$bwUp}Kbit/s queue 100 buckets 16");
1320
		} else {
1321
			$rules .= "pipe {$pipeno} config bw {$bwUp}Kbit/s queue 100 buckets 16\n";
1322
		}
1323

    
1324
		$pipedown = $pipeno + 1;
1325
		if ($pipeinrule == true) {
1326
			$_gb = @pfSense_ipfw_pipe("pipe {$pipedown} config bw {$bwDown}Kbit/s queue 100 buckets 16");
1327
		} else {
1328
			$rules .= "pipe {$pipedown} config bw {$bwDown}Kbit/s queue 100 buckets 16\n";
1329
		}
1330

    
1331
		$rules .= "table {$cpzone}_pipe_mac add any,{$macent['mac']} {$pipeup}\n";
1332
		$rules .= "table {$cpzone}_pipe_mac add {$macent['mac']},any {$pipedown}\n";
1333
	}
1334

    
1335
	return $rules;
1336
}
1337

    
1338
function captiveportal_passthrumac_delete_entry($macent) {
1339
	global $cpzone;
1340
	$rules = "";
1341

    
1342
	if ($macent['action'] == 'pass') {
1343
		$pipeno = captiveportal_get_dn_passthru_ruleno($macent['mac']);
1344

    
1345
		if (!empty($pipeno)) {
1346
			captiveportal_free_dn_ruleno($pipeno);
1347
			$rules .= "table {$cpzone}_pipe_mac delete any,{$macent['mac']}\n";
1348
			$rules .= "table {$cpzone}_pipe_mac delete {$macent['mac']},any\n";
1349
			$rules .= "pipe delete " . $pipeno . "\n";
1350
			$rules .= "pipe delete " . ++$pipeno . "\n";
1351
		}
1352
	}
1353

    
1354
	return $rules;
1355
}
1356

    
1357
function captiveportal_passthrumac_configure($filename = false, $startindex = 0, $stopindex = 0) {
1358
	global $config, $g, $cpzone;
1359

    
1360
	$rules = "";
1361

    
1362
	if (is_array($config['captiveportal'][$cpzone]['passthrumac'])) {
1363
		if ($stopindex > 0) {
1364
			$fd = fopen($filename, "w");
1365
			for ($idx = $startindex; $idx <= $stopindex; $idx++) {
1366
				if (isset($config['captiveportal'][$cpzone]['passthrumac'][$idx])) {
1367
					$rules = captiveportal_passthrumac_configure_entry($config['captiveportal'][$cpzone]['passthrumac'][$idx]);
1368
					fwrite($fd, $rules);
1369
				}
1370
			}
1371
			fclose($fd);
1372

    
1373
			return;
1374
		} else {
1375
			$nentries = count($config['captiveportal'][$cpzone]['passthrumac']);
1376
			if ($nentries > 2000) {
1377
				$nloops = $nentries / 1000;
1378
				$remainder= $nentries % 1000;
1379
				for ($i = 0; $i < $nloops; $i++) {
1380
					mwexec_bg("/usr/local/sbin/fcgicli -f /etc/rc.captiveportal_configure_mac -d \"cpzone={$cpzone}&startidx=" . ($i * 1000) . "&stopidx=" . ((($i+1) * 1000) - 1) . "\"");
1381
				}
1382
				if ($remainder > 0) {
1383
					mwexec_bg("/usr/local/sbin/fcgicli -f /etc/rc.captiveportal_configure_mac -d \"cpzone={$cpzone}&startidx=" . ($i * 1000) . "&stopidx=" . (($i* 1000) + $remainder) ."\"");
1384
				}
1385
			} else {
1386
				foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $macent) {
1387
					$rules .= captiveportal_passthrumac_configure_entry($macent, true);
1388
				}
1389
			}
1390
		}
1391
	}
1392

    
1393
	return $rules;
1394
}
1395

    
1396
function captiveportal_passthrumac_findbyname($username) {
1397
	global $config, $cpzone;
1398

    
1399
	if (is_array($config['captiveportal'][$cpzone]['passthrumac'])) {
1400
		foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $macent) {
1401
			if ($macent['username'] == $username) {
1402
				return $macent;
1403
			}
1404
		}
1405
	}
1406
	return NULL;
1407
}
1408

    
1409
/*
1410
 * table (3=IN)/(4=OUT) hold allowed ip's without bw limits
1411
 */
1412
function captiveportal_allowedip_configure_entry($ipent, $ishostname = false) {
1413
	global $g, $config, $cpzone;
1414

    
1415
	/*  Instead of copying this entire function for something
1416
	 *  easy such as hostname vs ip address add this check
1417
	 */
1418
	if ($ishostname === true) {
1419
		if (!platform_booting()) {
1420
			$ipaddress = gethostbyname($ipent['hostname']);
1421
			if (!is_ipaddr($ipaddress)) {
1422
				return;
1423
			}
1424
		} else {
1425
			$ipaddress = "";
1426
		}
1427
	} else {
1428
		$ipaddress = $ipent['ip'];
1429
	}
1430

    
1431
	$rules = "";
1432
	$cp_filterdns_conf = "";
1433
	$enBwup = 0;
1434
	if (!empty($ipent['bw_up'])) {
1435
		$enBwup = intval($ipent['bw_up']);
1436
	} else if (!empty($config['captiveportal'][$cpzone]['bwdefaultup'])) {
1437
		$enBwup = $config['captiveportal'][$cpzone]['bwdefaultup'];
1438
	}
1439
	$enBwdown = 0;
1440
	if (!empty($ipent['bw_down'])) {
1441
		$enBwdown = intval($ipent['bw_down']);
1442
	} else if (!empty($config['captiveportal'][$cpzone]['bwdefaultdn'])) {
1443
		$enBwdown = $config['captiveportal'][$cpzone]['bwdefaultdn'];
1444
	}
1445

    
1446
	$pipeup = captiveportal_get_next_dn_ruleno('allowed');
1447
	$_gb = @pfSense_ipfw_pipe("pipe {$pipeup} config bw {$enBwup}Kbit/s queue 100 buckets 16");
1448
	$pipedown = $pipeup + 1;
1449
	$_gb = @pfSense_ipfw_pipe("pipe {$pipedown} config bw {$enBwdown}Kbit/s queue 100 buckets 16");
1450

    
1451
	if ($ishostname === true) {
1452
		$cp_filterdns_conf .= "ipfw {$ipent['hostname']} {$cpzone}_allowed_up pipe {$pipeup}\n";
1453
		$cp_filterdns_conf .= "ipfw {$ipent['hostname']} {$cpzone}_allowed_down pipe {$pipedown}\n";
1454
		if (!is_ipaddr($ipaddress)) {
1455
			return array("", $cp_filterdns_conf);
1456
		}
1457
	}
1458

    
1459
	$subnet = "";
1460
	if (!empty($ipent['sn'])) {
1461
		$subnet = "/{$ipent['sn']}";
1462
	}
1463
	$rules .= "table {$cpzone}_allowed_up add {$ipaddress}{$subnet} {$pipeup}\n";
1464
	$rules .= "table {$cpzone}_allowed_down add {$ipaddress}{$subnet} {$pipedown}\n";
1465

    
1466
	if ($ishostname === true) {
1467
		return array($rules, $cp_filterdns_conf);
1468
	} else {
1469
		return $rules;
1470
	}
1471
}
1472

    
1473
function captiveportal_allowedhostname_configure() {
1474
	global $config, $g, $cpzone, $cpzoneid;
1475

    
1476
	$rules = "";
1477
	if (!is_array($config['captiveportal'][$cpzone]['allowedhostname'])) {
1478
		return $rules;
1479
	}
1480

    
1481
	$rules = "\n# captiveportal_allowedhostname_configure()\n";
1482
	$cp_filterdns_conf = "";
1483
	foreach ($config['captiveportal'][$cpzone]['allowedhostname'] as $hostnameent) {
1484
		$tmprules = captiveportal_allowedip_configure_entry($hostnameent, true);
1485
		$rules .= $tmprules[0];
1486
		$cp_filterdns_conf .= $tmprules[1];
1487
	}
1488
	$cp_filterdns_filename = "{$g['varetc_path']}/filterdns-{$cpzone}-captiveportal.conf";
1489
	@file_put_contents($cp_filterdns_filename, $cp_filterdns_conf);
1490
	unset($cp_filterdns_conf);
1491

    
1492
	return $rules;
1493
}
1494

    
1495
function captiveportal_filterdns_configure() {
1496
	global $config, $g, $cpzone, $cpzoneid;
1497

    
1498
	$cp_filterdns_filename = $g['varetc_path'] .
1499
	    "/filterdns-{$cpzone}-captiveportal.conf";
1500

    
1501
	if (isset($config['captiveportal'][$cpzone]['enable']) &&
1502
	    is_array($config['captiveportal'][$cpzone]['allowedhostname']) &&
1503
	    file_exists($cp_filterdns_filename)) {
1504
		if (isvalidpid($g['varrun_path'] .
1505
		    "/filterdns-{$cpzone}-cpah.pid")) {
1506
			sigkillbypid($g['varrun_path'] .
1507
			    "/filterdns-{$cpzone}-cpah.pid", "HUP");
1508
		} else {
1509
			mwexec("/usr/local/sbin/filterdns -p " .
1510
			    "{$g['varrun_path']}/filterdns-{$cpzone}-cpah.pid" .
1511
			    " -i 300 -c {$cp_filterdns_filename} -d 1");
1512
		}
1513
	} else {
1514
		killbypid("{$g['varrun_path']}/filterdns-{$cpzone}-cpah.pid");
1515
		@unlink("{$g['varrun_path']}/filterdns-{$cpzone}-cpah.pid");
1516
	}
1517

    
1518
	return $rules;
1519
}
1520

    
1521
function captiveportal_allowedip_configure() {
1522
	global $config, $g, $cpzone;
1523

    
1524
	$rules = "";
1525
	if (is_array($config['captiveportal'][$cpzone]['allowedip'])) {
1526
		foreach ($config['captiveportal'][$cpzone]['allowedip'] as $ipent) {
1527
			$rules .= captiveportal_allowedip_configure_entry($ipent);
1528
		}
1529
	}
1530

    
1531
	return $rules;
1532
}
1533

    
1534
/* get last activity timestamp given client IP address */
1535
function captiveportal_get_last_activity($ip) {
1536
	global $cpzone;
1537

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

    
1541
	foreach ($tables as $table) {
1542
		$ipfw = pfSense_ipfw_table_lookup($table, $ip);
1543
		if (is_array($ipfw)) {
1544
			/* Workaround for #46652 */
1545
			if ($ipfw['packets'] > 0) {
1546
				return $ipfw['timestamp'];
1547
			} else {
1548
				return 0;
1549
			}
1550
		}
1551
	}
1552

    
1553
	return 0;
1554
}
1555

    
1556

    
1557
/* log successful captive portal authentication to syslog */
1558
/* part of this code from php.net */
1559
function captiveportal_logportalauth($user, $mac, $ip, $status, $message = null) {
1560
	// Log it
1561
	if (!$message) {
1562
		$message = "{$status}: {$user}, {$mac}, {$ip}";
1563
	} else {
1564
		$message = trim($message);
1565
		$message = "{$status}: {$user}, {$mac}, {$ip}, {$message}";
1566
	}
1567
	captiveportal_syslog($message);
1568
}
1569

    
1570
/* log simple messages to syslog */
1571
function captiveportal_syslog($message) {
1572
	global $cpzone;
1573

    
1574
	$message = trim($message);
1575
	$message = "Zone: {$cpzone} - {$message}";
1576
	openlog("logportalauth", LOG_PID, LOG_LOCAL4);
1577
	// Log it
1578
	syslog(LOG_INFO, $message);
1579
	closelog();
1580
}
1581

    
1582
/* Authenticate users using Authentication Backend */
1583
function captiveportal_authenticate_user(&$login = '', &$password = '', $clientmac = '', $clientip = '', $pipeno = 'null', $context = 'first') {
1584
	global $g, $config, $cpzone;
1585
	$cpcfg = $config['captiveportal'][$cpzone];
1586

    
1587
	$login_status = 'FAILURE';
1588
	$login_msg = gettext('Invalid credentials specified');
1589
	$reply_attributes = array();
1590
	$auth_method = '';
1591
	$auth_result = null;
1592

    
1593
	/*
1594
	Management of the reply Message (reason why the authentication failed) :
1595
	multiple authentication servers can be used, so multiple reply messages could theoretically be returned.
1596
	But only one message is returned (the most important one).
1597
	The return value of authenticate_user() define how important messages are :
1598
		- Reply message of a successful auth is more important than reply message of
1599
		a user failed auth(invalid credentials/authorization)
1600

    
1601
		- Reply message of a user failed auth is more important than reply message of
1602
		a server failed auth (unable to contact server)
1603

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

    
1607
	The $authlevel variable is a flag indicating the status of authentication
1608
	0 = failed server auth
1609
	1 = failed user auth
1610
	2 = failed user auth with custom server reply recieved
1611
	3 = successful auth
1612
	*/
1613
	$authlevel = 0;
1614

    
1615
	/* Getting authentication servers from captiveportal configuration */
1616
	$auth_servers = array();
1617

    
1618
	if ($cpcfg['auth_method'] === 'none') {
1619
		$auth_servers[] = array('type' => 'none');
1620
	} else {
1621
		if ($context === 'second') {
1622
			$fullauthservers = explode(",", $cpcfg['auth_server2']);
1623
		} else {
1624
			$fullauthservers = explode(",", $cpcfg['auth_server']);
1625
		}
1626

    
1627
		foreach ($fullauthservers as $authserver) {
1628
			if (strpos($authserver, ' - ') !== false) {
1629
				$authserver = explode(' - ', $authserver);
1630
				array_shift($authserver);
1631
				$authserver = implode(' - ', $authserver);
1632

    
1633
				if (auth_get_authserver($authserver) !== null) {
1634
					$auth_servers[] = auth_get_authserver($authserver);
1635
				} else {
1636
					log_error("Zone: {$cpzone} - Captive portal was unable to find the settings of the server '{$authserver}' used for authentication !");
1637
				}
1638
			}
1639
		}
1640
	}
1641

    
1642
	/* Unable to find the any authentication server config - shouldn't happen! - bail out */
1643
	if (count($auth_servers) === 0) {
1644
		log_error("Zone: {$cpzone} - No valid server could be used for authentication.");
1645
		$login_msg = gettext("Internal Error");
1646
	} else {
1647
		foreach ($auth_servers as $authcfg) {
1648
			if ($authlevel < 3) {
1649
				$radmac_error = false;
1650
				$attributes = array("nas_identifier" => empty($cpcfg["radiusnasid"]) ? "CaptivePortal-{$cpzone}" : $cpcfg["radiusnasid"],
1651
					"nas_port_type" => RADIUS_ETHERNET,
1652
					"nas_port" => $pipeno,
1653
					"framed_ip" => $clientip);
1654
				if (mac_format($clientmac) !== null) {
1655
					$attributes["calling_station_id"] = mac_format($clientmac);
1656
				}
1657

    
1658
				$result = null;
1659
				$status = null;
1660
				$msg = null;
1661

    
1662
				/* Radius MAC authentication */
1663
				if ($context === 'radmac' && $clientmac) {
1664
					if ($authcfg['type'] === 'radius') {
1665
						$login = mac_format($clientmac);
1666
						$status = "MACHINE LOGIN";
1667
					} else {
1668
						/* Trying to perform a Radius MAC authentication on a non-radius server - shouldn't happen! - bail out */
1669
						$msg = gettext("Internal Error");
1670
						log_error("Zone: {$cpzone} - Trying to perform RADIUS MAC authentication on a non-RADIUS server !");
1671
						$radmac_error = true;
1672
						$result = null;
1673
					}
1674
				}
1675

    
1676
				if (!$radmac_error) {
1677
					if ($authcfg['type'] === 'none') {
1678
						$result = true;
1679
					} else {
1680
						$result = authenticate_user($login, $password, $authcfg, $attributes);
1681
					}
1682

    
1683
					if (!empty($attributes['error_message'])) {
1684
						$msg = $attributes['error_message'];
1685
					}
1686

    
1687
					if ($authcfg['type'] == 'Local Auth' && $result && isset($cpcfg['localauth_priv'])) {
1688
						if (!userHasPrivilege(getUserEntry($login), "user-services-captiveportal-login")) {
1689
							$result = false;
1690
							$msg = gettext("Access Denied");
1691
						}
1692
					}
1693
					if ($context === 'radmac' && $result === null && empty($attributes['reply_message'])) {
1694
						$msg = gettext("RADIUS MAC Authentication Failed.");
1695
					}
1696

    
1697
					if (empty($status)) {
1698
						if ($result === true) {
1699
							$status = "ACCEPT";
1700
						} elseif ($result === null) {
1701
							$status = "ERROR";
1702
						} else {
1703
							$status = "FAILURE";
1704
						}
1705
					}
1706

    
1707
					if ($context === 'radmac' && $login == mac_format($clientmac) || $authcfg['type'] === 'none' && empty($login)) {
1708
						$login = "unauthenticated";
1709
					}
1710
				}
1711
				// We determine a flag
1712
				if ($result === true) {
1713
					$val = 3;
1714
				} elseif ($result === false && !empty($attributes['reply_message'])) {
1715
					$val = 2;
1716
					$msg = $attributes['reply_message'];
1717
				} elseif ($result === false) {
1718
					$val = 1;
1719
				} elseif ($result === null) {
1720
					$val = 0;
1721
				}
1722

    
1723
				if ($val >= $authlevel) {
1724
					$authlevel = $val;
1725
					$auth_method = $authcfg['type'];
1726
					$login_status = $status;
1727
					$login_msg = $msg;
1728
					$reply_attributes = $attributes;
1729
					$auth_result = $result;
1730
				}
1731
			}
1732
		}
1733
	}
1734

    
1735
	return array('result'=>$auth_result, 'attributes'=>$reply_attributes, 'auth_method' =>$auth_method, 'login_status'=> $login_status, 'login_message' => $login_msg);
1736
}
1737

    
1738
function captiveportal_opendb() {
1739
	global $g, $config, $cpzone, $cpzoneid;
1740

    
1741
	$db_path = "{$g['vardb_path']}/captiveportal{$cpzone}.db";
1742
	$createquery = "CREATE TABLE IF NOT EXISTS captiveportal (" .
1743
				"allow_time INTEGER, pipeno INTEGER, ip TEXT, mac TEXT, username TEXT, " .
1744
				"sessionid TEXT, bpassword TEXT, session_timeout INTEGER, idle_timeout INTEGER, " .
1745
				"session_terminate_time INTEGER, interim_interval INTEGER, traffic_quota INTEGER, " .
1746
				"bw_up INTEGER, bw_down INTEGER, authmethod TEXT, context TEXT); " .
1747
			"CREATE UNIQUE INDEX IF NOT EXISTS idx_active ON captiveportal (sessionid, username); " .
1748
			"CREATE INDEX IF NOT EXISTS user ON captiveportal (username); " .
1749
			"CREATE INDEX IF NOT EXISTS ip ON captiveportal (ip); " .
1750
			"CREATE INDEX IF NOT EXISTS starttime ON captiveportal (allow_time)";
1751

    
1752
	try {
1753
		$DB = new SQLite3($db_path);
1754
		$DB->busyTimeout(60000);
1755
	} catch (Exception $e) {
1756
		captiveportal_syslog("Could not open {$db_path} as an sqlite database for {$cpzone}. Error message: " . $e->getMessage() . " -- Trying again.");
1757
		unlink_if_exists($db_path);
1758
		try {
1759
			$DB = new SQLite3($db_path);
1760
			$DB->busyTimeout(60000);
1761
		} catch (Exception $e) {
1762
			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.");
1763
			return;
1764
		}
1765
	}
1766

    
1767
	if (!$DB) {
1768
		captiveportal_syslog("Could not open {$db_path} as an sqlite database for {$cpzone}. Error message: {$DB->lastErrorMsg()}. Trying again.");
1769
		unlink_if_exists($db_path);
1770
		$DB = new SQLite3($db_path);
1771
		$DB->busyTimeout(60000);
1772
		if (!$DB) {
1773
			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.");
1774
			return;
1775
		}
1776
	}
1777

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

    
1781
		/* If unable to initialize the database, reset and try again. */
1782
		$DB->close();
1783
		unset($DB);
1784
		unlink_if_exists($db_path);
1785
		$DB = new SQLite3($db_path);
1786
		$DB->busyTimeout(60000);
1787
		if ($DB->exec($createquery)) {
1788
			captiveportal_syslog("Successfully reinitialized tables for {$cpzone} -- database has been reset.");
1789
			if (!is_numericint($cpzoneid)) {
1790
				if (is_array($config['captiveportal'])) {
1791
					foreach ($config['captiveportal'] as $cpkey => $cp) {
1792
						if ($cpzone == $cpkey) {
1793
							$cpzoneid = $cp['zoneid'];
1794
						}
1795
					}
1796
				}
1797
			}
1798
			if (is_numericint($cpzoneid)) {
1799
				$table_names = captiveportal_get_ipfw_table_names();
1800
				foreach ($table_names as $table_name) {
1801
					mwexec("/sbin/ipfw table {$table_name} flush");
1802
				}
1803
				captiveportal_syslog("Flushed tables for {$cpzone} after database reset.");
1804
			}
1805
		} else {
1806
			captiveportal_syslog("Still unable to create tables for {$cpzone}. Error message: {$DB->lastErrorMsg()}. Remove the database file manually and try again.");
1807
		}
1808
	}
1809

    
1810
	return $DB;
1811
}
1812

    
1813
/* Get all tables for specific cpzone */
1814
function captiveportal_get_ipfw_table_names() {
1815
	global $cpzone;
1816

    
1817
	$result = array();
1818
	$tables = pfSense_ipfw_tables_list();
1819

    
1820
	if (!is_array($tables)) {
1821
		return $result;
1822
	}
1823

    
1824
	$len = strlen($cpzone) + 1;
1825
	foreach ($tables as $table) {
1826
		if (substr($table['name'], 0, $len) != $cpzone . '_') {
1827
			continue;
1828
		}
1829

    
1830
		$result[] = $table['name'];
1831
	}
1832

    
1833
	return $result;
1834
}
1835

    
1836
/* read captive portal DB into array */
1837
function captiveportal_read_db($query = "") {
1838
	$cpdb = array();
1839

    
1840
	$DB = captiveportal_opendb();
1841
	if ($DB) {
1842
		$response = $DB->query("SELECT * FROM captiveportal {$query}");
1843
		if ($response != FALSE) {
1844
			while ($row = $response->fetchArray()) {
1845
				$cpdb[] = $row;
1846
			}
1847
		}
1848
		$DB->close();
1849
	}
1850

    
1851
	return $cpdb;
1852
}
1853

    
1854
function captiveportal_remove_entries($remove) {
1855

    
1856
	if (!is_array($remove) || empty($remove)) {
1857
		return;
1858
	}
1859

    
1860
	$query = "DELETE FROM captiveportal WHERE sessionid in (";
1861
	foreach ($remove as $idx => $unindex) {
1862
		$query .= "'{$unindex}'";
1863
		if ($idx < (count($remove) - 1)) {
1864
			$query .= ",";
1865
		}
1866
	}
1867
	$query .= ")";
1868
	captiveportal_write_db($query);
1869
}
1870

    
1871
/* write captive portal DB */
1872
function captiveportal_write_db($queries) {
1873
	global $g;
1874

    
1875
	if (is_array($queries)) {
1876
		$query = implode(";", $queries);
1877
	} else {
1878
		$query = $queries;
1879
	}
1880

    
1881
	$DB = captiveportal_opendb();
1882
	if ($DB) {
1883
		$DB->exec("BEGIN TRANSACTION");
1884
		$result = $DB->exec($query);
1885
		if (!$result) {
1886
			captiveportal_syslog("Trying to modify DB returned error: {$DB->lastErrorMsg()}");
1887
		} else {
1888
			$DB->exec("END TRANSACTION");
1889
		}
1890
		$DB->close();
1891
		return $result;
1892
	} else {
1893
		return true;
1894
	}
1895
}
1896

    
1897
function captiveportal_write_elements() {
1898
	global $g, $config, $cpzone;
1899

    
1900
	$cpcfg = $config['captiveportal'][$cpzone];
1901

    
1902
	if (!is_dir($g['captiveportal_element_path'])) {
1903
		@mkdir($g['captiveportal_element_path']);
1904
	}
1905

    
1906
	if (is_array($cpcfg['element'])) {
1907
		foreach ($cpcfg['element'] as $data) {
1908
			/* Do not attempt to decode or write out empty files. */
1909
			if (isset($data['nocontent'])) {
1910
					continue;
1911
			}
1912
			if (empty($data['content']) || empty(base64_decode($data['content']))) {
1913
				unlink_if_exists("{$g['captiveportal_element_path']}/{$data['name']}");
1914
				touch("{$g['captiveportal_element_path']}/{$data['name']}");
1915
			} elseif (!@file_put_contents("{$g['captiveportal_element_path']}/{$data['name']}", base64_decode($data['content']))) {
1916
				printf(gettext('Error: cannot open \'%1$s\' in captiveportal_write_elements()%2$s'), $data['name'], "\n");
1917
				return 1;
1918
			}
1919
			if (!file_exists("{$g['captiveportal_path']}/{$data['name']}")) {
1920
				@symlink("{$g['captiveportal_element_path']}/{$data['name']}", "{$g['captiveportal_path']}/{$data['name']}");
1921
			}
1922
		}
1923
	}
1924

    
1925
	return 0;
1926
}
1927

    
1928
function captiveportal_free_dnrules($rulenos_start = 2000,
1929
    $rulenos_range_max = 64500, $dry_run = false, $clear_auth_pipes = true) {
1930
	global $g, $cpzone;
1931

    
1932
	$removed_pipes = array();
1933

    
1934
	if (!file_exists("{$g['vardb_path']}/captiveportaldn.rules")) {
1935
		return $removed_pipes;
1936
	}
1937

    
1938
	if (!$dry_run) {
1939
		$cpruleslck = lock("captiveportalrulesdn", LOCK_EX);
1940
	}
1941

    
1942
	$rules = unserialize(file_get_contents(
1943
	    "{$g['vardb_path']}/captiveportaldn.rules"));
1944
	$ridx = $rulenos_start;
1945
	while ($ridx < $rulenos_range_max) {
1946
		if (substr($rules[$ridx], 0, strlen($cpzone . '_')) == $cpzone . '_') {
1947
			if (!$clear_auth_pipes && substr($rules[$ridx], 0, strlen($cpzone . '_auth')) == $cpzone . '_auth') {
1948
				$ridx += 2;
1949
			} else {
1950
				if (!$dry_run) {
1951
					$rules[$ridx] = false;
1952
				}
1953
				$removed_pipes[] = $ridx;
1954
				$ridx++;
1955
				if (!$dry_run) {
1956
					$rules[$ridx] = false;
1957
				}
1958
				$removed_pipes[] = $ridx;
1959
				$ridx++;
1960
			}
1961
		} else {
1962
			$ridx += 2;
1963
		}
1964
	}
1965

    
1966
	if (!$dry_run) {
1967
		file_put_contents("{$g['vardb_path']}/captiveportaldn.rules",
1968
		    serialize($rules));
1969
		unlock($cpruleslck);
1970
	}
1971

    
1972
	unset($rules);
1973

    
1974
	return $removed_pipes;
1975
}
1976

    
1977
function captiveportal_reserve_ruleno($ruleno) {
1978
	global $g, $cpzone;
1979

    
1980
	$cpruleslck = lock("captiveportalrulesdn", LOCK_EX);
1981
	if (file_exists("{$g['vardb_path']}/captiveportaldn.rules")) {
1982
		$rules = unserialize(file_get_contents("{$g['vardb_path']}/captiveportaldn.rules"));
1983
	} else {
1984
		$rules = array_pad(array(), 64500, false);
1985
	}
1986
	$rules[$ruleno] = $cpzone . '_auth';
1987
	$ruleno++;
1988
	$rules[$ruleno] = $cpzone . '_auth';
1989

    
1990
	file_put_contents("{$g['vardb_path']}/captiveportaldn.rules", serialize($rules));
1991
	unlock($cpruleslck);
1992
	unset($rules);
1993

    
1994
	return $ruleno;
1995
}
1996

    
1997
function captiveportal_get_next_dn_ruleno($rule_type = 'default', $rulenos_start = 2000, $rulenos_range_max = 64500) {
1998
	global $config, $g, $cpzone;
1999

    
2000
	$cpruleslck = lock("captiveportalrulesdn", LOCK_EX);
2001
	$ruleno = 0;
2002
	if (file_exists("{$g['vardb_path']}/captiveportaldn.rules")) {
2003
		$rules = unserialize(file_get_contents("{$g['vardb_path']}/captiveportaldn.rules"));
2004
		$ridx = $rulenos_start;
2005
		while ($ridx < $rulenos_range_max) {
2006
			if (empty($rules[$ridx])) {
2007
				$ruleno = $ridx;
2008
				$rules[$ridx] = $cpzone . '_' . $rule_type;
2009
				$ridx++;
2010
				$rules[$ridx] = $cpzone . '_' . $rule_type;
2011
				break;
2012
			} else {
2013
				$ridx += 2;
2014
			}
2015
		}
2016
	} else {
2017
		$rules = array_pad(array(), $rulenos_range_max, false);
2018
		$ruleno = $rulenos_start;
2019
		$rules[$rulenos_start] = $cpzone . '_' . $rule_type;
2020
		$rulenos_start++;
2021
		$rules[$rulenos_start] = $cpzone . '_' . $rule_type;
2022
	}
2023
	file_put_contents("{$g['vardb_path']}/captiveportaldn.rules", serialize($rules));
2024
	unlock($cpruleslck);
2025
	unset($rules);
2026

    
2027
	return $ruleno;
2028
}
2029

    
2030
function captiveportal_free_dn_ruleno($ruleno) {
2031
	global $config, $g;
2032

    
2033
	$cpruleslck = lock("captiveportalrulesdn", LOCK_EX);
2034
	if (file_exists("{$g['vardb_path']}/captiveportaldn.rules")) {
2035
		$rules = unserialize(file_get_contents("{$g['vardb_path']}/captiveportaldn.rules"));
2036
		$rules[$ruleno] = false;
2037
		$ruleno++;
2038
		$rules[$ruleno] = false;
2039
		file_put_contents("{$g['vardb_path']}/captiveportaldn.rules", serialize($rules));
2040
		unset($rules);
2041
	}
2042
	unlock($cpruleslck);
2043
}
2044

    
2045
function captiveportal_get_dn_passthru_ruleno($value) {
2046
	global $config, $g, $cpzone, $cpzoneid;
2047

    
2048
	$cpcfg = $config['captiveportal'][$cpzone];
2049
	if (!isset($cpcfg['enable'])) {
2050
		return NULL;
2051
	}
2052

    
2053
	$cpruleslck = lock("captiveportalrulesdn", LOCK_EX);
2054
	$ruleno = NULL;
2055
	if (file_exists("{$g['vardb_path']}/captiveportaldn.rules")) {
2056
		unset($output);
2057
		$item = pfSense_ipfw_table_lookup("{$cpzone}_pipe_mac",
2058
		    "any,{$value}");
2059
		if (!is_array($item) || empty($item['pipe'])) {
2060
			unlock($cpruleslck);
2061
			return NULL;
2062
		}
2063

    
2064
		$ruleno = intval($item['pipe']);
2065
		$rules = unserialize(file_get_contents("{$g['vardb_path']}/captiveportaldn.rules"));
2066
		if (!$rules[$ruleno]) {
2067
			$ruleno = NULL;
2068
		}
2069
		unset($rules);
2070
	}
2071
	unlock($cpruleslck);
2072

    
2073
	return $ruleno;
2074
}
2075

    
2076
/**
2077
 * This function will calculate the traffic produced by a client
2078
 * based on its firewall rule
2079
 *
2080
 * Point of view: NAS
2081
 *
2082
 * Input means: from the client
2083
 * Output means: to the client
2084
 *
2085
 */
2086

    
2087
function getVolume($ip) {
2088
	global $config, $cpzone;
2089

    
2090
	$reverse = isset($config['captiveportal'][$cpzone]['reverseacct'])
2091
	    ? true : false;
2092
	$volume = array();
2093
	// Initialize vars properly, since we don't want NULL vars
2094
	$volume['input_pkts'] = $volume['input_bytes'] = 0;
2095
	$volume['output_pkts'] = $volume['output_bytes'] = 0;
2096

    
2097
	$tables = array("allowed", "auth");
2098

    
2099
	foreach($tables as $table) {
2100
		$ipfw = pfSense_ipfw_table_lookup("{$cpzone}_{$table}_up", $ip);
2101
		if (!is_array($ipfw)) {
2102
			continue;
2103
		}
2104
		if ($reverse) {
2105
			$volume['output_pkts'] = $ipfw['packets'];
2106
			$volume['output_bytes'] = $ipfw['bytes'];
2107
		} else {
2108
			$volume['input_pkts'] = $ipfw['packets'];
2109
			$volume['input_bytes'] = $ipfw['bytes'];
2110
		}
2111
	}
2112

    
2113
	foreach($tables as $table) {
2114
		$ipfw = pfSense_ipfw_table_lookup("{$cpzone}_{$table}_down",
2115
		    $ip);
2116
		if (!is_array($ipfw)) {
2117
			continue;
2118
		}
2119
		if ($reverse) {
2120
			$volume['input_pkts'] = $ipfw['packets'];
2121
			$volume['input_bytes'] = $ipfw['bytes'];
2122
		} else {
2123
			$volume['output_pkts'] = $ipfw['packets'];
2124
			$volume['output_bytes'] = $ipfw['bytes'];
2125
		}
2126
	}
2127

    
2128
	return $volume;
2129
}
2130

    
2131
function portal_ip_from_client_ip($cliip) {
2132
	global $config, $cpzone;
2133

    
2134
	$isipv6 = is_ipaddrv6($cliip);
2135
	$interfaces = explode(",", $config['captiveportal'][$cpzone]['interface']);
2136
	foreach ($interfaces as $cpif) {
2137
		if ($isipv6) {
2138
			$ip = get_interface_ipv6($cpif);
2139
			$sn = get_interface_subnetv6($cpif);
2140
		} else {
2141
			$ip = get_interface_ip($cpif);
2142
			$sn = get_interface_subnet($cpif);
2143
		}
2144
		if (ip_in_subnet($cliip, "{$ip}/{$sn}")) {
2145
			return $ip;
2146
		}
2147
	}
2148

    
2149
	$inet = ($isipv6) ? '-inet6' : '-inet';
2150
	$iface = exec_command("/sbin/route -n get {$inet} {$cliip} | /usr/bin/awk '/interface/ { print \$2; };'");
2151
	$iface = trim($iface, "\n");
2152
	if (!empty($iface)) {
2153
		$ip = ($isipv6) ? find_interface_ipv6($iface) : find_interface_ip($iface);
2154
		if (is_ipaddr($ip)) {
2155
			return $ip;
2156
		}
2157
	}
2158

    
2159
	// doesn't match up to any particular interface
2160
	// so let's set the portal IP to what PHP says
2161
	// the server IP issuing the request is.
2162
	// allows same behavior as 1.2.x where IP isn't
2163
	// in the subnet of any CP interface (static routes, etc.)
2164
	// rather than forcing to DNS hostname resolution
2165
	$ip = $_SERVER['SERVER_ADDR'];
2166
	if (is_ipaddr($ip)) {
2167
		return $ip;
2168
	}
2169

    
2170
	return false;
2171
}
2172

    
2173
function portal_hostname_from_client_ip($cliip) {
2174
	global $config, $cpzone;
2175

    
2176
	$cpcfg = $config['captiveportal'][$cpzone];
2177

    
2178
	if (isset($cpcfg['httpslogin'])) {
2179
		$listenporthttps = $cpcfg['listenporthttps'] ? $cpcfg['listenporthttps'] : ($cpcfg['zoneid'] + 8001);
2180
		$ourhostname = $cpcfg['httpsname'];
2181

    
2182
		if ($listenporthttps != 443) {
2183
			$ourhostname .= ":" . $listenporthttps;
2184
		}
2185
	} else {
2186
		$listenporthttp = $cpcfg['listenporthttp'] ? $cpcfg['listenporthttp'] : ($cpcfg['zoneid'] + 8000);
2187
		$ifip = portal_ip_from_client_ip($cliip);
2188
		if (!$ifip) {
2189
			$ourhostname = "{$config['system']['hostname']}.{$config['system']['domain']}";
2190
		} else {
2191
			$ourhostname = (is_ipaddrv6($ifip)) ? "[{$ifip}]" : "{$ifip}";
2192
		}
2193

    
2194
		if ($listenporthttp != 80) {
2195
			$ourhostname .= ":" . $listenporthttp;
2196
		}
2197
	}
2198

    
2199
	return $ourhostname;
2200
}
2201

    
2202
/* functions move from index.php */
2203

    
2204
function portal_reply_page($redirurl, $type = null, $message = null, $clientmac = null, $clientip = null, $username = null, $password = null) {
2205
	global $g, $config, $cpzone;
2206

    
2207
	/* Get captive portal layout */
2208
	if ($type == "redir") {
2209
		header("Location: {$redirurl}");
2210
		return;
2211
	} else if ($type == "login") {
2212
		$htmltext = get_include_contents("{$g['varetc_path']}/captiveportal_{$cpzone}.html");
2213
	} else {
2214
		$htmltext = get_include_contents("{$g['varetc_path']}/captiveportal-{$cpzone}-error.html");
2215
	}
2216

    
2217
	$cpcfg = $config['captiveportal'][$cpzone];
2218

    
2219
	/* substitute the PORTAL_REDIRURL variable */
2220
	if ($cpcfg['preauthurl']) {
2221
		$htmltext = str_replace("\$PORTAL_REDIRURL\$", "{$cpcfg['preauthurl']}", $htmltext);
2222
		$htmltext = str_replace("#PORTAL_REDIRURL#", "{$cpcfg['preauthurl']}", $htmltext);
2223
	}
2224

    
2225
	/* substitute other variables */
2226
	$ourhostname = portal_hostname_from_client_ip($clientip);
2227
	$protocol = (isset($cpcfg['httpslogin'])) ? 'https://' : 'http://';
2228
	$htmltext = str_replace("\$PORTAL_ACTION\$", "{$protocol}{$ourhostname}/index.php?zone={$cpzone}", $htmltext);
2229
	$htmltext = str_replace("#PORTAL_ACTION#", "{$protocol}{$ourhostname}/index.php?zone={$cpzone}", $htmltext);
2230

    
2231
	$htmltext = str_replace("\$PORTAL_ZONE\$", htmlspecialchars($cpzone), $htmltext);
2232
	$htmltext = str_replace("\$PORTAL_REDIRURL\$", htmlspecialchars($redirurl), $htmltext);
2233
	$htmltext = str_replace("\$PORTAL_MESSAGE\$", htmlspecialchars($message), $htmltext);
2234
	$htmltext = str_replace("\$CLIENT_MAC\$", htmlspecialchars($clientmac), $htmltext);
2235
	$htmltext = str_replace("\$CLIENT_IP\$", htmlspecialchars($clientip), $htmltext);
2236

    
2237
	// Special handling case for captive portal master page so that it can be ran
2238
	// through the PHP interpreter using the include method above.  We convert the
2239
	// $VARIABLE$ case to #VARIABLE# in /etc/inc/captiveportal.inc before writing out.
2240
	$htmltext = str_replace("#PORTAL_ZONE#", htmlspecialchars($cpzone), $htmltext);
2241
	$htmltext = str_replace("#PORTAL_REDIRURL#", htmlspecialchars($redirurl), $htmltext);
2242
	$htmltext = str_replace("#PORTAL_MESSAGE#", htmlspecialchars($message), $htmltext);
2243
	$htmltext = str_replace("#CLIENT_MAC#", htmlspecialchars($clientmac), $htmltext);
2244
	$htmltext = str_replace("#CLIENT_IP#", htmlspecialchars($clientip), $htmltext);
2245
	$htmltext = str_replace("#USERNAME#", htmlspecialchars($username), $htmltext);
2246
	$htmltext = str_replace("#PASSWORD#", htmlspecialchars($password), $htmltext);
2247

    
2248
	echo $htmltext;
2249
}
2250

    
2251
function captiveportal_reapply_attributes($cpentry, $attributes) {
2252
	global $config, $cpzone, $g;
2253

    
2254
	if (isset($config['captiveportal'][$cpzone]['peruserbw'])) {
2255
		$dwfaultbw_up = !empty($config['captiveportal'][$cpzone]['bwdefaultup']) ? $config['captiveportal'][$cpzone]['bwdefaultup'] : 0;
2256
		$dwfaultbw_down = !empty($config['captiveportal'][$cpzone]['bwdefaultdn']) ? $config['captiveportal'][$cpzone]['bwdefaultdn'] : 0;
2257
	} else {
2258
		$dwfaultbw_up = $dwfaultbw_down = 0;
2259
	}
2260
	/* pipe throughputs must always be an integer, enforce that restriction again here. */
2261
	if (isset($config['captiveportal'][$cpzone]['radiusperuserbw'])) {
2262
		$bw_up = round(!empty($attributes['bw_up']) ? intval($attributes['bw_up'])/1000 : $dwfaultbw_up, 0);
2263
		$bw_down = round(!empty($attributes['bw_down']) ? intval($attributes['bw_down'])/1000 : $dwfaultbw_down, 0);
2264
	} else {
2265
		$bw_up = round($dwfaultbw_up,0);
2266
		$bw_down = round($dwfaultbw_down,0);
2267
	}
2268

    
2269
	$bw_up_pipeno = $cpentry[1];
2270
	$bw_down_pipeno = $cpentry[1]+1;
2271

    
2272
	if ($cpentry['bw_up'] !== $bw_up) {
2273
		$_gb = @pfSense_ipfw_pipe("pipe {$bw_up_pipeno} config bw {$bw_up}Kbit/s queue 100 buckets 16");
2274
		captiveportal_update_entry($cpentry['sessionid'], $bw_up, 'bw_up');
2275
	}
2276
	if ($cpentry['bw_down'] !== $bw_down) {
2277
		$_gb = @pfSense_ipfw_pipe("pipe {$bw_down_pipeno} config bw {$bw_down}Kbit/s queue 100 buckets 16");
2278
		captiveportal_update_entry($cpentry['sessionid'], $bw_down, 'bw_down');
2279
	}
2280
	unset($bw_up_pipeno, $bw_down_pipeno, $bw_up, $bw_down);
2281
}
2282

    
2283
function captiveportal_update_entry($sessionid, $new_value, $field_to_update) {
2284
	global $cpzone;
2285

    
2286
	if (!intval($new_value)) {
2287
		$new_value = "'{$new_value}'";
2288
	}
2289
	captiveportal_write_db("UPDATE captiveportal SET {$field_to_update} = {$new_value} WHERE sessionid = '{$sessionid}'");
2290
}
2291

    
2292
function portal_allow($clientip, $clientmac, $username, $password = null, $attributes = null, $pipeno = null, $authmethod = null, $context = 'first') {
2293
	global $redirurl, $g, $config, $type, $_POST, $cpzone, $cpzoneid;
2294

    
2295
	// Ensure we create an array if we are missing attributes
2296
	if (!is_array($attributes)) {
2297
		$attributes = array();
2298
	}
2299

    
2300
	unset($sessionid);
2301

    
2302
	/* Do not allow concurrent login execution. */
2303
	$cpdblck = lock("captiveportaldb{$cpzone}", LOCK_EX);
2304

    
2305
	if ($attributes['voucher']) {
2306
		$remaining_time = $attributes['session_timeout'];
2307
		$authmethod = "voucher"; // Set RADIUS-Attribute to Voucher to prevent ReAuth-Reqeuest for Vouchers Bug: #2155
2308
		$context = "voucher";
2309
	}
2310

    
2311
	$writecfg = false;
2312
	/* If both "Add MAC addresses of connected users as pass-through MAC" and "Disable concurrent logins" are checked, 
2313
	then we need to check if the user was already authenticated using another MAC Address, and if so remove the previous Pass-Through MAC. */	
2314
	if ((isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) && ($username != 'unauthenticated') && isset($config['captiveportal'][$cpzone]['passthrumacadd'])) {
2315
		$mac = captiveportal_passthrumac_findbyname($username);
2316
		if (!empty($mac)) {
2317
			foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $idx => $macent) {
2318
				if ($macent['mac'] != $mac['mac']) {
2319
					continue;
2320
				}
2321

    
2322
				$pipeno = captiveportal_get_dn_passthru_ruleno($mac['mac']);
2323
				if ($pipeno) {
2324
					captiveportal_free_dn_ruleno($pipeno);
2325
					@pfSense_ipfw_table("{$cpzone}_pipe_mac", IP_FW_TABLE_XDEL, "any,{$mac['mac']}");
2326
					@pfSense_ipfw_table("{$cpzone}_pipe_mac", IP_FW_TABLE_XDEL, "{$mac['mac']},any");
2327
					@pfSense_ipfw_pipe("pipe delete " . $pipeno+1);
2328
					@pfSense_ipfw_pipe("pipe delete " . $pipeno);
2329
				}
2330
				unset($config['captiveportal'][$cpzone]['passthrumac'][$idx]);
2331
			}
2332
		}
2333
	}
2334

    
2335
	/* read in client database */
2336
	$query = "WHERE ip = '{$clientip}'";
2337
	$tmpusername = SQLite3::escapeString(strtolower($username));
2338
	if (isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) {
2339
		$query .= " OR (username != 'unauthenticated' AND lower(username) = '{$tmpusername}')";
2340
	}
2341
	$cpdb = captiveportal_read_db($query);
2342

    
2343
	/* Snapshot the timestamp */
2344
	$allow_time = time();
2345
	$unsetindexes = array();
2346

    
2347
	foreach ($cpdb as $cpentry) {
2348
		/* on the same ip */
2349
		if ($cpentry[2] == $clientip) {
2350
			if (isset($config['captiveportal'][$cpzone]['nomacfilter']) || $cpentry[3] == $clientmac) {
2351
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT LOGIN - REUSING OLD SESSION");
2352
			} else {
2353
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT LOGIN - REUSING IP {$cpentry[2]} WITH DIFFERENT MAC ADDRESS {$cpentry[3]}");
2354
			}
2355
			$sessionid = $cpentry[5];
2356
			break;
2357
		} elseif (($attributes['voucher']) && ($username != 'unauthenticated') && ($cpentry[4] == $username)) {
2358
			// user logged in with an active voucher. Check for how long and calculate
2359
			// how much time we can give him (voucher credit - used time)
2360
			$remaining_time = $cpentry[0] + $cpentry[7] - $allow_time;
2361
			if ($remaining_time < 0) { // just in case.
2362
				$remaining_time = 0;
2363
			}
2364

    
2365
			/* This user was already logged in so we disconnect the old one */
2366
			captiveportal_disconnect($cpentry, 13);
2367
			captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT LOGIN - TERMINATING OLD SESSION");
2368
			$unsetindexes[] = $cpentry[5];
2369
			break;
2370
		} elseif ((isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) && ($username != 'unauthenticated')) {
2371
			/* on the same username */
2372
			if (strcasecmp($cpentry[4], $username) == 0) {
2373
				/* This user was already logged in so we disconnect the old one */
2374
				captiveportal_disconnect($cpentry, 13);
2375
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT LOGIN - TERMINATING OLD SESSION");
2376
				$unsetindexes[] = $cpentry[5];
2377
				break;
2378
			}
2379
		}
2380
	}
2381
	unset($cpdb);
2382

    
2383
	if (!empty($unsetindexes)) {
2384
		captiveportal_remove_entries($unsetindexes);
2385
	}
2386

    
2387
	if ($attributes['voucher'] && $remaining_time <= 0) {
2388
		return 0;       // voucher already used and no time left
2389
	}
2390

    
2391
	if (!isset($sessionid)) {
2392
		/* generate unique session ID */
2393
		$tod = gettimeofday();
2394
		$sessionid = substr(md5(mt_rand() . $tod['sec'] . $tod['usec'] . $clientip . $clientmac), 0, 16);
2395

    
2396
		if (isset($config['captiveportal'][$cpzone]['peruserbw'])) {
2397
			$dwfaultbw_up = !empty($config['captiveportal'][$cpzone]['bwdefaultup']) ? $config['captiveportal'][$cpzone]['bwdefaultup'] : 0;
2398
			$dwfaultbw_down = !empty($config['captiveportal'][$cpzone]['bwdefaultdn']) ? $config['captiveportal'][$cpzone]['bwdefaultdn'] : 0;
2399
		} else {
2400
			$dwfaultbw_up = $dwfaultbw_down = 0;
2401
		}
2402
		/* pipe throughputs must always be an integer, enforce that restriction again here. */
2403
		if (isset($config['captiveportal'][$cpzone]['radiusperuserbw'])) {
2404
			$bw_up = round(!empty($attributes['bw_up']) ? intval($attributes['bw_up'])/1000 : $dwfaultbw_up, 0);
2405
			$bw_down = round(!empty($attributes['bw_down']) ? intval($attributes['bw_down'])/1000 : $dwfaultbw_down, 0);
2406
		} else {
2407
			$bw_up = round($dwfaultbw_up,0);
2408
			$bw_down = round($dwfaultbw_down,0);
2409
		}
2410

    
2411
		if (isset($config['captiveportal'][$cpzone]['passthrumacadd'])) {
2412

    
2413
			$mac = array();
2414
			$mac['action'] = 'pass';
2415
			$mac['mac'] = $clientmac;
2416
			$mac['ip'] = $clientip; /* Used only for logging */
2417
			$mac['username'] = $username;
2418
			if ($attributes['voucher']) {
2419
				$mac['logintype'] = "voucher";
2420
			}
2421
			if ($username == "unauthenticated") {
2422
				$mac['descr'] = "Auto-added";
2423
			} else if ($authmethod == "voucher") {
2424
				$mac['descr'] = "Auto-added for voucher {$username}";
2425
			} else {
2426
				$mac['descr'] = "Auto-added for user {$username}";
2427
			}
2428
			if (!empty($bw_up)) {
2429
				$mac['bw_up'] = $bw_up;
2430
			}
2431
			if (!empty($bw_down)) {
2432
				$mac['bw_down'] = $bw_down;
2433
			}
2434
			if (!is_array($config['captiveportal'][$cpzone]['passthrumac'])) {
2435
				$config['captiveportal'][$cpzone]['passthrumac'] = array();
2436
			}
2437
			//check for mac duplicates before adding it to config.
2438
			$mac_duplicate = false;
2439
			foreach($config['captiveportal'][$cpzone]['passthrumac'] as $mac_check){
2440
				if($mac_check['mac'] == $mac['mac']){
2441
					$mac_duplicate = true;
2442
				}
2443
			}
2444
			if(!$mac_duplicate){
2445
				$config['captiveportal'][$cpzone]['passthrumac'][] = $mac;
2446
			}
2447
			unlock($cpdblck);
2448
			$macrules = captiveportal_passthrumac_configure_entry($mac);
2449
			file_put_contents("{$g['tmp_path']}/macentry_{$cpzone}.rules.tmp", $macrules);
2450
			mwexec("/sbin/ipfw -q {$g['tmp_path']}/macentry_{$cpzone}.rules.tmp");
2451
			$writecfg = true;
2452
		} else {
2453
			/* See if a pipeno is passed, if not start sessions because this means there isn't one atm */
2454
			if (is_null($pipeno)) {
2455
				$pipeno = captiveportal_get_next_dn_ruleno('auth');
2456
			}
2457

    
2458
			/* if the pool is empty, return appropriate message and exit */
2459
			if (is_null($pipeno)) {
2460
				portal_reply_page($redirurl, "error", "System reached maximum login capacity");
2461
				log_error("Zone: {$cpzone} - WARNING!  Captive portal has reached maximum login capacity");
2462
				unlock($cpdblck);
2463
				return;
2464
			}
2465

    
2466
			$bw_up_pipeno = $pipeno;
2467
			$bw_down_pipeno = $pipeno + 1;
2468
			//$bw_up /= 1000; // Scale to Kbit/s
2469
			$_gb = @pfSense_ipfw_pipe("pipe {$bw_up_pipeno} config bw {$bw_up}Kbit/s queue 100 buckets 16");
2470
			$_gb = @pfSense_ipfw_pipe("pipe {$bw_down_pipeno} config bw {$bw_down}Kbit/s queue 100 buckets 16");
2471

    
2472
			$rule_entry = "{$clientip}/" . (is_ipaddrv6($clientip) ? "128" : "32");
2473
			if (!isset($config['captiveportal'][$cpzone]['nomacfilter'])) {
2474
				$rule_entry .= ",{$clientmac}";
2475
			}
2476
			$_gb = @pfSense_ipfw_table("{$cpzone}_auth_up", IP_FW_TABLE_XADD, "{$rule_entry}", $bw_up_pipeno);
2477
			$_gb = @pfSense_ipfw_table("{$cpzone}_auth_down", IP_FW_TABLE_XADD, "{$rule_entry}", $bw_down_pipeno);
2478

    
2479
			if ($attributes['voucher']) {
2480
				$attributes['session_timeout'] = $remaining_time;
2481
			}
2482

    
2483
			/* handle empty attributes */
2484
			$session_timeout = (!empty($attributes['session_timeout'])) ? $attributes['session_timeout'] : 'NULL';
2485
			$idle_timeout = (!empty($attributes['idle_timeout'])) ? $attributes['idle_timeout'] : 'NULL';
2486
			$session_terminate_time = (!empty($attributes['session_terminate_time'])) ? $attributes['session_terminate_time'] : 'NULL';
2487
			$interim_interval = (!empty($attributes['interim_interval'])) ? $attributes['interim_interval'] : 'NULL';
2488
			$traffic_quota = (!empty($attributes['maxbytes'])) ? $attributes['maxbytes'] : 'NULL';
2489

    
2490
			/* escape username */
2491
			$safe_username = SQLite3::escapeString($username);
2492

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

    
2499
			/* store information to database */
2500
			captiveportal_write_db($insertquery);
2501
			unlock($cpdblck);
2502
			unset($insertquery, $bpassword);
2503

    
2504
			$radacct = isset($config['captiveportal'][$cpzone]['radacct_enable']) ? true : false;
2505
			if ($authmethod === 'radius' && $radacct) {
2506
				captiveportal_send_server_accounting('start',
2507
					$pipeno, // ruleno
2508
					$username, // username
2509
					$clientip, // clientip
2510
					$clientmac, // clientmac
2511
					$sessionid, // sessionid
2512
					time());  // start time
2513
			}
2514
		}
2515
	} else {
2516
		/* NOTE: #3062-11 If the pipeno has been allocated free it to not DoS the CP */
2517
		if (!is_null($pipeno)) {
2518
			captiveportal_free_dn_ruleno($pipeno);
2519
		}
2520

    
2521
		unlock($cpdblck);
2522
	}
2523

    
2524
	if ($writecfg == true) {
2525
		write_config(gettext("Captive Portal allowed users configuration changed"));
2526
	}
2527

    
2528
	/* redirect user to desired destination */
2529
	if (!empty($attributes['url_redirection'])) {
2530
		$my_redirurl = $attributes['url_redirection'];
2531
	} else if (!empty($redirurl)) {
2532
		$my_redirurl = $redirurl;
2533
	} else if (!empty($config['captiveportal'][$cpzone]['redirurl'])) {
2534
		$my_redirurl = $config['captiveportal'][$cpzone]['redirurl'];
2535
	}
2536

    
2537
	if (isset($config['captiveportal'][$cpzone]['logoutwin_enable']) && !isset($config['captiveportal'][$cpzone]['passthrumacadd'])) {
2538
		$ourhostname = portal_hostname_from_client_ip($clientip);
2539
		$protocol = (isset($config['captiveportal'][$cpzone]['httpslogin'])) ? 'https://' : 'http://';
2540
		$logouturl = "{$protocol}{$ourhostname}/";
2541

    
2542
		if (isset($attributes['reply_message'])) {
2543
			$message = $attributes['reply_message'];
2544
		} else {
2545
			$message = 0;
2546
		}
2547

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

    
2550
	} else {
2551
		portal_reply_page($my_redirurl, "redir", "Just redirect the user.");
2552
	}
2553

    
2554
	return $sessionid;
2555
}
2556

    
2557

    
2558
/*
2559
 * Used for when pass-through credits are enabled.
2560
 * Returns true when there was at least one free login to deduct for the MAC.
2561
 * Expired entries are removed as they are seen.
2562
 * Active entries are updated according to the configuration.
2563
 */
2564
function portal_consume_passthrough_credit($clientmac) {
2565
	global $config, $cpzone;
2566

    
2567
	if (!empty($config['captiveportal'][$cpzone]['freelogins_count']) && is_numeric($config['captiveportal'][$cpzone]['freelogins_count'])) {
2568
		$freeloginscount = $config['captiveportal'][$cpzone]['freelogins_count'];
2569
	} else {
2570
		return false;
2571
	}
2572

    
2573
	if (!empty($config['captiveportal'][$cpzone]['freelogins_resettimeout']) && is_numeric($config['captiveportal'][$cpzone]['freelogins_resettimeout'])) {
2574
		$resettimeout = $config['captiveportal'][$cpzone]['freelogins_resettimeout'];
2575
	} else {
2576
		return false;
2577
	}
2578

    
2579
	if ($freeloginscount < 1 || $resettimeout <= 0 || !$clientmac) {
2580
		return false;
2581
	}
2582

    
2583
	$updatetimeouts = isset($config['captiveportal'][$cpzone]['freelogins_updatetimeouts']);
2584

    
2585
	/*
2586
	 * Read database of used MACs.  Lines are a comma-separated list
2587
	 * of the time, MAC, then the count of pass-through credits remaining.
2588
	 */
2589
	$usedmacs = captiveportal_read_usedmacs_db();
2590

    
2591
	$currenttime = time();
2592
	$found = false;
2593
	foreach ($usedmacs as $key => $usedmac) {
2594
		$usedmac = explode(",", $usedmac);
2595

    
2596
		if ($usedmac[1] == $clientmac) {
2597
			if ($usedmac[0] + ($resettimeout * 3600) > $currenttime) {
2598
				if ($usedmac[2] < 1) {
2599
					if ($updatetimeouts) {
2600
						$usedmac[0] = $currenttime;
2601
						unset($usedmacs[$key]);
2602
						$usedmacs[] = implode(",", $usedmac);
2603
						captiveportal_write_usedmacs_db($usedmacs);
2604
					}
2605

    
2606
					return false;
2607
				} else {
2608
					$usedmac[2] -= 1;
2609
					$usedmacs[$key] = implode(",", $usedmac);
2610
				}
2611

    
2612
				$found = true;
2613
			} else {
2614
				unset($usedmacs[$key]);
2615
			}
2616

    
2617
			break;
2618
		} else if ($usedmac[0] + ($resettimeout * 3600) <= $currenttime) {
2619
			unset($usedmacs[$key]);
2620
		}
2621
	}
2622

    
2623
	if (!$found) {
2624
		$usedmac = array($currenttime, $clientmac, $freeloginscount - 1);
2625
		$usedmacs[] = implode(",", $usedmac);
2626
	}
2627

    
2628
	captiveportal_write_usedmacs_db($usedmacs);
2629
	return true;
2630
}
2631

    
2632
function captiveportal_read_usedmacs_db() {
2633
	global $g, $cpzone;
2634

    
2635
	$cpumaclck = lock("captiveusedmacs{$cpzone}");
2636
	if (file_exists("{$g['vardb_path']}/captiveportal_usedmacs_{$cpzone}.db")) {
2637
		$usedmacs = file("{$g['vardb_path']}/captiveportal_usedmacs_{$cpzone}.db", FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
2638
		if (!$usedmacs) {
2639
			$usedmacs = array();
2640
		}
2641
	} else {
2642
		$usedmacs = array();
2643
	}
2644

    
2645
	unlock($cpumaclck);
2646
	return $usedmacs;
2647
}
2648

    
2649
function captiveportal_write_usedmacs_db($usedmacs) {
2650
	global $g, $cpzone;
2651

    
2652
	$cpumaclck = lock("captiveusedmacs{$cpzone}", LOCK_EX);
2653
	@file_put_contents("{$g['vardb_path']}/captiveportal_usedmacs_{$cpzone}.db", implode("\n", $usedmacs));
2654
	unlock($cpumaclck);
2655
}
2656

    
2657
function captiveportal_blocked_mac($mac) {
2658
	global $config, $g, $cpzone;
2659

    
2660
	if (empty($mac) || !is_macaddr($mac)) {
2661
		return false;
2662
	}
2663

    
2664
	if (!is_array($config['captiveportal'][$cpzone]['passthrumac'])) {
2665
		return false;
2666
	}
2667

    
2668
	foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $passthrumac) {
2669
		if (($passthrumac['action'] == 'block') &&
2670
		    ($passthrumac['mac'] == strtolower($mac))) {
2671
			return true;
2672
		}
2673
	}
2674

    
2675
	return false;
2676

    
2677
}
2678

    
2679
/* Captiveportal Radius Accounting */
2680

    
2681
function gigawords($bytes) {
2682

    
2683
	/*
2684
	 * RFC2866 Specifies a 32bit unsigned integer, which is a max of 4294967295
2685
	 * Currently there is a fault in the PECL radius_put_int function which can handle only 32bit signed integer.
2686
	 */
2687

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

    
2691
	// We need to manually set this to a zero instead of NULL for put_int() safety
2692
	if (is_null($gigawords)) {
2693
		$gigawords = 0;
2694
	}
2695

    
2696
	return $gigawords;
2697
}
2698

    
2699
function remainder($bytes) {
2700
	// Calculate the bytes we are going to send to the radius
2701
	$bytes = bcmod($bytes, GIGAWORDS_RIGHT_OPERAND);
2702

    
2703
	if (is_null($bytes)) {
2704
		$bytes = 0;
2705
	}
2706

    
2707
    return $bytes;
2708
}
2709

    
2710
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) {
2711
	global $cpzone, $config;
2712

    
2713
	$cpcfg = $config['captiveportal'][$cpzone];
2714
	$acctcfg = auth_get_authserver($cpcfg['radacct_server']);
2715

    
2716
	if (!isset($cpcfg['radacct_enable']) || empty($acctcfg)) {
2717
		return null;
2718
	}
2719

    
2720
	if ($type === 'on') {
2721
		$racct = new Auth_RADIUS_Acct_On;
2722
	} elseif ($type === 'off') {
2723
		$racct = new Auth_RADIUS_Acct_Off;
2724
	} elseif ($type === 'start') {
2725
		$racct = new Auth_RADIUS_Acct_Start;
2726
		if (!is_int($start_time)) {
2727
			$start_time = time();
2728
		}
2729
	} elseif ($type === 'stop') {
2730
		$racct = new Auth_RADIUS_Acct_Stop;
2731
		if (!is_int($stop_time)) {
2732
			$stop_time = time();
2733
		}
2734
	} elseif ($type === 'update') {
2735
        $racct = new Auth_RADIUS_Acct_Update;
2736
		if (!is_int($stop_time)) {
2737
			$stop_time = time(); // "top time" here will be used only for calculating session time.
2738
		}
2739
	} else {
2740
		return null;
2741
	}
2742

    
2743
	$racct->addServer($acctcfg['host'], $acctcfg['radius_acct_port'],
2744
		$acctcfg['radius_secret'], $acctcfg['radius_timeout']);
2745

    
2746
	$racct->authentic = RADIUS_AUTH_RADIUS;
2747
	if ($cpcfg['auth_method'] === 'radmac' && $username === "unauthenticated" && !empty($clientmac)) {
2748
		$racct->username = mac_format($clientmac);
2749
	} elseif (!empty($username)) {
2750
		$racct->username = $username;
2751
	}
2752

    
2753
	if (PEAR::isError($racct->start())) {
2754
		captiveportal_syslog('RADIUS ACCOUNTING FAILED : '.$racct->getError());
2755
		$racct->close();
2756
		return null;
2757
	}
2758

    
2759
	$nasip = $acctcfg['radius_nasip_attribute'];
2760
	if (!is_ipaddr($nasip)) {
2761
		$nasip = get_interface_ip($nasip);
2762
		if (!is_ipaddr($nasip)) {
2763
			$nasip = get_interface_ip();//We use WAN interface IP as fallback for NAS-IP-Address
2764
		}
2765
	}
2766
	$nasmac = get_interface_mac(find_ip_interface($nasip));
2767
	$racct->putAttribute(RADIUS_NAS_IP_ADDRESS, $nasip, "addr");
2768

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

    
2771
	if (is_int($ruleno)) {
2772
		$racct->putAttribute(RADIUS_NAS_PORT_TYPE, RADIUS_ETHERNET);
2773
		$racct->putAttribute(RADIUS_NAS_PORT, intval($ruleno), 'integer');
2774
	}
2775

    
2776
	if (!empty($sessionid)) {
2777
		$racct->putAttribute(RADIUS_ACCT_SESSION_ID, $sessionid);
2778
	}
2779

    
2780
	if (!empty($clientip) && is_ipaddr($clientip)) {
2781
		$racct->putAttribute(RADIUS_FRAMED_IP_ADDRESS, $clientip, "addr");
2782
	}
2783
	if (!empty($clientmac)) {
2784
		$racct->putAttribute(RADIUS_CALLING_STATION_ID, mac_format($clientmac));
2785
	}
2786
	if (!empty($nasmac)) {
2787
		$racct->putAttribute(RADIUS_CALLED_STATION_ID, mac_format($nasmac).':'.gethostname());
2788
	}
2789

    
2790
	// Accounting request Stop and Update : send the current data volume
2791
	if (($type === 'stop' || $type === 'update') && is_int($start_time)) {
2792
		$volume = getVolume($clientip);
2793
		$session_time = $stop_time - $start_time;
2794
		$volume['input_bytes_radius'] = remainder($volume['input_bytes']);
2795
		$volume['input_gigawords'] = gigawords($volume['input_bytes']);
2796
		$volume['output_bytes_radius'] = remainder($volume['output_bytes']);
2797
		$volume['output_gigawords'] = gigawords($volume['output_bytes']);
2798

    
2799
		// Volume stuff: Ingress
2800
		$racct->putAttribute(RADIUS_ACCT_INPUT_PACKETS, intval($volume['input_pkts']), "integer");
2801
		$racct->putAttribute(RADIUS_ACCT_INPUT_OCTETS, intval($volume['input_bytes_radius']), "integer");
2802
		// Volume stuff: Outgress
2803
		$racct->putAttribute(RADIUS_ACCT_OUTPUT_PACKETS, intval($volume['output_pkts']), "integer");
2804
		$racct->putAttribute(RADIUS_ACCT_OUTPUT_OCTETS, intval($volume['output_bytes_radius']), "integer");
2805
		$racct->putAttribute(RADIUS_ACCT_SESSION_TIME, intval($session_time), "integer");
2806

    
2807
		$racct->putAttribute(CUSTOM_RADIUS_ACCT_OUTPUT_GIGAWORDS, intval($volume['output_gigawords']), "integer");
2808
		$racct->putAttribute(CUSTOM_RADIUS_ACCT_INPUT_GIGAWORDS, intval($volume['input_gigawords']), "integer");
2809
		// Set session_time
2810
		$racct->session_time = $session_time;
2811
	}
2812

    
2813
	if ($type === 'stop') {
2814
		if (empty($term_cause)) {
2815
			$term_cause = 1;
2816
		}
2817
		$racct->putAttribute(RADIUS_ACCT_TERMINATE_CAUSE, $term_cause);
2818
	}
2819

    
2820
	// Send request
2821
	$result = $racct->send();
2822

    
2823
	if (PEAR::isError($result)) {
2824
		 captiveportal_syslog('RADIUS ACCOUNTING FAILED : '.$racct->getError());
2825
		 $result = null;
2826
	} elseif ($result !== true) {
2827
		$result = false;
2828
	}
2829

    
2830
	$racct->close();
2831
	return $result;
2832
}
2833

    
2834
function captiveportal_isip_logged($clientip) {
2835
	global $g, $cpzone;
2836

    
2837
	/* read in client database */
2838
	$query = "WHERE ip = '{$clientip}'";
2839
	$cpdb = captiveportal_read_db($query);
2840
	foreach ($cpdb as $cpentry) {
2841
		return $cpentry;
2842
	}
2843
}
2844
?>
(6-6/59)