Project

General

Profile

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

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

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

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

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

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

    
94
<head>
95

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

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

    
123
EOD;
124
		}
125
		$htmltext .=<<<EOD
126
		<input type="text" name="auth_user" placeholder="{$translated_text1}" id="auth_user">
127
		<input type="password" name="auth_pass" placeholder="{$translated_text2}" id="auth_pass">
128
EOD;
129

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

    
138
			<input type="text" name="auth_user2" placeholder="{$translated_text1}" id="auth_user2">
139
			<input type="password" name="auth_pass2" placeholder="{$translated_text2}" id="auth_pass2">
140
			</div>
141
EOD;
142
		}
143

    
144

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

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

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

    
183
EOD;
184

    
185
	return $htmltext;
186
}
187

    
188
function captiveportal_load_modules() {
189
	global $config;
190

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

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

    
220
function captiveportal_configure() {
221
	global $config, $cpzone, $cpzoneid;
222

    
223
	if (is_array($config['captiveportal'])) {
224
		foreach ($config['captiveportal'] as $cpkey => $cp) {
225
			$cpzone = $cpkey;
226
			$cpzoneid = $cp['zoneid'];
227
			captiveportal_configure_zone($cp);
228
		}
229
	}
230
}
231

    
232
function captiveportal_configure_zone($cpcfg) {
233
	global $config, $g, $cpzone, $cpzoneid;
234

    
235
	$captiveportallck = lock("captiveportal{$cpzone}", LOCK_EX);
236

    
237
	if (isset($cpcfg['enable'])) {
238

    
239
		if (platform_booting()) {
240
			echo "Starting captive portal({$cpcfg['zone']})... ";
241
		} else {
242
			captiveportal_syslog("Reconfiguring captive portal({$cpcfg['zone']}).");
243
		}
244

    
245
		/* init ipfw rules */
246
		captiveportal_init_rules();
247

    
248
		/* kill any running minicron */
249
		killbypid("{$g['varrun_path']}/cp_prunedb_{$cpzone}.pid");
250

    
251
		/* initialize minicron interval value */
252
		$croninterval = $cpcfg['croninterval'] ? $cpcfg['croninterval'] : 60;
253

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

    
259
		/* write portal page */
260
		if (is_array($cpcfg['page']) && $cpcfg['page']['htmltext']) {
261
			$htmltext = base64_decode($cpcfg['page']['htmltext']);
262
		} else {
263
			/* example/template page */
264
			$htmltext = get_default_captive_portal_html();
265
		}
266

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

    
286
		/* write error page */
287
		if (is_array($cpcfg['page']) && $cpcfg['page']['errtext']) {
288
			$errtext = base64_decode($cpcfg['page']['errtext']);
289
		} else {
290
			/* example page  */
291
			$errtext = get_default_captive_portal_html();
292
		}
293

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

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

    
348
document.location.href="<?=\$my_redirurl;?>";
349
//]]>
350
</script>
351
</body>
352
</html>
353

    
354
EOD;
355
		}
356

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

    
364
		/* write elements */
365
		captiveportal_write_elements();
366

    
367
		/* kill any running CP nginx instances */
368
		killbypid("{$g['varrun_path']}/nginx-{$cpzone}-CaptivePortal.pid", 0.1);
369
		killbypid("{$g['varrun_path']}/nginx-{$cpzone}-CaptivePortal-SSL.pid", 0.1);
370

    
371
		/* start up the webserving daemon */
372
		captiveportal_init_webgui_zone($cpcfg);
373

    
374
		/* Kill any existing prunecaptiveportal processes */
375
		if (file_exists("{$g['varrun_path']}/cp_prunedb_{$cpzone}.pid")) {
376
			killbypid("{$g['varrun_path']}/cp_prunedb_{$cpzone}.pid");
377
		}
378

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

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

    
386
		if (platform_booting()) {
387
			/* send Accounting-On to server */
388
			captiveportal_send_server_accounting('on');
389
			echo "done\n";
390

    
391
			if (isset($cpcfg['preservedb']) ||
392
			    captiveportal_xmlrpc_sync_get_details($syncip, $syncport, $syncuser, $syncpass)) {
393

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

    
406
						$rule_entry = "{$clientip}/" . (is_ipaddrv6($clientip) ? "128" : "32");
407
						if (!isset($cpcfg['nomacfilter'])) {
408
							$rule_entry .= ",{$clientmac}";
409
						}
410

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

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

    
429
		captiveportal_radius_stop_all(10); // NAS-Request
430

    
431
		captiveportal_filterdns_configure();
432

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

    
440
		captiveportal_delete_rules($pipes_to_remove);
441

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

    
461
	unlock($captiveportallck);
462

    
463
	return 0;
464
}
465

    
466
function captiveportal_init_webgui() {
467
	global $config, $cpzone;
468

    
469
	if (is_array($config['captiveportal'])) {
470
		foreach ($config['captiveportal'] as $cpkey => $cp) {
471
			$cpzone = $cpkey;
472
			captiveportal_init_webgui_zone($cp);
473
		}
474
	}
475
}
476

    
477
function captiveportal_init_webgui_zonename($zone) {
478
	global $config, $cpzone;
479

    
480
	if (isset($config['captiveportal'][$zone])) {
481
		$cpzone = $zone;
482
		captiveportal_init_webgui_zone($config['captiveportal'][$zone]);
483
	}
484
}
485

    
486
function captiveportal_init_webgui_zone($cpcfg) {
487
	global $g, $config, $cpzone;
488

    
489
	if (!isset($cpcfg['enable'])) {
490
		return;
491
	}
492

    
493
	if (isset($cpcfg['httpslogin'])) {
494
		$cert = lookup_cert($cpcfg['certref']);
495
		$crt = base64_decode($cert['crt']);
496
		$key = base64_decode($cert['prv']);
497
		$ca = ca_chain($cert);
498

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

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

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

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

    
531
function captiveportal_init_rules_byinterface($interface) {
532
	global $cpzone, $cpzoneid, $config;
533

    
534
	if (!is_array($config['captiveportal'])) {
535
		return;
536
	}
537

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

    
549
/* Create basic rules used by all zones */
550
function captiveportal_init_general_rules($flush = false) {
551
	global $g;
552

    
553
	$flush_rule = '';
554
	if ($flush) {
555
		$flush_rule = 'flush';
556
	}
557

    
558
	/* Already loaded */
559
	if (!$flush && (mwexec("/sbin/ipfw list 1000", true) == 0)) {
560
		return;
561
	}
562

    
563
	$cprules = <<<EOD
564
{$flush_rule}
565
# Table with interfaces that have CP enabled
566
table cp_ifaces create type iface valtype skipto
567

    
568
# Redirect each CP interface to its specific rule
569
add 1000 skipto tablearg all from any to any via table(cp_ifaces)
570

    
571
# This interface has no cp zone configured
572
add 1100 allow all from any to any
573

    
574
# block everything else
575
add 65534 deny all from any to any
576
EOD;
577

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

    
585
/* Create a string with ipfw rule and increase rulenum */
586
function captiveportal_create_ipfw_rule($cmd, &$rulenum, $args) {
587
	$rule = "{$cmd} {$rulenum} {$args}\n";
588
	$rulenum++;
589

    
590
	return $rule;
591
}
592

    
593
/* Return first rule number for a cp zone */
594
function captiveportal_ipfw_ruleno($id) {
595
	global $g;
596

    
597
	return 2000 + $id * $g['captiveportal_rules_interval'];
598
}
599

    
600
/* reinit will disconnect all users, be careful! */
601
function captiveportal_init_rules($reinit = false) {
602
	global $config, $g, $cpzone;
603

    
604
	if (!isset($config['captiveportal'][$cpzone]['enable'])) {
605
		return;
606
	}
607

    
608
	captiveportal_load_modules();
609
	captiveportal_init_general_rules();
610

    
611
	/* Cleanup so nothing is leaked */
612
	captiveportal_free_dnrules(2000, 64500, false, $reinit);
613
	unlink_if_exists("{$g['vardb_path']}/captiveportal_{$cpzone}.rules");
614

    
615
	$cpzoneid = $config['captiveportal'][$cpzone]['zoneid'];
616
	$skipto = captiveportal_ipfw_ruleno($cpzoneid);
617

    
618
	$cprules = '';
619

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

    
633
		$cpipm = get_interface_ip($cpifgrp);
634

    
635
		if (!is_ipaddr($cpipm)) {
636
			continue;
637
		}
638

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

    
650
		$cprules .= "table cp_ifaces add {$tmpif} {$skipto}\n";
651
	}
652
	if (count($cpips) > 0) {
653
		$cpactive = true;
654
	} else {
655
		return false;
656
	}
657

    
658
	$captiveportallck = try_lock("captiveportal{$cpzone}", 0);
659

    
660
	$tables = captiveportal_get_ipfw_table_names();
661

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

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

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

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

    
721
	if (!empty($config['captiveportal'][$cpzone]['listenporthttp'])) {
722
		$listenporthttp = $config['captiveportal'][$cpzone]['listenporthttp'];
723
	} else {
724
		$listenporthttp = 8000 + $cpzoneid;
725
	}
726

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

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

    
749
	/* generate passthru mac database */
750
	$cprules .= captiveportal_passthrumac_configure(true);
751
	$cprules .= "\n";
752

    
753
	/* allowed ipfw rules to make allowed ip work */
754
	$cprules .= captiveportal_allowedip_configure();
755

    
756
	/* allowed ipfw rules to make allowed hostnames work */
757
	$cprules .= captiveportal_allowedhostname_configure();
758

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

    
766
	captiveportal_filterdns_configure();
767

    
768
	if ($captiveportallck) {
769
		unlock($captiveportallck);
770
	}
771
}
772

    
773
/* Delete all rules related to specific cpzone */
774
function captiveportal_delete_rules($pipes_to_remove = array(), $clear_auth_rules = true) {
775
	global $g, $config, $cpzone;
776

    
777
	$cpzoneid = $config['captiveportal'][$cpzone]['zoneid'];
778

    
779
	$skipto1 = captiveportal_ipfw_ruleno($cpzoneid);
780
	$skipto2 = $skipto1 + $g['captiveportal_rules_interval'];
781

    
782
	$cp_ifaces = pfSense_ipfw_table_list("cp_ifaces");
783
	if (is_array($cp_ifaces)) {
784
		foreach ($cp_ifaces as $cp_iface) {
785
			if (empty($cp_iface['skipto']) ||
786
			    $cp_iface['skipto'] != $skipto1) {
787
				continue;
788
			}
789

    
790
			pfSense_ipfw_table("cp_ifaces", IP_FW_TABLE_XDEL,
791
			    $cp_iface['iface']);
792
		}
793
	}
794

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

    
797
	$tables = captiveportal_get_ipfw_table_names();
798

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

    
808
	foreach ($pipes_to_remove as $pipeno) {
809
		$delrules .= "pipe delete {$pipeno}\n";
810
	}
811

    
812
	if (empty($delrules)) {
813
		return;
814
	}
815

    
816
	file_put_contents("{$g['tmp_path']}/ipfw_{$cpzone}.deltable.rules", $delrules);
817
	mwexec("/sbin/ipfw -q {$g['tmp_path']}/ipfw_{$cpzone}.deltable.rules", true);
818
	@unlink("{$g['tmp_path']}/ipfw_{$cpzone}.deltable.rules");
819
}
820

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

    
830
	if (empty($cpzone)) {
831
		return;
832
	}
833

    
834
	$cpcfg = $config['captiveportal'][$cpzone];
835
	$vcpcfg = $config['voucher'][$cpzone];
836

    
837
	/* check for expired entries */
838
	$idletimeout = 0;
839
	$timeout = 0;
840
	if (!empty($cpcfg['timeout']) && is_numeric($cpcfg['timeout'])) {
841
		$timeout = $cpcfg['timeout'] * 60;
842
	}
843

    
844
	if (!empty($cpcfg['idletimeout']) && is_numeric($cpcfg['idletimeout'])) {
845
		$idletimeout = $cpcfg['idletimeout'] * 60;
846
	}
847

    
848
	/* check for entries exceeding their traffic quota */
849
	$trafficquota = 0;
850
	if (!empty($cpcfg['trafficquota']) && is_numeric($cpcfg['trafficquota'])) {
851
		$trafficquota = $cpcfg['trafficquota'] * 1048576;
852
	}
853

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

    
862

    
863
	/* Read database */
864
	/* NOTE: while this can be simplified in non radius case keep as is for now */
865
	$cpdb = captiveportal_read_db();
866

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

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

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

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

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

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

    
946
		if ($timedout) {
947
			captiveportal_disconnect($cpentry, $term_cause, $stop_time);
948
			captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], $logout_cause);
949
			$unsetindexes[] = $cpentry[5];
950
		}
951

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

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

    
1066
	captiveportal_prune_old_automac();
1067

    
1068
	if ($voucher_needs_sync == true) {
1069
		/* perform in-use vouchers expiration using check_reload_status */
1070
		send_event("service sync vouchers");
1071
	}
1072

    
1073
	/* write database */
1074
	if (!empty($unsetindexes)) {
1075
		captiveportal_remove_entries($unsetindexes);
1076
	}
1077
}
1078

    
1079
function captiveportal_prune_old_automac() {
1080
	global $g, $config, $cpzone, $cpzoneid;
1081

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

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

    
1127
	$stop_time = (empty($stop_time)) ? time() : $stop_time;
1128

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

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

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

    
1186
		if (!empty($removed_pipes)) {
1187
			$_gb = @pfSense_ipfw_pipe("pipe delete {$dbent[1]}");
1188
			$_gb = @pfSense_ipfw_pipe("pipe delete " .
1189
			    ($dbent[1]+1));
1190

    
1191
			/*
1192
			 * Release the ruleno so it can be reallocated to new
1193
			 * clients
1194
			 */
1195
			captiveportal_free_dn_ruleno($dbent[1]);
1196
		}
1197
	}
1198

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

    
1211
		$rpc_client->xmlrpc_method('captive_portal_sync',
1212
			array(
1213
				'op' => 'disconnect_user',
1214
				'zone' => $cpzone,
1215
				'session' => base64_encode(serialize($arguments))
1216
			)
1217
		);
1218
	}
1219
	return true;
1220
}
1221

    
1222
/* remove a single client by sessionid */
1223
function captiveportal_disconnect_client($sessionid, $term_cause = 1, $logoutReason = "LOGOUT") {
1224
	global $g, $config;
1225

    
1226
	$sessionid = SQLite3::escapeString($sessionid);
1227
	/* read database */
1228
	$result = captiveportal_read_db("WHERE sessionid = '{$sessionid}'");
1229

    
1230
	/* find entry */
1231
	if (!empty($result)) {
1232

    
1233
		foreach ($result as $cpentry) {
1234
			captiveportal_disconnect($cpentry, $term_cause);
1235
			captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "DISCONNECT");
1236
		}
1237
		captiveportal_remove_entries(array($sessionid));
1238
		unset($result);
1239
	}
1240
}
1241

    
1242
/* remove all clients */
1243
function captiveportal_disconnect_all($term_cause = 6, $logoutReason = "DISCONNECT", $carp_loop = false) {
1244
	global $g, $config, $cpzone, $cpzoneid;
1245

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

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

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

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

    
1276
	captiveportal_radius_stop_all($term_cause, $logoutReason);
1277

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

    
1286
	/* reinit ipfw rules */
1287
	captiveportal_init_rules(true);
1288

    
1289
	unlock($cpdblck);
1290
	unlock($rcprunelock);
1291
	return true;
1292
}
1293

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

    
1298
	$cpdb = captiveportal_read_db();
