<?php
/*
 * index.php
 *
 * part of pfSense (https://www.pfsense.org)
 * Copyright (c) 2004-2013 BSD Perimeter
 * Copyright (c) 2013-2016 Electric Sheep Fencing
 * Copyright (c) 2014-2024 Rubicon Communications, LLC (Netgate)
 * All rights reserved.
 *
 * Originally part of m0n0wall (http://m0n0.ch/wall)
 * Copyright (c) 2003-2006 Manuel Kasper <mk@neon1.net>.
 * All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

require_once("auth.inc");
require_once("util.inc");
require_once("functions.inc");
require_once("captiveportal.inc");

header("Expires: 0");
header("Cache-Control: no-cache, no-store, must-revalidate");
header("Connection: close");

global $cpzone, $cpzoneid, $cpzoneprefix;

$cpzone = strtolower($_REQUEST['zone']);
$cpcfg = config_get_path("captiveportal/{$cpzone}", []);

/* NOTE: IE 8/9 is buggy and that is why this is needed */
$orig_request = trim($_REQUEST['redirurl'], " /");

/* If the post-auth redirect is set, always use it. Otherwise take what was supplied in URL. */
if (!empty($cpcfg) && is_URL($cpcfg['redirurl'], true)) {
	$redirurl = $cpcfg['redirurl'];
} elseif (preg_match("/redirurl=(.*)/", $orig_request, $matches)) {
	$redirurl = urldecode($matches[1]);
} elseif ($_REQUEST['redirurl']) {
	$redirurl = $_REQUEST['redirurl'];
}
/* Sanity check: If the redirect target is not a URL, do not attempt to use it like one. */
if (!is_URL(urldecode($redirurl), true)) {
	$redirurl = "";
}

if (empty($cpcfg)) {
	log_error("Submission to captiveportal with unknown parameter zone: " . htmlspecialchars($cpzone));
	portal_reply_page($redirurl, "error", gettext("Internal error"));
	ob_flush();
	return;
}

$cpzoneid = $cpcfg['zoneid'];
$cpzoneprefix = CPPREFIX . $cpzoneid;
$orig_host = $_SERVER['HTTP_HOST'];
$clientip = $_SERVER['REMOTE_ADDR'];

if (!$clientip) {
	/* not good - bail out */
	log_error("Zone: {$cpzone} - Captive portal could not determine client's IP address.");
	$errormsg = gettext("An error occurred.  Please check the system logs for more information.");
	portal_reply_page($redirurl, "error", $errormsg);
	ob_flush();
	return;
}

$ourhostname = portal_hostname_from_client_ip($clientip);
$protocol = (config_path_enabled("captiveportal/{$cpzone}", "httpslogin")) ? 'https://' : 'http://';
$logouturl = "{$protocol}{$ourhostname}/";

$cpsession = captiveportal_isip_logged($clientip);
if (!empty($cpsession)) {
  $sessionid = $cpsession['sessionid'];
}

/* ----------------------CUSTOM CODE -------------------------------------------------------------------------
        24.11 Stbl Index.php Custom Code for Kea DHCP, check MAC address, not IP to verify client validation
        Kea flushes the IP quickly after the client disconnects, creating IP/MAC conflicts 
          within the captive portal database of validated clients.  
        It is the MAC that it validated, not the IP as IP can change frequently with Kea Reclamation policies
        This code updates the IP in the database to the current one for this MAC and passes the client through
          the Portal respecting that the client has a validated MAC.   Dale Harron, 17 December 2024. 
------------------------------------------------------------------------------------------------------------- */

$dbchanged = "no";

$ARPforIP = shell_exec("arp '$clientip'");
$clientmac = substr($ARPforIP, 17, 24);
if (preg_match('/([a-fA-F0-9]{2}[:|\-]?){6}/', "'{$clientmac}'", $matches)) {
    $clientmac = $matches[0];
}

/* query the cp database for this client MAC address to see if it is already authorized */
$query = "WHERE mac = '{$clientmac}'";
$cpdbqry = captiveportal_read_db($query);
foreach ($cpdbqry as $cpentrymac) {
  $cpsessionmac = $cpentrymac;
}
$sessionidmac = $cpentrymac[5];
$cpdbip = $cpentrymac[2];
$cpdbmac = $cpentrymac[3];
unset ($cpdbqry);

