Project

General

Profile

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

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

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

    
42
function get_default_captive_portal_html() {
43
	global $config, $g, $cpzone;
44

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

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

    
87
<head>
88

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

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

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

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

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

    
137

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

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

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

    
176
EOD;
177

    
178
	return $htmltext;
179
}
180

    
181
function captiveportal_load_modules() {
182
	global $config;
183

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

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

    
213
function captiveportal_configure() {
214
	global $config, $cpzone, $cpzoneid;
215

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

    
225
function captiveportal_configure_zone($cpcfg) {
226
	global $config, $g, $cpzone, $cpzoneid;
227

    
228
	$captiveportallck = lock("captiveportal{$cpzone}", LOCK_EX);
229

    
230
	if (isset($cpcfg['enable'])) {
231

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

    
238
		/* (re)init ipfw rules. Cause all users to disconnect */
239
		captiveportal_init_rules(true);
240

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

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

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

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

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

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

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

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

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

    
347
EOD;
348
		}
349

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

    
357
		/* write elements */
358
		captiveportal_write_elements();
359

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

    
364
		/* start up the webserving daemon */
365
		captiveportal_init_webgui_zone($cpcfg);
366

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

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

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

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

    
385
	} else {
386
		killbypid("{$g['varrun_path']}/nginx-{$cpzone}-CaptivePortal.pid");
387
		killbypid("{$g['varrun_path']}/nginx-{$cpzone}-CaptivePortal-SSL.pid");
388
		killbypid("{$g['varrun_path']}/cp_prunedb_{$cpzone}.pid");
389
		@unlink("{$g['varetc_path']}/captiveportal_{$cpzone}.html");
390
		@unlink("{$g['varetc_path']}/captiveportal-{$cpzone}-error.html");
391
		@unlink("{$g['varetc_path']}/captiveportal-{$cpzone}-logout.html");
392

    
393
		captiveportal_radius_stop_all(10); // NAS-Request
394

    
395
		captiveportal_filterdns_configure();
396

    
397
		/* remove old information */
398
		unlink_if_exists("{$g['vardb_path']}/captiveportal{$cpzone}.db");
399
		unlink_if_exists("{$g['vardb_path']}/captiveportal_radius_{$cpzone}.db");
400
		unlink_if_exists("{$g['vardb_path']}/captiveportal_{$cpzone}.rules");
401
		/* Release allocated pipes for this zone */
402
		$pipes_to_remove = captiveportal_free_dnrules();
403

    
404
		captiveportal_delete_rules($pipes_to_remove);
405

    
406
		if (empty($config['captiveportal'])) {
407
			set_single_sysctl("net.link.ether.ipfw", "0");
408
		} else {
409
			/* Deactivate ipfw(4) if not needed */
410
			$cpactive = false;
411
			if (is_array($config['captiveportal'])) {
412
				foreach ($config['captiveportal'] as $cpkey => $cp) {
413
					if (isset($cp['enable'])) {
414
						$cpactive = true;
415
						break;
416
					}
417
				}
418
			}
419
			if ($cpactive === false) {
420
				set_single_sysctl("net.link.ether.ipfw", "0");
421
			}
422
		}
423
	}
424

    
425
	unlock($captiveportallck);
426

    
427
	return 0;
428
}
429

    
430
function captiveportal_init_webgui() {
431
	global $config, $cpzone;
432

    
433
	if (is_array($config['captiveportal'])) {
434
		foreach ($config['captiveportal'] as $cpkey => $cp) {
435
			$cpzone = $cpkey;
436
			captiveportal_init_webgui_zone($cp);
437
		}
438
	}
439
}
440

    
441
function captiveportal_init_webgui_zonename($zone) {
442
	global $config, $cpzone;
443

    
444
	if (isset($config['captiveportal'][$zone])) {
445
		$cpzone = $zone;
446
		captiveportal_init_webgui_zone($config['captiveportal'][$zone]);
447
	}
448
}
449

    
450
function captiveportal_init_webgui_zone($cpcfg) {
451
	global $g, $config, $cpzone;
452

    
453
	if (!isset($cpcfg['enable'])) {
454
		return;
455
	}
456

    
457
	if (isset($cpcfg['httpslogin'])) {
458
		$cert = lookup_cert($cpcfg['certref']);
459
		$crt = base64_decode($cert['crt']);
460
		$key = base64_decode($cert['prv']);
461
		$ca = ca_chain($cert);
462

    
463
		/* generate nginx configuration */
464
		if (!empty($cpcfg['listenporthttps'])) {
465
			$listenporthttps = $cpcfg['listenporthttps'];
466
		} else {
467
			$listenporthttps = 8001 + $cpcfg['zoneid'];
468
		}
469
		system_generate_nginx_config("{$g['varetc_path']}/nginx-{$cpzone}-CaptivePortal-SSL.conf",
470
			$crt, $key, $ca, "nginx-{$cpzone}-CaptivePortal-SSL.pid", $listenporthttps, "/usr/local/captiveportal",
471
			"cert-{$cpzone}-portal.pem", "ca-{$cpzone}-portal.pem", $cpzone, false);
472
	}
473

    
474
	/* generate nginx configuration */
475
	if (!empty($cpcfg['listenporthttp'])) {
476
		$listenporthttp = $cpcfg['listenporthttp'];
477
	} else {
478
		$listenporthttp = 8000 + $cpcfg['zoneid'];
479
	}
480
	system_generate_nginx_config("{$g['varetc_path']}/nginx-{$cpzone}-CaptivePortal.conf",
481
		"", "", "", "nginx-{$cpzone}-CaptivePortal.pid", $listenporthttp, "/usr/local/captiveportal",
482
		"", "", $cpzone, false);
483

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

    
488
	/* fire up https instance */
489
	if (isset($cpcfg['httpslogin'])) {
490
		@unlink("{$g['varrun']}/nginx-{$cpzone}-CaptivePortal-SSL.pid");
491
		$res = mwexec("/usr/local/sbin/nginx -c {$g['varetc_path']}/nginx-{$cpzone}-CaptivePortal-SSL.conf");
492
	}
493
}
494

    
495
function captiveportal_init_rules_byinterface($interface) {
496
	global $cpzone, $cpzoneid, $config;
497

    
498
	if (!is_array($config['captiveportal'])) {
499
		return;
500
	}
501

    
502
	foreach ($config['captiveportal'] as $cpkey => $cp) {
503
		$cpzone = $cpkey;
504
		$cpzoneid = $cp['zoneid'];
505
		$cpinterfaces = explode(",", $cp['interface']);
506
		if (in_array($interface, $cpinterfaces)) {
507
			captiveportal_init_rules();
508
			break;
509
		}
510
	}
511
}
512

    
513
/* Create basic rules used by all zones */
514
function captiveportal_init_general_rules($flush = false) {
515
	global $g;
516

    
517
	$flush_rule = '';
518
	if ($flush) {
519
		$flush_rule = 'flush';
520
	}
521

    
522
	/* Already loaded */
523
	if (!$flush && (mwexec("/sbin/ipfw list 1000", true) == 0)) {
524
		return;
525
	}
526

    
527
	$cprules = <<<EOD
528
{$flush_rule}
529
# Table with interfaces that have CP enabled
530
table cp_ifaces create type iface valtype skipto
531

    
532
# Redirect each CP interface to its specific rule
533
add 1000 skipto tablearg all from any to any via table(cp_ifaces)
534

    
535
# This interface has no cp zone configured
536
add 1100 allow all from any to any
537

    
538
# block everything else
539
add 65534 deny all from any to any
540
EOD;
541

    
542
	/* load rules */
543
	file_put_contents("{$g['tmp_path']}/ipfw.cp.rules", $cprules);
544
	mwexec("/sbin/ipfw -q {$g['tmp_path']}/ipfw.cp.rules", true);
545
	@unlink("{$g['tmp_path']}/ipfw_{$cpzone}.cp.rules");
546
	unset($cprules);
547
}
548

    
549
/* Create a string with ipfw rule and increase rulenum */
550
function captiveportal_create_ipfw_rule($cmd, &$rulenum, $args) {
551
	$rule = "{$cmd} {$rulenum} {$args}\n";
552
	$rulenum++;
553

    
554
	return $rule;
555
}
556

    
557
/* Return first rule number for a cp zone */
558
function captiveportal_ipfw_ruleno($id) {
559
	global $g;
560

    
561
	return 2000 + $id * $g['captiveportal_rules_interval'];
562
}
563

    
564
/* reinit will disconnect all users, be careful! */
565
function captiveportal_init_rules($reinit = false) {
566
	global $config, $g, $cpzone, $cpzoneid;
567

    
568
	if (!isset($config['captiveportal'][$cpzone]['enable'])) {
569
		return;
570
	}
571

    
572
	captiveportal_load_modules();
573
	captiveportal_init_general_rules();
574

    
575
	/* Cleanup so nothing is leaked */
576
	captiveportal_free_dnrules();
577
	unlink_if_exists("{$g['vardb_path']}/captiveportal_{$cpzone}.rules");
578

    
579
	$skipto = captiveportal_ipfw_ruleno($cpzoneid);
580

    
581
	$cprules = '';
582

    
583
	$cpips = array();
584
	$ifaces = get_configured_interface_list();
585
	$cpinterfaces = explode(",", $config['captiveportal'][$cpzone]['interface']);
586
	$firsttime = 0;
587
	foreach ($cpinterfaces as $cpifgrp) {
588
		if (!isset($ifaces[$cpifgrp])) {
589
			continue;
590
		}
591
		$tmpif = get_real_interface($cpifgrp);
592
		if (empty($tmpif)) {
593
			continue;
594
		}
595

    
596
		$cpipm = get_interface_ip($cpifgrp);
597

    
598
		if (!is_ipaddr($cpipm)) {
599
			continue;
600
		}
601

    
602
		$cpips[] = $cpipm;
603
		if (is_array($config['virtualip']['vip'])) {
604
			foreach ($config['virtualip']['vip'] as $vip) {
605
				if (($vip['interface'] == $cpifgrp) &&
606
				    (($vip['mode'] == "carp") ||
607
				    ($vip['mode'] == "ipalias"))) {
608
					$cpips[] = $vip['subnet'];
609
				}
610
			}
611
		}
612

    
613
		$cprules .= "table cp_ifaces add {$tmpif} {$skipto}\n";
614
	}
615
	if (count($cpips) > 0) {
616
		$cpactive = true;
617
	} else {
618
		return false;
619
	}
620

    
621
	if ($reinit == false) {
622
		$captiveportallck = lock("captiveportal{$cpzone}");
623
	}
624

    
625
	$rulenum = $skipto;
626
	$cprules .= "table {$cpzone}_pipe_mac create type mac valtype pipe\n";
627
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
628
	    "pipe tablearg MAC table({$cpzone}_pipe_mac)");
629
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
630
	    "allow pfsync from any to any");
631
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
632
	    "allow carp from any to any\n");
633
	$cprules .= "# layer 2: pass ARP\n";
634
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
635
	    "pass layer2 mac-type arp,rarp");
636
	$cprules .= "# pfsense requires for WPA\n";
637
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
638
	    "pass layer2 mac-type 0x888e,0x88c7");
639
	$cprules .= "# PPP Over Ethernet Session Stage/Discovery Stage\n";
640
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
641
	    "pass layer2 mac-type 0x8863,0x8864\n");
642
	$cprules .= "# layer 2: block anything else non-IP(v4/v6)\n";
643
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
644
	    "deny layer2 not mac-type ip,ipv6");
645

    
646
	/* These tables contain host ips */
647
	$cprules .= "table {$cpzone}_host_ips create type addr\n";
648
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
649
	    "pass ip from any to table({$cpzone}_host_ips) in");
650
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
651
	    "pass ip from table({$cpzone}_host_ips) to any out");
652
	foreach ($cpips as $cpip) {
653
		$cprules .= "table {$cpzone}_host_ips add {$cpip}\n";
654
	}
655
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
656
	    "pass ip from any to 255.255.255.255 in");
657
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
658
	    "pass ip from 255.255.255.255 to any out");
659

    
660
	/* Allowed ips */
661
	$cprules .= "table {$cpzone}_allowed_up create type addr valtype pipe\n";
662
	$cprules .= "table {$cpzone}_allowed_down create type addr valtype pipe\n";
663
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
664
	    "pipe tablearg ip from table({$cpzone}_allowed_up) to any in");
665
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
666
	    "pipe tablearg ip from any to table({$cpzone}_allowed_down) in");
667
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
668
	    "pipe tablearg ip from table({$cpzone}_allowed_up) to any out");
669
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
670
	    "pipe tablearg ip from any to table({$cpzone}_allowed_down) out");
671

    
672
	/* Authenticated users rules. */
673
	$cprules .= "table {$cpzone}_auth_up create type addr valtype pipe\n";
674
	$cprules .= "table {$cpzone}_auth_down create type addr valtype pipe\n";
675
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
676
	    "pipe tablearg ip from table({$cpzone}_auth_up) to any layer2 in");
677
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
678
	    "pipe tablearg ip from any to table({$cpzone}_auth_down) layer2 out");
679

    
680
	if (!empty($config['captiveportal'][$cpzone]['listenporthttp'])) {
681
		$listenporthttp = $config['captiveportal'][$cpzone]['listenporthttp'];
682
	} else {
683
		$listenporthttp = 8000 + $cpzoneid;
684
	}