1299

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

    
1324
function captiveportal_passthrumac_configure_entry($macent, $pipeinrule = false) {
1325
	global $config, $g, $cpzone;
1326

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

    
1342
	if ($macent['action'] == 'pass') {
1343
		$rules = "";
1344

    
1345
		$pipeno = captiveportal_get_next_dn_ruleno('pipe_mac');
1346

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

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

    
1361
		$rules .= "table {$cpzone}_pipe_mac add any,{$macent['mac']} {$pipeup}\n";
1362
		$rules .= "table {$cpzone}_pipe_mac add {$macent['mac']},any {$pipedown}\n";
1363
	}
1364

    
1365
	return $rules;
1366
}
1367

    
1368
function captiveportal_passthrumac_delete_entry($macent) {
1369
	global $cpzone;
1370
	$rules = "";
1371

    
1372
	if ($macent['action'] == 'pass') {
1373
		$pipeno = captiveportal_get_dn_passthru_ruleno($macent['mac']);
1374

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

    
1384
	return $rules;
1385
}
1386

    
1387
function captiveportal_passthrumac_configure($filename = false, $startindex = 0, $stopindex = 0) {
1388
	global $config, $g, $cpzone;
1389

    
1390
	$rules = "";
1391

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

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

    
1423
	return $rules;
1424
}
1425

    
1426
function captiveportal_passthrumac_findbyname($username) {
1427
	global $config, $cpzone;
1428

    
1429
	if (is_array($config['captiveportal'][$cpzone]['passthrumac'])) {
1430
		foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $macent) {
1431
			if ($macent['username'] == $username) {
1432
				return $macent;
1433
			}
1434
		}
1435
	}
1436
	return NULL;
1437
}
1438

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

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

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

    
1478
	$pipeup = captiveportal_get_next_dn_ruleno('allowed');
