<?php

/*
 * Copyright (C) 2025 Deciso B.V.
 * Copyright (C) 2008 Shrew Soft Inc. <mgrooms@shrew.net>
 * Copyright (C) 2007-2008 Scott Ullrich <sullrich@gmail.com>
 * Copyright (C) 2005-2006 Bill Marquette <bill.marquette@gmail.com>
 * Copyright (C) 2006 Paul Taylor <paultaylor@winn-dixie.com>
 * Copyright (C) 2003-2006 Manuel Kasper <mk@neon1.net>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
 * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
 * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

require_once("auth.inc");

$acl = new OPNsense\Core\ACL();
$priv_list = $acl->getPrivList();

function set_language()
{
    global $config, $userindex;

    $lang = 'en_US';

    if (!empty($config['system']['language'])) {
        $lang = $config['system']['language'];
    }

    if (
        !empty($_SESSION['Username']) && array_key_exists($_SESSION['Username'], $userindex) &&
        !empty($config['system']['user'][$userindex[$_SESSION['Username']]]['language'])
    ) {
        $lang = $config['system']['user'][$userindex[$_SESSION['Username']]]['language'];
    }

    $lang_encoding = $lang . '.UTF-8';

    putenv('LANG=' . $lang_encoding);
    putenv('LANGUAGE=' . $lang_encoding);
    putenv('LC_ALL=' . $lang_encoding);
    setlocale(LC_ALL, $lang_encoding);

    $textdomain = 'OPNsense';

    textdomain($textdomain);
    bindtextdomain($textdomain, '/usr/local/share/locale');
    bind_textdomain_codeset($textdomain, $lang_encoding);
}

/* DNS ReBinding attack prevention, return true when rebind detected*/
function check_security_dns_rebind()
{
    global $config;
    if (!isset($config['system']['webgui']['nodnsrebindcheck'])) {
        /* either an IPv6 address with or without an alternate port */
        if (strstr($_SERVER['HTTP_HOST'], "]")) {
            $http_host_port = explode("]", $_SERVER['HTTP_HOST']);
            /* v6 address has more parts, drop the last part */
            if (count($http_host_port) > 1) {
                array_pop($http_host_port);
                $http_host = str_replace(["[", "]"], "", implode(":", $http_host_port));
            } else {
                $http_host = str_replace(["[", "]"], "", implode(":", $http_host_port));
            }
        } else {
            $http_host = explode(":", $_SERVER['HTTP_HOST']);
            $http_host = $http_host[0];
        }
        $this_host = [
            $config['system']['hostname'] . "." . $config['system']['domain'],
            $config['system']['hostname'],
            "localhost"
        ];
        if (!empty($config['system']['webgui']['althostnames'])) {
            $this_host = array_merge($this_host, explode(" ", $config['system']['webgui']['althostnames']));
        }
        if (is_ipaddr($http_host) || in_array($_SERVER['SERVER_ADDR'], ["127.0.0.1", "::1"])) {
            return false;
        } elseif (in_array(strtolower($http_host), array_map('strtolower', $this_host))) {
            return false;
        }
        return true;
    }
    return false;
}

/* HTTP referer detection, return true when being forwarded from an unknown referer*/
function check_security_http_referer_enforcement()
{
    global $config;
    if (!isset($config['system']['webgui']['nohttpreferercheck']) && isset($_SERVER['HTTP_REFERER'])) {
        $referrer_host = str_replace(["[", "]"], "", parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST));
        $this_host = [$config['system']['hostname'] . "." . $config['system']['domain'], $config['system']['hostname']];
        if (!empty($config['system']['webgui']['althostnames'])) {
            $this_host = array_merge($this_host, explode(" ", $config['system']['webgui']['althostnames']));
        }
        if ($referrer_host) {
            if (in_array(strtolower($referrer_host), array_map('strtolower', $this_host))) {
                return false;
            } elseif (isAuthLocalIP($referrer_host)) {
                return false;
            } elseif ($referrer_host == "127.0.0.1" || $referrer_host == "localhost") {
                // allow SSH port forwarded connections and links from localhost
                return false;
            }
        }
        return true;
    }
    return false;
}