685

    
686
	if (isset($config['captiveportal'][$cpzone]['httpslogin'])) {
687
		if (!empty($config['captiveportal'][$cpzone]['listenporthttps'])) {
688
			$listenporthttps = $config['captiveportal'][$cpzone]['listenporthttps'];
689
		} else {
690
			$listenporthttps = 8001 + $cpzoneid;
691
		}
692
		if (!isset($config['captiveportal'][$cpzone]['nohttpsforwards'])) {
693
			$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
694
			    "fwd 127.0.0.1,{$listenporthttps} tcp from any to any dst-port 443 in");
695
		}
696
	}
697

    
698
	$cprules .= "# redirect non-authenticated clients to captive portal\n";
699
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
700
	    "fwd 127.0.0.1,{$listenporthttp} tcp from any to any dst-port 80 in");
701
	$cprules .= "# let the responses from the captive portal web server back out\n";
702
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
703
	    "pass tcp from any to any out");
704
	$cprules .= "# This CP zone is over, skip to last rule\n";
705
	$cprules .= captiveportal_create_ipfw_rule("add", $rulenum,
706
	    "skipto 65534 all from any to any");
707

    
708
	/* generate passthru mac database */
709
	$cprules .= captiveportal_passthrumac_configure(true);
710
	$cprules .= "\n";
711

    
712
	/* allowed ipfw rules to make allowed ip work */
713
	$cprules .= captiveportal_allowedip_configure();
714

    
715
	/* allowed ipfw rules to make allowed hostnames work */
716
	$cprules .= captiveportal_allowedhostname_configure();
717

    
718
	/* load rules */
719
	captiveportal_delete_rules();
720
	file_put_contents("{$g['tmp_path']}/ipfw_{$cpzone}.cp.rules", $cprules);
721
	mwexec("/sbin/ipfw -q {$g['tmp_path']}/ipfw_{$cpzone}.cp.rules", true);
722
	@unlink("{$g['tmp_path']}/ipfw_{$cpzone}.cp.rules");
723
	unset($cprules);
724

    
725
	captiveportal_filterdns_configure();
726

    
727
	if ($reinit == false) {
728
		unlock($captiveportallck);
729
	}
730
}
731

    
732
/* Delete all rules related to specific cpzone */
733
function captiveportal_delete_rules($pipes_to_remove = array()) {
734
	global $g, $cpzoneid, $cpzone;
735

    
736
	$skipto1 = captiveportal_ipfw_ruleno($cpzoneid);
737
	$skipto2 = $skipto1 + $g['captiveportal_rules_interval'];
738

    
739
	$cp_ifaces = pfSense_ipfw_table_list("cp_ifaces");
740
	if (is_array($cp_ifaces)) {
741
		foreach ($cp_ifaces as $cp_iface) {
742
			if (empty($cp_iface['skipto']) ||
743
			    $cp_iface['skipto'] != $skipto1) {
744
				continue;
745
			}
746

    
747
			pfSense_ipfw_table("cp_ifaces", IP_FW_TABLE_XDEL,
748
			    $cp_iface['iface']);
749
		}
750
	}
751

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

    
754
	$tables = captiveportal_get_ipfw_table_names();
755

    
756
	$delrules = "";
757
	foreach ($tables as $table) {
758
		$delrules .= "table {$table} destroy\n";
759
	}
760

    
761
	foreach ($pipes_to_remove as $pipeno) {
762
		$delrules .= "pipe delete {$pipeno}\n";
763
	}
764

    
765
	if (empty($delrules)) {
766
		return;
767
	}
768

    
769
	file_put_contents("{$g['tmp_path']}/ipfw_{$cpzone}.deltable.rules", $delrules);
770
	mwexec("/sbin/ipfw -q {$g['tmp_path']}/ipfw_{$cpzone}.deltable.rules", true);
771
	@unlink("{$g['tmp_path']}/ipfw_{$cpzone}.deltable.rules");
772
}
773

    
774
/*
775
 * Remove clients that have been around for longer than the specified amount of time
776
 * db file structure:
777
 * timestamp,ipfw_rule_no,clientip,clientmac,username,sessionid,password,session_timeout,idle_timeout,session_terminate_time,interim_interval,traffic_quota,auth_method,context
778
 * (password is in Base64 and only saved when reauthentication is enabled)
779
 */
780
function captiveportal_prune_old() {
781
	global $g, $config, $cpzone, $cpzoneid;
782

    
783
	if (empty($cpzone)) {
784
		return;
785
	}
786

    
787
	$cpcfg = $config['captiveportal'][$cpzone];
788
	$vcpcfg = $config['voucher'][$cpzone];
789

    
790
	/* check for expired entries */
791
	$idletimeout = 0;
792
	$timeout = 0;
793
	if (!empty($cpcfg['timeout']) && is_numeric($cpcfg['timeout'])) {
794
		$timeout = $cpcfg['timeout'] * 60;
795
	}
796

    
797
	if (!empty($cpcfg['idletimeout']) && is_numeric($cpcfg['idletimeout'])) {
798
		$idletimeout = $cpcfg['idletimeout'] * 60;
799
	}
800

    
801
	/* check for entries exceeding their traffic quota */
802
	$trafficquota = 0;
803
	if (!empty($cpcfg['trafficquota']) && is_numeric($cpcfg['trafficquota'])) {
804
		$trafficquota = $cpcfg['trafficquota'] * 1048576;
805
	}
806

    
807
	/* Is there any job to do? */
808
	if (!$timeout && !$idletimeout && !$trafficquota && !isset($cpcfg['reauthenticate']) &&
809
	    !isset($cpcfg['radiussession_timeout']) && !isset($cpcfg['radiustraffic_quota']) &&
810
	    !isset($vcpcfg['enable']) && !isset($cpcfg['radacct_enable'])) {
811
		return;
812
	}
813

    
814

    
815
	/* Read database */
816
	/* NOTE: while this can be simplified in non radius case keep as is for now */
817
	$cpdb = captiveportal_read_db();
818

    
819
	$unsetindexes = array();
820
	$voucher_needs_sync = false;
821
	/*
822
	 * Snapshot the time here to use for calculation to speed up the process.
823
	 * If something is missed next run will catch it!
824
	 */
825
	$pruning_time = time();
826
	foreach ($cpdb as $cpentry) {
827
		$stop_time = $pruning_time;
828

    
829
		$timedout = false;
830
		$term_cause = 1;
831
		/* hard timeout or session_timeout from radius if enabled */
832
		if (isset($cpcfg['radiussession_timeout'])) {
833
			$timeout = (is_numeric($cpentry[7])) ? $cpentry[7] : $timeout;
834
		}
835
		if ($timeout) {
836
			if (($pruning_time - $cpentry[0]) >= $timeout) {
837
				$timedout = true;
838
				$term_cause = 5; // Session-Timeout
839
				$logout_cause = 'SESSION TIMEOUT';
840
			}
841
		}
842

    
843
		/* Session-Terminate-Time */
844
		if (!$timedout && !empty($cpentry[9])) {
845
			if ($pruning_time >= $cpentry[9]) {
846
				$timedout = true;
847
				$term_cause = 5; // Session-Timeout
848
				$logout_cause = 'SESSION TIMEOUT';
849
			}
850
		}
851

    
852
		/* check if an idle_timeout has been set and if its set change the idletimeout to this value */
853
		$uidletimeout = (is_numeric($cpentry[8])) ? $cpentry[8] : $idletimeout;
854
		/* if an idle timeout is specified, get last activity timestamp from ipfw */
855
		if (!$timedout && $uidletimeout > 0) {
856
			$lastact = captiveportal_get_last_activity($cpentry[2]);
857
			/*	If the user has logged on but not sent any traffic they will never be logged out.
858
			 *	We "fix" this by setting lastact to the login timestamp.
859
			 */
860
			$lastact = $lastact ? $lastact : $cpentry[0];
861
			if ($lastact && (($pruning_time - $lastact) >= $uidletimeout)) {
862
				$timedout = true;
863
				$term_cause = 4; // Idle-Timeout
864
				$logout_cause = 'IDLE TIMEOUT';
865
				if (!isset($config['captiveportal'][$cpzone]['includeidletime'])) {
866
					$stop_time = $lastact;
867
				}
868
			}
869
		}
870

    
871
		/* if vouchers are configured, activate session timeouts */
872
		if (!$timedout && isset($vcpcfg['enable']) && !empty($cpentry[7])) {
873
			if ($pruning_time >= ($cpentry[0] + $cpentry[7])) {
874
				$timedout = true;
875
				$term_cause = 5; // Session-Timeout
876
				$logout_cause = 'SESSION TIMEOUT';
877
				$voucher_needs_sync = true;
878
			}
879
		}
880

    
881
		/* traffic quota, value retrieved from the radius attribute if the option is enabled */
882
		if (isset($cpcfg['radiustraffic_quota'])) {
883
			$trafficquota = (is_numeric($cpentry[11])) ? $cpentry[11] : $trafficquota;
884
		}
885
		if (!$timedout && $trafficquota > 0) {
886
			$volume = getVolume($cpentry[2], $cpentry[3]);
887
			if (($volume['input_bytes'] + $volume['output_bytes']) > $trafficquota) {
888
				$timedout = true;
889
				$term_cause = 10; // NAS-Request
890
				$logout_cause = 'QUOTA EXCEEDED';
891
			}
892
		}
893

    
894
		if ($timedout) {
895
			captiveportal_disconnect($cpentry, $term_cause, $stop_time);
896
			captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], $logout_cause);
897
			$unsetindexes[] = $cpentry[5];
898
		}
899

    
900
		/* do periodic reauthentication? For Radius servers, send accounting updates? */
901
		if (!$timedout) {
902
			//Radius servers : send accounting
903
			if (isset($cpcfg['radacct_enable']) && $cpentry[12] === 'radius') {
904
				if (substr($cpcfg['reauthenticateacct'], 0, 9) === "stopstart") {
905
					/* stop and restart accounting */
906
					if ($cpcfg['reauthenticateacct'] === "stopstartfreeradius") {
907
						$rastart_time = 0;
908
						$rastop_time = 60;
909
					} else {
910
						$rastart_time = $cpentry[0];
911
						$rastop_time = time();
912
					}
913
					captiveportal_send_server_accounting('stop',
914
						$cpentry[1], // ruleno
915
						$cpentry[4], // username
916
						$cpentry[2], // clientip
917
						$cpentry[3], // clientmac
918
						$cpentry[5], // sessionid
919
						$rastart_time, // start time
920
						$rastop_time, // Stop Time
921
						10); // NAS Request
922
					$clientsn = (is_ipaddrv6($cpentry[2])) ? 128 : 32;
923
					pfSense_ipfw_table_zerocnt("{$cpzone}_auth_up", "{$cpentry[2]}/{$clientsn}");
924
					pfSense_ipfw_table_zerocnt("{$cpzone}_auth_down", "{$cpentry[2]}/{$clientsn}");
925
					if ($cpcfg['reauthenticateacct'] == "stopstartfreeradius") {
926
						/* Need to pause here or the FreeRADIUS server gets confused about packet ordering. */
927
						sleep(1);
928
					}
929
					captiveportal_send_server_accounting('start',
930
						$cpentry[1], // ruleno
931
						$cpentry[4], // username
932
						$cpentry[2], // clientip
933
						$cpentry[3], // clientmac
934
						$cpentry[5]); // sessionid
935
				} else if ($cpcfg['reauthenticateacct'] == "interimupdate") {
936
					$session_time = $pruning_time - $cpentry[0];
937
					if (!empty($cpentry[10]) && $cpentry[10] > 60) {
938
						$interval = $cpentry[10];
939
					} else {
940
						$interval = 0;
941
					}
942
					$past_interval_min = ($session_time > $interval);
943
					if ($interval != 0) {
944
						$within_interval = ($session_time % $interval >= 0 && $session_time % $interval <= 59);
945
					}
946
					if ($interval === 0 || ($interval > 0 && $past_interval_min && $within_interval)) {
947
					captiveportal_send_server_accounting('update',
948
						$cpentry[1], // ruleno
949
						$cpentry[4], // username
950
						$cpentry[2], // clientip
951
						$cpentry[3], // clientmac
952
						$cpentry[5], // sessionid
953
						$cpentry[0]); // start time
954
					}
955
				}
956
			}
957

    
958
			/* check this user again */
959
			if (isset($cpcfg['reauthenticate']) && $cpentry[13] !== 'voucher') {
960
				$auth_result = captiveportal_authenticate_user(
961
					$cpentry[4], // username
962
					base64_decode($cpentry[6]), // password
963
					$cpentry[3], // clientmac
964
					$cpentry[2], // clientip
965
					$cpentry[1], // ruleno
966
					$cpentry[13]); // context
967
				if ($auth_result['result'] === false) {
968
					captiveportal_disconnect($cpentry, 17);
969
					captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "DISCONNECT - REAUTHENTICATION FAILED", $auth_list['reply_message']);
970
					$unsetindexes[] = $cpentry[5];
971
				} else if ($auth_result['result'] === true) {
972
					if ($cpentry[12] !== $auth_result['auth_method']) {
973
						// if the user got authenticated against another server type:  we update the database
974
						if (!empty($cpentry[5])) {
975
							captiveportal_write_db("UPDATE captiveportal SET authmethod = '{$auth_result['auth_method']}' WHERE sessionid = '{$cpentry[5]}'");
976
							captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CHANGED AUTHENTICATION SERVER", $auth_list['reply_message']);
977
						}
978
						// User was logged on a RADIUS server, but is now logged in by another server type : we send an accounting Stop
979
						if (isset($config['captiveportal'][$cpzone]['radacct_enable']) && $cpentry[12] =='radius') {
980
							if ($cpcfg['reauthenticateacct'] === "stopstartfreeradius") {
981
								$rastart_time = 0;
982
								$rastop_time = 60;
983
							} else {
984
								$rastart_time = $cpentry[0];
985
								$rastop_time = time();
986
							}
987
							captiveportal_send_server_accounting('stop',
988
								$cpentry[1], // ruleno
989
								$cpentry[4], // username
990
								$cpentry[2], // clientip
991
								$cpentry[3], // clientmac
992
								$cpentry[5], // sessionid
993
								$rastart_time, // start time
994
								$rastop_time, // Stop Time
995
								3); // Lost Service
996
						// User was logged on a non-RADIUS Server but is now logged in by a RADIUS server : we send an accounting Start
997
						} else if(isset($config['captiveportal'][$cpzone]['radacct_enable']) && $auth_result['auth_method'] === 'radius') {
998
							captiveportal_send_server_accounting('start',
999
								$cpentry[1], // ruleno
1000
								$cpentry[4], // username
1001
								$cpentry[2], // clientip
1002
								$cpentry[3], // clientmac
1003
								$cpentry[5], // sessionid
1004
								$cpentry[0]); // start_time
1005
						}
1006
					}
1007
					captiveportal_reapply_attributes($cpentry, $auth_result['attributes']);
1008
				}
1009
			}