1479
	$_gb = @pfSense_ipfw_pipe("pipe {$pipeup} config bw {$enBwup}Kbit/s queue 100 buckets 16");
1480
	$pipedown = $pipeup + 1;
1481
	$_gb = @pfSense_ipfw_pipe("pipe {$pipedown} config bw {$enBwdown}Kbit/s queue 100 buckets 16");
1482

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

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

    
1500
	if ($ishostname === true) {
1501
		return array($rules, $cp_filterdns_conf);
1502
	} else {
1503
		return $rules;
1504
	}
1505
}
1506

    
1507
function captiveportal_allowedhostname_configure() {
1508
	global $config, $g, $cpzone, $cpzoneid;
1509

    
1510
	$rules = "";
1511
	if (!is_array($config['captiveportal'][$cpzone]['allowedhostname'])) {
1512
		return $rules;
1513
	}
1514

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

    
1526
	return $rules;
1527
}
1528

    
1529
function captiveportal_filterdns_configure() {
1530
	global $config, $g, $cpzone, $cpzoneid;
1531

    
1532
	$cp_filterdns_filename = $g['varetc_path'] .
1533
	    "/filterdns-{$cpzone}-captiveportal.conf";
1534

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

    
1552
	return $rules;
1553
}
1554

    
1555
function captiveportal_allowedip_configure() {
1556
	global $config, $g, $cpzone;
1557

    
1558
	$rules = "";
1559
	if (is_array($config['captiveportal'][$cpzone]['allowedip'])) {
1560
		foreach ($config['captiveportal'][$cpzone]['allowedip'] as $ipent) {
1561
			$rules .= captiveportal_allowedip_configure_entry($ipent);
1562
		}
1563
	}
1564

    
1565
	return $rules;
1566
}
1567

    
1568
/* get last activity timestamp given client IP address */
1569
function captiveportal_get_last_activity($ip) {
1570
	global $cpzone;
1571

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

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

    
1587
	return 0;
1588
}
1589

    
1590

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

    
1604
/* log simple messages to syslog */
1605
function captiveportal_syslog($message) {
1606
	global $cpzone;
1607

    
1608
	$message = trim($message);
1609
	$message = "Zone: {$cpzone} - {$message}";
1610
	openlog("logportalauth", LOG_PID, LOG_LOCAL4);
1611
	// Log it
1612
	syslog(LOG_INFO, $message);
1613
	closelog();
1614
}
1615

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

    
1621
	$login_status = 'FAILURE';
