Merge pull request #1086 from virtualtam/refactor/login

Refactor user login and session management
This commit is contained in:
VirtualTam 2018-06-03 18:26:32 +02:00 committed by GitHub
commit d9cd27322a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 1116 additions and 563 deletions

View file

@ -1,7 +1,7 @@
<?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)
@ -415,6 +415,37 @@ function getIpAddressFromProxy($server, $trustedIps)
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).

View file

@ -1,134 +0,0 @@
<?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;
}
}

View file

@ -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'])) {

View file

@ -1,83 +0,0 @@
<?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;
}
}

View file

@ -0,0 +1,265 @@
<?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;
}
}

View file

@ -0,0 +1,199 @@
<?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;
}
}

View file

@ -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"
}
}
}

240
index.php
View file

@ -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')

View file

@ -0,0 +1,52 @@
<?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'
])
);
}
}

View file

@ -1,149 +0,0 @@
<?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=')
);
}
}

View file

@ -1,5 +1,5 @@
<?php
namespace Shaarli;
namespace Shaarli\Security;
require_once 'tests/utils/FakeConfigManager.php';
use \PHPUnit\Framework\TestCase;
@ -9,15 +9,54 @@ 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
*/
@ -27,7 +66,12 @@ class LoginManagerTest extends TestCase
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,
@ -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,
"<?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']);
}
@ -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)
);
}
}

View file

@ -0,0 +1,273 @@
<?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'));
}
}

View file

@ -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);
}
}

View file

@ -136,7 +136,7 @@
<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>
@ -179,7 +179,7 @@
<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}">
@ -196,7 +196,7 @@
</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>
@ -236,7 +236,7 @@
{if="$link_plugin_counter - 1 != $counter"}&middot;{/if}
{/loop}
{/if}
{if="isLoggedIn()"}
{if="$is_logged_in"}
&middot;
<a href="?delete_link&amp;lf_linkdate={$value.id}&amp;token={$token}"
title="{$strDelete}" class="delete-link confirm-delete">

View file

@ -1,11 +1,11 @@
<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>

View file

@ -4,7 +4,7 @@
<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}
&middot;

View file

@ -17,7 +17,7 @@
{$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}
@ -50,7 +50,7 @@
<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>
@ -74,7 +74,7 @@
<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"
@ -120,7 +120,7 @@
</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">
@ -155,7 +155,7 @@
</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">

View file

@ -49,7 +49,7 @@
{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>&nbsp;&nbsp;
<a href="?do=changetag&fromtag={$key|urlencode}" class="rename-tag">
<i class="fa fa-pencil-square-o {$key}"></i>
@ -63,7 +63,7 @@
{$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>
@ -81,7 +81,7 @@
</div>
</div>
{if="isLoggedIn()===true"}
{if="$is_logged_in===true"}
<input type="hidden" name="taglist" value="{loop="$tags"}{$key} {/loop}"
{/if}

View file

@ -53,7 +53,7 @@
<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>

View file

@ -82,7 +82,7 @@
<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}">
@ -102,7 +102,7 @@
</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}">

View file

@ -1,5 +1,5 @@
<div class="paging">
{if="isLoggedIn()"}
{if="$is_logged_in"}
<div class="paging_privatelinks">
<a href="?visibility=private">
{if="$visibility=='private'"}

View file

@ -25,7 +25,7 @@
<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}

View file

@ -17,7 +17,7 @@
{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>
@ -46,7 +46,7 @@
</ul>
</div>
{if="!empty($plugin_errors) && isLoggedIn()"}
{if="!empty($plugin_errors) && $is_logged_in"}
<ul class="errors">
{loop="$plugin_errors"}
<li>{$value}</li>