1010
		}
1011
	}
1012
	unset($cpdb);
1013

    
1014
	captiveportal_prune_old_automac();
1015

    
1016
	if ($voucher_needs_sync == true) {
1017
		/* Trigger a sync of the vouchers on config */
1018
		send_event("service sync vouchers");
1019
	}
1020

    
1021
	/* write database */
1022
	if (!empty($unsetindexes)) {
1023
		captiveportal_remove_entries($unsetindexes);
1024
	}
1025
}
1026

    
1027
function captiveportal_prune_old_automac() {
1028
	global $g, $config, $cpzone, $cpzoneid;
1029

    
1030
	if (is_array($config['captiveportal'][$cpzone]['passthrumac']) &&
1031
	    isset($config['captiveportal'][$cpzone]['passthrumacaddusername'])) {
1032
		$tmpvoucherdb = array();
1033
		$macrules = "";
1034
		$writecfg = false;
1035
		foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $eid => $emac) {
1036
			if ($emac['logintype'] != "voucher") {
1037
				continue;
1038
			}
1039
			if (isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) {
1040
				if (isset($tmpvoucherdb[$emac['username']])) {
1041
					$temac = $config['captiveportal'][$cpzone]['passthrumac'][$tmpvoucherdb[$emac['username']]];
1042
					$pipeno = captiveportal_get_dn_passthru_ruleno($temac['mac']);
1043
					if ($pipeno) {
1044
						captiveportal_free_dn_ruleno($pipeno);
1045
						$macrules .= "table {$cpzone}_pipe_mac delete any,{$temac['mac']}\n";
1046
						$macrules .= "table {$cpzone}_pipe_mac delete {$temac['mac']},any\n";
1047
						$macrules .= "pipe delete {$pipeno}\n";
1048
						++$pipeno;
1049
						$macrules .= "pipe delete {$pipeno}\n";
1050
					}
1051
					$writecfg = true;
1052
					captiveportal_logportalauth($temac['username'], $temac['mac'],
1053
					    $temac['ip'], "DUPLICATE {$temac['username']} LOGIN - TERMINATING OLD SESSION");
1054
					unset($config['captiveportal'][$cpzone]['passthrumac'][$tmpvoucherdb[$emac['username']]]);
1055
				}
1056
				$tmpvoucherdb[$emac['username']] = $eid;
1057
			}
1058
			if (voucher_auth($emac['username']) <= 0) {
1059
				$pipeno = captiveportal_get_dn_passthru_ruleno($emac['mac']);
1060
				if ($pipeno) {
1061
					captiveportal_free_dn_ruleno($pipeno);
1062
					$macrules .= "table {$cpzone}_pipe_mac delete any,{$emac['mac']}\n";
1063
					$macrules .= "table {$cpzone}_pipe_mac delete {$emac['mac']},any\n";
1064
					$macrules .= "pipe delete {$pipeno}\n";
1065
					++$pipeno;
1066
					$macrules .= "pipe delete {$pipeno}\n";
1067
				}
1068
				$writecfg = true;
1069
				captiveportal_logportalauth($emac['username'], $emac['mac'],
1070
				    $emac['ip'], "EXPIRED {$emac['username']} LOGIN - TERMINATING SESSION");
1071
				unset($config['captiveportal'][$cpzone]['passthrumac'][$eid]);
1072
			}
1073
		}
1074
		unset($tmpvoucherdb);
1075
		if (!empty($macrules)) {
1076
			@file_put_contents("{$g['tmp_path']}/macentry.prunerules.tmp", $macrules);
1077
			unset($macrules);
1078
			mwexec("/sbin/ipfw -q {$g['tmp_path']}/macentry.prunerules.tmp");
1079
		}
1080
		if ($writecfg === true) {
1081
			write_config("Prune session for auto-added macs");
1082
		}
1083
	}
1084
}
1085

    
1086
/* remove a single client according to the DB entry */
1087
function captiveportal_disconnect($dbent, $term_cause = 1, $stop_time = null) {
1088
	global $g, $config, $cpzone, $cpzoneid;
1089

    
1090
	$stop_time = (empty($stop_time)) ? time() : $stop_time;
1091

    
1092
	/* this client needs to be deleted - remove ipfw rules */
1093
	if (isset($config['captiveportal'][$cpzone]['radacct_enable']) && $dbent[12] =='radius') {
1094
		if ($config['captiveportal'][$cpzone]['reauthenticateacct'] == "stopstartfreeradius") {
1095
			/*
1096
			 * Interim updates are on so the session time must be
1097
			 * reported as the elapsed time since the previous
1098
			 * interim update.
1099
			 */
1100
			$session_time = ($stop_time - $dbent[0]) % 60;
1101
			$start_time = $stop_time - $session_time;
1102
		} else {
1103
			$start_time = $dbent[0];
1104
		}
1105
		captiveportal_send_server_accounting('stop',
1106
			$dbent[1], // ruleno
1107
			$dbent[4], // username
1108
			$dbent[2], // clientip
1109
			$dbent[3], // clientmac
1110
			$dbent[5], // sessionid
1111
			$start_time, // start time
1112
			$stop_time, // stop time
1113
			$term_cause); // Acct-Terminate-Cause
1114
	}
1115

    
1116
	if (is_ipaddr($dbent[2])) {
1117
		/*
1118
		 * Delete client's ip entry from tables auth_up and auth_down.
1119
		 * It's not necessary to explicit specify mac address here
1120
		 */
1121
		$cpsession = captiveportal_isip_logged($dbent[2]);
1122
		if (!empty($cpsession)) {
1123
			$clientsn = (is_ipaddrv6($dbent[2])) ? 128 : 32;
1124
			pfSense_ipfw_table("{$cpzone}_auth_up",
1125
			    IP_FW_TABLE_XDEL, "{$dbent[2]}/{$clientsn}");
1126
			pfSense_ipfw_table("{$cpzone}_auth_down",
1127
			    IP_FW_TABLE_XDEL, "{$dbent[2]}/{$clientsn}");
1128
		}
1129
		/* XXX: Redundant?! Ensure all pf(4) states are killed. */
1130
		$_gb = @pfSense_kill_states($dbent[2]);
1131
		$_gb = @pfSense_kill_srcstates($dbent[2]);
1132
	}
1133

    
1134
	/*
1135
	 * These are the pipe numbers we use to control traffic shaping for
1136
	 * each logged in user via captive portal
1137
	 * We could get an error if the pipe doesn't exist but everything
1138
	 * should still be fine
1139
	 */
1140
	if (!empty($dbent[1])) {
1141
		/*
1142
		 * Call captiveportal_free_dnrules() in dry_run mode to verify
1143
		 * if there are pipes to be removed and prevent the attempt to
1144
		 * delete invalid pipes
1145
		 */
1146
		$removed_pipes = captiveportal_free_dnrules($dbent[1],
1147
		    $dbent[1]+1, true);
1148

    
1149
		if (!empty($removed_pipes)) {
1150
			$_gb = @pfSense_ipfw_pipe("pipe delete {$dbent[1]}");
1151
			$_gb = @pfSense_ipfw_pipe("pipe delete " .
1152
			    ($dbent[1]+1));
1153

    
1154
			/*
1155
			 * Release the ruleno so it can be reallocated to new
1156
			 * clients
1157
			 */
1158
			captiveportal_free_dn_ruleno($dbent[1]);
1159
		}
1160
	}
1161

    
1162
	// XMLRPC Call over to the master Voucher node
1163
	if (xmlrpc_sync_voucher_details($syncip, $syncport,
1164
	    $vouchersyncusername, $syncpass)) {
1165
		$remote_status = xmlrpc_sync_voucher_disconnect($dbent, $syncip,
1166
		    $syncport, $syncpass, $vouchersyncusername, $term_cause,
1167
		    $stop_time);
1168
	}
1169

    
1170
}
1171

    
1172
/* remove a single client by sessionid */
1173
function captiveportal_disconnect_client($sessionid, $term_cause = 1, $logoutReason = "LOGOUT") {
1174
	global $g, $config;
1175

    
1176
	$sessionid = SQLite3::escapeString($sessionid);
1177
	/* read database */
1178
	$result = captiveportal_read_db("WHERE sessionid = '{$sessionid}'");
1179

    
1180
	/* find entry */
1181
	if (!empty($result)) {
1182

    
1183
		foreach ($result as $cpentry) {
1184
			captiveportal_disconnect($cpentry, $term_cause);
1185
			captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "DISCONNECT");
1186
		}
1187
		captiveportal_remove_entries(array($sessionid));
1188
		unset($result);
1189
	}
1190
}
1191

    
1192
/* remove all clients */
1193
function captiveportal_disconnect_all($term_cause = 6, $logoutReason = "DISCONNECT") {
1194
	global $g, $config, $cpzone, $cpzoneid;
1195

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

    
1199
	/* if we still don't have the lock, unlock forcefully and take it */
1200
	if (!$rcprunelock) {
1201
		log_error("CP zone ${cpzone}: could not obtain the lock for more than 15 seconds, lock taken forcefully to disconnect all users");
1202
		unlock_force("rcprunecaptiveportal{$cpzone}");
1203
		$rcprunelock = lock("rcprunecaptiveportal{$cpzone}", LOCK_EX);
1204
	}
1205

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

    
1209
	captiveportal_radius_stop_all($term_cause, $logoutReason);
1210

    
1211
	/* remove users from the database */
1212
	$cpdb = captiveportal_read_db();
1213
	$unsetindexes = array_column($cpdb,5);
1214
	if (!empty($unsetindexes)) {
1215
		captiveportal_remove_entries($unsetindexes);
1216
	}
1217

    
1218
	/* reinit ipfw rules */
1219
	captiveportal_init_rules(true);
1220

    
1221
	unlock($cpdblck);
1222
	unlock($rcprunelock);
1223
}
1224

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

    
1229
	$cpdb = captiveportal_read_db();
1230

    
1231
	$radacct = isset($config['captiveportal'][$cpzone]['radacct_enable']) ? true : false;
1232
	foreach ($cpdb as $cpentry) {
1233
		if ($cpentry[12] === 'radius' && $radacct) {
1234
			if ($config['captiveportal'][$cpzone]['reauthenticateacct'] == "stopstartfreeradius") {
1235
				$session_time = (time() - $cpentry[0]) % 60;
1236
				$start_time = time() - $session_time;
1237
			} else {
1238
				$start_time = $cpentry[0];
1239
			}
1240
			captiveportal_send_server_accounting('stop',
1241
				$cpentry[1], // ruleno
1242
				$cpentry[4], // username
1243
				$cpentry[2], // clientip
1244
				$cpentry[3], // clientmac
1245
				$cpentry[5], // sessionid
1246
				$start_time, // start time
1247
				$stop_time, // stop time
1248
				$term_cause); // Acct-Terminate-Cause
1249
		}
1250
		captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], $logoutReason);
1251
	}
1252
	unset($cpdb);
