Project

General

Profile

Feature #15904 » index-2411-Dec17.php

Dale Harron, 12/17/2024 11:10 AM

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

    
28
require_once("auth.inc");
29
require_once("util.inc");
30
require_once("functions.inc");
31
require_once("captiveportal.inc");
32

    
33
header("Expires: 0");
34
header("Cache-Control: no-cache, no-store, must-revalidate");
35
header("Connection: close");
36

    
37
global $cpzone, $cpzoneid, $cpzoneprefix;
38

    
39
$cpzone = strtolower($_REQUEST['zone']);
40
$cpcfg = config_get_path("captiveportal/{$cpzone}", []);
41

    
42
/* NOTE: IE 8/9 is buggy and that is why this is needed */
43
$orig_request = trim($_REQUEST['redirurl'], " /");
44

    
45
/* If the post-auth redirect is set, always use it. Otherwise take what was supplied in URL. */
46
if (!empty($cpcfg) && is_URL($cpcfg['redirurl'], true)) {
47
	$redirurl = $cpcfg['redirurl'];
48
} elseif (preg_match("/redirurl=(.*)/", $orig_request, $matches)) {
49
	$redirurl = urldecode($matches[1]);
50
} elseif ($_REQUEST['redirurl']) {
51
	$redirurl = $_REQUEST['redirurl'];
52
}
53
/* Sanity check: If the redirect target is not a URL, do not attempt to use it like one. */
54
if (!is_URL(urldecode($redirurl), true)) {
55
	$redirurl = "";
56
}
57

    
58
if (empty($cpcfg)) {
59
	log_error("Submission to captiveportal with unknown parameter zone: " . htmlspecialchars($cpzone));
60
	portal_reply_page($redirurl, "error", gettext("Internal error"));
61
	ob_flush();
62
	return;
63
}
64

    
65
$cpzoneid = $cpcfg['zoneid'];
66
$cpzoneprefix = CPPREFIX . $cpzoneid;
67
$orig_host = $_SERVER['HTTP_HOST'];
68
$clientip = $_SERVER['REMOTE_ADDR'];
69

    
70
if (!$clientip) {
71
	/* not good - bail out */
72
	log_error("Zone: {$cpzone} - Captive portal could not determine client's IP address.");
73
	$errormsg = gettext("An error occurred.  Please check the system logs for more information.");
74
	portal_reply_page($redirurl, "error", $errormsg);
75
	ob_flush();
76
	return;
77
}
78

    
79
$ourhostname = portal_hostname_from_client_ip($clientip);
80
$protocol = (config_path_enabled("captiveportal/{$cpzone}", "httpslogin")) ? 'https://' : 'http://';
81
$logouturl = "{$protocol}{$ourhostname}/";
82

    
83
$cpsession = captiveportal_isip_logged($clientip);
84
if (!empty($cpsession)) {
85
  $sessionid = $cpsession['sessionid'];
86
}
87

    
88
/* ----------------------CUSTOM CODE -------------------------------------------------------------------------
89
        24.11 Stbl Index.php Custom Code for Kea DHCP, check MAC address, not IP to verify client validation
90
        Kea flushes the IP quickly after the client disconnects, creating IP/MAC conflicts 
91
          within the captive portal database of validated clients.  
92
        It is the MAC that it validated, not the IP as IP can change frequently with Kea Reclamation policies
93
        This code updates the IP in the database to the current one for this MAC and passes the client through
94
          the Portal respecting that the client has a validated MAC.   Dale Harron, 17 December 2024. 
95
------------------------------------------------------------------------------------------------------------- */
96

    
97
$dbchanged = "no";
98

    
99
$ARPforIP = shell_exec("arp '$clientip'");
100
$clientmac = substr($ARPforIP, 17, 24);
101
if (preg_match('/([a-fA-F0-9]{2}[:|\-]?){6}/', "'{$clientmac}'", $matches)) {
102
    $clientmac = $matches[0];
103
}
104

    
105
/* query the cp database for this client MAC address to see if it is already authorized */
106
$query = "WHERE mac = '{$clientmac}'";
107
$cpdbqry = captiveportal_read_db($query);
108
foreach ($cpdbqry as $cpentrymac) {
109
  $cpsessionmac = $cpentrymac;
110
}
111
$sessionidmac = $cpentrymac[5];
112
$cpdbip = $cpentrymac[2];
113
$cpdbmac = $cpentrymac[3];
114
unset ($cpdbqry);
115

    
116
// If either MACs or IPs don't match, fix the CP DB otherwise do nothing and run original index.php code
117
if ($cpdbip <> $clientip || $cpdbmac <> $clientmac) {
118
  //  captiveportal_logportalauth($clientmac, $cpdbmac, $cpdbip, $clientip, $clientmac . "=" . $cpdbmac . "=" . $clientip . "=" . $cpdbip . "=" . $clientmac);
119

    
120
  if( ($clientip <> $cpdbip) && ($cpdbmac == $clientmac)) {
121
    // MACs match so update the old IP in the CP DB to current IP for the authorized MAC of this device
122
    $updt_field = "ip";
123
    $new_value = "'{$clientip}'";
124
    captiveportal_write_db("UPDATE captiveportal SET {$updt_field} = {$new_value} WHERE sessionid = '{$sessionidmac}'");
125
    $dbchanged = "yes";
126
  }  
127

    
128
  // If this is a new MAC that has an authorized IP but is not yet authorized itself, change the IP in the cp database to OFFLINE.
129
  // This will retain the old MAC's authorization and free up the IP to be assigned to this device's MAC; present the login page.
130

    
131
  /* query the cp database for this client IP address to see if it is already associated with an authorized MAC*/
132
  $query = "WHERE ip = '{$clientip}'";
133
  $cpdbqry = captiveportal_read_db($query);
134
  foreach ($cpdbqry as $cpentryip) {
135
    $cpsessionip = $cpentryip;
136
  }
137
  $sessionidip = $cpentryip[5];
138
  $cpdbip = $cpentryip[2];
139

    
140
  if (!$cpdbmac && $cpdbip == $clientip) {
141
    $updt_field = 'ip';
142
    $new_value_tmp = "OFFLINE";
143
    $new_value = "'{$new_value_tmp}'";
144
    captiveportal_write_db("UPDATE captiveportal SET {$updt_field} = {$new_value} WHERE sessionid = '{$sessionid}'");
145
    $dbchanged = "yes";
146
  }
147
  unset($cpdbqry);
148

    
149
  // If this MAC/IP pair does not match the MAC/IP in the Captive Portal database, then the IP has changed
150
  // update the Captive Portal database to associate the current IP with this already authorized MAC
151
  // Note, this could result in multiple IP/MAC pairs of the same IP in the Captive Portal database. 
152
  // Set all conflicting IPs to OFFLINE to retain authorization in CP DB
153

    
154
  if(($cpidmac <> $clientmac) && ($cpdbip == $clientip)) {  
155
    //Remove IP conflicts assigned to other MACs in the Captive Portal Database
156
    $query = "WHERE ip = '{$clientip}'";
157
    $cpdb_ip = captiveportal_read_db($query);
158
    foreach ($cpdb_ip as $cpentryip) {
159
      $cpsessionip = $cpentryip;
160
      $sessionidip = $cpsessionip['sessionid'];
161
      $updt_field = "ip";
162
      $unassignip = 'OFFLINE';
163
      $new_value = "'{$unassignip}'";
164
      if($clientmac <> $cpentryip[3]) {
165
        captiveportal_write_db("UPDATE captiveportal SET {$updt_field} = {$new_value} WHERE sessionid = '{$sessionidip}'");
166
        $dbchanged = "yes";
167
      //captiveportal_logportalauth($clientmac, $cpdbmac, $clientip, $cpdbip, $clientmac . "=" . $cpdbmac . "=" . $clientip . "=" . $cpdbip . "=" . $clientmac);
168
      }
169
      unset($cpdb_ip);
170
    }
171
  }
172

    
173
  //If the Database has been updated, reload the database for this zone.
174
  if( $dbchanged == "yes" ) {
175
    //Now reload this database with these changes to make them active
176
    if (isset($cpcfg['preservedb']) || $reload_rules ||
177
      captiveportal_xmlrpc_sync_get_details($syncip, $syncport, $syncuser, $syncpass)) {
178
      $connected_users = captiveportal_read_db();
179
      if (!empty($connected_users)) {
180
        //echo "Reconnecting users to captive portal {$cpcfg['zone']}... ";
181
        foreach ($connected_users as $user) {
182
        captiveportal_reserve_ruleno($user['pipeno']);
183
        captiveportal_ether_configure_entry($user, 'auth', true);
184
        }
185
      //echo "done\n";
186
      }
187
      $cpsession = captiveportal_isip_logged($clientip);
188
      if (!empty($cpsession)) {
189
        $sessionid = $cpsession['sessionid'];
190
      }  //ensure the $sessionid matches the now updated IP of this client
191
    } 
192
  } //reindex as CP DB has changed
193
} //if either IPs or MACs are not equal do something
194
//captiveportal_logportalauth($clientmac, $cpdbmac, $clientip, $cpdbip, $clientmac . "=" . $cpdbmac . "=" . $clientip . "=" . $cpdbip . "=" . $clientmac);
195

    
196
/* ----------- End Custom Code  ----------- */
197

    
198
/* Automatically switching to the logout page requires a custom logout page to be present. */
199
if ((!empty($cpsession)) && (! $_POST['logout_id']) && (!empty($cpcfg['page']['logouttext']))) {
200
	/* if client already connected and a custom logout page is set : show logout page */
201
	$attributes = array();
202
	if (!empty($cpsession['session_timeout']))
203
		$attributes['session_timeout'] = $cpsession['session_timeout'];
204
	if (!empty($cpsession['session_terminate_time']))
205
		$attributes['session_terminate_time'] = $cpsession['session_terminate_time'];
206

    
207
	include("{$g['varetc_path']}/captiveportal-{$cpzone}-logout.html");
208
	ob_flush();
209
	return;
210
} elseif (!empty($cpsession) && !isset($_POST['logout_id'])) {
211
	/* If the client tries to access the captive portal page while already connected,
212
		but no custom logout page exists */
213
	$logo_src = "{$protocol}{$ourhostname}/" . get_captive_portal_logo();
214
	$bg_src = get_captive_portal_bg();
215
?>
216
<!DOCTYPE html>
217
<html>
218
<head>
219
  <meta charset="UTF-8">
220
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
221
  <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
222
  <title>Captive Portal</title>
223
  <style>
224
	  #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;}
225
  </style>
226
</head>
227

    
228
<body>
229
<div id="content">
230
	<div class="login-card">
231
		<img src="<?= $logo_src ?>"/><br>
232
		<h1></h1>
233
		<div class="login-help">
234
			<?= gettext("The portal session is connected.") ?>
235
<?php if (!empty($redirurl)):
236
		$redirurl = htmlspecialchars($redirurl); ?>
237
			<br/><br/>
238
			<?= gettext("Proceed to: ") ?>
239
			<a href="<?=$redirurl?>"><?=$redirurl?></a>
240
<?php endif; ?>
241
		</div>
242
<br/>
243
	<form method="POST" action="<?=$logouturl;?>">
244
		<input name="logout_id" type="hidden" value="<?=$sessionid;?>" />
245
		<input name="zone" type="hidden" value="<?=$cpzone;?>" />
246
		<input name="logout" type="submit" value="<?= gettext("Disconnect") ?>" />
247
	</form>
248
	<br  />
249
	<span> <i>Made with &hearts; by</i> <strong>Netgate</strong></span>
250
	</div>
251
</div>
252
</body>
253
</html>
254
<?php
255
	ob_flush();
256
	return;
257
} elseif ($orig_host != $ourhostname) {
258
	/* the client thinks it's connected to the desired web server, but instead
259
	   it's connected to us. Issue a redirect... */
260
	$protocol = (isset($cpcfg['httpslogin'])) ? 'https://' : 'http://';
261
	header("Location: {$protocol}{$ourhostname}/index.php?zone={$cpzone}&redirurl=" . urlencode("http://{$orig_host}/{$orig_request}"));
262

    
263
	ob_flush();
264
	return;
265
}
266

    
267
/* find MAC address for client */
268
$tmpres = pfSense_ip_to_mac($clientip);
269
if (!is_array($tmpres)) {
270
	if (!isset($cpcfg['nomacfilter']) || isset($cpcfg['passthrumacadd'])) {
271
		/* unable to find MAC address - shouldn't happen! - bail out */
272
		captiveportal_logportalauth("unauthenticated", "noclientmac", $clientip, "ERROR");
273
		echo "An error occurred.  Please check the system logs for more information.";
274
		log_error("Zone: {$cpzone} - Captive portal could not determine client's MAC address.  Disable MAC address filtering in captive portal if you do not need this functionality.");
275
		ob_flush();
276
		return;
277
	}
278
} else {
279
	/* always save MAC address in DB to allow macfilter/nomacfilter switching without flushing all clients */
280
	$clientmac = $tmpres['macaddr'];
281
}
282
unset($tmpres);
283

    
284
if ($_POST['logout_id']) {
285
	$safe_logout_id = SQLite3::escapeString($_POST['logout_id']);
286
	captiveportal_disconnect_client($safe_logout_id);
287
	header("Location: index.php?zone=" . $cpzone);
288
	ob_flush();
289
	return;
290
} elseif (($_POST['accept'] || $cpcfg['auth_method'] === 'radmac' || !empty($cpcfg['blockedmacsurl'])) && !isset($cpcfg['nomacfilter']) && $clientmac && captiveportal_blocked_mac($clientmac)) {
291
	captiveportal_logportalauth($clientmac, $clientmac, $clientip, "Blocked MAC address");
292
	if (!empty($cpcfg['blockedmacsurl'])) {
293
		portal_reply_page($cpcfg['blockedmacsurl'], "redir");
294
	} else {
295
		if ($cpcfg['auth_method'] === 'radmac') {
296
			echo gettext("This MAC address has been blocked");
297
		} else {
298
			portal_reply_page($redirurl, "error", "This MAC address has been blocked", $clientmac, $clientip);
299
		}
300
	}
301
} elseif (portal_consume_passthrough_credit($clientmac)) {
302
	/* allow the client through if it had a pass-through credit for its MAC */
303
	captiveportal_logportalauth("unauthenticated", $clientmac, $clientip, "ACCEPT");
304
	portal_allow($clientip, $clientmac, "unauthenticated", null, $redirurl);
305

    
306
} elseif (config_path_enabled("voucher/{$cpzone}") && ($_POST['accept'] && $_POST['auth_voucher']) || $_GET['voucher']) {
307
	if (isset($_POST['auth_voucher'])) {
308
		$voucher = trim($_POST['auth_voucher']);
309
	} else {
310
		/* submit voucher via URL, see https://redmine.pfsense.org/issues/1984 */
311
		$voucher = trim($_GET['voucher']);
312
		portal_reply_page($redirurl, "login", null, $clientmac, $clientip, null, null, $voucher);
313
		return;
314
	}
315
	$errormsg = gettext("Invalid credentials specified.");
316
	$timecredit = voucher_auth($voucher);
317
	// $timecredit contains either a credit in minutes or an error message
318
	if ($timecredit > 0) {  // voucher is valid. Remaining minutes returned
319
		// if multiple vouchers given, use the first as username
320
		$a_vouchers = preg_split("/[\t\n\r ]+/s", $voucher);
321
		$voucher = $a_vouchers[0];
322
		$attr = array(
323
			'voucher' => 1,
324
			'session_timeout' => $timecredit*60,
325
			'session_terminate_time' => 0);
326
		if (portal_allow($clientip, $clientmac, $voucher, null, $redirurl, $attr, null, 'voucher', 'voucher') === 2) {
327
			portal_reply_page($redirurl, "error", "Reuse of identification not allowed.", $clientmac, $clientip);
328
		} elseif (portal_allow($clientip, $clientmac, $voucher, null, $redirurl, $attr, null, 'voucher', 'voucher')) {
329
			// YES: user is good for $timecredit minutes.
330
			captiveportal_logportalauth($voucher, $clientmac, $clientip, "Voucher login good for $timecredit min.");
331
		} else {
332
			portal_reply_page($redirurl, "error", config_get_path("voucher/{$cpzone}/descrmsgexpired") ? config_get_path("voucher/{$cpzone}/descrmsgexpired"): $errormsg, $clientmac, $clientip);
333
		}
334
	} elseif (-1 == $timecredit) {  // valid but expired
335
		captiveportal_logportalauth($voucher, $clientmac, $clientip, "FAILURE", "voucher expired");
336
		portal_reply_page($redirurl, "error", config_get_path("voucher/{$cpzone}/descrmsgexpired") ? config_get_path("voucher/{$cpzone}/descrmsgexpired"): $errormsg, $clientmac, $clientip);
337
	} else {
338
		captiveportal_logportalauth($voucher, $clientmac, $clientip, "FAILURE");
339
		portal_reply_page($redirurl, "error", config_get_path("voucher/{$cpzone}/descrmsgnoaccess") ? config_get_path("voucher/{$cpzone}/descrmsgnoaccess") : $errormsg, $clientmac, $clientip);
340
	}
341

    
342
} elseif ($_POST['accept'] || $cpcfg['auth_method'] === 'radmac') {
343
	
344
		if ($cpcfg['auth_method'] === 'radmac' && !isset($_POST['accept'])) {
345
			$user = $clientmac; 
346
			$passwd = $cpcfg['radmac_secret'];
347
			$context = 'radmac'; // Radius MAC authentication
348
		} elseif (!empty(trim($_POST['auth_user2']))) { 
349
			$user = trim($_POST['auth_user2']);
350
			$passwd = $_POST['auth_pass2'];
351
			$context = 'second'; // Assume users to use the first context if auth_user2 is empty/does not exist
352
		} else {
353
			$user = trim($_POST['auth_user']);
354
			$passwd = $_POST['auth_pass'];
355
			$context = 'first';
356
		}
357
	
358
	$pipeno = captiveportal_get_next_dn_ruleno('auth', 2000, 64500, true);
359
	/* if the pool is empty, return appropriate message and exit */
360
	if (is_null($pipeno)) {
361
		$replymsg = gettext("System reached maximum login capacity");
362
		if ($cpcfg['auth_method'] === 'radmac') {
363
			echo $replymsg;
364
			ob_flush();
365
			return;
366
		} else {
367
			portal_reply_page($redirurl, "error", $replymsg, $clientmac, $clientip);
368
		}
369
		log_error("Zone: {$cpzone} - WARNING!  Captive portal has reached maximum login capacity");
370
		
371
	}
372
	
373
	$auth_result = captiveportal_authenticate_user($user, $passwd, $clientmac, $clientip, $pipeno, $context);
374
	
375
	if ($auth_result['result']) {
376
		captiveportal_logportalauth($user, $clientmac, $clientip, $auth_result['login_status']);
377
		portal_allow($clientip, $clientmac, $user, $passwd, $redirurl, $auth_result['attributes'], null, $auth_result['auth_method'], $context);
378
	} else {
379
		$type = "error";
380
			
381
		if (is_URL($auth_result['attributes']['url_redirection'], true)) {
382
			$redirurl = $auth_result['attributes']['url_redirection'];
383
			$type = "redir";
384
		}
385
		
386
		if ($auth_result['login_message']) {
387
			$replymsg = $auth_result['login_message'];
388
		} else {
389
			$replymsg = gettext("Invalid credentials specified.");
390
		}
391
		
392
		captiveportal_logportalauth($user, $clientmac, $clientip, $auth_result['login_status'], $replymsg);
393

    
394
		/* Radius MAC authentication. */
395
		if ($context === 'radmac' && $type !== 'redir' && !isset($cpcfg['radmac_fallback'])) {
396
			echo $replymsg;
397
		} else {
398
			portal_reply_page($redirurl, $type, $replymsg, $clientmac, $clientip);
399
		}
400
	}
401
} else {
402
	/* display captive portal page */
403
	portal_reply_page($redirurl, "login", null, $clientmac, $clientip);
404
}
405

    
406
ob_flush();
407

    
408
?>
(7-7/7)