<?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 -------------------------------------------------------------------------
        Index.php Custom Code for Kea DHCP, check MAC address instead of IP to verify a sesson ID exists
              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 changes frequently
        This code matches the current IP to the MAC in the database and Reconnects users to the Portal
        so that the corrected login immediately works.   Dale Harron, 10 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];

//  captiveportal_logportalauth($clientmac, $cpdbmac, $cpdbip, $clientip, $clientmac . "=" . $cpdbmac . "=" . $clientip . "=" . $cpdbip . "=" . $clientmac);
//  $dbchanged = "yes";
if( ($clientip <> $cpdbip) && ($cpdbmac == $clientmac)) {
  // Next update the IP in the Captive Portal database 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";
}  

unset($cpdbqry);


// 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 = "OFFLINE";
  $new_value = "'{$new_value_tmp}'";
  captiveportal_write_db("UPDATE captiveportal SET {$updt_field} = {$new_value} WHERE sessionid = '{$sessionid}'");
  $dbchanged = "yes";
}
unset($cpdbqry);


/*
//For Testing, Enable changing CP database IP
// Disconnect device with first client IP from cp, fill in the existing temp IP and new value IP
//uncomment the top and bottom /*... */ save and log into the client IP system.  Recomment the test code

if ($clientip == "192.168.50.8") {

  $tempip = "192.168.50.2";
  $query = "WHERE ip = '{$tempip}'";
  $cpdbqry = captiveportal_read_db($query);
  foreach ($cpdbqry as $cpentryip) {
    $cpsession = $cpentryip;
  }
  $sessionidtmp = $cpentryip[5];
  $clientiptmp = $cpentryip[2];
  $clientmactmp = $cpentryip[3];
  $updt_field = 'ip';
  $new_value_tmp = "192.168.50.99";
  $new_value = "'{$new_value_tmp}'";
  captiveportal_write_db("UPDATE captiveportal SET {$updt_field} = {$new_value} WHERE sessionid = '{$sessionidtmp}'");
  $dbchanged = "yes";
  $sessionid=$sessionidtmp;
  unset($cpdbqry);
  captiveportal_logportalauth($clientmac, $clientmactmp, $clientip, $clientiptmp, $clientmact . "=" . $clientmactmp . "=" . $clientip . "=" . $clientiptmp . "=" . $clientmac);
}
*/

/*
//For Testing, Enable changing CP database MAC
// Disconnect device with first client IP from cp, fill in the existing temp IP and new value IP
//uncomment the top and bottom /*... */ save and log into the client IP system.  Recomment the test code

if ($clientip == "192.168.50.8") {

  $tempmac = "9c:fc:e8:9f:3e:22";
  $tempip = "OFFLINE";
  $query = "WHERE ip = '{$tempip}'";
  $cpdbqry = captiveportal_read_db($query);
  foreach ($cpdbqry as $cpentryip) {
    $cpsession = $cpentryip;
  }
  $sessionidtmp = $cpentryip[5];
  $clientiptmp = $cpentryip[2];
  $clientmactmp = $cpentryip[3];
  $updt_field = 'mac';
  $new_value_tmp = $tempmac;
  $new_value = "'{$new_value_tmp}'";
  captiveportal_write_db("UPDATE captiveportal SET {$updt_field} = {$new_value} WHERE sessionid = '{$sessionidtmp}'");
  $dbchanged = "yes";
  $sessionid=$sessionidtmp;
  unset($cpdbqry);
  captiveportal_logportalauth($clientmactmp, $tempmac, $clientiptmp, $clientip, $clientmactmp . "=" . $tempmac . "=" . $clientiptmp . "=" . $clientip . "=" . $clientmac);
}
*/


/* 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. Repair all invalid conflicts */

        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 = 'OFFLINE';
            $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'];
    }
  }
}

//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();

?>