1253
}
1254

    
1255
function captiveportal_passthrumac_configure_entry($macent, $pipeinrule = false) {
1256
	global $config, $g, $cpzone;
1257

    
1258
	$bwUp = 0;
1259
	if (!empty($macent['bw_up'])) {
1260
		$bwUp = $macent['bw_up'];
1261
	} else if (!empty($config['captiveportal'][$cpzone]['bwdefaultup'])) {
1262
		$bwUp = $config['captiveportal'][$cpzone]['bwdefaultup'];
1263
	}
1264
	$bwDown = 0;
1265
	if (!empty($macent['bw_down'])) {
1266
		$bwDown = $macent['bw_down'];
1267
	} else if (!empty($config['captiveportal'][$cpzone]['bwdefaultdn'])) {
1268
		$bwDown = $config['captiveportal'][$cpzone]['bwdefaultdn'];
1269
	}
1270

    
1271
	if ($macent['action'] == 'pass') {
1272
		$rules = "";
1273

    
1274
		$pipeno = captiveportal_get_next_dn_ruleno();
1275

    
1276
		$pipeup = $pipeno;
1277
		if ($pipeinrule == true) {
1278
			$_gb = @pfSense_ipfw_pipe("pipe {$pipeno} config bw {$bwUp}Kbit/s queue 100 buckets 16");
1279
		} else {
1280
			$rules .= "pipe {$pipeno} config bw {$bwUp}Kbit/s queue 100 buckets 16\n";
1281
		}
1282

    
1283
		$pipedown = $pipeno + 1;
1284
		if ($pipeinrule == true) {
1285
			$_gb = @pfSense_ipfw_pipe("pipe {$pipedown} config bw {$bwDown}Kbit/s queue 100 buckets 16");
1286
		} else {
1287
			$rules .= "pipe {$pipedown} config bw {$bwDown}Kbit/s queue 100 buckets 16\n";
1288
		}
1289

    
1290
		$rules .= "table {$cpzone}_pipe_mac add any,{$macent['mac']} {$pipeup}\n";
1291
		$rules .= "table {$cpzone}_pipe_mac add {$macent['mac']},any {$pipedown}\n";
1292
	}
1293

    
1294
	return $rules;
1295
}
1296

    
1297
function captiveportal_passthrumac_delete_entry($macent) {
1298
	global $cpzone;
1299
	$rules = "";
1300

    
1301
	if ($macent['action'] == 'pass') {
1302
		$pipeno = captiveportal_get_dn_passthru_ruleno($macent['mac']);
1303

    
1304
		if (!empty($pipeno)) {
1305
			captiveportal_free_dn_ruleno($pipeno);
1306
			$rules .= "table {$cpzone}_pipe_mac delete any,{$macent['mac']}\n";
1307
			$rules .= "table {$cpzone}_pipe_mac delete {$macent['mac']},any\n";
1308
			$rules .= "pipe delete " . $pipeno . "\n";
1309
			$rules .= "pipe delete " . ++$pipeno . "\n";
1310
		}
1311
	}
1312

    
1313
	return $rules;
1314
}
1315

    
1316
function captiveportal_passthrumac_configure($filename = false, $startindex = 0, $stopindex = 0) {
1317
	global $config, $g, $cpzone;
1318

    
1319
	$rules = "";
1320

    
1321
	if (is_array($config['captiveportal'][$cpzone]['passthrumac'])) {
1322
		if ($stopindex > 0) {
1323
			$fd = fopen($filename, "w");
1324
			for ($idx = $startindex; $idx <= $stopindex; $idx++) {
1325
				if (isset($config['captiveportal'][$cpzone]['passthrumac'][$idx])) {
1326
					$rules = captiveportal_passthrumac_configure_entry($config['captiveportal'][$cpzone]['passthrumac'][$idx]);
1327
					fwrite($fd, $rules);
1328
				}
1329
			}
1330
			fclose($fd);
1331

    
1332
			return;
1333
		} else {
1334
			$nentries = count($config['captiveportal'][$cpzone]['passthrumac']);
1335
			if ($nentries > 2000) {
1336
				$nloops = $nentries / 1000;
1337
				$remainder= $nentries % 1000;
1338
				for ($i = 0; $i < $nloops; $i++) {
1339
					mwexec_bg("/usr/local/sbin/fcgicli -f /etc/rc.captiveportal_configure_mac -d \"cpzone={$cpzone}&startidx=" . ($i * 1000) . "&stopidx=" . ((($i+1) * 1000) - 1) . "\"");
1340
				}
1341
				if ($remainder > 0) {
1342
					mwexec_bg("/usr/local/sbin/fcgicli -f /etc/rc.captiveportal_configure_mac -d \"cpzone={$cpzone}&startidx=" . ($i * 1000) . "&stopidx=" . (($i* 1000) + $remainder) ."\"");
1343
				}
1344
			} else {
1345
				foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $macent) {
1346
					$rules .= captiveportal_passthrumac_configure_entry($macent, true);
1347
				}
1348
			}
1349
		}
1350
	}
1351

    
1352
	return $rules;
1353
}
1354

    
1355
function captiveportal_passthrumac_findbyname($username) {
1356
	global $config, $cpzone;
1357

    
1358
	if (is_array($config['captiveportal'][$cpzone]['passthrumac'])) {
1359
		foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $macent) {
1360
			if ($macent['username'] == $username) {
1361
				return $macent;
1362
			}
1363
		}
1364
	}
1365
	return NULL;
1366
}
1367

    
1368
/*
1369
 * table (3=IN)/(4=OUT) hold allowed ip's without bw limits
1370
 */
1371
function captiveportal_allowedip_configure_entry($ipent, $ishostname = false) {
1372
	global $g, $config, $cpzone;
1373

    
1374
	/*  Instead of copying this entire function for something
1375
	 *  easy such as hostname vs ip address add this check
1376
	 */
1377
	if ($ishostname === true) {
1378
		if (!platform_booting()) {
1379
			$ipaddress = gethostbyname($ipent['hostname']);
1380
			if (!is_ipaddr($ipaddress)) {
1381
				return;
1382
			}
1383
		} else {
1384
			$ipaddress = "";
1385
		}
1386
	} else {
1387
		$ipaddress = $ipent['ip'];
1388
	}
1389

    
1390
	$rules = "";
1391
	$cp_filterdns_conf = "";
1392
	$enBwup = 0;
1393
	if (!empty($ipent['bw_up'])) {
1394
		$enBwup = intval($ipent['bw_up']);
1395
	} else if (!empty($config['captiveportal'][$cpzone]['bwdefaultup'])) {
1396
		$enBwup = $config['captiveportal'][$cpzone]['bwdefaultup'];
1397
	}
1398
	$enBwdown = 0;
1399
	if (!empty($ipent['bw_down'])) {
1400
		$enBwdown = intval($ipent['bw_down']);
1401
	} else if (!empty($config['captiveportal'][$cpzone]['bwdefaultdn'])) {
1402
		$enBwdown = $config['captiveportal'][$cpzone]['bwdefaultdn'];
1403
	}
1404

    
1405
	$pipeup = captiveportal_get_next_dn_ruleno();
1406
	$_gb = @pfSense_ipfw_pipe("pipe {$pipeup} config bw {$enBwup}Kbit/s queue 100 buckets 16");
1407
	$pipedown = $pipeup + 1;
1408
	$_gb = @pfSense_ipfw_pipe("pipe {$pipedown} config bw {$enBwdown}Kbit/s queue 100 buckets 16");
1409

    
1410
	if ($ishostname === true) {
1411
		$cp_filterdns_conf .= "ipfw {$ipent['hostname']} {$cpzone}_allowed_up pipe {$pipeup}\n";
1412
		$cp_filterdns_conf .= "ipfw {$ipent['hostname']} {$cpzone}_allowed_down pipe {$pipedown}\n";
1413
		if (!is_ipaddr($ipaddress)) {
1414
			return array("", $cp_filterdns_conf);
1415
		}
1416
	}
1417

    
1418
	$subnet = "";
1419
	if (!empty($ipent['sn'])) {
1420
		$subnet = "/{$ipent['sn']}";
1421
	}
1422
	$rules .= "table {$cpzone}_allowed_up add {$ipaddress}{$subnet} {$pipeup}\n";
1423
	$rules .= "table {$cpzone}_allowed_down add {$ipaddress}{$subnet} {$pipedown}\n";
1424

    
1425
	if ($ishostname === true) {
1426
		return array($rules, $cp_filterdns_conf);
1427
	} else {
1428
		return $rules;
1429
	}
1430
}
1431

    
1432
function captiveportal_allowedhostname_configure() {
1433
	global $config, $g, $cpzone, $cpzoneid;
1434

    
1435
	$rules = "";
1436
	if (!is_array($config['captiveportal'][$cpzone]['allowedhostname'])) {
1437
		return $rules;
1438
	}
1439

    
1440
	$rules = "\n# captiveportal_allowedhostname_configure()\n";
1441
	$cp_filterdns_conf = "";
1442
	foreach ($config['captiveportal'][$cpzone]['allowedhostname'] as $hostnameent) {
1443
		$tmprules = captiveportal_allowedip_configure_entry($hostnameent, true);
1444
		$rules .= $tmprules[0];
1445
		$cp_filterdns_conf .= $tmprules[1];
1446
	}
1447
	$cp_filterdns_filename = "{$g['varetc_path']}/filterdns-{$cpzone}-captiveportal.conf";
1448
	@file_put_contents($cp_filterdns_filename, $cp_filterdns_conf);
1449
	unset($cp_filterdns_conf);
1450

    
1451
	return $rules;
1452
}
1453

    
1454
function captiveportal_filterdns_configure() {
1455
	global $config, $g, $cpzone, $cpzoneid;
1456

    
1457
	$cp_filterdns_filename = $g['varetc_path'] .
1458
	    "/filterdns-{$cpzone}-captiveportal.conf";
1459

    
1460
	if (isset($config['captiveportal'][$cpzone]['enable']) &&
1461
	    is_array($config['captiveportal'][$cpzone]['allowedhostname']) &&
1462
	    file_exists($cp_filterdns_filename)) {
1463
		if (isvalidpid($g['varrun_path'] .
1464
		    "/filterdns-{$cpzone}-cpah.pid")) {
1465
			sigkillbypid($g['varrun_path'] .
1466
			    "/filterdns-{$cpzone}-cpah.pid", "HUP");
1467
		} else {
1468
			mwexec("/usr/local/sbin/filterdns -p " .
1469
			    "{$g['varrun_path']}/filterdns-{$cpzone}-cpah.pid" .
1470
			    " -i 300 -c {$cp_filterdns_filename} -d 1");
1471
		}
1472
	} else {
1473
		killbypid("{$g['varrun_path']}/filterdns-{$cpzone}-cpah.pid");
1474
		@unlink("{$g['varrun_path']}/filterdns-{$cpzone}-cpah.pid");
1475
	}
1476

    
1477
	return $rules;
1478
}
1479

    
1480
function captiveportal_allowedip_configure() {
1481
	global $config, $g, $cpzone;
1482

    
1483
	$rules = "";
1484
	if (is_array($config['captiveportal'][$cpzone]['allowedip'])) {
1485
		foreach ($config['captiveportal'][$cpzone]['allowedip'] as $ipent) {
1486
			$rules .= captiveportal_allowedip_configure_entry($ipent);
1487
		}
1488
	}
1489

    
1490
	return $rules;
1491
}
1492

    
1493
/* get last activity timestamp given client IP address */
1494
function captiveportal_get_last_activity($ip) {
1495
	global $cpzone;
1496

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

    
1500
	foreach ($tables as $table) {
1501
		$ipfw = pfSense_ipfw_table_lookup($table, $ip);
1502
		if (is_array($ipfw)) {
1503
			/* Workaround for #46652 */
1504
			if ($ipfw['packets'] > 0) {
1505
				return $ipfw['timestamp'];
1506
			} else {
1507
				return 0;
1508
			}
1509
		}
1510
	}
1511

    
1512
	return 0;
1513
}
1514

    
1515

    
1516
/* log successful captive portal authentication to syslog */
1517
/* part of this code from php.net */
1518
function captiveportal_logportalauth($user, $mac, $ip, $status, $message = null) {
1519
	// Log it
1520
	if (!$message) {
1521
		$message = "{$status}: {$user}, {$mac}, {$ip}";
1522
	} else {
1523
		$message = trim($message);
1524
		$message = "{$status}: {$user}, {$mac}, {$ip}, {$message}";
1525
	}
1526
	captiveportal_syslog($message);
1527
}
1528

    
1529
/* log simple messages to syslog */
1530
function captiveportal_syslog($message) {
1531
	global $cpzone;
1532

    
1533
	$message = trim($message);
1534
	$message = "Zone: {$cpzone} - {$message}";
1535
	openlog("logportalauth", LOG_PID, LOG_LOCAL4);
1536
	// Log it
1537
	syslog(LOG_INFO, $message);
1538
	closelog();
1539
}
1540

    
1541
/* Authenticate users using Authentication Backend */
1542
function captiveportal_authenticate_user(&$login = '', &$password = '', $clientmac = '', $clientip = '', $pipeno = 'null', $context = 'first') {
1543
	global $g, $config, $cpzone;
1544
	$cpcfg = $config['captiveportal'][$cpzone];
1545

    
1546
	$login_status = 'FAILURE';
1547
	$login_msg = gettext('Invalid credentials specified');
1548
	$reply_attributes = array();
1549
	$auth_method = '';
1550
	$auth_result = null;
1551

    
1552
	/*
1553
	Management of the reply Message (reason why the authentication failed) :
1554
	multiple authentication servers can be used, so multiple reply messages could theoretically be returned.
1555
	But only one message is returned (the most important one).
1556
	The return value of authenticate_user() define how important messages are :
1557
		- Reply message of a successful auth is more important than reply message of
1558
		a user failed auth(invalid credentials/authorization)
1559

    
1560
		- Reply message of a user failed auth is more important than reply message of
1561
		a server failed auth (unable to contact server)
1562

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

    
1566
	The $authlevel variable is a flag indicating the status of authentication
1567
	0 = failed server auth
1568
	1 = failed user auth
1569
	2 = failed user auth with custom server reply recieved
1570
	3 = successful auth
1571
	*/
1572
	$authlevel = 0;
1573

    
1574
	/* Getting authentication servers from captiveportal configuration */
1575
	$auth_servers = array();
1576

    
1577
	if ($cpcfg['auth_method'] === 'none') {
1578
		$auth_servers[] = array('type' => 'none');
1579
	} else {
1580
		if ($context === 'second') {
1581
			$fullauthservers = explode(",", $cpcfg['auth_server2']);
1582
		} else {
1583
			$fullauthservers = explode(",", $cpcfg['auth_server']);
1584
		}
1585

    
1586
		foreach ($fullauthservers as $authserver) {
1587
			if (strpos($authserver, ' - ') !== false) {
1588
				$authserver = explode(' - ', $authserver);
1589
				array_shift($authserver);
1590
				$authserver = implode(' - ', $authserver);
1591

    
1592
				if (auth_get_authserver($authserver) !== null) {
1593
					$auth_servers[] = auth_get_authserver($authserver);
1594
				} else {
1595
					log_error("Zone: {$cpzone} - Captive portal was unable to find the settings of the server '{$authserver}' used for authentication !");
1596
				}
1597
			}
1598
		}
1599
	}
1600

    
1601
	/* Unable to find the any authentication server config - shouldn't happen! - bail out */
1602
	if (count($auth_servers) === 0) {
1603
		log_error("Zone: {$cpzone} - No valid server could be used for authentication.");
1604
		$login_msg = gettext("Internal Error");
1605
	} else {
1606
		foreach ($auth_servers as $authcfg) {
1607
			if ($authlevel < 3) {
1608
				$radmac_error = false;
1609
				$attributes = array("nas_identifier" => "CaptivePortal",
1610
					"nas_port_type" => RADIUS_ETHERNET,
1611
					"nas_port" => $pipeno,
1612
					"framed_ip" => $clientip);
1613
				if (mac_format($clientmac) !== null) {
1614
					$attributes["calling_station_id"] = mac_format($clientmac);
1615
				}
1616

    
1617
				$result = null;
1618
				$status = null;
1619
				$msg = null;
1620

    
1621
				/* Radius MAC authentication */
1622
				if ($cpcfg['auth_method'] === 'radmac' && $clientmac) {
1623
					if ($authcfg['type'] === 'radius') {
1624
						$login = mac_format($clientmac);
1625
						$password = $cpcfg['radmac_secret'];
1626
						$status = "MACHINE LOGIN";
1627
					} else {
1628
						/* Trying to perform a Radius MAC authentication on a non-radius server - shouldn't happen! - bail out */
1629
						$msg = gettext("Internal Error");
1630
						log_error("Zone: {$cpzone} - Trying to perform RADIUS MAC authentication on a non-RADIUS server !");
1631
						$radmac_error = true;
1632
						$result = null;
1633
					}
1634
				}
1635

    
1636
				if (!$radmac_error) {
1637
					if ($authcfg['type'] === 'none') {
1638
						$result = true;
1639
					} else {
1640
						$result = authenticate_user($login, $password, $authcfg, $attributes);
1641
					}
1642

    
1643
					if (!empty($attributes['error_message'])) {
1644
						$msg = $attributes['error_message'];
1645
					}
1646

    
1647
					if ($authcfg['type'] == 'Local Auth' && $result && isset($cpcfg['localauth_priv'])) {
1648
						if (!userHasPrivilege(getUserEntry($login), "user-services-captiveportal-login")) {
1649
							$result = false;
1650
							$msg = gettext("Access Denied");
1651
						}
1652
					}
1653

    
1654
					if (empty($status)) {
1655
						if ($result === true) {
1656
							$status = "ACCEPT";
1657
						} elseif ($result === null) {
1658
							$status = "ERROR";
1659
						} else {
1660
							$status = "FAILURE";
1661
						}
1662
					}
1663

    
1664
					if ($cpcfg['auth_method'] === 'radmac' && $login == mac_format($clientmac) || $authcfg['type'] === 'none' && empty($login)) {
1665
						$login = "unauthenticated";
1666
					}
1667
					// We determine a flag
1668
					if ($result === true) {
1669
						$val = 3;
1670
					} elseif ($result === false && !empty($attributes['reply_message'])) {
1671
						$val = 2;
1672
						$msg = $attributes['reply_message'];
1673
					} elseif ($result === false) {
1674
						$val = 1;
1675
					} elseif ($result === null) {
1676
						$val = 0;
1677
					}
1678
				}
1679

    
1680
				if ($val >= $auth_val) {
1681
					$auth_val = $val;
1682
					$auth_method = $authcfg['type'];
1683
					$login_status = $status;
1684
					$login_msg = $msg;
1685
					$reply_attributes = $attributes;
1686
					$auth_result = $result;
1687
				}
1688
			}
1689
		}
1690
	}
1691

    
1692
	return array('result'=>$auth_result, 'attributes'=>$reply_attributes, 'auth_method' =>$auth_method, 'login_status'=> $login_status, 'login_message' => $login_msg);
1693
}
1694

    
1695
function captiveportal_opendb() {
1696
	global $g, $config, $cpzone, $cpzoneid;
1697

    
1698
	$db_path = "{$g['vardb_path']}/captiveportal{$cpzone}.db";
1699
	$createquery = "CREATE TABLE IF NOT EXISTS captiveportal (" .
1700
				"allow_time INTEGER, pipeno INTEGER, ip TEXT, mac TEXT, username TEXT, " .
1701
				"sessionid TEXT, bpassword TEXT, session_timeout INTEGER, idle_timeout INTEGER, " .
1702
				"session_terminate_time INTEGER, interim_interval INTEGER, traffic_quota INTEGER, " .
1703
				"authmethod TEXT, context TEXT); " .
1704
			"CREATE UNIQUE INDEX IF NOT EXISTS idx_active ON captiveportal (sessionid, username); " .
1705
			"CREATE INDEX IF NOT EXISTS user ON captiveportal (username); " .
1706
			"CREATE INDEX IF NOT EXISTS ip ON captiveportal (ip); " .
1707
			"CREATE INDEX IF NOT EXISTS starttime ON captiveportal (allow_time)";
1708

    
1709
	try {
1710
		$DB = new SQLite3($db_path);
1711
		$DB->busyTimeout(60000);
1712
	} catch (Exception $e) {
1713
		captiveportal_syslog("Could not open {$db_path} as an sqlite database for {$cpzone}. Error message: " . $e->getMessage() . " -- Trying again.");
1714
		unlink_if_exists($db_path);
1715
		try {
1716
			$DB = new SQLite3($db_path);
1717
			$DB->busyTimeout(60000);
1718
		} catch (Exception $e) {
1719
			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.");
1720
			return;
1721
		}
1722
	}
1723

    
1724
	if (!$DB) {
1725
		captiveportal_syslog("Could not open {$db_path} as an sqlite database for {$cpzone}. Error message: {$DB->lastErrorMsg()}. Trying again.");
1726
		unlink_if_exists($db_path);
1727
		$DB = new SQLite3($db_path);
1728
		$DB->busyTimeout(60000);
1729
		if (!$DB) {
1730
			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.");
1731
			return;
1732
		}
1733
	}
1734

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

    
1738
		/* If unable to initialize the database, reset and try again. */
1739
		$DB->close();
1740
		unset($DB);
1741
		unlink_if_exists($db_path);
1742
		$DB = new SQLite3($db_path);
1743
		$DB->busyTimeout(60000);
1744
		if ($DB->exec($createquery)) {
1745
			captiveportal_syslog("Successfully reinitialized tables for {$cpzone} -- database has been reset.");
1746
			if (!is_numericint($cpzoneid)) {
1747
				if (is_array($config['captiveportal'])) {
1748
					foreach ($config['captiveportal'] as $cpkey => $cp) {
1749
						if ($cpzone == $cpkey) {
1750
							$cpzoneid = $cp['zoneid'];
1751
						}
1752
					}
1753
				}
1754
			}
1755
			if (is_numericint($cpzoneid)) {
1756
				$table_names = captiveportal_get_ipfw_table_names();
1757
				foreach ($table_names as $table_name) {
1758
					mwexec("/sbin/ipfw table {$table_name} flush");
1759
				}
1760
				captiveportal_syslog("Flushed tables for {$cpzone} after database reset.");
1761
			}
1762
		} else {
1763
			captiveportal_syslog("Still unable to create tables for {$cpzone}. Error message: {$DB->lastErrorMsg()}. Remove the database file manually and try again.");
1764
		}
1765
	}
