<?php
/**
* GET an HTTP URL to retrieve its content
- * Uses the cURL library or a fallback method
+ * Uses the cURL library or a fallback method
*
* @param string $url URL to get (http://...)
* @param int $timeout network timeout (in seconds)
return array_pop($ips);
}
+
+/**
+ * Return an identifier based on the advertised client IP address(es)
+ *
+ * This aims at preventing session hijacking from users behind the same proxy
+ * by relying on HTTP headers.
+ *
+ * See:
+ * - https://secure.php.net/manual/en/reserved.variables.server.php
+ * - https://stackoverflow.com/questions/3003145/how-to-get-the-client-ip-address-in-php
+ * - https://stackoverflow.com/questions/12233406/preventing-session-hijacking
+ * - https://stackoverflow.com/questions/21354859/trusting-x-forwarded-for-to-identify-a-visitor
+ *
+ * @param array $server The $_SERVER array
+ *
+ * @return string An identifier based on client IP address information
+ */
+function client_ip_id($server)
+{
+ $ip = $server['REMOTE_ADDR'];
+
+ 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;
+}
+
+
/**
* Returns true if Shaarli's currently browsed in HTTPS.
* Supports reverse proxies (if the headers are correctly set).
+++ /dev/null
-<?php
-namespace Shaarli;
-
-/**
- * User login management
- */
-class LoginManager
-{
- protected $globals = [];
- protected $configManager = null;
- protected $banFile = '';
-
- /**
- * Constructor
- *
- * @param array $globals The $GLOBALS array (reference)
- * @param ConfigManager $configManager Configuration Manager instance.
- */
- public function __construct(& $globals, $configManager)
- {
- $this->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,
- "<?php\n\$GLOBALS['IPBANS']=" . var_export($this->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;
- }
-}
* @var LinkDB $linkDB instance.
*/
protected $linkDB;
+
+ /** @var bool $isLoggedIn Whether the user is logged in **/
+ protected $isLoggedIn = false;
/**
* PageBuilder constructor.
* @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;
}
/**
$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));
$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'])) {
+++ /dev/null
-<?php
-namespace Shaarli;
-
-/**
- * Manages the server-side session
- */
-class SessionManager
-{
- protected $session = [];
-
- /**
- * Constructor
- *
- * @param array $session The $_SESSION array (reference)
- * @param ConfigManager $conf ConfigManager instance
- */
- public function __construct(& $session, $conf)
- {
- $this->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;
- }
-}
--- /dev/null
+<?php
+namespace Shaarli\Security;
+
+use Shaarli\Config\ConfigManager;
+
+/**
+ * User login management
+ */
+class LoginManager
+{
+ /** @var string Name of the cookie set after logging in **/
+ public static $STAY_SIGNED_IN_COOKIE = 'shaarli_staySignedIn';
+
+ /** @var array A reference to the $_GLOBALS array */
+ protected $globals = [];
+
+ /** @var ConfigManager Configuration Manager instance **/
+ protected $configManager = null;
+
+ /** @var SessionManager Session Manager instance **/
+ protected $sessionManager = null;
+
+ /** @var string Path to the file containing IP bans */
+ protected $banFile = '';
+
+ /** @var bool Whether the user is logged in **/
+ protected $isLoggedIn = false;
+
+ /** @var bool Whether the Shaarli instance is open to public edition **/
+ protected $openShaarli = false;
+
+ /** @var string User sign-in token depending on remote IP and credentials */
+ protected $staySignedInToken = '';
+
+ /**
+ * Constructor
+ *
+ * @param array $globals The $GLOBALS array (reference)
+ * @param ConfigManager $configManager Configuration Manager instance
+ * @param SessionManager $sessionManager SessionManager instance
+ */
+ public function __construct(& $globals, $configManager, $sessionManager)
+ {
+ $this->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,
+ "<?php\n\$GLOBALS['IPBANS']=" . var_export($this->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;
+ }
+}
--- /dev/null
+<?php
+namespace Shaarli\Security;
+
+use Shaarli\Config\ConfigManager;
+
+/**
+ * Manages the server-side session
+ */
+class SessionManager
+{
+ /** @var int Session expiration timeout, in seconds */
+ public static $SHORT_TIMEOUT = 3600; // 1 hour
+
+ /** @var int Session expiration timeout, in seconds */
+ public static $LONG_TIMEOUT = 31536000; // 1 year
+
+ /** @var array Local reference to the global $_SESSION array */
+ protected $session = [];
+
+ /** @var ConfigManager Configuration Manager instance **/
+ protected $conf = null;
+
+ /** @var bool Whether the user should stay signed in (LONG_TIMEOUT) */
+ protected $staySignedIn = false;
+
+ /**
+ * Constructor
+ *
+ * @param array $session The $_SESSION array (reference)
+ * @param ConfigManager $conf ConfigManager instance
+ */
+ public function __construct(& $session, $conf)
+ {
+ $this->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;
+ }
+}
"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"
}
}
}
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 {
// 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).
}
$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')) {
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']);
* 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)) {
// 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')
* @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'])) {
/* 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/
* @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');
}
read_updates_file($conf->get('resource.updates')),
$LINKSDB,
$conf,
- isLoggedIn()
+ $loginManager->isLoggedIn()
);
try {
$newUpdates = $updater->update();
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
$pluginManager->executeHooks('render_' . $name, $plugin_data,
array(
'target' => $targetPage,
- 'loggedin' => isLoggedIn()
+ 'loggedin' => $loginManager->isLoggedIn()
)
);
$PAGE->assign('plugins_' . $name, $plugin_data);
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;
}
$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);
'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);
'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);
// Daily page.
if ($targetPage == Router::$PAGE_DAILY) {
- showDaily($PAGE, $LINKSDB, $conf, $pluginManager);
+ showDaily($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager);
}
// ATOM and RSS feed.
$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)) {
}
// 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,
));
}
// -------- 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=...
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;
$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(
$conf->set('translation.language', escape($_POST['language']));
try {
- $conf->write(isLoggedIn());
+ $conf->write($loginManager->isLoggedIn());
$history->updateSettings();
invalidateCaches($conf->get('resource.page_cache'));
}
else {
$conf->set('general.enabled_plugins', save_plugin_config($_POST));
}
- $conf->write(isLoggedIn());
+ $conf->write($loginManager->isLoggedIn());
$history->updateSettings();
}
catch (Exception $e) {
}
// -------- Otherwise, simply display search form and links:
- showLinkList($PAGE, $LINKSDB, $conf, $pluginManager);
+ showLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager);
exit;
}
* @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'])) {
$keys[] = $key;
}
-
-
// Select articles according to paging.
$pagecount = ceil(count($keys) / $_SESSION['LINKS_PER_PAGE']);
$pagecount = $pagecount == 0 ? 1 : $pagecount;
$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);
);
try {
// Everything is ok, let's create config file.
- $conf->write(isLoggedIn());
+ $conf->write($loginManager->isLoggedIn());
}
catch(Exception $e) {
error_log(
$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')
--- /dev/null
+<?php
+/**
+ * HttpUtils' tests
+ */
+
+require_once 'application/HttpUtils.php';
+
+/**
+ * Unitary tests for client_ip_id()
+ */
+class ClientIpIdTest extends PHPUnit_Framework_TestCase
+{
+ /**
+ * Get a remote client ID based on its IP
+ */
+ public function testClientIpIdRemote()
+ {
+ $this->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'
+ ])
+ );
+ }
+}
+++ /dev/null
-<?php
-require_once 'tests/utils/FakeConfigManager.php';
-
-// Initialize reference data _before_ PHPUnit starts a session
-require_once 'tests/utils/ReferenceSessionIdHashes.php';
-ReferenceSessionIdHashes::genAllHashes();
-
-use \Shaarli\SessionManager;
-use \PHPUnit\Framework\TestCase;
-
-
-/**
- * Test coverage for SessionManager
- */
-class SessionManagerTest extends TestCase
-{
- // Session ID hashes
- protected static $sidHashes = null;
-
- // Fake ConfigManager
- protected static $conf = null;
-
- /**
- * Assign reference data
- */
- public static function setUpBeforeClass()
- {
- self::$sidHashes = ReferenceSessionIdHashes::getHashes();
- self::$conf = new FakeConfigManager();
- }
-
- /**
- * Generate a session token
- */
- public function testGenerateToken()
- {
- $session = [];
- $sessionManager = new SessionManager($session, self::$conf);
-
- $token = $sessionManager->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=')
- );
- }
-}
<?php
-namespace Shaarli;
+namespace Shaarli\Security;
require_once 'tests/utils/FakeConfigManager.php';
use \PHPUnit\Framework\TestCase;
*/
class LoginManagerTest extends TestCase
{
+ /** @var \FakeConfigManager Configuration Manager instance */
protected $configManager = null;
+
+ /** @var LoginManager Login Manager instance */
protected $loginManager = null;
+
+ /** @var SessionManager Session Manager instance */
+ protected $sessionManager = null;
+
+ /** @var string Banned IP filename */
protected $banFile = 'sandbox/ipbans.php';
+
+ /** @var string Log filename */
protected $logFile = 'sandbox/shaarli.log';
+
+ /** @var array Simulates the $_COOKIE array */
+ protected $cookie = [];
+
+ /** @var array Simulates the $GLOBALS array */
protected $globals = [];
- protected $ipAddr = '127.0.0.1';
+
+ /** @var array Simulates the $_SERVER array */
protected $server = [];
+
+ /** @var array Simulates the $_SESSION array */
+ protected $session = [];
+
+ /** @var string Advertised client IP address */
+ protected $clientIpAddress = '10.1.47.179';
+
+ /** @var string Local client IP address */
+ protected $ipAddr = '127.0.0.1';
+
+ /** @var string Trusted proxy IP address */
protected $trustedProxy = '10.1.1.100';
+ /** @var string User login */
+ protected $login = 'johndoe';
+
+ /** @var string User password */
+ protected $password = 'IC4nHazL0g1n?';
+
+ /** @var string Hash of the salted user password */
+ protected $passwordHash = '';
+
+ /** @var string Salt used by hash functions */
+ protected $salt = '669e24fa9c5a59a613f98e8e38327384504a4af2';
+
/**
* Prepare or reset test resources
*/
unlink($this->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,
'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;
}
$this->banFile,
"<?php\n\$GLOBALS['IPBANS']=array('FAILURES' => 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']);
}
$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)
+ );
+ }
}
--- /dev/null
+<?php
+require_once 'tests/utils/FakeConfigManager.php';
+
+// Initialize reference data _before_ PHPUnit starts a session
+require_once 'tests/utils/ReferenceSessionIdHashes.php';
+ReferenceSessionIdHashes::genAllHashes();
+
+use \Shaarli\Security\SessionManager;
+use \PHPUnit\Framework\TestCase;
+
+
+/**
+ * Test coverage for SessionManager
+ */
+class SessionManagerTest extends TestCase
+{
+ /** @var array Session ID hashes */
+ protected static $sidHashes = null;
+
+ /** @var \FakeConfigManager ConfigManager substitute for testing */
+ protected $conf = null;
+
+ /** @var array $_SESSION array for testing */
+ protected $session = [];
+
+ /** @var SessionManager Server-side session management abstraction */
+ protected $sessionManager = null;
+
+ /**
+ * Assign reference data
+ */
+ public static function setUpBeforeClass()
+ {
+ self::$sidHashes = ReferenceSessionIdHashes::getHashes();
+ }
+
+ /**
+ * Initialize or reset test resources
+ */
+ public function setUp()
+ {
+ $this->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'));
+ }
+}
}
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);
+ }
}
<div class="linklist-item-thumbnail">{$thumb}</div>
{/if}
- {if="isLoggedIn()"}
+ {if="$is_logged_in"}
<div class="linklist-item-editbuttons">
{if="$value.private"}
<span class="label label-private">{$strPrivate}</span>
<div class="linklist-item-infos-date-url-block pure-g">
<div class="linklist-item-infos-dateblock pure-u-lg-7-12 pure-u-1">
- {if="isLoggedIn()"}
+ {if="$is_logged_in"}
<div class="linklist-item-infos-controls-group pure-u-0 pure-u-lg-visible">
<span class="linklist-item-infos-controls-item ctrl-checkbox">
<input type="checkbox" class="delete-checkbox" value="{$value.id}">
</div>
{/if}
<a href="?{$value.shorturl}" title="{$strPermalink}">
- {if="!$hide_timestamps || isLoggedIn()"}
+ {if="!$hide_timestamps || $is_logged_in"}
{$updated=$value.updated_timestamp ? $strEdited. format_date($value.updated) : $strPermalink}
<span class="linkdate" title="{$updated}">
<i class="fa fa-clock-o"></i>
{if="$link_plugin_counter - 1 != $counter"}·{/if}
{/loop}
{/if}
- {if="isLoggedIn()"}
+ {if="$is_logged_in"}
·
<a href="?delete_link&lf_linkdate={$value.id}&token={$token}"
title="{$strDelete}" class="delete-link confirm-delete">
<div class="linklist-paging">
<div class="paging pure-g">
<div class="linklist-filters pure-u-1-3">
- {if="isLoggedIn() or !empty($action_plugin)"}
+ {if="$is_logged_in or !empty($action_plugin)"}
<span class="linklist-filters-text pure-u-0 pure-u-lg-visible">
{'Filters'|t}
</span>
- {if="isLoggedIn()"}
+ {if="$is_logged_in"}
<a href="?visibility=private" title="{'Only display private links'|t}"
class="{if="$visibility==='private'"}filter-on{else}filter-off{/if}"
><i class="fa fa-user-secret"></i></a>
<div class="pure-u-2-24"></div>
<div id="footer" class="pure-u-20-24 footer-container">
<strong><a href="https://github.com/shaarli/Shaarli">Shaarli</a></strong>
- {if="isLoggedIn()===true"}
+ {if="$is_logged_in===true"}
{$version}
{/if}
·
{$shaarlititle}
</a>
</li>
- {if="isLoggedIn() || $openshaarli"}
+ {if="$is_logged_in || $openshaarli"}
<li class="pure-menu-item">
<a href="?do=addlink" class="pure-menu-link" id="shaarli-menu-shaare">
<i class="fa fa-plus" ></i> {'Shaare'|t}
<li class="pure-menu-item pure-u-lg-0 shaarli-menu-mobile" id="shaarli-menu-mobile-rss">
<a href="?do={$feed_type}{$searchcrits}" class="pure-menu-link">{'RSS Feed'|t}</a>
</li>
- {if="isLoggedIn()"}
+ {if="$is_logged_in"}
<li class="pure-menu-item pure-u-lg-0 shaarli-menu-mobile" id="shaarli-menu-mobile-logout">
<a href="?do=logout" class="pure-menu-link">{'Logout'|t}</a>
</li>
<i class="fa fa-rss"></i>
</a>
</li>
- {if="!isLoggedIn()"}
+ {if="!$is_logged_in"}
<li class="pure-menu-item" id="shaarli-menu-desktop-login">
<a href="?do=login" class="pure-menu-link"
data-open-id="header-login-form"
</div>
</div>
</div>
- {if="!isLoggedIn()"}
+ {if="!$is_logged_in"}
<form method="post" name="loginform">
<div class="subheader-form header-login-form" id="header-login-form">
<input type="text" name="login" placeholder="{'Username'|t}" tabindex="3">
</div>
{/if}
-{if="!empty($plugin_errors) && isLoggedIn()"}
+{if="!empty($plugin_errors) && $is_logged_in"}
<div class="pure-g new-version-message pure-alert pure-alert-error pure-alert-closable" id="shaarli-errors-alert">
<div class="pure-u-2-24"></div>
<div class="pure-u-20-24">
{loop="tags"}
<div class="tag-list-item pure-g" data-tag="{$key}">
<div class="pure-u-1">
- {if="isLoggedIn()===true"}
+ {if="$is_logged_in===true"}
<a href="#" class="delete-tag"><i class="fa fa-trash"></i></a>
<a href="?do=changetag&fromtag={$key|urlencode}" class="rename-tag">
<i class="fa fa-pencil-square-o {$key}"></i>
{$value}
{/loop}
</div>
- {if="isLoggedIn()===true"}
+ {if="$is_logged_in===true"}
<div class="rename-tag-form pure-u-1">
<input type="text" name="{$key}" value="{$key}" class="rename-tag-input" />
<a href="#" class="validate-rename-tag"><i class="fa fa-check"></i></a>
</div>
</div>
-{if="isLoggedIn()===true"}
+{if="$is_logged_in===true"}
<input type="hidden" name="taglist" value="{loop="$tags"}{$key} {/loop}"
{/if}
<img src="img/squiggle.png" width="25" height="26" title="permalink" alt="permalink">
</a>
</div>
- {if="!$hide_timestamps || isLoggedIn()"}
+ {if="!$hide_timestamps || $is_logged_in"}
<div class="dailyEntryLinkdate">
<a href="?{$value.shorturl}">{function="strftime('%c', $link.timestamp)"}</a>
</div>
<a id="{$value.shorturl}"></a>
<div class="thumbnail">{$value.url|thumbnail}</div>
<div class="linkcontainer">
- {if="isLoggedIn()"}
+ {if="$is_logged_in"}
<div class="linkeditbuttons">
<form method="GET" class="buttoneditform">
<input type="hidden" name="edit_link" value="{$value.id}">
</span>
<br>
{if="$value.description"}<div class="linkdescription">{$value.description}</div>{/if}
- {if="!$hide_timestamps || isLoggedIn()"}
+ {if="!$hide_timestamps || $is_logged_in"}
{$updated=$value.updated_timestamp ? 'Edited: '. format_date($value.updated) : 'Permalink'}
<span class="linkdate" title="Permalink">
<a href="?{$value.shorturl}">
<div class="paging">
-{if="isLoggedIn()"}
+{if="$is_logged_in"}
<div class="paging_privatelinks">
<a href="?visibility=private">
{if="$visibility=='private'"}
<script src="js/shaarli.min.js"></script>
-{if="isLoggedIn()"}
+{if="$is_logged_in"}
<script>function confirmDeleteLink() { var agree=confirm("Are you sure you want to delete this link ?"); if (agree) return true ; else return false ; }</script>
{/if}
{ignore} When called as a popup from bookmarklet, do not display menu. {/ignore}
{else}
<li><a href="{$titleLink}" class="nomobile">Home</a></li>
- {if="isLoggedIn()"}
+ {if="$is_logged_in"}
<li><a href="?do=logout">Logout</a></li>
<li><a href="?do=tools">Tools</a></li>
<li><a href="?do=addlink">Add link</a></li>
</ul>
</div>
-{if="!empty($plugin_errors) && isLoggedIn()"}
+{if="!empty($plugin_errors) && $is_logged_in"}
<ul class="errors">
{loop="$plugin_errors"}
<li>{$value}</li>