1622
	$login_msg = gettext('Invalid credentials specified');
1623
	$reply_attributes = array();
1624
	$auth_method = '';
1625
	$auth_result = null;
1626

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

    
1635
		- Reply message of a user failed auth is more important than reply message of
1636
		a server failed auth (unable to contact server)
1637

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

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

    
1649
	/* Getting authentication servers from captiveportal configuration */
1650
	$auth_servers = array();
1651

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

    
1661
		foreach ($fullauthservers as $authserver) {
1662
			if (strpos($authserver, ' - ') !== false) {
1663
				$authserver = explode(' - ', $authserver);
1664
				array_shift($authserver);
1665
				$authserver = implode(' - ', $authserver);
1666

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

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

    
1692
				$result = null;
1693
				$status = null;
1694
				$msg = null;
1695

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

    
1710
				if (!$radmac_error) {
1711
					if ($authcfg['type'] === 'none') {
1712
						$result = true;
1713
					} else {
1714
						$result = authenticate_user($login, $password, $authcfg, $attributes);
1715
					}
1716

    
1717
					if (!empty($attributes['error_message'])) {
1718
						$msg = $attributes['error_message'];
1719
					}
1720

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

    
1731
					if (empty($status)) {
1732
						if ($result === true) {
1733
							$status = "ACCEPT";
1734
						} elseif ($result === null) {
1735
							$status = "ERROR";
1736
						} else {
1737
							$status = "FAILURE";
1738
						}
1739
					}
1740

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

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

    
1769
	return array('result'=>$auth_result, 'attributes'=>$reply_attributes, 'auth_method' =>$auth_method, 'login_status'=> $login_status, 'login_message' => $login_msg);
1770
}
1771

    
1772
function captiveportal_opendb() {
1773
	global $g, $config, $cpzone, $cpzoneid;
1774

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

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

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

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

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

    
1844
	return $DB;
1845
}
1846

    
1847
/* Get all tables for specific cpzone */
1848
function captiveportal_get_ipfw_table_names() {
1849
	global $cpzone;
1850

    
1851
	$result = array();
1852
	$tables = pfSense_ipfw_tables_list();
1853

    
1854
	if (!is_array($tables)) {
1855
		return $result;
1856
	}
1857

    
1858
	$len = strlen($cpzone) + 1;
1859
	foreach ($tables as $table) {
1860
		if (substr($table['name'], 0, $len) != $cpzone . '_') {
1861
			continue;
1862
		}
1863

    
1864
		$result[] = $table['name'];
1865
	}
1866

    
1867
	return $result;
1868
}
1869

    
1870
/* read captive portal DB into array */
1871
function captiveportal_read_db($query = "") {
1872
	$cpdb = array();
1873

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

    
1885
	return $cpdb;
1886
}
1887

    
1888
function captiveportal_remove_entries($remove, $carp_loop = false) {
1889
	global $cpzone;
1890

    
1891
	if (!is_array($remove) || empty($remove)) {
1892
		return;
1893
	}
1894

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

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

    
1920
/* write captive portal DB */
1921
function captiveportal_write_db($queries) {
1922
	global $g;
1923

    
1924
	if (is_array($queries)) {
1925
		$query = implode(";", $queries);
1926
	} else {
1927
		$query = $queries;
1928
	}
1929

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

    
1946
function captiveportal_write_elements() {
1947
	global $g, $config, $cpzone;
1948

    
1949
	$cpcfg = $config['captiveportal'][$cpzone];
1950

    
1951
	if (!is_dir($g['captiveportal_element_path'])) {
1952
		@mkdir($g['captiveportal_element_path']);
1953
	}
1954

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

    
1974
	return 0;
1975
}
1976

    
1977
function captiveportal_free_dnrules($rulenos_start = 2000,
1978
    $rulenos_range_max = 64500, $dry_run = false, $clear_auth_pipes = true) {
1979
	global $g, $cpzone;
1980

    
1981
	$removed_pipes = array();
1982

    
1983
	if (!file_exists("{$g['vardb_path']}/captiveportaldn.rules")) {
1984
		return $removed_pipes;
1985
	}
1986

    
1987
	if (!$dry_run) {
1988
		$cpruleslck = lock("captiveportalrulesdn", LOCK_EX);
1989
	}
1990

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

    
2015
	if (!$dry_run) {
2016
		file_put_contents("{$g['vardb_path']}/captiveportaldn.rules",
2017
		    serialize($rules));
2018
		unlock($cpruleslck);
2019
	}
2020

    
2021
	unset($rules);
2022

    
2023
	return $removed_pipes;
2024
}
2025

    
2026
function captiveportal_reserve_ruleno($ruleno) {
2027
	global $g, $cpzone;
2028

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

    
2039
	file_put_contents("{$g['vardb_path']}/captiveportaldn.rules", serialize($rules));
2040
	unlock($cpruleslck);
2041
	unset($rules);
2042

    
2043
	return $ruleno;
2044
}
2045

    
2046
function captiveportal_get_next_dn_ruleno($rule_type = 'default', $rulenos_start = 2000, $rulenos_range_max = 64500) {
2047
	global $config, $g, $cpzone;
2048

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

    
2076
	return $ruleno;
2077
}
2078

    
2079
function captiveportal_free_dn_ruleno($ruleno) {
2080
	global $config, $g;
2081

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

    
2094
function captiveportal_get_dn_passthru_ruleno($value) {
2095
	global $config, $g, $cpzone, $cpzoneid;
2096

    
2097
	$cpcfg = $config['captiveportal'][$cpzone];
2098
	if (!isset($cpcfg['enable'])) {
2099
		return NULL;
2100
	}
2101

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

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

    
2122
	return $ruleno;
2123
}
2124

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

    
2136
function getVolume($ip) {
2137
	global $config, $cpzone;
2138

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

    
2146
	$tables = array("allowed", "auth");
2147

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

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

    
2177
	return $volume;
2178
}
2179

    
2180
function portal_ip_from_client_ip($cliip) {
2181
	global $config, $cpzone;
2182

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

    
2198
	$route = route_get($cliip, 'inet', true);
2199
	if (empty($route)) {
2200
		return false;
2201
	}
2202

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

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

    
2223
	return false;
2224
}
2225

    
2226
function portal_hostname_from_client_ip($cliip) {
2227
	global $config, $cpzone;
2228

    
2229
	$cpcfg = $config['captiveportal'][$cpzone];
2230

    
2231
	if (isset($cpcfg['httpslogin'])) {
2232
		$listenporthttps = $cpcfg['listenporthttps'] ? $cpcfg['listenporthttps'] : ($cpcfg['zoneid'] + 8001);
2233
		$ourhostname = $cpcfg['httpsname'];
2234

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

    
2247
		if ($listenporthttp != 80) {
2248
			$ourhostname .= ":" . $listenporthttp;
2249
		}
2250
	}
2251

    
2252
	return $ourhostname;
2253
}
2254

    
2255
/* functions move from index.php */
2256

    
2257
function portal_reply_page($redirurl, $type = null, $message = null, $clientmac = null, $clientip = null, $username = null, $password = null, $voucher = null) {
2258
	global $g, $config, $cpzone;
2259

    
2260
	$cpcfg = $config['captiveportal'][$cpzone];
2261
	$ourhostname = portal_hostname_from_client_ip($clientip);
2262
	$protocol = (isset($cpcfg['httpslogin'])) ? 'https://' : 'http://';
2263
	$portal_url = "{$protocol}{$ourhostname}/index.php?zone={$cpzone}";
2264

    
2265
	/* Get captive portal layout */
2266
	if ($type == "redir") {
2267
		$redirurl = is_URL($redirurl, true) ? $redirurl : $portal_url;
2268
		header("Location: {$redirurl}");
2269
		return;
2270
	} else if ($type == "login") {
2271
		$htmltext = get_include_contents("{$g['varetc_path']}/captiveportal_{$cpzone}.html");
2272
	} else {
2273
		$htmltext = get_include_contents("{$g['varetc_path']}/captiveportal-{$cpzone}-error.html");
2274
	}
2275

    
2276
	/* substitute the PORTAL_REDIRURL variable */
2277
	if ($cpcfg['preauthurl']) {
2278
		$htmltext = str_replace("\$PORTAL_REDIRURL\$", "{$cpcfg['preauthurl']}", $htmltext);
2279
		$htmltext = str_replace("#PORTAL_REDIRURL#", "{$cpcfg['preauthurl']}", $htmltext);
2280
	}
2281

    
2282
	/* substitute other variables */
2283
	$htmltext = str_replace("\$PORTAL_ACTION\$", $portal_url, $htmltext);
2284
	$htmltext = str_replace("#PORTAL_ACTION#", $portal_url, $htmltext);
2285

    
2286
	$htmltext = str_replace("\$PORTAL_ZONE\$", htmlspecialchars($cpzone), $htmltext);
2287
	$htmltext = str_replace("\$PORTAL_REDIRURL\$", htmlspecialchars($redirurl), $htmltext);
2288
	$htmltext = str_replace("\$PORTAL_MESSAGE\$", htmlspecialchars($message), $htmltext);
2289
	$htmltext = str_replace("\$CLIENT_MAC\$", htmlspecialchars($clientmac), $htmltext);
2290
	$htmltext = str_replace("\$CLIENT_IP\$", htmlspecialchars($clientip), $htmltext);
2291

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

    
2304
	echo $htmltext;
2305
}
2306

    
2307
function captiveportal_reapply_attributes($cpentry, $attributes) {
2308
	global $config, $cpzone, $g;
2309

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

    
2325
	$bw_up_pipeno = $cpentry[1];
2326
	$bw_down_pipeno = $cpentry[1]+1;
2327

    
2328
	if ($cpentry['bw_up'] !== $bw_up) {
2329
		$_gb = @pfSense_ipfw_pipe("pipe {$bw_up_pipeno} config bw {$bw_up}Kbit/s queue 100 buckets 16");
2330
		captiveportal_update_entry($cpentry['sessionid'], $bw_up, 'bw_up');
2331
	}
2332
	if ($cpentry['bw_down'] !== $bw_down) {
2333
		$_gb = @pfSense_ipfw_pipe("pipe {$bw_down_pipeno} config bw {$bw_down}Kbit/s queue 100 buckets 16");
2334
		captiveportal_update_entry($cpentry['sessionid'], $bw_down, 'bw_down');
2335
	}
2336
	unset($bw_up_pipeno, $bw_down_pipeno, $bw_up, $bw_down);
2337
}
2338

    
2339
function captiveportal_update_entry($sessionid, $new_value, $field_to_update) {
2340
	global $cpzone;
2341

    
2342
	if (!intval($new_value)) {
2343
		$new_value = "'{$new_value}'";
2344
	}
2345
	captiveportal_write_db("UPDATE captiveportal SET {$field_to_update} = {$new_value} WHERE sessionid = '{$sessionid}'");
2346
}
2347

    
2348
function portal_allow($clientip, $clientmac, $username, $password = null, $redirurl = null,
2349
    $attributes = null, $pipeno = null, $authmethod = null, $context = 'first', $existing_sessionid = null) {
2350
	global $g, $config, $cpzone;
2351

    
2352
	// Ensure we create an array if we are missing attributes
2353
	if (!is_array($attributes)) {
2354
		$attributes = array();
2355
	}
2356

    
2357
	unset($sessionid);
2358

    
2359
	/* Do not allow concurrent login execution. */
2360
	$cpdblck = lock("captiveportaldb{$cpzone}", LOCK_EX);
2361

    
2362
	if ($attributes['voucher']) {
2363
		$remaining_time = $attributes['session_timeout'];
2364
		$authmethod = "voucher"; // Set RADIUS-Attribute to Voucher to prevent ReAuth-Request for Vouchers Bug: #2155
2365
		$context = "voucher";
2366
	}
2367

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

    
2379
				$pipeno = captiveportal_get_dn_passthru_ruleno($mac['mac']);
2380
				if ($pipeno) {
2381
					captiveportal_free_dn_ruleno($pipeno);
2382
					@pfSense_ipfw_table("{$cpzone}_pipe_mac", IP_FW_TABLE_XDEL, "any,{$mac['mac']}");
2383
					@pfSense_ipfw_table("{$cpzone}_pipe_mac", IP_FW_TABLE_XDEL, "{$mac['mac']},any");
2384
					@pfSense_ipfw_pipe("pipe delete " . ($pipeno+1));
2385
					@pfSense_ipfw_pipe("pipe delete " . $pipeno);
2386
				}
2387
				unset($config['captiveportal'][$cpzone]['passthrumac'][$idx]);
2388
			}
2389
		}
2390
	}
2391

    
2392
	/* read in client database */
2393
	$query = "WHERE ip = '{$clientip}'";
2394
	$tmpusername = SQLite3::escapeString(strtolower($username));
2395
	if (isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) {
2396
		$query .= " OR (username != 'unauthenticated' AND lower(username) = '{$tmpusername}')";
2397
	}
2398
	$cpdb = captiveportal_read_db($query);
2399

    
2400
	/* Snapshot the timestamp */
2401
	$allow_time = time();
2402

    
2403
	if ($existing_sessionid !== null) {
2404
		// If we recieved this connection through XMLRPC sync :
2405
		// we fetch allow_time from the info given by the other node
2406
		$allow_time = $attributes['allow_time'];
2407
	}
2408
	$unsetindexes = array();
2409

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
2653
		unlock($cpdblck);
2654
	}
2655

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

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

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

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

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

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

    
2693
	return $sessionid;
2694
}
2695

    
2696

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
2839
	return false;
2840

    
2841
}
2842

    
2843
/* Captiveportal Radius Accounting */
2844

    
2845
function gigawords($bytes) {
2846

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

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

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

    
2860
	return $gigawords;
2861
}
2862

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

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

    
2871
    return $bytes;
2872
}
2873

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
3010
?>
(6-6/61)