1766

    
1767
	return $DB;
1768
}
1769

    
1770
/* Get all tables for specific cpzone */
1771
function captiveportal_get_ipfw_table_names() {
1772
	global $cpzone;
1773

    
1774
	$result = array();
1775
	$tables = pfSense_ipfw_tables_list();
1776

    
1777
	if (!is_array($tables)) {
1778
		return $result;
1779
	}
1780

    
1781
	$len = strlen($cpzone) + 1;
1782
	foreach ($tables as $table) {
1783
		if (substr($table['name'], 0, $len) != $cpzone . '_') {
1784
			continue;
1785
		}
1786

    
1787
		$result[] = $table['name'];
1788
	}
1789

    
1790
	return $result;
1791
}
1792

    
1793
/* read captive portal DB into array */
1794
function captiveportal_read_db($query = "") {
1795
	$cpdb = array();
1796

    
1797
	$DB = captiveportal_opendb();
1798
	if ($DB) {
1799
		$response = $DB->query("SELECT * FROM captiveportal {$query}");
1800
		if ($response != FALSE) {
1801
			while ($row = $response->fetchArray()) {
1802
				$cpdb[] = $row;
1803
			}
1804
		}
1805
		$DB->close();
1806
	}
1807

    
1808
	return $cpdb;
1809
}
1810

    
1811
function captiveportal_remove_entries($remove) {
1812

    
1813
	if (!is_array($remove) || empty($remove)) {
1814
		return;
1815
	}
1816

    
1817
	$query = "DELETE FROM captiveportal WHERE sessionid in (";
1818
	foreach ($remove as $idx => $unindex) {
1819
		$query .= "'{$unindex}'";
1820
		if ($idx < (count($remove) - 1)) {
1821
			$query .= ",";
1822
		}
1823
	}
1824
	$query .= ")";
1825
	captiveportal_write_db($query);
1826
}
1827

    
1828
/* write captive portal DB */
1829
function captiveportal_write_db($queries) {
1830
	global $g;
1831

    
1832
	if (is_array($queries)) {
1833
		$query = implode(";", $queries);
1834
	} else {
1835
		$query = $queries;
1836
	}
1837

    
1838
	$DB = captiveportal_opendb();
1839
	if ($DB) {
1840
		$DB->exec("BEGIN TRANSACTION");
1841
		$result = $DB->exec($query);
1842
		if (!$result) {
1843
			captiveportal_syslog("Trying to modify DB returned error: {$DB->lastErrorMsg()}");
1844
		} else {
1845
			$DB->exec("END TRANSACTION");
1846
		}
1847
		$DB->close();
1848
		return $result;
1849
	} else {
1850
		return true;
1851
	}
1852
}
1853

    
1854
function captiveportal_write_elements() {
1855
	global $g, $config, $cpzone;
1856

    
1857
	$cpcfg = $config['captiveportal'][$cpzone];
1858

    
1859
	if (!is_dir($g['captiveportal_element_path'])) {
1860
		@mkdir($g['captiveportal_element_path']);
1861
	}
1862

    
1863
	if (is_array($cpcfg['element'])) {
1864
		foreach ($cpcfg['element'] as $data) {
1865
			/* Do not attempt to decode or write out empty files. */
1866
			if (isset($data['nocontent'])) {
1867
					continue;
1868
			}
1869
			if (empty($data['content']) || empty(base64_decode($data['content']))) {
1870
				unlink_if_exists("{$g['captiveportal_element_path']}/{$data['name']}");
1871
				touch("{$g['captiveportal_element_path']}/{$data['name']}");
1872
			} elseif (!@file_put_contents("{$g['captiveportal_element_path']}/{$data['name']}", base64_decode($data['content']))) {
1873
				printf(gettext('Error: cannot open \'%1$s\' in captiveportal_write_elements()%2$s'), $data['name'], "\n");
1874
				return 1;
1875
			}
1876
			if (!file_exists("{$g['captiveportal_path']}/{$data['name']}")) {
1877
				@symlink("{$g['captiveportal_element_path']}/{$data['name']}", "{$g['captiveportal_path']}/{$data['name']}");
1878
			}
1879
		}
1880
	}
1881

    
1882
	return 0;
1883
}
1884

    
1885
function captiveportal_free_dnrules($rulenos_start = 2000,
1886
    $rulenos_range_max = 64500, $dry_run = false) {
1887
	global $g, $cpzone;
1888

    
1889
	$removed_pipes = array();
1890

    
1891
	if (!file_exists("{$g['vardb_path']}/captiveportaldn.rules")) {
1892
		return $removed_pipes;
1893
	}
1894

    
1895
	if (!$dry_run) {
1896
		$cpruleslck = lock("captiveportalrulesdn", LOCK_EX);
1897
	}
1898

    
1899
	$rules = unserialize(file_get_contents(
1900
	    "{$g['vardb_path']}/captiveportaldn.rules"));
1901
	$ridx = $rulenos_start;
1902
	while ($ridx < $rulenos_range_max) {
1903
		if ($rules[$ridx] == $cpzone) {
1904
			if (!$dry_run) {
1905
				$rules[$ridx] = false;
1906
			}
1907
			$removed_pipes[] = $ridx;
1908
			$ridx++;
1909
			if (!$dry_run) {
1910
				$rules[$ridx] = false;
1911
			}
1912
			$removed_pipes[] = $ridx;
1913
			$ridx++;
1914
		} else {
1915
			$ridx += 2;
1916
		}
1917
	}
1918

    
1919
	if (!$dry_run) {
1920
		file_put_contents("{$g['vardb_path']}/captiveportaldn.rules",
1921
		    serialize($rules));
1922
		unlock($cpruleslck);
1923
	}
1924

    
1925
	unset($rules);
1926

    
1927
	return $removed_pipes;
1928
}
1929

    
1930
function captiveportal_get_next_dn_ruleno($rulenos_start = 2000, $rulenos_range_max = 64500) {
1931
	global $config, $g, $cpzone;
1932

    
1933
	$cpruleslck = lock("captiveportalrulesdn", LOCK_EX);
1934
	$ruleno = 0;
1935
	if (file_exists("{$g['vardb_path']}/captiveportaldn.rules")) {
1936
		$rules = unserialize(file_get_contents("{$g['vardb_path']}/captiveportaldn.rules"));
1937
		$ridx = $rulenos_start;
1938
		while ($ridx < $rulenos_range_max) {
1939
			if (empty($rules[$ridx])) {
1940
				$ruleno = $ridx;
1941
				$rules[$ridx] = $cpzone;
1942
				$ridx++;
1943
				$rules[$ridx] = $cpzone;
1944
				break;
1945
			} else {
1946
				$ridx += 2;
1947
			}
1948
		}
1949
	} else {
1950
		$rules = array_pad(array(), $rulenos_range_max, false);
1951
		$ruleno = $rulenos_start;
1952
		$rules[$rulenos_start] = $cpzone;
1953
		$rulenos_start++;
1954
		$rules[$rulenos_start] = $cpzone;
1955
	}
1956
	file_put_contents("{$g['vardb_path']}/captiveportaldn.rules", serialize($rules));
1957
	unlock($cpruleslck);
1958
	unset($rules);
1959

    
1960
	return $ruleno;
1961
}
1962

    
1963
function captiveportal_free_dn_ruleno($ruleno) {
1964
	global $config, $g;
1965

    
1966
	$cpruleslck = lock("captiveportalrulesdn", LOCK_EX);
1967
	if (file_exists("{$g['vardb_path']}/captiveportaldn.rules")) {
1968
		$rules = unserialize(file_get_contents("{$g['vardb_path']}/captiveportaldn.rules"));
1969
		$rules[$ruleno] = false;
1970
		$ruleno++;
1971
		$rules[$ruleno] = false;
1972
		file_put_contents("{$g['vardb_path']}/captiveportaldn.rules", serialize($rules));
1973
		unset($rules);
1974
	}
1975
	unlock($cpruleslck);
1976
}
1977

    
1978
function captiveportal_get_dn_passthru_ruleno($value) {
1979
	global $config, $g, $cpzone, $cpzoneid;
1980

    
1981
	$cpcfg = $config['captiveportal'][$cpzone];
1982
	if (!isset($cpcfg['enable'])) {
1983
		return NULL;
1984
	}
1985

    
1986
	$cpruleslck = lock("captiveportalrulesdn", LOCK_EX);
1987
	$ruleno = NULL;
1988
	if (file_exists("{$g['vardb_path']}/captiveportaldn.rules")) {
1989
		unset($output);
1990
		$item = pfSense_ipfw_table_lookup("{$cpzone}_pipe_mac",
1991
		    "any,{$value}");
1992
		if (!is_array($item) || empty($item['pipe'])) {
1993
			unlock($cpruleslck);
1994
			return NULL;
1995
		}
1996

    
1997
		$ruleno = intval($item['pipe']);
1998
		$rules = unserialize(file_get_contents("{$g['vardb_path']}/captiveportaldn.rules"));
1999
		if (!$rules[$ruleno]) {
2000
			$ruleno = NULL;
2001
		}
2002
		unset($rules);
2003
	}
2004
	unlock($cpruleslck);
2005

    
2006
	return $ruleno;
2007
}
2008

    
2009
/**
2010
 * This function will calculate the traffic produced by a client
2011
 * based on its firewall rule
2012
 *
2013
 * Point of view: NAS
2014
 *
2015
 * Input means: from the client
2016
 * Output means: to the client
2017
 *
2018
 */