function session_auth()
{
    global $config;

    if (session_status() == PHP_SESSION_NONE) {
        // Handle HTTPS httponly and secure flags
        $currentCookieParams = session_get_cookie_params();
        session_set_cookie_params(
            $currentCookieParams["lifetime"],
            $currentCookieParams["path"],
            null,
            ($config['system']['webgui']['protocol'] == "https"),
            true
        );
        session_set_cookie_params(["SameSite" => "Lax"]);
        session_start();
    }

    // Detect protocol change
    if (!isset($_POST['login']) && !empty($_SESSION['Username']) && $_SESSION['protocol'] != $config['system']['webgui']['protocol']) {
        session_write_close();
        return false;
    }
    // check additional security measures
    if (empty($_SESSION['Username'])) {
        if (check_security_dns_rebind()) {
            display_error_form(sprintf(gettext('A potential %sDNS Rebind attack%s has been detected.%sTry to access the router by IP address instead of by hostname. You can disable this check if needed under System: Settings: Administration.'), '<a href="http://en.wikipedia.org/wiki/DNS_rebinding">', '</a>', '<br />'));
            exit;
        } elseif (check_security_http_referer_enforcement()) {
            display_error_form(sprintf(
                gettext('The HTTP_REFERER "%s" does not match the predefined settings. You can disable this check if needed under System: Settings: Administration.'),
                html_safe($_SERVER['HTTP_REFERER'])
            ));
            exit;
        }
    }

    /* Validate incoming login request */
    if (isset($_POST['login']) && !empty($_POST['usernamefld']) && !empty($_POST['passwordfld'])) {
        $authFactory = new \OPNsense\Auth\AuthenticationFactory();
        $is_authenticated = $authFactory->authenticate("WebGui", $_POST['usernamefld'], $_POST['passwordfld']);

        if ($is_authenticated) {
            $authenticator = $authFactory->lastUsedAuth;
            // Generate a new id to avoid session fixation
            session_regenerate_id();
            $_SESSION['Username'] = $authenticator->getUserName($_POST['usernamefld']);
            $_SESSION['last_access'] = time();
            $_SESSION['protocol'] = $config['system']['webgui']['protocol'];
            if ($authenticator != null && $authenticator->shouldChangePassword($_SESSION['Username'], $_POST['passwordfld'])) {
                $_SESSION['user_shouldChangePassword'] = true;
            }
            if (!isset($config['system']['webgui']['quietlogin'])) {
                auth_log(sprintf("Successful login for user '%s' from: %s", $_POST['usernamefld'], $_SERVER['REMOTE_ADDR']), LOG_NOTICE);
            }
            if (!empty($_GET['url'])) {
                $uri = (new OPNsense\Core\SanitizeFilter())->sanitize($_GET['url'], 'local_uri');
                if (!empty($uri)) {
                    /**
                     * Intentionally kept url_safe(), although vsprintf isn't a safety measure here.
                     * ref https://github.com/opnsense/core/issues/1168
                     */
                    header(url_safe("Location: {$uri}"));
                }
            } elseif (!empty($_SESSION['user_shouldChangePassword'])) {
                header("Location: /ui/user_portal");
            } else {
                if ($_SERVER['REQUEST_URI'] == "/") {
                    // default landing page
                    $acl = new OPNsense\Core\ACL();
                    $url = $acl->getLandingPage($_SESSION['Username']);
                    header(url_safe("Location: /{$url}"));
                } else {
                    header(url_safe("Location: {$_SERVER['REQUEST_URI']}"));
                }
            }
            exit;
        } else {
            auth_log("Web GUI authentication error for '{$_POST['usernamefld']}' from {$_SERVER['REMOTE_ADDR']}");
        }
    }

    /* Show login page if they aren't logged in */
    if (empty($_SESSION['Username'])) {
        return false;
    }

    /* If session timeout isn't set, we don't mark sessions stale */
    if (empty($config['system']['webgui']['session_timeout'])) {
        /* Default to 4 hour timeout if one is not set */
        if ($_SESSION['last_access'] < (time() - 14400)) {
            $_GET['logout'] = true;
            $_SESSION['Logout'] = true;
        } else {
            $_SESSION['last_access'] = time();
        }
    } else {
        /* Check for stale session */
        if ($_SESSION['last_access'] < (time() - ($config['system']['webgui']['session_timeout'] * 60))) {
            $_GET['logout'] = true;
            $_SESSION['Logout'] = true;
        } else {
            $_SESSION['last_access'] = time();
        }
    }

    /* user hit the logout button */
    if (isset($_GET['logout'])) {
        if (isset($_SESSION['Logout'])) {
            auth_log(sprintf("Session timed out for user '%s' from: %s", $_SESSION['Username'], $_SERVER['REMOTE_ADDR']), LOG_NOTICE);
        } else {
            auth_log(sprintf("User logged out for user '%s' from: %s", $_SESSION['Username'], $_SERVER['REMOTE_ADDR']), LOG_NOTICE);
        }

        /* wipe out $_SESSION */
        $_SESSION = array();

        if (isset($_COOKIE[session_name()])) {
            $secure = $config['system']['webgui']['protocol'] == "https";
            setcookie(session_name(), '', time() - 42000, '/', '', $secure, true);
        }

        /* and destroy it */
        session_destroy();

        header(url_safe("Location: /"));
        exit;
    }

    session_write_close();
    return true;
}