// If either MACs or IPs don't match, fix the CP DB otherwise do nothing and run original index.php code
if ($cpdbip <> $clientip || $cpdbmac <> $clientmac) {
  //  captiveportal_logportalauth($clientmac, $cpdbmac, $cpdbip, $clientip, $clientmac . "=" . $cpdbmac . "=" . $clientip . "=" . $cpdbip . "=" . $clientmac);

  if( ($clientip <> $cpdbip) && ($cpdbmac == $clientmac)) {
    // MACs match so update the old IP in the CP DB to current IP for the authorized MAC of this device
    $updt_field = "ip";
    $new_value = "'{$clientip}'";
    captiveportal_write_db("UPDATE captiveportal SET {$updt_field} = {$new_value} WHERE sessionid = '{$sessionidmac}'");
    $dbchanged = "yes";
  }  

  // 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.
  // 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.

  /* query the cp database for this client IP address to see if it is already associated with an authorized MAC*/
  $query = "WHERE ip = '{$clientip}'";
  $cpdbqry = captiveportal_read_db($query);
  foreach ($cpdbqry as $cpentryip) {
    $cpsessionip = $cpentryip;
  }
  $sessionidip = $cpentryip[5];
  $cpdbip = $cpentryip[2];

  if (!$cpdbmac && $cpdbip == $clientip) {
    $updt_field = 'ip';
    $new_value_tmp = "NOIP-" .  $cpzone;
    $new_value = "'{$new_value_tmp}'";
    captiveportal_write_db("UPDATE captiveportal SET {$updt_field} = {$new_value} WHERE sessionid = '{$sessionid}'");
    $dbchanged = "yes";
  }
  unset($cpdbqry);

  // If this MAC/IP pair does not match the MAC/IP in the Captive Portal database, then the IP has changed
  // update the Captive Portal database to associate the current IP with this already authorized MAC
  // Note, this could result in multiple IP/MAC pairs of the same IP in the Captive Portal database. 
  // Set all conflicting IPs to OFFLINE to retain authorization in CP DB

  if(($cpidmac <> $clientmac) && ($cpdbip == $clientip)) {  
    //Remove IP conflicts assigned to other MACs in the Captive Portal Database
    $query = "WHERE ip = '{$clientip}'";
    $cpdb_ip = captiveportal_read_db($query);
    foreach ($cpdb_ip as $cpentryip) {
      $cpsessionip = $cpentryip;
      $sessionidip = $cpsessionip['sessionid'];
      $updt_field = "ip";
      $unassignip = "NOIP-" .  $cpzone;
      $new_value = "'{$unassignip}'";
      if($clientmac <> $cpentryip[3]) {
        captiveportal_write_db("UPDATE captiveportal SET {$updt_field} = {$new_value} WHERE sessionid = '{$sessionidip}'");
        $dbchanged = "yes";
      //captiveportal_logportalauth($clientmac, $cpdbmac, $clientip, $cpdbip, $clientmac . "=" . $cpdbmac . "=" . $clientip . "=" . $cpdbip . "=" . $clientmac);
      }
      unset($cpdb_ip);
    }
  }

  //If the Database has been updated, reload the database for this zone.
  if( $dbchanged == "yes" ) {
    //Now reload this database with these changes to make them active
    if (isset($cpcfg['preservedb']) || $reload_rules ||
      captiveportal_xmlrpc_sync_get_details($syncip, $syncport, $syncuser, $syncpass)) {
      $connected_users = captiveportal_read_db();
      if (!empty($connected_users)) {
        //echo "Reconnecting users to captive portal {$cpcfg['zone']}... ";
        foreach ($connected_users as $user) {
        captiveportal_reserve_ruleno($user['pipeno']);
        captiveportal_ether_configure_entry($user, 'auth', true);
        }
      //echo "done\n";
      }
      $cpsession = captiveportal_isip_logged($clientip);
      if (!empty($cpsession)) {
        $sessionid = $cpsession['sessionid'];
      }  //ensure the $sessionid matches the now updated IP of this client
    } 
  } //reindex as CP DB has changed
} //if either IPs or MACs are not equal do something
//captiveportal_logportalauth($clientmac, $cpdbmac, $clientip, $cpdbip, $clientmac . "=" . $cpdbmac . "=" . $clientip . "=" . $cpdbip . "=" . $clientmac);

/* ----------- End Custom Code  ----------- */