2019

    
2020
function getVolume($ip) {
2021
	global $config, $cpzone;
2022

    
2023
	$reverse = isset($config['captiveportal'][$cpzone]['reverseacct'])
2024
	    ? true : false;
2025
	$volume = array();
2026
	// Initialize vars properly, since we don't want NULL vars
2027
	$volume['input_pkts'] = $volume['input_bytes'] = 0;
2028
	$volume['output_pkts'] = $volume['output_bytes'] = 0;
2029

    
2030
	$tables = array("allowed", "auth");
2031

    
2032
	foreach($tables as $table) {
2033
		$ipfw = pfSense_ipfw_table_lookup("{$cpzone}_{$table}_up", $ip);
2034
		if (!is_array($ipfw)) {
2035
			continue;
2036
		}
2037
		if ($reverse) {
2038
			$volume['output_pkts'] = $ipfw['packets'];
2039
			$volume['output_bytes'] = $ipfw['bytes'];
2040
		} else {
2041
			$volume['input_pkts'] = $ipfw['packets'];
2042
			$volume['input_bytes'] = $ipfw['bytes'];
2043
		}
2044
	}
2045

    
2046
	foreach($tables as $table) {
2047
		$ipfw = pfSense_ipfw_table_lookup("{$cpzone}_{$table}_down",
2048
		    $ip);
2049
		if (!is_array($ipfw)) {
2050
			continue;
2051
		}
2052
		if ($reverse) {
2053
			$volume['input_pkts'] = $ipfw['packets'];
2054
			$volume['input_bytes'] = $ipfw['bytes'];
2055
		} else {
2056
			$volume['output_pkts'] = $ipfw['packets'];
2057
			$volume['output_bytes'] = $ipfw['bytes'];
2058
		}
2059
	}
2060

    
2061
	return $volume;
2062
}
2063

    
2064
function portal_ip_from_client_ip($cliip) {
2065
	global $config, $cpzone;
2066

    
2067
	$isipv6 = is_ipaddrv6($cliip);
2068
	$interfaces = explode(",", $config['captiveportal'][$cpzone]['interface']);
2069
	foreach ($interfaces as $cpif) {
2070
		if ($isipv6) {
2071
			$ip = get_interface_ipv6($cpif);
2072
			$sn = get_interface_subnetv6($cpif);
2073
		} else {
2074
			$ip = get_interface_ip($cpif);
2075
			$sn = get_interface_subnet($cpif);
2076
		}
2077
		if (ip_in_subnet($cliip, "{$ip}/{$sn}")) {
2078
			return $ip;
2079
		}
2080
	}
2081

    
2082
	$inet = ($isipv6) ? '-inet6' : '-inet';
2083
	$iface = exec_command("/sbin/route -n get {$inet} {$cliip} | /usr/bin/awk '/interface/ { print \$2; };'");
2084
	$iface = trim($iface, "\n");
2085
	if (!empty($iface)) {
2086
		$ip = ($isipv6) ? find_interface_ipv6($iface) : find_interface_ip($iface);
2087
		if (is_ipaddr($ip)) {
2088
			return $ip;
2089
		}
2090
	}
2091

    
2092
	// doesn't match up to any particular interface
2093
	// so let's set the portal IP to what PHP says
2094
	// the server IP issuing the request is.
2095
	// allows same behavior as 1.2.x where IP isn't
2096
	// in the subnet of any CP interface (static routes, etc.)
2097
	// rather than forcing to DNS hostname resolution
2098
	$ip = $_SERVER['SERVER_ADDR'];
2099
	if (is_ipaddr($ip)) {
2100
		return $ip;
2101
	}
2102

    
2103
	return false;
2104
}
2105

    
2106
function portal_hostname_from_client_ip($cliip) {
2107
	global $config, $cpzone;
2108

    
2109
	$cpcfg = $config['captiveportal'][$cpzone];
2110

    
2111
	if (isset($cpcfg['httpslogin'])) {
2112
		$listenporthttps = $cpcfg['listenporthttps'] ? $cpcfg['listenporthttps'] : ($cpcfg['zoneid'] + 8001);
2113
		$ourhostname = $cpcfg['httpsname'];
2114

    
2115
		if ($listenporthttps != 443) {
2116
			$ourhostname .= ":" . $listenporthttps;
2117
		}
2118
	} else {
2119
		$listenporthttp = $cpcfg['listenporthttp'] ? $cpcfg['listenporthttp'] : ($cpcfg['zoneid'] + 8000);
2120
		$ifip = portal_ip_from_client_ip($cliip);
2121
		if (!$ifip) {
2122
			$ourhostname = "{$config['system']['hostname']}.{$config['system']['domain']}";
2123
		} else {
2124
			$ourhostname = (is_ipaddrv6($ifip)) ? "[{$ifip}]" : "{$ifip}";
2125
		}
2126

    
2127
		if ($listenporthttp != 80) {
2128
			$ourhostname .= ":" . $listenporthttp;
2129
		}
2130
	}
2131

    
2132
	return $ourhostname;
2133
}
2134

    
2135
/* functions move from index.php */
2136

    
2137
function portal_reply_page($redirurl, $type = null, $message = null, $clientmac = null, $clientip = null, $username = null, $password = null) {
2138
	global $g, $config, $cpzone;
2139

    
2140
	/* Get captive portal layout */
2141
	if ($type == "redir") {
2142
		header("Location: {$redirurl}");
2143
		return;
2144
	} else if ($type == "login") {
2145
		$htmltext = get_include_contents("{$g['varetc_path']}/captiveportal_{$cpzone}.html");
2146
	} else {
2147
		$htmltext = get_include_contents("{$g['varetc_path']}/captiveportal-{$cpzone}-error.html");
2148
	}
2149

    
2150
	$cpcfg = $config['captiveportal'][$cpzone];
2151

    
2152
	/* substitute the PORTAL_REDIRURL variable */
2153
	if ($cpcfg['preauthurl']) {
2154
		$htmltext = str_replace("\$PORTAL_REDIRURL\$", "{$cpcfg['preauthurl']}", $htmltext);
2155
		$htmltext = str_replace("#PORTAL_REDIRURL#", "{$cpcfg['preauthurl']}", $htmltext);
2156
	}
2157

    
2158
	/* substitute other variables */
2159
	$ourhostname = portal_hostname_from_client_ip($clientip);
2160
	$protocol = (isset($cpcfg['httpslogin'])) ? 'https://' : 'http://';
2161
	$htmltext = str_replace("\$PORTAL_ACTION\$", "{$protocol}{$ourhostname}/index.php?zone={$cpzone}", $htmltext);
2162
	$htmltext = str_replace("#PORTAL_ACTION#", "{$protocol}{$ourhostname}/index.php?zone={$cpzone}", $htmltext);
2163

    
2164
	$htmltext = str_replace("\$PORTAL_ZONE\$", htmlspecialchars($cpzone), $htmltext);
2165
	$htmltext = str_replace("\$PORTAL_REDIRURL\$", htmlspecialchars($redirurl), $htmltext);
2166
	$htmltext = str_replace("\$PORTAL_MESSAGE\$", htmlspecialchars($message), $htmltext);
2167
	$htmltext = str_replace("\$CLIENT_MAC\$", htmlspecialchars($clientmac), $htmltext);
2168
	$htmltext = str_replace("\$CLIENT_IP\$", htmlspecialchars($clientip), $htmltext);
2169

    
2170
	// Special handling case for captive portal master page so that it can be ran
2171
	// through the PHP interpreter using the include method above.  We convert the
2172
	// $VARIABLE$ case to #VARIABLE# in /etc/inc/captiveportal.inc before writing out.
2173
	$htmltext = str_replace("#PORTAL_ZONE#", htmlspecialchars($cpzone), $htmltext);
2174
	$htmltext = str_replace("#PORTAL_REDIRURL#", htmlspecialchars($redirurl), $htmltext);
2175
	$htmltext = str_replace("#PORTAL_MESSAGE#", htmlspecialchars($message), $htmltext);
2176
	$htmltext = str_replace("#CLIENT_MAC#", htmlspecialchars($clientmac), $htmltext);
2177
	$htmltext = str_replace("#CLIENT_IP#", htmlspecialchars($clientip), $htmltext);
2178
	$htmltext = str_replace("#USERNAME#", htmlspecialchars($username), $htmltext);
2179
	$htmltext = str_replace("#PASSWORD#", htmlspecialchars($password), $htmltext);
2180

    
2181
	echo $htmltext;
2182
}
2183

    
2184
function captiveportal_reapply_attributes($cpentry, $attributes) {
2185
	global $config, $cpzone, $g;
2186

    
2187
	if (isset($config['captiveportal'][$cpzone]['peruserbw'])) {
2188
		$dwfaultbw_up = !empty($config['captiveportal'][$cpzone]['bwdefaultup']) ? $config['captiveportal'][$cpzone]['bwdefaultup'] : 0;
2189
		$dwfaultbw_down = !empty($config['captiveportal'][$cpzone]['bwdefaultdn']) ? $config['captiveportal'][$cpzone]['bwdefaultdn'] : 0;
2190
	} else {
2191
		$dwfaultbw_up = $dwfaultbw_down = 0;
2192
	}
2193
	/* pipe throughputs must always be an integer, enforce that restriction again here. */
2194
	if (isset($config['captiveportal'][$cpzone]['radiusperuserbw'])) {
2195
		$bw_up = round(!empty($attributes['bw_up']) ? intval($attributes['bw_up'])/1000 : $dwfaultbw_up, 0);
2196
		$bw_down = round(!empty($attributes['bw_down']) ? intval($attributes['bw_down'])/1000 : $dwfaultbw_down, 0);
2197
	} else {
2198
		$bw_up = round($dwfaultbw_up,0);
2199
		$bw_down = round($dwfaultbw_down,0);
2200
	}
2201

    
2202
	$bw_up_pipeno = $cpentry[1];
2203
	$bw_down_pipeno = $cpentry[1]+1;
2204

    
2205
	$_gb = @pfSense_ipfw_pipe("pipe {$bw_up_pipeno} config bw {$bw_up}Kbit/s queue 100 buckets 16");
2206
	$_gb = @pfSense_ipfw_pipe("pipe {$bw_down_pipeno} config bw {$bw_down}Kbit/s queue 100 buckets 16");
2207
	//captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "RADIUS_BANDWIDTH_REAPPLY", "{$bw_up}/{$bw_down}");
2208

    
2209
	unset($bw_up_pipeno, $bw_down_pipeno, $bw_up, $bw_down);
2210
}
2211

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

    
2215
	// Ensure we create an array if we are missing attributes