/* Authenticate user - exit if failed */
if (!session_auth()) {
    set_language();
    display_login_form(!empty($_POST['usernamefld']) ? gettext('Wrong username or password.') : null);
    exit;
}

set_language();

/*
 * redirect to first allowed page if requesting a wrong url
 */
if ($_SERVER['REQUEST_URI'] == '/') {
    $page = '/index.php';
} else {
    /* reconstruct page uri to use actual script location, mimic realpath() behaviour */
    $page = $_SERVER['SCRIPT_NAME'];
    $tmp_uri = parse_url($_SERVER['REQUEST_URI']);
    if (!empty($tmp_uri['query'])) {
        $page .= '?' . $tmp_uri['query'];
    }
}
if ($_SESSION['Username'] != 'root' && !$acl->isPageAccessible($_SESSION['Username'], $page)) {
    if (session_status() == PHP_SESSION_NONE) {
        session_start();
    }
    $page = $acl->getLandingPage($_SESSION['Username']);
    if (!empty($page)) {
        $username = empty($_SESSION['Username']) ? '(unknown)' : $_SESSION['Username'];
        if (!empty($_SERVER['REMOTE_ADDR'])) {
            $username .= '@' . $_SERVER['REMOTE_ADDR'];
        }
        log_msg("{$username} attempted to access {$_SERVER['REQUEST_URI']} but does not have access to that page. Redirecting to {$page}.");
        header(url_safe("Location: /{$page}"));
        exit;
    } else {
        display_error_form(gettext('No page assigned to this user! Click here to logout.'));
        exit;
    }
}

/*
 * determine if the user is allowed access to the requested page
 */
function display_error_form($text)
{
    $product = product::getInstance();

    ?><!doctype html>
<html lang="<?= get_current_lang() ?>" class="no-js">
  <head>

    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge">

    <meta name="robots" content="noindex, nofollow" />
    <meta name="keywords" content="" />
    <meta name="description" content="" />
    <meta name="copyright" content="" />
    <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
    <meta name="mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-capable" content="yes">

    <title><?= gettext('Error') ?> | <?= $product->name() ?></title>

    <link href="<?= cache_safe(get_themed_filename('/css/main.css')) ?>" rel="stylesheet">
    <link href="<?= cache_safe(get_themed_filename('/images/favicon.png')) ?>" rel="shortcut icon">

    <script src="/ui/js/jquery-3.5.1.min.js"></script>

  </head>
  <body class="page-login">
    <div id=container">
      <p>&nbsp;</p>
      <p style="text-align: center;">
        <a href="/index.php?logout"><?= $text ?></a>
      </p>
    </div>
  </body>
</html>
<?php }

