From: VirtualTam Date: Sun, 3 Jun 2018 16:26:32 +0000 (+0200) Subject: Merge pull request #1086 from virtualtam/refactor/login X-Git-Tag: v0.10.0~23 X-Git-Url: https://git.immae.eu/?a=commitdiff_plain;h=d9cd27322a97e6ed3a8b11a380ef0080e3baf79c;hp=8f816d8ddfe9219e15580cef6e5c9037d1d4fd28;p=github%2Fshaarli%2FShaarli.git Merge pull request #1086 from virtualtam/refactor/login Refactor user login and session management --- diff --git a/application/HttpUtils.php b/application/HttpUtils.php index 83a4c5e2..e9282506 100644 --- a/application/HttpUtils.php +++ b/application/HttpUtils.php @@ -1,7 +1,7 @@ globals = &$globals; - $this->configManager = $configManager; - $this->banFile = $this->configManager->get('resource.ban_file', 'data/ipbans.php'); - $this->readBanFile(); - } - - /** - * Read a file containing banned IPs - */ - protected function readBanFile() - { - if (! file_exists($this->banFile)) { - return; - } - include $this->banFile; - } - - /** - * Write the banned IPs to a file - */ - protected function writeBanFile() - { - if (! array_key_exists('IPBANS', $this->globals)) { - return; - } - file_put_contents( - $this->banFile, - "globals['IPBANS'], true) . ";\n?>" - ); - } - - /** - * Handle a failed login and ban the IP after too many failed attempts - * - * @param array $server The $_SERVER array - */ - public function handleFailedLogin($server) - { - $ip = $server['REMOTE_ADDR']; - $trusted = $this->configManager->get('security.trusted_proxies', []); - - if (in_array($ip, $trusted)) { - $ip = getIpAddressFromProxy($server, $trusted); - if (! $ip) { - // the IP is behind a trusted forward proxy, but is not forwarded - // in the HTTP headers, so we do nothing - return; - } - } - - // increment the fail count for this IP - if (isset($this->globals['IPBANS']['FAILURES'][$ip])) { - $this->globals['IPBANS']['FAILURES'][$ip]++; - } else { - $this->globals['IPBANS']['FAILURES'][$ip] = 1; - } - - if ($this->globals['IPBANS']['FAILURES'][$ip] >= $this->configManager->get('security.ban_after')) { - $this->globals['IPBANS']['BANS'][$ip] = time() + $this->configManager->get('security.ban_duration', 1800); - logm( - $this->configManager->get('resource.log'), - $server['REMOTE_ADDR'], - 'IP address banned from login' - ); - } - $this->writeBanFile(); - } - - /** - * Handle a successful login - * - * @param array $server The $_SERVER array - */ - public function handleSuccessfulLogin($server) - { - $ip = $server['REMOTE_ADDR']; - // FIXME unban when behind a trusted proxy? - - unset($this->globals['IPBANS']['FAILURES'][$ip]); - unset($this->globals['IPBANS']['BANS'][$ip]); - - $this->writeBanFile(); - } - - /** - * Check if the user can login from this IP - * - * @param array $server The $_SERVER array - * - * @return bool true if the user is allowed to login - */ - public function canLogin($server) - { - $ip = $server['REMOTE_ADDR']; - - if (! isset($this->globals['IPBANS']['BANS'][$ip])) { - // the user is not banned - return true; - } - - if ($this->globals['IPBANS']['BANS'][$ip] > time()) { - // the user is still banned - return false; - } - - // the ban has expired, the user can attempt to log in again - logm($this->configManager->get('resource.log'), $server['REMOTE_ADDR'], 'Ban lifted.'); - unset($this->globals['IPBANS']['FAILURES'][$ip]); - unset($this->globals['IPBANS']['BANS'][$ip]); - - $this->writeBanFile(); - return true; - } -} diff --git a/application/PageBuilder.php b/application/PageBuilder.php index 3233d6b6..a4483870 100644 --- a/application/PageBuilder.php +++ b/application/PageBuilder.php @@ -25,6 +25,9 @@ class PageBuilder * @var LinkDB $linkDB instance. */ protected $linkDB; + + /** @var bool $isLoggedIn Whether the user is logged in **/ + protected $isLoggedIn = false; /** * PageBuilder constructor. @@ -34,12 +37,13 @@ class PageBuilder * @param LinkDB $linkDB instance. * @param string $token Session token */ - public function __construct(&$conf, $linkDB = null, $token = null) + public function __construct(&$conf, $linkDB = null, $token = null, $isLoggedIn = false) { $this->tpl = false; $this->conf = $conf; $this->linkDB = $linkDB; $this->token = $token; + $this->isLoggedIn = $isLoggedIn; } /** @@ -55,7 +59,7 @@ class PageBuilder $this->conf->get('resource.update_check'), $this->conf->get('updates.check_updates_interval'), $this->conf->get('updates.check_updates'), - isLoggedIn(), + $this->isLoggedIn, $this->conf->get('updates.check_updates_branch') ); $this->tpl->assign('newVersion', escape($version)); @@ -67,6 +71,7 @@ class PageBuilder $this->tpl->assign('versionError', escape($exc->getMessage())); } + $this->tpl->assign('is_logged_in', $this->isLoggedIn); $this->tpl->assign('feedurl', escape(index_url($_SERVER))); $searchcrits = ''; // Search criteria if (!empty($_GET['searchtags'])) { diff --git a/application/SessionManager.php b/application/SessionManager.php deleted file mode 100644 index 71f0b38d..00000000 --- a/application/SessionManager.php +++ /dev/null @@ -1,83 +0,0 @@ -session = &$session; - $this->conf = $conf; - } - - /** - * Generates a session token - * - * @return string token - */ - public function generateToken() - { - $token = sha1(uniqid('', true) .'_'. mt_rand() . $this->conf->get('credentials.salt')); - $this->session['tokens'][$token] = 1; - return $token; - } - - /** - * Checks the validity of a session token, and destroys it afterwards - * - * @param string $token The token to check - * - * @return bool true if the token is valid, else false - */ - public function checkToken($token) - { - if (! isset($this->session['tokens'][$token])) { - // the token is wrong, or has already been used - return false; - } - - // destroy the token to prevent future use - unset($this->session['tokens'][$token]); - return true; - } - - /** - * Validate session ID to prevent Full Path Disclosure. - * - * See #298. - * The session ID's format depends on the hash algorithm set in PHP settings - * - * @param string $sessionId Session ID - * - * @return true if valid, false otherwise. - * - * @see http://php.net/manual/en/function.hash-algos.php - * @see http://php.net/manual/en/session.configuration.php - */ - public static function checkId($sessionId) - { - if (empty($sessionId)) { - return false; - } - - if (!$sessionId) { - return false; - } - - if (!preg_match('/^[a-zA-Z0-9,-]{2,128}$/', $sessionId)) { - return false; - } - - return true; - } -} diff --git a/application/security/LoginManager.php b/application/security/LoginManager.php new file mode 100644 index 00000000..d6784d6d --- /dev/null +++ b/application/security/LoginManager.php @@ -0,0 +1,265 @@ +globals = &$globals; + $this->configManager = $configManager; + $this->sessionManager = $sessionManager; + $this->banFile = $this->configManager->get('resource.ban_file', 'data/ipbans.php'); + $this->readBanFile(); + if ($this->configManager->get('security.open_shaarli') === true) { + $this->openShaarli = true; + } + } + + /** + * Generate a token depending on deployment salt, user password and client IP + * + * @param string $clientIpAddress The remote client IP address + */ + public function generateStaySignedInToken($clientIpAddress) + { + $this->staySignedInToken = sha1( + $this->configManager->get('credentials.hash') + . $clientIpAddress + . $this->configManager->get('credentials.salt') + ); + } + + /** + * Return the user's client stay-signed-in token + * + * @return string User's client stay-signed-in token + */ + public function getStaySignedInToken() + { + return $this->staySignedInToken; + } + + /** + * Check user session state and validity (expiration) + * + * @param array $cookie The $_COOKIE array + * @param string $clientIpId Client IP address identifier + */ + public function checkLoginState($cookie, $clientIpId) + { + if (! $this->configManager->exists('credentials.login')) { + // Shaarli is not configured yet + $this->isLoggedIn = false; + return; + } + + if (isset($cookie[self::$STAY_SIGNED_IN_COOKIE]) + && $cookie[self::$STAY_SIGNED_IN_COOKIE] === $this->staySignedInToken + ) { + // The user client has a valid stay-signed-in cookie + // Session information is updated with the current client information + $this->sessionManager->storeLoginInfo($clientIpId); + + } elseif ($this->sessionManager->hasSessionExpired() + || $this->sessionManager->hasClientIpChanged($clientIpId) + ) { + $this->sessionManager->logout(); + $this->isLoggedIn = false; + return; + } + + $this->isLoggedIn = true; + $this->sessionManager->extendSession(); + } + + /** + * Return whether the user is currently logged in + * + * @return true when the user is logged in, false otherwise + */ + public function isLoggedIn() + { + if ($this->openShaarli) { + return true; + } + return $this->isLoggedIn; + } + + /** + * Check user credentials are valid + * + * @param string $remoteIp Remote client IP address + * @param string $clientIpId Client IP address identifier + * @param string $login Username + * @param string $password Password + * + * @return bool true if the provided credentials are valid, false otherwise + */ + public function checkCredentials($remoteIp, $clientIpId, $login, $password) + { + $hash = sha1($password . $login . $this->configManager->get('credentials.salt')); + + if ($login != $this->configManager->get('credentials.login') + || $hash != $this->configManager->get('credentials.hash') + ) { + logm( + $this->configManager->get('resource.log'), + $remoteIp, + 'Login failed for user ' . $login + ); + return false; + } + + $this->sessionManager->storeLoginInfo($clientIpId); + logm( + $this->configManager->get('resource.log'), + $remoteIp, + 'Login successful' + ); + return true; + } + + /** + * Read a file containing banned IPs + */ + protected function readBanFile() + { + if (! file_exists($this->banFile)) { + return; + } + include $this->banFile; + } + + /** + * Write the banned IPs to a file + */ + protected function writeBanFile() + { + if (! array_key_exists('IPBANS', $this->globals)) { + return; + } + file_put_contents( + $this->banFile, + "globals['IPBANS'], true) . ";\n?>" + ); + } + + /** + * Handle a failed login and ban the IP after too many failed attempts + * + * @param array $server The $_SERVER array + */ + public function handleFailedLogin($server) + { + $ip = $server['REMOTE_ADDR']; + $trusted = $this->configManager->get('security.trusted_proxies', []); + + if (in_array($ip, $trusted)) { + $ip = getIpAddressFromProxy($server, $trusted); + if (! $ip) { + // the IP is behind a trusted forward proxy, but is not forwarded + // in the HTTP headers, so we do nothing + return; + } + } + + // increment the fail count for this IP + if (isset($this->globals['IPBANS']['FAILURES'][$ip])) { + $this->globals['IPBANS']['FAILURES'][$ip]++; + } else { + $this->globals['IPBANS']['FAILURES'][$ip] = 1; + } + + if ($this->globals['IPBANS']['FAILURES'][$ip] >= $this->configManager->get('security.ban_after')) { + $this->globals['IPBANS']['BANS'][$ip] = time() + $this->configManager->get('security.ban_duration', 1800); + logm( + $this->configManager->get('resource.log'), + $server['REMOTE_ADDR'], + 'IP address banned from login' + ); + } + $this->writeBanFile(); + } + + /** + * Handle a successful login + * + * @param array $server The $_SERVER array + */ + public function handleSuccessfulLogin($server) + { + $ip = $server['REMOTE_ADDR']; + // FIXME unban when behind a trusted proxy? + + unset($this->globals['IPBANS']['FAILURES'][$ip]); + unset($this->globals['IPBANS']['BANS'][$ip]); + + $this->writeBanFile(); + } + + /** + * Check if the user can login from this IP + * + * @param array $server The $_SERVER array + * + * @return bool true if the user is allowed to login + */ + public function canLogin($server) + { + $ip = $server['REMOTE_ADDR']; + + if (! isset($this->globals['IPBANS']['BANS'][$ip])) { + // the user is not banned + return true; + } + + if ($this->globals['IPBANS']['BANS'][$ip] > time()) { + // the user is still banned + return false; + } + + // the ban has expired, the user can attempt to log in again + logm($this->configManager->get('resource.log'), $server['REMOTE_ADDR'], 'Ban lifted.'); + unset($this->globals['IPBANS']['FAILURES'][$ip]); + unset($this->globals['IPBANS']['BANS'][$ip]); + + $this->writeBanFile(); + return true; + } +} diff --git a/application/security/SessionManager.php b/application/security/SessionManager.php new file mode 100644 index 00000000..b8b8ab8d --- /dev/null +++ b/application/security/SessionManager.php @@ -0,0 +1,199 @@ +session = &$session; + $this->conf = $conf; + } + + /** + * Define whether the user should stay signed in across browser sessions + * + * @param bool $staySignedIn Keep the user signed in + */ + public function setStaySignedIn($staySignedIn) + { + $this->staySignedIn = $staySignedIn; + } + + /** + * Generates a session token + * + * @return string token + */ + public function generateToken() + { + $token = sha1(uniqid('', true) .'_'. mt_rand() . $this->conf->get('credentials.salt')); + $this->session['tokens'][$token] = 1; + return $token; + } + + /** + * Checks the validity of a session token, and destroys it afterwards + * + * @param string $token The token to check + * + * @return bool true if the token is valid, else false + */ + public function checkToken($token) + { + if (! isset($this->session['tokens'][$token])) { + // the token is wrong, or has already been used + return false; + } + + // destroy the token to prevent future use + unset($this->session['tokens'][$token]); + return true; + } + + /** + * Validate session ID to prevent Full Path Disclosure. + * + * See #298. + * The session ID's format depends on the hash algorithm set in PHP settings + * + * @param string $sessionId Session ID + * + * @return true if valid, false otherwise. + * + * @see http://php.net/manual/en/function.hash-algos.php + * @see http://php.net/manual/en/session.configuration.php + */ + public static function checkId($sessionId) + { + if (empty($sessionId)) { + return false; + } + + if (!$sessionId) { + return false; + } + + if (!preg_match('/^[a-zA-Z0-9,-]{2,128}$/', $sessionId)) { + return false; + } + + return true; + } + + /** + * Store user login information after a successful login + * + * @param string $clientIpId Client IP address identifier + */ + public function storeLoginInfo($clientIpId) + { + $this->session['ip'] = $clientIpId; + $this->session['username'] = $this->conf->get('credentials.login'); + $this->extendTimeValidityBy(self::$SHORT_TIMEOUT); + } + + /** + * Extend session validity + */ + public function extendSession() + { + if ($this->staySignedIn) { + return $this->extendTimeValidityBy(self::$LONG_TIMEOUT); + } + return $this->extendTimeValidityBy(self::$SHORT_TIMEOUT); + } + + /** + * Extend expiration time + * + * @param int $duration Expiration time extension (seconds) + * + * @return int New session expiration time + */ + protected function extendTimeValidityBy($duration) + { + $expirationTime = time() + $duration; + $this->session['expires_on'] = $expirationTime; + return $expirationTime; + } + + /** + * Logout a user by unsetting all login information + * + * See: + * - https://secure.php.net/manual/en/function.setcookie.php + */ + public function logout() + { + if (isset($this->session)) { + unset($this->session['ip']); + unset($this->session['expires_on']); + unset($this->session['username']); + unset($this->session['visibility']); + unset($this->session['untaggedonly']); + } + } + + /** + * Check whether the session has expired + * + * @param string $clientIpId Client IP address identifier + * + * @return bool true if the session has expired, false otherwise + */ + public function hasSessionExpired() + { + if (empty($this->session['expires_on'])) { + return true; + } + if (time() >= $this->session['expires_on']) { + return true; + } + return false; + } + + /** + * Check whether the client IP address has changed + * + * @param string $clientIpId Client IP address identifier + * + * @return bool true if the IP has changed, false if it has not, or + * if session protection has been disabled + */ + public function hasClientIpChanged($clientIpId) + { + if ($this->conf->get('security.session_protection_disabled') === true) { + return false; + } + if (isset($this->session['ip']) && $this->session['ip'] === $clientIpId) { + return false; + } + return true; + } +} diff --git a/composer.json b/composer.json index 15e082f8..0d4c623c 100644 --- a/composer.json +++ b/composer.json @@ -36,7 +36,8 @@ "Shaarli\\Api\\Controllers\\": "application/api/controllers", "Shaarli\\Api\\Exceptions\\": "application/api/exceptions", "Shaarli\\Config\\": "application/config/", - "Shaarli\\Config\\Exception\\": "application/config/exception" + "Shaarli\\Config\\Exception\\": "application/config/exception", + "Shaarli\\Security\\": "application/security" } } } diff --git a/index.php b/index.php index 2fe3f821..c34434dd 100644 --- a/index.php +++ b/index.php @@ -78,8 +78,8 @@ require_once 'application/Updater.php'; use \Shaarli\Languages; use \Shaarli\ThemeUtils; use \Shaarli\Config\ConfigManager; -use \Shaarli\LoginManager; -use \Shaarli\SessionManager; +use \Shaarli\Security\LoginManager; +use \Shaarli\Security\SessionManager; // Ensure the PHP version is supported try { @@ -101,8 +101,6 @@ if (dirname($_SERVER['SCRIPT_NAME']) != '/') { // Set default cookie expiration and path. session_set_cookie_params($cookie['lifetime'], $cookiedir, $_SERVER['SERVER_NAME']); // Set session parameters on server side. -// If the user does not access any page within this time, his/her session is considered expired. -define('INACTIVITY_TIMEOUT', 3600); // in seconds. // Use cookies to store session. ini_set('session.use_cookies', 1); // Force cookies for session (phpsessionID forbidden in URL). @@ -123,8 +121,10 @@ if (isset($_COOKIE['shaarli']) && !SessionManager::checkId($_COOKIE['shaarli'])) } $conf = new ConfigManager(); -$loginManager = new LoginManager($GLOBALS, $conf); $sessionManager = new SessionManager($_SESSION, $conf); +$loginManager = new LoginManager($GLOBALS, $conf, $sessionManager); +$loginManager->generateStaySignedInToken($_SERVER['REMOTE_ADDR']); +$clientIpId = client_ip_id($_SERVER); // LC_MESSAGES isn't defined without php-intl, in this case use LC_COLLATE locale instead. if (! defined('LC_MESSAGES')) { @@ -177,157 +177,61 @@ if (! is_file($conf->getConfigFileExt())) { install($conf, $sessionManager); } -// a token depending of deployment salt, user password, and the current ip -define('STAY_SIGNED_IN_TOKEN', sha1($conf->get('credentials.hash') . $_SERVER['REMOTE_ADDR'] . $conf->get('credentials.salt'))); +$loginManager->checkLoginState($_COOKIE, $clientIpId); /** - * Checking session state (i.e. is the user still logged in) + * Adapter function to ensure compatibility with third-party templates * - * @param ConfigManager $conf The configuration manager. + * @see https://github.com/shaarli/Shaarli/pull/1086 * - * @return bool: true if the user is logged in, false otherwise. + * @return bool true when the user is logged in, false otherwise */ -function setup_login_state($conf) -{ - if ($conf->get('security.open_shaarli')) { - return true; - } - $userIsLoggedIn = false; // By default, we do not consider the user as logged in; - $loginFailure = false; // If set to true, every attempt to authenticate the user will fail. This indicates that an important condition isn't met. - if (! $conf->exists('credentials.login')) { - $userIsLoggedIn = false; // Shaarli is not configured yet. - $loginFailure = true; - } - if (isset($_COOKIE['shaarli_staySignedIn']) && - $_COOKIE['shaarli_staySignedIn']===STAY_SIGNED_IN_TOKEN && - !$loginFailure) - { - fillSessionInfo($conf); - $userIsLoggedIn = true; - } - // If session does not exist on server side, or IP address has changed, or session has expired, logout. - if (empty($_SESSION['uid']) - || ($conf->get('security.session_protection_disabled') === false && $_SESSION['ip'] != allIPs()) - || time() >= $_SESSION['expires_on']) - { - logout(); - $userIsLoggedIn = false; - $loginFailure = true; - } - if (!empty($_SESSION['longlastingsession'])) { - $_SESSION['expires_on']=time()+$_SESSION['longlastingsession']; // In case of "Stay signed in" checked. - } - else { - $_SESSION['expires_on']=time()+INACTIVITY_TIMEOUT; // Standard session expiration date. - } - if (!$loginFailure) { - $userIsLoggedIn = true; - } - - return $userIsLoggedIn; -} -$userIsLoggedIn = setup_login_state($conf); - -// ------------------------------------------------------------------------------------------ -// Session management - -// Returns the IP address of the client (Used to prevent session cookie hijacking.) -function allIPs() -{ - $ip = $_SERVER['REMOTE_ADDR']; - // Then we use more HTTP headers to prevent session hijacking from users behind the same proxy. - if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { $ip=$ip.'_'.$_SERVER['HTTP_X_FORWARDED_FOR']; } - if (isset($_SERVER['HTTP_CLIENT_IP'])) { $ip=$ip.'_'.$_SERVER['HTTP_CLIENT_IP']; } - return $ip; -} - -/** - * Load user session. - * - * @param ConfigManager $conf Configuration Manager instance. - */ -function fillSessionInfo($conf) -{ - $_SESSION['uid'] = sha1(uniqid('',true).'_'.mt_rand()); // Generate unique random number (different than phpsessionid) - $_SESSION['ip']=allIPs(); // We store IP address(es) of the client to make sure session is not hijacked. - $_SESSION['username']= $conf->get('credentials.login'); - $_SESSION['expires_on']=time()+INACTIVITY_TIMEOUT; // Set session expiration. -} - -/** - * Check that user/password is correct. - * - * @param string $login Username - * @param string $password User password - * @param ConfigManager $conf Configuration Manager instance. - * - * @return bool: authentication successful or not. - */ -function check_auth($login, $password, $conf) -{ - $hash = sha1($password . $login . $conf->get('credentials.salt')); - if ($login == $conf->get('credentials.login') && $hash == $conf->get('credentials.hash')) - { // Login/password is correct. - fillSessionInfo($conf); - logm($conf->get('resource.log'), $_SERVER['REMOTE_ADDR'], 'Login successful'); - return true; - } - logm($conf->get('resource.log'), $_SERVER['REMOTE_ADDR'], 'Login failed for user '.$login); - return false; -} - -// Returns true if the user is logged in. function isLoggedIn() { - global $userIsLoggedIn; - return $userIsLoggedIn; + global $loginManager; + return $loginManager->isLoggedIn(); } -// Force logout. -function logout() { - if (isset($_SESSION)) { - unset($_SESSION['uid']); - unset($_SESSION['ip']); - unset($_SESSION['username']); - unset($_SESSION['visibility']); - unset($_SESSION['untaggedonly']); - } - setcookie('shaarli_staySignedIn', FALSE, 0, WEB_PATH); -} // ------------------------------------------------------------------------------------------ // Process login form: Check if login/password is correct. -if (isset($_POST['login'])) -{ +if (isset($_POST['login'])) { if (! $loginManager->canLogin($_SERVER)) { die(t('I said: NO. You are banned for the moment. Go away.')); } if (isset($_POST['password']) && $sessionManager->checkToken($_POST['token']) - && (check_auth($_POST['login'], $_POST['password'], $conf)) + && $loginManager->checkCredentials($_SERVER['REMOTE_ADDR'], $clientIpId, $_POST['login'], $_POST['password']) ) { - // Login/password is OK. $loginManager->handleSuccessfulLogin($_SERVER); - // If user wants to keep the session cookie even after the browser closes: - if (!empty($_POST['longlastingsession'])) { - $_SESSION['longlastingsession'] = 31536000; // (31536000 seconds = 1 year) - $expiration = time() + $_SESSION['longlastingsession']; // calculate relative cookie expiration (1 year from now) - setcookie('shaarli_staySignedIn', STAY_SIGNED_IN_TOKEN, $expiration, WEB_PATH); - $_SESSION['expires_on'] = $expiration; // Set session expiration on server-side. - - $cookiedir = ''; if(dirname($_SERVER['SCRIPT_NAME'])!='/') $cookiedir=dirname($_SERVER["SCRIPT_NAME"]).'/'; - session_set_cookie_params($_SESSION['longlastingsession'],$cookiedir,$_SERVER['SERVER_NAME']); // Set session cookie expiration on client side + $cookiedir = ''; + if (dirname($_SERVER['SCRIPT_NAME']) != '/') { // Note: Never forget the trailing slash on the cookie path! - session_regenerate_id(true); // Send cookie with new expiration date to browser. + $cookiedir = dirname($_SERVER["SCRIPT_NAME"]) . '/'; } - else // Standard session expiration (=when browser closes) - { - $cookiedir = ''; if(dirname($_SERVER['SCRIPT_NAME'])!='/') $cookiedir=dirname($_SERVER["SCRIPT_NAME"]).'/'; - session_set_cookie_params(0,$cookiedir,$_SERVER['SERVER_NAME']); // 0 means "When browser closes" - session_regenerate_id(true); + + if (!empty($_POST['longlastingsession'])) { + // Keep the session cookie even after the browser closes + $sessionManager->setStaySignedIn(true); + $expirationTime = $sessionManager->extendSession(); + + setcookie( + $loginManager::$STAY_SIGNED_IN_COOKIE, + $loginManager->getStaySignedInToken(), + $expirationTime, + WEB_PATH + ); + + } else { + // Standard session expiration (=when browser closes) + $expirationTime = 0; } + // Send cookie with the new expiration date to the browser + session_set_cookie_params($expirationTime, $cookiedir, $_SERVER['SERVER_NAME']); + session_regenerate_id(true); + // Optional redirect after login: if (isset($_GET['post'])) { $uri = '?post='. urlencode($_GET['post']); @@ -380,15 +284,16 @@ if (!isset($_SESSION['tokens'])) $_SESSION['tokens']=array(); // Token are atta * Gives the last 7 days (which have links). * This RSS feed cannot be filtered. * - * @param ConfigManager $conf Configuration Manager instance. + * @param ConfigManager $conf Configuration Manager instance + * @param LoginManager $loginManager LoginManager instance */ -function showDailyRSS($conf) { +function showDailyRSS($conf, $loginManager) { // Cache system $query = $_SERVER['QUERY_STRING']; $cache = new CachedPage( $conf->get('config.PAGE_CACHE'), page_url($_SERVER), - startsWith($query,'do=dailyrss') && !isLoggedIn() + startsWith($query,'do=dailyrss') && !$loginManager->isLoggedIn() ); $cached = $cache->cachedVersion(); if (!empty($cached)) { @@ -400,7 +305,7 @@ function showDailyRSS($conf) { // Read links from database (and filter private links if used it not logged in). $LINKSDB = new LinkDB( $conf->get('resource.datastore'), - isLoggedIn(), + $loginManager->isLoggedIn(), $conf->get('privacy.hide_public_links'), $conf->get('redirector.url'), $conf->get('redirector.encode_url') @@ -482,9 +387,10 @@ function showDailyRSS($conf) { * @param PageBuilder $pageBuilder Template engine wrapper. * @param LinkDB $LINKSDB LinkDB instance. * @param ConfigManager $conf Configuration Manager instance. - * @param PluginManager $pluginManager Plugin Manager instane. + * @param PluginManager $pluginManager Plugin Manager instance. + * @param LoginManager $loginManager Login Manager instance */ -function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager) +function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager, $loginManager) { $day = date('Ymd', strtotime('-1 day')); // Yesterday, in format YYYYMMDD. if (isset($_GET['day'])) { @@ -542,7 +448,7 @@ function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager) /* Hook is called before column construction so that plugins don't have to deal with columns. */ - $pluginManager->executeHooks('render_daily', $data, array('loggedin' => isLoggedIn())); + $pluginManager->executeHooks('render_daily', $data, array('loggedin' => $loginManager->isLoggedIn())); /* We need to spread the articles on 3 columns. I did not want to use a JavaScript lib like http://masonry.desandro.com/ @@ -586,8 +492,8 @@ function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager) * @param ConfigManager $conf Configuration Manager instance. * @param PluginManager $pluginManager Plugin Manager instance. */ -function showLinkList($PAGE, $LINKSDB, $conf, $pluginManager) { - buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager); // Compute list of links to display +function showLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager) { + buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager, $loginManager); $PAGE->renderPage('linklist'); } @@ -607,7 +513,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, read_updates_file($conf->get('resource.updates')), $LINKSDB, $conf, - isLoggedIn() + $loginManager->isLoggedIn() ); try { $newUpdates = $updater->update(); @@ -622,18 +528,18 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, die($e->getMessage()); } - $PAGE = new PageBuilder($conf, $LINKSDB, $sessionManager->generateToken()); + $PAGE = new PageBuilder($conf, $LINKSDB, $sessionManager->generateToken(), $loginManager->isLoggedIn()); $PAGE->assign('linkcount', count($LINKSDB)); $PAGE->assign('privateLinkcount', count_private($LINKSDB)); $PAGE->assign('plugin_errors', $pluginManager->getErrors()); // Determine which page will be rendered. $query = (isset($_SERVER['QUERY_STRING'])) ? $_SERVER['QUERY_STRING'] : ''; - $targetPage = Router::findPage($query, $_GET, isLoggedIn()); + $targetPage = Router::findPage($query, $_GET, $loginManager->isLoggedIn()); if ( // if the user isn't logged in - !isLoggedIn() && + !$loginManager->isLoggedIn() && // and Shaarli doesn't have public content... $conf->get('privacy.hide_public_links') && // and is configured to enforce the login @@ -661,7 +567,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, $pluginManager->executeHooks('render_' . $name, $plugin_data, array( 'target' => $targetPage, - 'loggedin' => isLoggedIn() + 'loggedin' => $loginManager->isLoggedIn() ) ); $PAGE->assign('plugins_' . $name, $plugin_data); @@ -686,7 +592,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=logout')) { invalidateCaches($conf->get('resource.page_cache')); - logout(); + $sessionManager->logout(); + setcookie(LoginManager::$STAY_SIGNED_IN_COOKIE, 'false', 0, WEB_PATH); header('Location: ?'); exit; } @@ -713,7 +620,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, $data = array( 'linksToDisplay' => $linksToDisplay, ); - $pluginManager->executeHooks('render_picwall', $data, array('loggedin' => isLoggedIn())); + $pluginManager->executeHooks('render_picwall', $data, array('loggedin' => $loginManager->isLoggedIn())); foreach ($data as $key => $value) { $PAGE->assign($key, $value); @@ -760,7 +667,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, 'search_tags' => $searchTags, 'tags' => $tagList, ); - $pluginManager->executeHooks('render_tagcloud', $data, array('loggedin' => isLoggedIn())); + $pluginManager->executeHooks('render_tagcloud', $data, array('loggedin' => $loginManager->isLoggedIn())); foreach ($data as $key => $value) { $PAGE->assign($key, $value); @@ -793,7 +700,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, 'search_tags' => $searchTags, 'tags' => $tags, ]; - $pluginManager->executeHooks('render_taglist', $data, ['loggedin' => isLoggedIn()]); + $pluginManager->executeHooks('render_taglist', $data, ['loggedin' => $loginManager->isLoggedIn()]); foreach ($data as $key => $value) { $PAGE->assign($key, $value); @@ -807,7 +714,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, // Daily page. if ($targetPage == Router::$PAGE_DAILY) { - showDaily($PAGE, $LINKSDB, $conf, $pluginManager); + showDaily($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager); } // ATOM and RSS feed. @@ -820,7 +727,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, $cache = new CachedPage( $conf->get('resource.page_cache'), page_url($_SERVER), - startsWith($query,'do='. $targetPage) && !isLoggedIn() + startsWith($query,'do='. $targetPage) && !$loginManager->isLoggedIn() ); $cached = $cache->cachedVersion(); if (!empty($cached)) { @@ -829,15 +736,15 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, } // Generate data. - $feedGenerator = new FeedBuilder($LINKSDB, $feedType, $_SERVER, $_GET, isLoggedIn()); + $feedGenerator = new FeedBuilder($LINKSDB, $feedType, $_SERVER, $_GET, $loginManager->isLoggedIn()); $feedGenerator->setLocale(strtolower(setlocale(LC_COLLATE, 0))); - $feedGenerator->setHideDates($conf->get('privacy.hide_timestamps') && !isLoggedIn()); + $feedGenerator->setHideDates($conf->get('privacy.hide_timestamps') && !$loginManager->isLoggedIn()); $feedGenerator->setUsePermalinks(isset($_GET['permalinks']) || !$conf->get('feed.rss_permalinks')); $data = $feedGenerator->buildData(); // Process plugin hook. $pluginManager->executeHooks('render_feed', $data, array( - 'loggedin' => isLoggedIn(), + 'loggedin' => $loginManager->isLoggedIn(), 'target' => $targetPage, )); @@ -985,7 +892,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, } // -------- Handle other actions allowed for non-logged in users: - if (!isLoggedIn()) + if (!$loginManager->isLoggedIn()) { // User tries to post new link but is not logged in: // Show login screen, then redirect to ?post=... @@ -1001,7 +908,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, exit; } - showLinkList($PAGE, $LINKSDB, $conf, $pluginManager); + showLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager); if (isset($_GET['edit_link'])) { header('Location: ?do=login&edit_link='. escape($_GET['edit_link'])); exit; @@ -1052,7 +959,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, $conf->set('credentials.salt', sha1(uniqid('', true) .'_'. mt_rand())); $conf->set('credentials.hash', sha1($_POST['setpassword'] . $conf->get('credentials.login') . $conf->get('credentials.salt'))); try { - $conf->write(isLoggedIn()); + $conf->write($loginManager->isLoggedIn()); } catch(Exception $e) { error_log( @@ -1103,7 +1010,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, $conf->set('translation.language', escape($_POST['language'])); try { - $conf->write(isLoggedIn()); + $conf->write($loginManager->isLoggedIn()); $history->updateSettings(); invalidateCaches($conf->get('resource.page_cache')); } @@ -1555,7 +1462,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, else { $conf->set('general.enabled_plugins', save_plugin_config($_POST)); } - $conf->write(isLoggedIn()); + $conf->write($loginManager->isLoggedIn()); $history->updateSettings(); } catch (Exception $e) { @@ -1580,7 +1487,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, } // -------- Otherwise, simply display search form and links: - showLinkList($PAGE, $LINKSDB, $conf, $pluginManager); + showLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager); exit; } @@ -1592,8 +1499,9 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, * @param LinkDB $LINKSDB LinkDB instance. * @param ConfigManager $conf Configuration Manager instance. * @param PluginManager $pluginManager Plugin Manager instance. + * @param LoginManager $loginManager LoginManager instance */ -function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager) +function buildLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager) { // Used in templates if (isset($_GET['searchtags'])) { @@ -1632,8 +1540,6 @@ function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager) $keys[] = $key; } - - // Select articles according to paging. $pagecount = ceil(count($keys) / $_SESSION['LINKS_PER_PAGE']); $pagecount = $pagecount == 0 ? 1 : $pagecount; @@ -1714,7 +1620,7 @@ function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager) $data['pagetitle'] .= '- '. $conf->get('general.title'); } - $pluginManager->executeHooks('render_linklist', $data, array('loggedin' => isLoggedIn())); + $pluginManager->executeHooks('render_linklist', $data, array('loggedin' => $loginManager->isLoggedIn())); foreach ($data as $key => $value) { $PAGE->assign($key, $value); @@ -1985,7 +1891,7 @@ function install($conf, $sessionManager) { ); try { // Everything is ok, let's create config file. - $conf->write(isLoggedIn()); + $conf->write($loginManager->isLoggedIn()); } catch(Exception $e) { error_log( @@ -2249,7 +2155,7 @@ try { $linkDb = new LinkDB( $conf->get('resource.datastore'), - isLoggedIn(), + $loginManager->isLoggedIn(), $conf->get('privacy.hide_public_links'), $conf->get('redirector.url'), $conf->get('redirector.encode_url') diff --git a/tests/HttpUtils/ClientIpIdTest.php b/tests/HttpUtils/ClientIpIdTest.php new file mode 100644 index 00000000..c15ac5cc --- /dev/null +++ b/tests/HttpUtils/ClientIpIdTest.php @@ -0,0 +1,52 @@ +assertEquals( + '10.1.167.42', + client_ip_id(['REMOTE_ADDR' => '10.1.167.42']) + ); + } + + /** + * Get a remote client ID based on its IP and proxy information (1) + */ + public function testClientIpIdRemoteForwarded() + { + $this->assertEquals( + '10.1.167.42_127.0.1.47', + client_ip_id([ + 'REMOTE_ADDR' => '10.1.167.42', + 'HTTP_X_FORWARDED_FOR' => '127.0.1.47' + ]) + ); + } + + /** + * Get a remote client ID based on its IP and proxy information (2) + */ + public function testClientIpIdRemoteForwardedClient() + { + $this->assertEquals( + '10.1.167.42_10.1.167.56_127.0.1.47', + client_ip_id([ + 'REMOTE_ADDR' => '10.1.167.42', + 'HTTP_X_FORWARDED_FOR' => '10.1.167.56', + 'HTTP_CLIENT_IP' => '127.0.1.47' + ]) + ); + } +} diff --git a/tests/SessionManagerTest.php b/tests/SessionManagerTest.php deleted file mode 100644 index aa75962a..00000000 --- a/tests/SessionManagerTest.php +++ /dev/null @@ -1,149 +0,0 @@ -generateToken(); - - $this->assertEquals(1, $session['tokens'][$token]); - $this->assertEquals(40, strlen($token)); - } - - /** - * Check a session token - */ - public function testCheckToken() - { - $token = '4dccc3a45ad9d03e5542b90c37d8db6d10f2b38b'; - $session = [ - 'tokens' => [ - $token => 1, - ], - ]; - $sessionManager = new SessionManager($session, self::$conf); - - // check and destroy the token - $this->assertTrue($sessionManager->checkToken($token)); - $this->assertFalse(isset($session['tokens'][$token])); - - // ensure the token has been destroyed - $this->assertFalse($sessionManager->checkToken($token)); - } - - /** - * Generate and check a session token - */ - public function testGenerateAndCheckToken() - { - $session = []; - $sessionManager = new SessionManager($session, self::$conf); - - $token = $sessionManager->generateToken(); - - // ensure a token has been generated - $this->assertEquals(1, $session['tokens'][$token]); - $this->assertEquals(40, strlen($token)); - - // check and destroy the token - $this->assertTrue($sessionManager->checkToken($token)); - $this->assertFalse(isset($session['tokens'][$token])); - - // ensure the token has been destroyed - $this->assertFalse($sessionManager->checkToken($token)); - } - - /** - * Check an invalid session token - */ - public function testCheckInvalidToken() - { - $session = []; - $sessionManager = new SessionManager($session, self::$conf); - - $this->assertFalse($sessionManager->checkToken('4dccc3a45ad9d03e5542b90c37d8db6d10f2b38b')); - } - - /** - * Test SessionManager::checkId with a valid ID - TEST ALL THE HASHES! - * - * This tests extensively covers all hash algorithms / bit representations - */ - public function testIsAnyHashSessionIdValid() - { - foreach (self::$sidHashes as $algo => $bpcs) { - foreach ($bpcs as $bpc => $hash) { - $this->assertTrue(SessionManager::checkId($hash)); - } - } - } - - /** - * Test checkId with a valid ID - SHA-1 hashes - */ - public function testIsSha1SessionIdValid() - { - $this->assertTrue(SessionManager::checkId(sha1('shaarli'))); - } - - /** - * Test checkId with a valid ID - SHA-256 hashes - */ - public function testIsSha256SessionIdValid() - { - $this->assertTrue(SessionManager::checkId(hash('sha256', 'shaarli'))); - } - - /** - * Test checkId with a valid ID - SHA-512 hashes - */ - public function testIsSha512SessionIdValid() - { - $this->assertTrue(SessionManager::checkId(hash('sha512', 'shaarli'))); - } - - /** - * Test checkId with invalid IDs. - */ - public function testIsSessionIdInvalid() - { - $this->assertFalse(SessionManager::checkId('')); - $this->assertFalse(SessionManager::checkId([])); - $this->assertFalse( - SessionManager::checkId('c0ZqcWF3VFE2NmJBdm1HMVQ0ZHJ3UmZPbTFsNGhkNHI=') - ); - } -} diff --git a/tests/LoginManagerTest.php b/tests/security/LoginManagerTest.php similarity index 54% rename from tests/LoginManagerTest.php rename to tests/security/LoginManagerTest.php index 4159038e..f26cd1eb 100644 --- a/tests/LoginManagerTest.php +++ b/tests/security/LoginManagerTest.php @@ -1,5 +1,5 @@ banFile); } + $this->passwordHash = sha1($this->password . $this->login . $this->salt); + $this->configManager = new \FakeConfigManager([ + 'credentials.login' => $this->login, + 'credentials.hash' => $this->passwordHash, + 'credentials.salt' => $this->salt, 'resource.ban_file' => $this->banFile, 'resource.log' => $this->logFile, 'security.ban_after' => 4, @@ -35,10 +79,15 @@ class LoginManagerTest extends TestCase 'security.trusted_proxies' => [$this->trustedProxy], ]); + $this->cookie = []; + $this->globals = &$GLOBALS; unset($this->globals['IPBANS']); - $this->loginManager = new LoginManager($this->globals, $this->configManager); + $this->session = []; + + $this->sessionManager = new SessionManager($this->session, $this->configManager); + $this->loginManager = new LoginManager($this->globals, $this->configManager, $this->sessionManager); $this->server['REMOTE_ADDR'] = $this->ipAddr; } @@ -59,7 +108,7 @@ class LoginManagerTest extends TestCase $this->banFile, " array('127.0.0.1' => 99));\n?>" ); - new LoginManager($this->globals, $this->configManager); + new LoginManager($this->globals, $this->configManager, null); $this->assertEquals(99, $this->globals['IPBANS']['FAILURES']['127.0.0.1']); } @@ -196,4 +245,130 @@ class LoginManagerTest extends TestCase $this->globals['IPBANS']['BANS'][$this->ipAddr] = time() - 3600; $this->assertTrue($this->loginManager->canLogin($this->server)); } + + /** + * Generate a token depending on the user credentials and client IP + */ + public function testGenerateStaySignedInToken() + { + $this->loginManager->generateStaySignedInToken($this->clientIpAddress); + + $this->assertEquals( + sha1($this->passwordHash . $this->clientIpAddress . $this->salt), + $this->loginManager->getStaySignedInToken() + ); + } + + /** + * Check user login - Shaarli has not yet been configured + */ + public function testCheckLoginStateNotConfigured() + { + $configManager = new \FakeConfigManager([ + 'resource.ban_file' => $this->banFile, + ]); + $loginManager = new LoginManager($this->globals, $configManager, null); + $loginManager->checkLoginState([], ''); + + $this->assertFalse($loginManager->isLoggedIn()); + } + + /** + * Check user login - the client cookie does not match the server token + */ + public function testCheckLoginStateStaySignedInWithInvalidToken() + { + // simulate a previous login + $this->session = [ + 'ip' => $this->clientIpAddress, + 'expires_on' => time() + 100, + ]; + $this->loginManager->generateStaySignedInToken($this->clientIpAddress); + $this->cookie[LoginManager::$STAY_SIGNED_IN_COOKIE] = 'nope'; + + $this->loginManager->checkLoginState($this->cookie, $this->clientIpAddress); + + $this->assertTrue($this->loginManager->isLoggedIn()); + $this->assertTrue(empty($this->session['username'])); + } + + /** + * Check user login - the client cookie matches the server token + */ + public function testCheckLoginStateStaySignedInWithValidToken() + { + $this->loginManager->generateStaySignedInToken($this->clientIpAddress); + $this->cookie[LoginManager::$STAY_SIGNED_IN_COOKIE] = $this->loginManager->getStaySignedInToken(); + + $this->loginManager->checkLoginState($this->cookie, $this->clientIpAddress); + + $this->assertTrue($this->loginManager->isLoggedIn()); + $this->assertEquals($this->login, $this->session['username']); + $this->assertEquals($this->clientIpAddress, $this->session['ip']); + } + + /** + * Check user login - the session has expired + */ + public function testCheckLoginStateSessionExpired() + { + $this->loginManager->generateStaySignedInToken($this->clientIpAddress); + $this->session['expires_on'] = time() - 100; + + $this->loginManager->checkLoginState($this->cookie, $this->clientIpAddress); + + $this->assertFalse($this->loginManager->isLoggedIn()); + } + + /** + * Check user login - the remote client IP has changed + */ + public function testCheckLoginStateClientIpChanged() + { + $this->loginManager->generateStaySignedInToken($this->clientIpAddress); + + $this->loginManager->checkLoginState($this->cookie, '10.7.157.98'); + + $this->assertFalse($this->loginManager->isLoggedIn()); + } + + /** + * Check user credentials - wrong login supplied + */ + public function testCheckCredentialsWrongLogin() + { + $this->assertFalse( + $this->loginManager->checkCredentials('', '', 'b4dl0g1n', $this->password) + ); + } + + /** + * Check user credentials - wrong password supplied + */ + public function testCheckCredentialsWrongPassword() + { + $this->assertFalse( + $this->loginManager->checkCredentials('', '', $this->login, 'b4dp455wd') + ); + } + + /** + * Check user credentials - wrong login and password supplied + */ + public function testCheckCredentialsWrongLoginAndPassword() + { + $this->assertFalse( + $this->loginManager->checkCredentials('', '', 'b4dl0g1n', 'b4dp455wd') + ); + } + + /** + * Check user credentials - correct login and password supplied + */ + public function testCheckCredentialsGoodLoginAndPassword() + { + $this->assertTrue( + $this->loginManager->checkCredentials('', '', $this->login, $this->password) + ); + } } diff --git a/tests/security/SessionManagerTest.php b/tests/security/SessionManagerTest.php new file mode 100644 index 00000000..9bd868f8 --- /dev/null +++ b/tests/security/SessionManagerTest.php @@ -0,0 +1,273 @@ +conf = new FakeConfigManager([ + 'credentials.login' => 'johndoe', + 'credentials.salt' => 'salt', + 'security.session_protection_disabled' => false, + ]); + $this->session = []; + $this->sessionManager = new SessionManager($this->session, $this->conf); + } + + /** + * Generate a session token + */ + public function testGenerateToken() + { + $token = $this->sessionManager->generateToken(); + + $this->assertEquals(1, $this->session['tokens'][$token]); + $this->assertEquals(40, strlen($token)); + } + + /** + * Check a session token + */ + public function testCheckToken() + { + $token = '4dccc3a45ad9d03e5542b90c37d8db6d10f2b38b'; + $session = [ + 'tokens' => [ + $token => 1, + ], + ]; + $sessionManager = new SessionManager($session, $this->conf); + + // check and destroy the token + $this->assertTrue($sessionManager->checkToken($token)); + $this->assertFalse(isset($session['tokens'][$token])); + + // ensure the token has been destroyed + $this->assertFalse($sessionManager->checkToken($token)); + } + + /** + * Generate and check a session token + */ + public function testGenerateAndCheckToken() + { + $token = $this->sessionManager->generateToken(); + + // ensure a token has been generated + $this->assertEquals(1, $this->session['tokens'][$token]); + $this->assertEquals(40, strlen($token)); + + // check and destroy the token + $this->assertTrue($this->sessionManager->checkToken($token)); + $this->assertFalse(isset($this->session['tokens'][$token])); + + // ensure the token has been destroyed + $this->assertFalse($this->sessionManager->checkToken($token)); + } + + /** + * Check an invalid session token + */ + public function testCheckInvalidToken() + { + $this->assertFalse($this->sessionManager->checkToken('4dccc3a45ad9d03e5542b90c37d8db6d10f2b38b')); + } + + /** + * Test SessionManager::checkId with a valid ID - TEST ALL THE HASHES! + * + * This tests extensively covers all hash algorithms / bit representations + */ + public function testIsAnyHashSessionIdValid() + { + foreach (self::$sidHashes as $algo => $bpcs) { + foreach ($bpcs as $bpc => $hash) { + $this->assertTrue(SessionManager::checkId($hash)); + } + } + } + + /** + * Test checkId with a valid ID - SHA-1 hashes + */ + public function testIsSha1SessionIdValid() + { + $this->assertTrue(SessionManager::checkId(sha1('shaarli'))); + } + + /** + * Test checkId with a valid ID - SHA-256 hashes + */ + public function testIsSha256SessionIdValid() + { + $this->assertTrue(SessionManager::checkId(hash('sha256', 'shaarli'))); + } + + /** + * Test checkId with a valid ID - SHA-512 hashes + */ + public function testIsSha512SessionIdValid() + { + $this->assertTrue(SessionManager::checkId(hash('sha512', 'shaarli'))); + } + + /** + * Test checkId with invalid IDs. + */ + public function testIsSessionIdInvalid() + { + $this->assertFalse(SessionManager::checkId('')); + $this->assertFalse(SessionManager::checkId([])); + $this->assertFalse( + SessionManager::checkId('c0ZqcWF3VFE2NmJBdm1HMVQ0ZHJ3UmZPbTFsNGhkNHI=') + ); + } + + /** + * Store login information after a successful login + */ + public function testStoreLoginInfo() + { + $this->sessionManager->storeLoginInfo('ip_id'); + + $this->assertGreaterThan(time(), $this->session['expires_on']); + $this->assertEquals('ip_id', $this->session['ip']); + $this->assertEquals('johndoe', $this->session['username']); + } + + /** + * Extend a server-side session by SessionManager::$SHORT_TIMEOUT + */ + public function testExtendSession() + { + $this->sessionManager->extendSession(); + + $this->assertGreaterThan(time(), $this->session['expires_on']); + $this->assertLessThanOrEqual( + time() + SessionManager::$SHORT_TIMEOUT, + $this->session['expires_on'] + ); + } + + /** + * Extend a server-side session by SessionManager::$LONG_TIMEOUT + */ + public function testExtendSessionStaySignedIn() + { + $this->sessionManager->setStaySignedIn(true); + $this->sessionManager->extendSession(); + + $this->assertGreaterThan(time(), $this->session['expires_on']); + $this->assertGreaterThan( + time() + SessionManager::$LONG_TIMEOUT - 10, + $this->session['expires_on'] + ); + $this->assertLessThanOrEqual( + time() + SessionManager::$LONG_TIMEOUT, + $this->session['expires_on'] + ); + } + + /** + * Unset session variables after logging out + */ + public function testLogout() + { + $this->session = [ + 'ip' => 'ip_id', + 'expires_on' => time() + 1000, + 'username' => 'johndoe', + 'visibility' => 'public', + 'untaggedonly' => false, + ]; + $this->sessionManager->logout(); + + $this->assertFalse(isset($this->session['ip'])); + $this->assertFalse(isset($this->session['expires_on'])); + $this->assertFalse(isset($this->session['username'])); + $this->assertFalse(isset($this->session['visibility'])); + $this->assertFalse(isset($this->session['untaggedonly'])); + } + + /** + * The session is active and expiration time has been reached + */ + public function testHasExpiredTimeElapsed() + { + $this->session['expires_on'] = time() - 10; + + $this->assertTrue($this->sessionManager->hasSessionExpired()); + } + + /** + * The session is active and expiration time has not been reached + */ + public function testHasNotExpired() + { + $this->session['expires_on'] = time() + 1000; + + $this->assertFalse($this->sessionManager->hasSessionExpired()); + } + + /** + * Session hijacking protection is disabled, we assume the IP has not changed + */ + public function testHasClientIpChangedNoSessionProtection() + { + $this->conf->set('security.session_protection_disabled', true); + + $this->assertFalse($this->sessionManager->hasClientIpChanged('')); + } + + /** + * The client IP identifier has not changed + */ + public function testHasClientIpChangedNope() + { + $this->session['ip'] = 'ip_id'; + $this->assertFalse($this->sessionManager->hasClientIpChanged('ip_id')); + } + + /** + * The client IP identifier has changed + */ + public function testHasClientIpChanged() + { + $this->session['ip'] = 'ip_id_one'; + $this->assertTrue($this->sessionManager->hasClientIpChanged('ip_id_two')); + } +} diff --git a/tests/utils/FakeConfigManager.php b/tests/utils/FakeConfigManager.php index 85434de7..360b34a9 100644 --- a/tests/utils/FakeConfigManager.php +++ b/tests/utils/FakeConfigManager.php @@ -42,4 +42,16 @@ class FakeConfigManager } return $key; } + + /** + * Check if a setting exists + * + * @param string $setting Asked setting, keys separated with dots + * + * @return bool true if the setting exists, false otherwise + */ + public function exists($setting) + { + return array_key_exists($setting, $this->values); + } } diff --git a/tpl/default/linklist.html b/tpl/default/linklist.html index d546be0a..322cddd5 100644 --- a/tpl/default/linklist.html +++ b/tpl/default/linklist.html @@ -136,7 +136,7 @@ {/if} - {if="isLoggedIn()"} + {if="$is_logged_in"}