2216
	if (!is_array($attributes)) {
2217
		$attributes = array();
2218
	}
2219

    
2220
	unset($sessionid);
2221

    
2222
	/* Do not allow concurrent login execution. */
2223
	$cpdblck = lock("captiveportaldb{$cpzone}", LOCK_EX);
2224

    
2225
	if ($attributes['voucher']) {
2226
		$remaining_time = $attributes['session_timeout'];
2227
		$authmethod = "voucher"; // Set RADIUS-Attribute to Voucher to prevent ReAuth-Reqeuest for Vouchers Bug: #2155
2228
		$context = "voucher";
2229
	}
2230

    
2231
	$writecfg = false;
2232
	/* If both "Add MAC addresses of connected users as pass-through MAC" and "Disable concurrent logins" are checked, 
2233
	then we need to check if the user was already authenticated using another MAC Address, and if so remove the previous Pass-Through MAC. */	
2234
	if ((isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) &&
2235
	    $passthrumac &&
2236
	    isset($config['captiveportal'][$cpzone]['passthrumacadd'])) {
2237
		$mac = captiveportal_passthrumac_findbyname($username);
2238
		if (!empty($mac)) {
2239
			foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $idx => $macent) {
2240
				if ($macent['mac'] != $mac['mac']) {
2241
					continue;
2242
				}
2243

    
2244
				$pipeno = captiveportal_get_dn_passthru_ruleno($mac['mac']);
2245
				if ($pipeno) {
2246
					captiveportal_free_dn_ruleno($pipeno);
2247
					@pfSense_ipfw_table("{$cpzone}_pipe_mac", IP_FW_TABLE_XDEL, "any,{$mac['mac']}");
2248
					@pfSense_ipfw_table("{$cpzone}_pipe_mac", IP_FW_TABLE_XDEL, "{$mac['mac']},any");
2249
					@pfSense_ipfw_pipe("pipe delete " . $pipeno+1);
2250
					@pfSense_ipfw_pipe("pipe delete " . $pipeno);
2251
				}
2252
				unset($config['captiveportal'][$cpzone]['passthrumac'][$idx]);
2253
			}
2254
		}
2255
	}
2256

    
2257
	/* read in client database */
2258
	$query = "WHERE ip = '{$clientip}'";
2259
	$tmpusername = SQLite3::escapeString(strtolower($username));
2260
	if (isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) {
2261
		$query .= " OR (username != 'unauthenticated' AND lower(username) = '{$tmpusername}')";
2262
	}
2263
	$cpdb = captiveportal_read_db($query);
2264

    
2265
	/* Snapshot the timestamp */
2266
	$allow_time = time();
2267
	$unsetindexes = array();
2268

    
2269
	foreach ($cpdb as $cpentry) {
2270
		/* on the same ip */
2271
		if ($cpentry[2] == $clientip) {
2272
			if (isset($config['captiveportal'][$cpzone]['nomacfilter']) || $cpentry[3] == $clientmac) {
2273
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT LOGIN - REUSING OLD SESSION");
2274
			} else {
2275
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT LOGIN - REUSING IP {$cpentry[2]} WITH DIFFERENT MAC ADDRESS {$cpentry[3]}");
2276
			}
2277
			$sessionid = $cpentry[5];
2278
			break;
2279
		} elseif (($attributes['voucher']) && ($username != 'unauthenticated') && ($cpentry[4] == $username)) {
2280
			// user logged in with an active voucher. Check for how long and calculate
2281
			// how much time we can give him (voucher credit - used time)
2282
			$remaining_time = $cpentry[0] + $cpentry[7] - $allow_time;
2283
			if ($remaining_time < 0) { // just in case.
2284
				$remaining_time = 0;
2285
			}
2286

    
2287
			/* This user was already logged in so we disconnect the old one */
2288
			captiveportal_disconnect($cpentry, 13);
2289
			captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT LOGIN - TERMINATING OLD SESSION");
2290
			$unsetindexes[] = $cpentry[5];
2291
			break;
2292
		} elseif ((isset($config['captiveportal'][$cpzone]['noconcurrentlogins'])) && ($username != 'unauthenticated')) {
2293
			/* on the same username */
2294
			if (strcasecmp($cpentry[4], $username) == 0) {
2295
				/* This user was already logged in so we disconnect the old one */
2296
				captiveportal_disconnect($cpentry, 13);
2297
				captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "CONCURRENT LOGIN - TERMINATING OLD SESSION");
2298
				$unsetindexes[] = $cpentry[5];
2299
				break;
2300
			}
2301
		}
2302
	}
2303
	unset($cpdb);
2304

    
2305
	if (!empty($unsetindexes)) {
2306
		captiveportal_remove_entries($unsetindexes);
2307
	}
2308

    
2309
	if ($attributes['voucher'] && $remaining_time <= 0) {
2310
		return 0;       // voucher already used and no time left
2311
	}
2312

    
2313
	if (!isset($sessionid)) {
2314
		/* generate unique session ID */
2315
		$tod = gettimeofday();
2316
		$sessionid = substr(md5(mt_rand() . $tod['sec'] . $tod['usec'] . $clientip . $clientmac), 0, 16);
2317

    
2318
		if (isset($config['captiveportal'][$cpzone]['peruserbw'])) {
2319
			$dwfaultbw_up = !empty($config['captiveportal'][$cpzone]['bwdefaultup']) ? $config['captiveportal'][$cpzone]['bwdefaultup'] : 0;
2320
			$dwfaultbw_down = !empty($config['captiveportal'][$cpzone]['bwdefaultdn']) ? $config['captiveportal'][$cpzone]['bwdefaultdn'] : 0;
2321
		} else {
2322
			$dwfaultbw_up = $dwfaultbw_down = 0;
2323
		}
2324
		/* pipe throughputs must always be an integer, enforce that restriction again here. */
2325
		if (isset($config['captiveportal'][$cpzone]['radiusperuserbw'])) {
2326
			$bw_up = round(!empty($attributes['bw_up']) ? intval($attributes['bw_up'])/1000 : $dwfaultbw_up, 0);
2327
			$bw_down = round(!empty($attributes['bw_down']) ? intval($attributes['bw_down'])/1000 : $dwfaultbw_down, 0);
2328
		} else {
2329
			$bw_up = round($dwfaultbw_up,0);
2330
			$bw_down = round($dwfaultbw_down,0);
2331
		}
2332

    
2333
		if ($passthrumac) {
2334

    
2335
			$mac = array();
2336
			$mac['action'] = 'pass';
2337
			$mac['mac'] = $clientmac;
2338
			$mac['ip'] = $clientip; /* Used only for logging */
2339
			if (isset($config['captiveportal'][$cpzone]['passthrumacaddusername'])) {
2340
				$mac['username'] = $username;
2341
				if ($attributes['voucher']) {
2342
					$mac['logintype'] = "voucher";
2343
				}
2344
			}
2345
			if ($username == "unauthenticated") {
2346
				$mac['descr'] = "Auto-added";
2347
			} else {
2348
				$mac['descr'] = "Auto-added for user {$username}";
2349
			}
2350
			if (!empty($bw_up)) {
2351
				$mac['bw_up'] = $bw_up;
2352
			}
2353
			if (!empty($bw_down)) {
2354
				$mac['bw_down'] = $bw_down;
2355
			}
2356
			if (!is_array($config['captiveportal'][$cpzone]['passthrumac'])) {
2357
				$config['captiveportal'][$cpzone]['passthrumac'] = array();
2358
			}
2359
			//check for mac duplicates before adding it to config.
2360
			$mac_duplicate = false;
2361
			foreach($config['captiveportal'][$cpzone]['passthrumac'] as $mac_check){
2362
				if($mac_check['mac'] == $mac['mac']){
2363
					$mac_duplicate = true;
2364
				}
2365
			}
2366
			if(!$mac_duplicate){
2367
				$config['captiveportal'][$cpzone]['passthrumac'][] = $mac;
2368
			}
2369
			unlock($cpdblck);
2370
			$macrules = captiveportal_passthrumac_configure_entry($mac);
2371
			file_put_contents("{$g['tmp_path']}/macentry_{$cpzone}.rules.tmp", $macrules);
2372
			mwexec("/sbin/ipfw -q {$g['tmp_path']}/macentry_{$cpzone}.rules.tmp");
2373
			$writecfg = true;
2374
		} else {
2375
			/* See if a pipeno is passed, if not start sessions because this means there isn't one atm */
2376
			if (is_null($pipeno)) {
2377
				$pipeno = captiveportal_get_next_dn_ruleno();
2378
			}
2379

    
2380
			/* if the pool is empty, return appropriate message and exit */
2381
			if (is_null($pipeno)) {
2382
				portal_reply_page($redirurl, "error", "System reached maximum login capacity");
2383
				log_error("Zone: {$cpzone} - WARNING!  Captive portal has reached maximum login capacity");
2384
				unlock($cpdblck);
2385
				return;
2386
			}
2387

    
2388
			$bw_up_pipeno = $pipeno;
2389
			$bw_down_pipeno = $pipeno + 1;
2390
			//$bw_up /= 1000; // Scale to Kbit/s
2391
			$_gb = @pfSense_ipfw_pipe("pipe {$bw_up_pipeno} config bw {$bw_up}Kbit/s queue 100 buckets 16");
2392
			$_gb = @pfSense_ipfw_pipe("pipe {$bw_down_pipeno} config bw {$bw_down}Kbit/s queue 100 buckets 16");
2393

    
2394
			$rule_entry = "{$clientip}/" . (is_ipaddrv6($clientip) ? "128" : "32");
2395
			if (!isset($config['captiveportal'][$cpzone]['nomacfilter'])) {
2396
				$rule_entry .= ",{$clientmac}";
2397
			}
2398
			$_gb = @pfSense_ipfw_table("{$cpzone}_auth_up", IP_FW_TABLE_XADD, "{$rule_entry}", $bw_up_pipeno);
2399
			$_gb = @pfSense_ipfw_table("{$cpzone}_auth_down", IP_FW_TABLE_XADD, "{$rule_entry}", $bw_down_pipeno);
2400

    
2401
			if ($attributes['voucher']) {
2402
				$attributes['session_timeout'] = $remaining_time;
2403
			}
2404

    
2405
			/* handle empty attributes */
2406
			$session_timeout = (!empty($attributes['session_timeout'])) ? $attributes['session_timeout'] : 'NULL';
2407
			$idle_timeout = (!empty($attributes['idle_timeout'])) ? $attributes['idle_timeout'] : 'NULL';
2408
			$session_terminate_time = (!empty($attributes['session_terminate_time'])) ? $attributes['session_terminate_time'] : 'NULL';
2409
			$interim_interval = (!empty($attributes['interim_interval'])) ? $attributes['interim_interval'] : 'NULL';
2410
			$traffic_quota = (!empty($attributes['maxbytes'])) ? $attributes['maxbytes'] : 'NULL';
2411

    
2412
			/* escape username */
2413
			$safe_username = SQLite3::escapeString($username);
2414

    
2415
			/* encode password in Base64 just in case it contains commas */
2416
			$bpassword = (isset($config['captiveportal'][$cpzone]['reauthenticate'])) ? base64_encode($password) : '';
2417
			$insertquery = "INSERT INTO captiveportal (allow_time, pipeno, ip, mac, username, sessionid, bpassword, session_timeout, idle_timeout, session_terminate_time, interim_interval, traffic_quota, authmethod, context) ";
2418
			$insertquery .= "VALUES ({$allow_time}, {$pipeno}, '{$clientip}', '{$clientmac}', '{$safe_username}', '{$sessionid}', '{$bpassword}', ";
2419
			$insertquery .= "{$session_timeout}, {$idle_timeout}, {$session_terminate_time}, {$interim_interval}, {$traffic_quota}, '{$authmethod}', '{$context}')";
2420

    
2421
			/* store information to database */
2422
			captiveportal_write_db($insertquery);
2423
			unlock($cpdblck);
2424
			unset($insertquery, $bpassword);
2425

    
2426
			$radacct = isset($config['captiveportal'][$cpzone]['radacct_enable']) ? true : false;
2427
			if ($authmethod === 'radius' && $radacct) {
2428
				captiveportal_send_server_accounting('start',
2429
					$pipeno, // ruleno
2430
					$username, // username
2431
					$clientip, // clientip
2432
					$clientmac, // clientmac
2433
					$sessionid, // sessionid
2434
					time());  // start time
2435
			}
2436
		}
2437
	} else {
2438
		/* NOTE: #3062-11 If the pipeno has been allocated free it to not DoS the CP */
2439
		if (!is_null($pipeno)) {
2440
			captiveportal_free_dn_ruleno($pipeno);
2441
		}
2442

    
2443
		unlock($cpdblck);
2444
	}
2445

    
2446
	if ($writecfg == true) {
2447
		write_config(gettext("Captive Portal allowed users configuration changed"));
2448
	}