function display_login_form($Login_Error)
{
    global $config;

    $product = product::getInstance();

    setcookie("cookie_test", bin2hex(random_bytes(16)), time() + 3600, '/', '', $config['system']['webgui']['protocol'] == 'https', true);
    $have_cookies = isset($_COOKIE["cookie_test"]);
    ?><!doctype html>
<html lang="<?= get_current_lang() ?>" class="no-js">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge">

    <meta name="robots" content="noindex, nofollow" />
    <meta name="keywords" content="" />
    <meta name="description" content="" />
    <meta name="copyright" content="" />
    <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
    <meta name="mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-capable" content="yes">

    <title><?= gettext('Login') ?> | <?= $product->name() ?></title>

    <link href="<?= cache_safe(get_themed_filename('/css/main.css')) ?>" rel="stylesheet">
    <link href="<?= cache_safe(get_themed_filename('/images/favicon.png')) ?>" rel="shortcut icon">

    <script src="/ui/js/jquery-3.5.1.min.js"></script>

    <?php if (get_themed_filename('/js/theme.js', true)) : ?>
    <script src="<?= cache_safe(get_themed_filename('/js/theme.js')) ?>"></script>
    <?php endif ?>

  </head>
  <body class="page-login">

  <div class="container">
    <main class="login-modal-container">
      <header class="login-modal-head" style="height:50px;">
        <div class="navbar-brand">
    <?php if (get_themed_filename('/images/default-logo.svg', true)) : ?>
          <img src="<?= cache_safe(get_themed_filename('/images/default-logo.svg')) ?>" height="30" alt="logo" />
    <?php else : ?>
          <img src="<?= cache_safe(get_themed_filename('/images/default-logo.png')) ?>" height="30" alt="logo" />
    <?php endif ?>
        </div>
      </header>

      <div class="login-modal-content">
        <div id="inputerrors" class="text-danger"><?= !empty($Login_Error) ? $Login_Error : '&nbsp;' ?></div><br />

            <form class="clearfix" id="iform" name="iform" method="post" autocomplete="off">

        <div class="form-group">
          <label for="usernamefld"><?=gettext("Username:"); ?></label>
          <input id="usernamefld" type="text" name="usernamefld" class="form-control user" tabindex="1" autofocus="autofocus" autocapitalize="off" autocorrect="off" />
        </div>

        <div class="form-group">
          <label for="passwordfld"><?=gettext("Password:"); ?></label>
          <input id="passwordfld" type="password" name="passwordfld" class="form-control pwd" tabindex="2" />
        </div>

        <button type="submit" name="login" value="1" class="btn btn-primary pull-right"><?=gettext("Login"); ?></button>

      </form>


<?php    foreach ((new \OPNsense\Auth\AuthenticationFactory())->listSSOproviders('WebGui') as $provider):?>
<div class="login-sso-link-container">
    <?=$provider->renderLink();?>
</div>
<?php    endforeach;?>

      <?php if (!$have_cookies && isset($_POST['login'])) : ?>
        <br /><br />
        <span class="text-danger">
            <?= gettext("Your browser must support cookies to login."); ?>
        </span>
      <?php endif; ?>

          </div>

      </main>
      <div class="login-foot text-center">
        <a target="_blank" href="<?= $product->website() ?>"><?= $product->name() ?></a> (c) <?= $product->copyright_years() ?>
        <a target="_blank" href="<?= $product->copyright_url() ?>"><?= $product->copyright_owner() ?></a>
      </div>

    </div>

    </body>
  </html>
<?php }