/* Automatically switching to the logout page requires a custom logout page to be present. */
if ((!empty($cpsession)) && (! $_POST['logout_id']) && (!empty($cpcfg['page']['logouttext']))) {
	/* if client already connected and a custom logout page is set : show logout page */
	$attributes = array();
	if (!empty($cpsession['session_timeout']))
		$attributes['session_timeout'] = $cpsession['session_timeout'];
	if (!empty($cpsession['session_terminate_time']))
		$attributes['session_terminate_time'] = $cpsession['session_terminate_time'];

	include("{$g['varetc_path']}/captiveportal-{$cpzone}-logout.html");
	ob_flush();
	return;
} elseif (!empty($cpsession) && !isset($_POST['logout_id'])) {
	/* If the client tries to access the captive portal page while already connected,
		but no custom logout page exists */
	$logo_src = "{$protocol}{$ourhostname}/" . get_captive_portal_logo();
	$bg_src = get_captive_portal_bg();
?>
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
  <title>Captive Portal</title>
  <style>
	  #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;}
  </style>
</head>

<body>
<div id="content">
	<div class="login-card">
		<img src="<?= $logo_src ?>"/><br>
		<h1></h1>
		<div class="login-help">
			<?= gettext("The portal session is connected.") ?>
<?php if (!empty($redirurl)):
		$redirurl = htmlspecialchars($redirurl); ?>
			<br/><br/>
			<?= gettext("Proceed to: ") ?>
			<a href="<?=$redirurl?>"><?=$redirurl?></a>
<?php endif; ?>
		</div>
<br/>
	<form method="POST" action="<?=$logouturl;?>">
		<input name="logout_id" type="hidden" value="<?=$sessionid;?>" />
		<input name="zone" type="hidden" value="<?=$cpzone;?>" />
		<input name="logout" type="submit" value="<?= gettext("Disconnect") ?>" />
	</form>
	<br  />
	<span> <i>Made with &hearts; by</i> <strong>Netgate</strong></span>
	</div>
</div>
</body>
</html>
<?php
	ob_flush();
	return;
} elseif ($orig_host != $ourhostname) {
	/* the client thinks it's connected to the desired web server, but instead
	   it's connected to us. Issue a redirect... */
	$protocol = (isset($cpcfg['httpslogin'])) ? 'https://' : 'http://';
	header("Location: {$protocol}{$ourhostname}/index.php?zone={$cpzone}&redirurl=" . urlencode("http://{$orig_host}/{$orig_request}"));

	ob_flush();
	return;
}

/* find MAC address for client */
$tmpres = pfSense_ip_to_mac($clientip);
if (!is_array($tmpres)) {
	if (!isset($cpcfg['nomacfilter']) || isset($cpcfg['passthrumacadd'])) {
		/* unable to find MAC address - shouldn't happen! - bail out */
		captiveportal_logportalauth("unauthenticated", "noclientmac", $clientip, "ERROR");
		echo "An error occurred.  Please check the system logs for more information.";
		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.");
		ob_flush();
		return;
	}
} else {
	/* always save MAC address in DB to allow macfilter/nomacfilter switching without flushing all clients */
	$clientmac = $tmpres['macaddr'];
}
unset($tmpres);