2449

    
2450
	/* redirect user to desired destination */
2451
	if (!empty($attributes['url_redirection'])) {
2452
		$my_redirurl = $attributes['url_redirection'];
2453
	} else if (!empty($redirurl)) {
2454
		$my_redirurl = $redirurl;
2455
	} else if (!empty($config['captiveportal'][$cpzone]['redirurl'])) {
2456
		$my_redirurl = $config['captiveportal'][$cpzone]['redirurl'];
2457
	}
2458

    
2459
	if (isset($config['captiveportal'][$cpzone]['logoutwin_enable']) && !$passthrumac) {
2460
		$ourhostname = portal_hostname_from_client_ip($clientip);
2461
		$protocol = (isset($config['captiveportal'][$cpzone]['httpslogin'])) ? 'https://' : 'http://';
2462
		$logouturl = "{$protocol}{$ourhostname}/";
2463

    
2464
		if (isset($attributes['reply_message'])) {
2465
			$message = $attributes['reply_message'];
2466
		} else {
2467
			$message = 0;
2468
		}
2469

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

    
2472
	} else {
2473
		portal_reply_page($my_redirurl, "redir", "Just redirect the user.");
2474
	}
2475

    
2476
	return $sessionid;
2477
}
2478

    
2479

    
2480
/*
2481
 * Used for when pass-through credits are enabled.
2482
 * Returns true when there was at least one free login to deduct for the MAC.
2483
 * Expired entries are removed as they are seen.
2484
 * Active entries are updated according to the configuration.
2485
 */
2486
function portal_consume_passthrough_credit($clientmac) {
2487
	global $config, $cpzone;
2488

    
2489
	if (!empty($config['captiveportal'][$cpzone]['freelogins_count']) && is_numeric($config['captiveportal'][$cpzone]['freelogins_count'])) {
2490
		$freeloginscount = $config['captiveportal'][$cpzone]['freelogins_count'];
2491
	} else {
2492
		return false;
2493
	}
2494

    
2495
	if (!empty($config['captiveportal'][$cpzone]['freelogins_resettimeout']) && is_numeric($config['captiveportal'][$cpzone]['freelogins_resettimeout'])) {
2496
		$resettimeout = $config['captiveportal'][$cpzone]['freelogins_resettimeout'];
2497
	} else {
2498
		return false;
2499
	}
2500

    
2501
	if ($freeloginscount < 1 || $resettimeout <= 0 || !$clientmac) {
2502
		return false;
2503
	}
2504

    
2505
	$updatetimeouts = isset($config['captiveportal'][$cpzone]['freelogins_updatetimeouts']);
2506

    
2507
	/*
2508
	 * Read database of used MACs.  Lines are a comma-separated list
2509
	 * of the time, MAC, then the count of pass-through credits remaining.
2510
	 */
2511
	$usedmacs = captiveportal_read_usedmacs_db();
2512

    
2513
	$currenttime = time();
2514
	$found = false;
2515
	foreach ($usedmacs as $key => $usedmac) {
2516
		$usedmac = explode(",", $usedmac);
2517

    
2518
		if ($usedmac[1] == $clientmac) {
2519
			if ($usedmac[0] + ($resettimeout * 3600) > $currenttime) {
2520
				if ($usedmac[2] < 1) {
2521
					if ($updatetimeouts) {
2522
						$usedmac[0] = $currenttime;
2523
						unset($usedmacs[$key]);
2524
						$usedmacs[] = implode(",", $usedmac);
2525
						captiveportal_write_usedmacs_db($usedmacs);
2526
					}
2527

    
2528
					return false;
2529
				} else {
2530
					$usedmac[2] -= 1;
2531
					$usedmacs[$key] = implode(",", $usedmac);
2532
				}
2533

    
2534
				$found = true;
2535
			} else {
2536
				unset($usedmacs[$key]);
2537
			}
2538

    
2539
			break;
2540
		} else if ($usedmac[0] + ($resettimeout * 3600) <= $currenttime) {
2541
			unset($usedmacs[$key]);
2542
		}
2543
	}
2544

    
2545
	if (!$found) {
2546
		$usedmac = array($currenttime, $clientmac, $freeloginscount - 1);
2547
		$usedmacs[] = implode(",", $usedmac);
2548
	}
2549

    
2550
	captiveportal_write_usedmacs_db($usedmacs);
2551
	return true;
2552
}
2553

    
2554
function captiveportal_read_usedmacs_db() {
2555
	global $g, $cpzone;
2556

    
2557
	$cpumaclck = lock("captiveusedmacs{$cpzone}");
2558
	if (file_exists("{$g['vardb_path']}/captiveportal_usedmacs_{$cpzone}.db")) {
2559
		$usedmacs = file("{$g['vardb_path']}/captiveportal_usedmacs_{$cpzone}.db", FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
2560
		if (!$usedmacs) {
2561
			$usedmacs = array();
2562
		}
2563
	} else {
2564
		$usedmacs = array();
2565
	}
2566

    
2567
	unlock($cpumaclck);
2568
	return $usedmacs;
2569
}
2570

    
2571
function captiveportal_write_usedmacs_db($usedmacs) {
2572
	global $g, $cpzone;
2573

    
2574
	$cpumaclck = lock("captiveusedmacs{$cpzone}", LOCK_EX);
2575
	@file_put_contents("{$g['vardb_path']}/captiveportal_usedmacs_{$cpzone}.db", implode("\n", $usedmacs));
2576
	unlock($cpumaclck);
2577
}
2578

    
2579
function captiveportal_blocked_mac($mac) {
2580
	global $config, $g, $cpzone;
2581

    
2582
	if (empty($mac) || !is_macaddr($mac)) {
2583
		return false;
2584
	}
2585

    
2586
	if (!is_array($config['captiveportal'][$cpzone]['passthrumac'])) {
2587
		return false;
2588
	}
2589

    
2590
	foreach ($config['captiveportal'][$cpzone]['passthrumac'] as $passthrumac) {
2591
		if (($passthrumac['action'] == 'block') &&
2592
		    ($passthrumac['mac'] == strtolower($mac))) {
2593
			return true;
2594
		}
2595
	}
2596

    
2597
	return false;
2598

    
2599
}
2600

    
2601
/* Captiveportal Radius Accounting */
2602

    
2603
function gigawords($bytes) {
2604

    
2605
	/*
2606
	 * RFC2866 Specifies a 32bit unsigned integer, which is a max of 4294967295
2607
	 * Currently there is a fault in the PECL radius_put_int function which can handle only 32bit signed integer.
2608
	 */
2609

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

    
2613
	// We need to manually set this to a zero instead of NULL for put_int() safety
2614
	if (is_null($gigawords)) {
2615
		$gigawords = 0;
2616
	}
2617

    
2618
	return $gigawords;
2619
}
2620

    
2621
function remainder($bytes) {
2622
	// Calculate the bytes we are going to send to the radius
2623
	$bytes = bcmod($bytes, GIGAWORDS_RIGHT_OPERAND);
2624

    
2625
	if (is_null($bytes)) {
2626
		$bytes = 0;
2627
	}
2628

    
2629
    return $bytes;
2630
}
2631

    
2632
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) {
2633
	global $cpzone, $config;
2634

    
2635
	$cpcfg = $config['captiveportal'][$cpzone];
2636
	$acctcfg = auth_get_authserver($cpcfg['radacct_server']);
2637

    
2638
	if (!isset($cpcfg['radacct_enable']) || empty($acctcfg)) {
2639
		return null;
2640
	}
2641

    
2642
	if ($type === 'on') {
2643
		$racct = new Auth_RADIUS_Acct_On;
2644
	} elseif ($type === 'off') {
2645
		$racct = new Auth_RADIUS_Acct_Off;
2646
	} elseif ($type === 'start') {
2647
		$racct = new Auth_RADIUS_Acct_Start;
2648
		if (!is_int($start_time)) {
2649
			$start_time = time();
2650
		}
2651
	} elseif ($type === 'stop') {
2652
		$racct = new Auth_RADIUS_Acct_Stop;
2653
		if (!is_int($stop_time)) {
2654
			$stop_time = time();
2655
		}
2656
	} elseif ($type === 'update') {
2657
        $racct = new Auth_RADIUS_Acct_Update;
2658
		if (!is_int($stop_time)) {
2659
			$stop_time = time(); // "top time" here will be used only for calcuating session time.
2660
		}
2661
	} else {
2662
		return null;
2663
	}
2664

    
2665
	$racct->addServer($acctcfg['host'], $acctcfg['radius_acct_port'],
2666
		$acctcfg['radius_secret'], $acctcfg['radius_timeout']);
2667

    
2668
	$racct->authentic = RADIUS_AUTH_RADIUS;
2669
	if (!empty($username)) {
2670
		$racct->username = $username;
2671
	}
2672

    
2673
	if (PEAR::isError($racct->start())) {
2674
		captiveportal_syslog('RADIUS ACCOUNTING FAILED : '.$racct->getError());
2675
		$racct->close();
2676
		return null;
2677
	}
2678

    
2679
	$nasip = $acctcfg['radius_nasip_attribute'];
2680
	if (!is_ipaddr($nasip)) {
2681
		$nasip = get_interface_ip($nasip);
2682
		if (!is_ipaddr($nasip)) {
2683
			$nasip = get_interface_ip();//We use WAN interface IP as fallback for NAS-IP-Address
2684
		}
2685
	}
2686
	$nasmac = get_interface_mac(find_ip_interface($nasip));
2687
	$racct->putAttribute(RADIUS_NAS_IP_ADDRESS, $nasip, "addr");
2688
	$racct->putAttribute(RADIUS_NAS_IDENTIFIER, 'CaptivePortal');
2689

    
2690
	if (is_int($ruleno)) {
2691
		$racct->putAttribute(RADIUS_NAS_PORT_TYPE, RADIUS_ETHERNET);
2692
		$racct->putAttribute(RADIUS_NAS_PORT, intval($ruleno), 'integer');
2693
	}
2694

    
2695
	if (!empty($sessionid)) {
2696
		$racct->putAttribute(RADIUS_ACCT_SESSION_ID, $sessionid);
2697
	}
2698

    
2699
	if (!empty($clientip) && is_ipaddr($clientip)) {
2700
		$racct->putAttribute(RADIUS_FRAMED_IP_ADDRESS, $clientip, "addr");
2701
	}
2702
	if (!empty($clientmac)) {
2703
		$racct->putAttribute(RADIUS_CALLING_STATION_ID, mac_format($clientmac));
2704
	}
2705
	if (!empty($nasmac)) {
2706
		$racct->putAttribute(RADIUS_CALLED_STATION_ID, mac_format($nasmac).':'.gethostname());
2707
	}
2708

    
2709
	// Accounting request Stop and Update : send the current data volume
2710
	if (($type === 'stop' || $type === 'update') && is_int($start_time)) {
2711
		$volume = getVolume($clientip);
2712
		$session_time = $stop_time - $start_time;
2713
		$volume['input_bytes_radius'] = remainder($volume['input_bytes']);
2714
		$volume['input_gigawords'] = gigawords($volume['input_bytes']);
2715
		$volume['output_bytes_radius'] = remainder($volume['output_bytes']);
2716
		$volume['output_gigawords'] = gigawords($volume['output_bytes']);
2717

    
2718
		// Volume stuff: Ingress
2719
		$racct->putAttribute(RADIUS_ACCT_INPUT_PACKETS, intval($volume['input_pkts']), "integer");
2720
		$racct->putAttribute(RADIUS_ACCT_INPUT_OCTETS, intval($volume['input_bytes_radius']), "integer");
2721
		// Volume stuff: Outgress
2722
		$racct->putAttribute(RADIUS_ACCT_OUTPUT_PACKETS, intval($volume['output_pkts']), "integer");
2723
		$racct->putAttribute(RADIUS_ACCT_OUTPUT_OCTETS, intval($volume['output_bytes_radius']), "integer");
2724
		$racct->putAttribute(RADIUS_ACCT_SESSION_TIME, intval($session_time), "integer");
2725

    
2726
		$racct->putAttribute(CUSTOM_RADIUS_ACCT_OUTPUT_GIGAWORDS, intval($volume['output_gigawords']), "integer");
2727
		$racct->putAttribute(CUSTOM_RADIUS_ACCT_INPUT_GIGAWORDS, intval($volume['input_gigawords']), "integer");
2728
		// Set session_time
2729
		$racct->session_time = $session_time;
2730
	}
2731

    
2732
	if ($type === 'stop') {
2733
		if (empty($term_cause)) {
2734
			$term_cause = 1;
2735
		}
2736
		$racct->putAttribute(RADIUS_ACCT_TERMINATE_CAUSE, $term_cause);
2737
	}
2738

    
2739
	// Send request
2740
	$result = $racct->send();
2741

    
2742
	if (PEAR::isError($result)) {
2743
		 captiveportal_syslog('RADIUS ACCOUNTING FAILED : '.$racct->getError());
2744
		 $result = null;
2745
	} elseif ($result !== true) {
2746
		$result = false;
2747
	}
2748

    
2749
	$racct->close();
2750
	return $result;
2751
}
2752

    
2753
function captiveportal_isip_logged($clientip) {
2754
	global $g, $cpzone;
2755

    
2756
	/* read in client database */
2757
	$query = "WHERE ip = '{$clientip}'";
2758
	$cpdb = captiveportal_read_db($query);
2759
	foreach ($cpdb as $cpentry) {
2760
		return $cpentry;
2761
	}
2762
}
2763
?>
(6-6/60)