if ($_POST['logout_id']) {
	$safe_logout_id = SQLite3::escapeString($_POST['logout_id']);
	captiveportal_disconnect_client($safe_logout_id);
	header("Location: index.php?zone=" . $cpzone);
	ob_flush();
	return;
} elseif (($_POST['accept'] || $cpcfg['auth_method'] === 'radmac' || !empty($cpcfg['blockedmacsurl'])) && !isset($cpcfg['nomacfilter']) && $clientmac && captiveportal_blocked_mac($clientmac)) {
	captiveportal_logportalauth($clientmac, $clientmac, $clientip, "Blocked MAC address");
	if (!empty($cpcfg['blockedmacsurl'])) {
		portal_reply_page($cpcfg['blockedmacsurl'], "redir");
	} else {
		if ($cpcfg['auth_method'] === 'radmac') {
			echo gettext("This MAC address has been blocked");
		} else {
			portal_reply_page($redirurl, "error", "This MAC address has been blocked", $clientmac, $clientip);
		}
	}
} elseif (portal_consume_passthrough_credit($clientmac)) {
	/* allow the client through if it had a pass-through credit for its MAC */
	captiveportal_logportalauth("unauthenticated", $clientmac, $clientip, "ACCEPT");
	portal_allow($clientip, $clientmac, "unauthenticated", null, $redirurl);

} elseif (config_path_enabled("voucher/{$cpzone}") && ($_POST['accept'] && $_POST['auth_voucher']) || $_GET['voucher']) {
	if (isset($_POST['auth_voucher'])) {
		$voucher = trim($_POST['auth_voucher']);
	} else {
		/* submit voucher via URL, see https://redmine.pfsense.org/issues/1984 */
		$voucher = trim($_GET['voucher']);
		portal_reply_page($redirurl, "login", null, $clientmac, $clientip, null, null, $voucher);
		return;
	}
	$errormsg = gettext("Invalid credentials specified.");
	$timecredit = voucher_auth($voucher);
	// $timecredit contains either a credit in minutes or an error message
	if ($timecredit > 0) {  // voucher is valid. Remaining minutes returned
		// if multiple vouchers given, use the first as username
		$a_vouchers = preg_split("/[\t\n\r ]+/s", $voucher);
		$voucher = $a_vouchers[0];
		$attr = array(
			'voucher' => 1,
			'session_timeout' => $timecredit*60,
			'session_terminate_time' => 0);
		if (portal_allow($clientip, $clientmac, $voucher, null, $redirurl, $attr, null, 'voucher', 'voucher') === 2) {
			portal_reply_page($redirurl, "error", "Reuse of identification not allowed.", $clientmac, $clientip);
		} elseif (portal_allow($clientip, $clientmac, $voucher, null, $redirurl, $attr, null, 'voucher', 'voucher')) {
			// YES: user is good for $timecredit minutes.
			captiveportal_logportalauth($voucher, $clientmac, $clientip, "Voucher login good for $timecredit min.");
		} else {
			portal_reply_page($redirurl, "error", config_get_path("voucher/{$cpzone}/descrmsgexpired") ? config_get_path("voucher/{$cpzone}/descrmsgexpired"): $errormsg, $clientmac, $clientip);
		}
	} elseif (-1 == $timecredit) {  // valid but expired
		captiveportal_logportalauth($voucher, $clientmac, $clientip, "FAILURE", "voucher expired");
		portal_reply_page($redirurl, "error", config_get_path("voucher/{$cpzone}/descrmsgexpired") ? config_get_path("voucher/{$cpzone}/descrmsgexpired"): $errormsg, $clientmac, $clientip);
	} else {
		captiveportal_logportalauth($voucher, $clientmac, $clientip, "FAILURE");
		portal_reply_page($redirurl, "error", config_get_path("voucher/{$cpzone}/descrmsgnoaccess") ? config_get_path("voucher/{$cpzone}/descrmsgnoaccess") : $errormsg, $clientmac, $clientip);
	}

} elseif ($_POST['accept'] || $cpcfg['auth_method'] === 'radmac') {
	
		if ($cpcfg['auth_method'] === 'radmac' && !isset($_POST['accept'])) {
			$user = $clientmac; 
			$passwd = $cpcfg['radmac_secret'];
			$context = 'radmac'; // Radius MAC authentication
		} elseif (!empty(trim($_POST['auth_user2']))) { 
			$user = trim($_POST['auth_user2']);
			$passwd = $_POST['auth_pass2'];
			$context = 'second'; // Assume users to use the first context if auth_user2 is empty/does not exist
		} else {
			$user = trim($_POST['auth_user']);
			$passwd = $_POST['auth_pass'];
			$context = 'first';
		}
	
	$pipeno = captiveportal_get_next_dn_ruleno('auth', 2000, 64500, true);
	/* if the pool is empty, return appropriate message and exit */
	if (is_null($pipeno)) {
		$replymsg = gettext("System reached maximum login capacity");
		if ($cpcfg['auth_method'] === 'radmac') {
			echo $replymsg;
			ob_flush();
			return;
		} else {
			portal_reply_page($redirurl, "error", $replymsg, $clientmac, $clientip);
		}
		log_error("Zone: {$cpzone} - WARNING!  Captive portal has reached maximum login capacity");
		
	}
	
	$auth_result = captiveportal_authenticate_user($user, $passwd, $clientmac, $clientip, $pipeno, $context);
	
	if ($auth_result['result']) {
		captiveportal_logportalauth($user, $clientmac, $clientip, $auth_result['login_status']);
		portal_allow($clientip, $clientmac, $user, $passwd, $redirurl, $auth_result['attributes'], null, $auth_result['auth_method'], $context);
	} else {
		$type = "error";
			
		if (is_URL($auth_result['attributes']['url_redirection'], true)) {
			$redirurl = $auth_result['attributes']['url_redirection'];
			$type = "redir";
		}
		
		if ($auth_result['login_message']) {
			$replymsg = $auth_result['login_message'];
		} else {
			$replymsg = gettext("Invalid credentials specified.");
		}
		
		captiveportal_logportalauth($user, $clientmac, $clientip, $auth_result['login_status'], $replymsg);

		/* Radius MAC authentication. */
		if ($context === 'radmac' && $type !== 'redir' && !isset($cpcfg['radmac_fallback'])) {
			echo $replymsg;
		} else {
			portal_reply_page($redirurl, $type, $replymsg, $clientmac, $clientip);
		}
	}
} else {
	/* display captive portal page */
	portal_reply_page($redirurl, "login", null, $clientmac, $clientip);
}

ob_flush();

?>
