Merge pull request #1086 from virtualtam/refactor/login
Refactor user login and session management
This commit is contained in:
commit
d9cd27322a
23 changed files with 1116 additions and 563 deletions
|
@ -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).
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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'])) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
265
application/security/LoginManager.php
Normal file
265
application/security/LoginManager.php
Normal 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;
|
||||
}
|
||||
}
|
199
application/security/SessionManager.php
Normal file
199
application/security/SessionManager.php
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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
240
index.php
|
@ -78,8 +78,8 @@ require_once 'application/Updater.php';
|
|||
use \Shaarli\Languages;
|
||||
use \Shaarli\ThemeUtils;
|
||||
use \Shaarli\Config\ConfigManager;
|
||||
use \Shaarli\LoginManager;
|
||||
use \Shaarli\SessionManager;
|
||||
use \Shaarli\Security\LoginManager;
|
||||
use \Shaarli\Security\SessionManager;
|
||||
|
||||
// Ensure the PHP version is supported
|
||||
try {
|
||||
|
@ -101,8 +101,6 @@ if (dirname($_SERVER['SCRIPT_NAME']) != '/') {
|
|||
// Set default cookie expiration and path.
|
||||
session_set_cookie_params($cookie['lifetime'], $cookiedir, $_SERVER['SERVER_NAME']);
|
||||
// Set session parameters on server side.
|
||||
// If the user does not access any page within this time, his/her session is considered expired.
|
||||
define('INACTIVITY_TIMEOUT', 3600); // in seconds.
|
||||
// Use cookies to store session.
|
||||
ini_set('session.use_cookies', 1);
|
||||
// Force cookies for session (phpsessionID forbidden in URL).
|
||||
|
@ -123,8 +121,10 @@ if (isset($_COOKIE['shaarli']) && !SessionManager::checkId($_COOKIE['shaarli']))
|
|||
}
|
||||
|
||||
$conf = new ConfigManager();
|
||||
$loginManager = new LoginManager($GLOBALS, $conf);
|
||||
$sessionManager = new SessionManager($_SESSION, $conf);
|
||||
$loginManager = new LoginManager($GLOBALS, $conf, $sessionManager);
|
||||
$loginManager->generateStaySignedInToken($_SERVER['REMOTE_ADDR']);
|
||||
$clientIpId = client_ip_id($_SERVER);
|
||||
|
||||
// LC_MESSAGES isn't defined without php-intl, in this case use LC_COLLATE locale instead.
|
||||
if (! defined('LC_MESSAGES')) {
|
||||
|
@ -177,157 +177,61 @@ if (! is_file($conf->getConfigFileExt())) {
|
|||
install($conf, $sessionManager);
|
||||
}
|
||||
|
||||
// a token depending of deployment salt, user password, and the current ip
|
||||
define('STAY_SIGNED_IN_TOKEN', sha1($conf->get('credentials.hash') . $_SERVER['REMOTE_ADDR'] . $conf->get('credentials.salt')));
|
||||
$loginManager->checkLoginState($_COOKIE, $clientIpId);
|
||||
|
||||
/**
|
||||
* Checking session state (i.e. is the user still logged in)
|
||||
* Adapter function to ensure compatibility with third-party templates
|
||||
*
|
||||
* @param ConfigManager $conf The configuration manager.
|
||||
* @see https://github.com/shaarli/Shaarli/pull/1086
|
||||
*
|
||||
* @return bool: true if the user is logged in, false otherwise.
|
||||
* @return bool true when the user is logged in, false otherwise
|
||||
*/
|
||||
function setup_login_state($conf)
|
||||
{
|
||||
if ($conf->get('security.open_shaarli')) {
|
||||
return true;
|
||||
}
|
||||
$userIsLoggedIn = false; // By default, we do not consider the user as logged in;
|
||||
$loginFailure = false; // If set to true, every attempt to authenticate the user will fail. This indicates that an important condition isn't met.
|
||||
if (! $conf->exists('credentials.login')) {
|
||||
$userIsLoggedIn = false; // Shaarli is not configured yet.
|
||||
$loginFailure = true;
|
||||
}
|
||||
if (isset($_COOKIE['shaarli_staySignedIn']) &&
|
||||
$_COOKIE['shaarli_staySignedIn']===STAY_SIGNED_IN_TOKEN &&
|
||||
!$loginFailure)
|
||||
{
|
||||
fillSessionInfo($conf);
|
||||
$userIsLoggedIn = true;
|
||||
}
|
||||
// If session does not exist on server side, or IP address has changed, or session has expired, logout.
|
||||
if (empty($_SESSION['uid'])
|
||||
|| ($conf->get('security.session_protection_disabled') === false && $_SESSION['ip'] != allIPs())
|
||||
|| time() >= $_SESSION['expires_on'])
|
||||
{
|
||||
logout();
|
||||
$userIsLoggedIn = false;
|
||||
$loginFailure = true;
|
||||
}
|
||||
if (!empty($_SESSION['longlastingsession'])) {
|
||||
$_SESSION['expires_on']=time()+$_SESSION['longlastingsession']; // In case of "Stay signed in" checked.
|
||||
}
|
||||
else {
|
||||
$_SESSION['expires_on']=time()+INACTIVITY_TIMEOUT; // Standard session expiration date.
|
||||
}
|
||||
if (!$loginFailure) {
|
||||
$userIsLoggedIn = true;
|
||||
}
|
||||
|
||||
return $userIsLoggedIn;
|
||||
}
|
||||
$userIsLoggedIn = setup_login_state($conf);
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// Session management
|
||||
|
||||
// Returns the IP address of the client (Used to prevent session cookie hijacking.)
|
||||
function allIPs()
|
||||
{
|
||||
$ip = $_SERVER['REMOTE_ADDR'];
|
||||
// Then we use more HTTP headers to prevent session hijacking from users behind the same proxy.
|
||||
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { $ip=$ip.'_'.$_SERVER['HTTP_X_FORWARDED_FOR']; }
|
||||
if (isset($_SERVER['HTTP_CLIENT_IP'])) { $ip=$ip.'_'.$_SERVER['HTTP_CLIENT_IP']; }
|
||||
return $ip;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load user session.
|
||||
*
|
||||
* @param ConfigManager $conf Configuration Manager instance.
|
||||
*/
|
||||
function fillSessionInfo($conf)
|
||||
{
|
||||
$_SESSION['uid'] = sha1(uniqid('',true).'_'.mt_rand()); // Generate unique random number (different than phpsessionid)
|
||||
$_SESSION['ip']=allIPs(); // We store IP address(es) of the client to make sure session is not hijacked.
|
||||
$_SESSION['username']= $conf->get('credentials.login');
|
||||
$_SESSION['expires_on']=time()+INACTIVITY_TIMEOUT; // Set session expiration.
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that user/password is correct.
|
||||
*
|
||||
* @param string $login Username
|
||||
* @param string $password User password
|
||||
* @param ConfigManager $conf Configuration Manager instance.
|
||||
*
|
||||
* @return bool: authentication successful or not.
|
||||
*/
|
||||
function check_auth($login, $password, $conf)
|
||||
{
|
||||
$hash = sha1($password . $login . $conf->get('credentials.salt'));
|
||||
if ($login == $conf->get('credentials.login') && $hash == $conf->get('credentials.hash'))
|
||||
{ // Login/password is correct.
|
||||
fillSessionInfo($conf);
|
||||
logm($conf->get('resource.log'), $_SERVER['REMOTE_ADDR'], 'Login successful');
|
||||
return true;
|
||||
}
|
||||
logm($conf->get('resource.log'), $_SERVER['REMOTE_ADDR'], 'Login failed for user '.$login);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Returns true if the user is logged in.
|
||||
function isLoggedIn()
|
||||
{
|
||||
global $userIsLoggedIn;
|
||||
return $userIsLoggedIn;
|
||||
global $loginManager;
|
||||
return $loginManager->isLoggedIn();
|
||||
}
|
||||
|
||||
// Force logout.
|
||||
function logout() {
|
||||
if (isset($_SESSION)) {
|
||||
unset($_SESSION['uid']);
|
||||
unset($_SESSION['ip']);
|
||||
unset($_SESSION['username']);
|
||||
unset($_SESSION['visibility']);
|
||||
unset($_SESSION['untaggedonly']);
|
||||
}
|
||||
setcookie('shaarli_staySignedIn', FALSE, 0, WEB_PATH);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// Process login form: Check if login/password is correct.
|
||||
if (isset($_POST['login']))
|
||||
{
|
||||
if (isset($_POST['login'])) {
|
||||
if (! $loginManager->canLogin($_SERVER)) {
|
||||
die(t('I said: NO. You are banned for the moment. Go away.'));
|
||||
}
|
||||
if (isset($_POST['password'])
|
||||
&& $sessionManager->checkToken($_POST['token'])
|
||||
&& (check_auth($_POST['login'], $_POST['password'], $conf))
|
||||
&& $loginManager->checkCredentials($_SERVER['REMOTE_ADDR'], $clientIpId, $_POST['login'], $_POST['password'])
|
||||
) {
|
||||
// Login/password is OK.
|
||||
$loginManager->handleSuccessfulLogin($_SERVER);
|
||||
|
||||
// If user wants to keep the session cookie even after the browser closes:
|
||||
if (!empty($_POST['longlastingsession'])) {
|
||||
$_SESSION['longlastingsession'] = 31536000; // (31536000 seconds = 1 year)
|
||||
$expiration = time() + $_SESSION['longlastingsession']; // calculate relative cookie expiration (1 year from now)
|
||||
setcookie('shaarli_staySignedIn', STAY_SIGNED_IN_TOKEN, $expiration, WEB_PATH);
|
||||
$_SESSION['expires_on'] = $expiration; // Set session expiration on server-side.
|
||||
|
||||
$cookiedir = ''; if(dirname($_SERVER['SCRIPT_NAME'])!='/') $cookiedir=dirname($_SERVER["SCRIPT_NAME"]).'/';
|
||||
session_set_cookie_params($_SESSION['longlastingsession'],$cookiedir,$_SERVER['SERVER_NAME']); // Set session cookie expiration on client side
|
||||
$cookiedir = '';
|
||||
if (dirname($_SERVER['SCRIPT_NAME']) != '/') {
|
||||
// Note: Never forget the trailing slash on the cookie path!
|
||||
session_regenerate_id(true); // Send cookie with new expiration date to browser.
|
||||
$cookiedir = dirname($_SERVER["SCRIPT_NAME"]) . '/';
|
||||
}
|
||||
else // Standard session expiration (=when browser closes)
|
||||
{
|
||||
$cookiedir = ''; if(dirname($_SERVER['SCRIPT_NAME'])!='/') $cookiedir=dirname($_SERVER["SCRIPT_NAME"]).'/';
|
||||
session_set_cookie_params(0,$cookiedir,$_SERVER['SERVER_NAME']); // 0 means "When browser closes"
|
||||
session_regenerate_id(true);
|
||||
|
||||
if (!empty($_POST['longlastingsession'])) {
|
||||
// Keep the session cookie even after the browser closes
|
||||
$sessionManager->setStaySignedIn(true);
|
||||
$expirationTime = $sessionManager->extendSession();
|
||||
|
||||
setcookie(
|
||||
$loginManager::$STAY_SIGNED_IN_COOKIE,
|
||||
$loginManager->getStaySignedInToken(),
|
||||
$expirationTime,
|
||||
WEB_PATH
|
||||
);
|
||||
|
||||
} else {
|
||||
// Standard session expiration (=when browser closes)
|
||||
$expirationTime = 0;
|
||||
}
|
||||
|
||||
// Send cookie with the new expiration date to the browser
|
||||
session_set_cookie_params($expirationTime, $cookiedir, $_SERVER['SERVER_NAME']);
|
||||
session_regenerate_id(true);
|
||||
|
||||
// Optional redirect after login:
|
||||
if (isset($_GET['post'])) {
|
||||
$uri = '?post='. urlencode($_GET['post']);
|
||||
|
@ -380,15 +284,16 @@ if (!isset($_SESSION['tokens'])) $_SESSION['tokens']=array(); // Token are atta
|
|||
* Gives the last 7 days (which have links).
|
||||
* This RSS feed cannot be filtered.
|
||||
*
|
||||
* @param ConfigManager $conf Configuration Manager instance.
|
||||
* @param ConfigManager $conf Configuration Manager instance
|
||||
* @param LoginManager $loginManager LoginManager instance
|
||||
*/
|
||||
function showDailyRSS($conf) {
|
||||
function showDailyRSS($conf, $loginManager) {
|
||||
// Cache system
|
||||
$query = $_SERVER['QUERY_STRING'];
|
||||
$cache = new CachedPage(
|
||||
$conf->get('config.PAGE_CACHE'),
|
||||
page_url($_SERVER),
|
||||
startsWith($query,'do=dailyrss') && !isLoggedIn()
|
||||
startsWith($query,'do=dailyrss') && !$loginManager->isLoggedIn()
|
||||
);
|
||||
$cached = $cache->cachedVersion();
|
||||
if (!empty($cached)) {
|
||||
|
@ -400,7 +305,7 @@ function showDailyRSS($conf) {
|
|||
// Read links from database (and filter private links if used it not logged in).
|
||||
$LINKSDB = new LinkDB(
|
||||
$conf->get('resource.datastore'),
|
||||
isLoggedIn(),
|
||||
$loginManager->isLoggedIn(),
|
||||
$conf->get('privacy.hide_public_links'),
|
||||
$conf->get('redirector.url'),
|
||||
$conf->get('redirector.encode_url')
|
||||
|
@ -482,9 +387,10 @@ function showDailyRSS($conf) {
|
|||
* @param PageBuilder $pageBuilder Template engine wrapper.
|
||||
* @param LinkDB $LINKSDB LinkDB instance.
|
||||
* @param ConfigManager $conf Configuration Manager instance.
|
||||
* @param PluginManager $pluginManager Plugin Manager instane.
|
||||
* @param PluginManager $pluginManager Plugin Manager instance.
|
||||
* @param LoginManager $loginManager Login Manager instance
|
||||
*/
|
||||
function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager)
|
||||
function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager, $loginManager)
|
||||
{
|
||||
$day = date('Ymd', strtotime('-1 day')); // Yesterday, in format YYYYMMDD.
|
||||
if (isset($_GET['day'])) {
|
||||
|
@ -542,7 +448,7 @@ function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager)
|
|||
|
||||
/* Hook is called before column construction so that plugins don't have
|
||||
to deal with columns. */
|
||||
$pluginManager->executeHooks('render_daily', $data, array('loggedin' => isLoggedIn()));
|
||||
$pluginManager->executeHooks('render_daily', $data, array('loggedin' => $loginManager->isLoggedIn()));
|
||||
|
||||
/* We need to spread the articles on 3 columns.
|
||||
I did not want to use a JavaScript lib like http://masonry.desandro.com/
|
||||
|
@ -586,8 +492,8 @@ function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager)
|
|||
* @param ConfigManager $conf Configuration Manager instance.
|
||||
* @param PluginManager $pluginManager Plugin Manager instance.
|
||||
*/
|
||||
function showLinkList($PAGE, $LINKSDB, $conf, $pluginManager) {
|
||||
buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager); // Compute list of links to display
|
||||
function showLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager) {
|
||||
buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager, $loginManager);
|
||||
$PAGE->renderPage('linklist');
|
||||
}
|
||||
|
||||
|
@ -607,7 +513,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
|
|||
read_updates_file($conf->get('resource.updates')),
|
||||
$LINKSDB,
|
||||
$conf,
|
||||
isLoggedIn()
|
||||
$loginManager->isLoggedIn()
|
||||
);
|
||||
try {
|
||||
$newUpdates = $updater->update();
|
||||
|
@ -622,18 +528,18 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
|
|||
die($e->getMessage());
|
||||
}
|
||||
|
||||
$PAGE = new PageBuilder($conf, $LINKSDB, $sessionManager->generateToken());
|
||||
$PAGE = new PageBuilder($conf, $LINKSDB, $sessionManager->generateToken(), $loginManager->isLoggedIn());
|
||||
$PAGE->assign('linkcount', count($LINKSDB));
|
||||
$PAGE->assign('privateLinkcount', count_private($LINKSDB));
|
||||
$PAGE->assign('plugin_errors', $pluginManager->getErrors());
|
||||
|
||||
// Determine which page will be rendered.
|
||||
$query = (isset($_SERVER['QUERY_STRING'])) ? $_SERVER['QUERY_STRING'] : '';
|
||||
$targetPage = Router::findPage($query, $_GET, isLoggedIn());
|
||||
$targetPage = Router::findPage($query, $_GET, $loginManager->isLoggedIn());
|
||||
|
||||
if (
|
||||
// if the user isn't logged in
|
||||
!isLoggedIn() &&
|
||||
!$loginManager->isLoggedIn() &&
|
||||
// and Shaarli doesn't have public content...
|
||||
$conf->get('privacy.hide_public_links') &&
|
||||
// and is configured to enforce the login
|
||||
|
@ -661,7 +567,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
|
|||
$pluginManager->executeHooks('render_' . $name, $plugin_data,
|
||||
array(
|
||||
'target' => $targetPage,
|
||||
'loggedin' => isLoggedIn()
|
||||
'loggedin' => $loginManager->isLoggedIn()
|
||||
)
|
||||
);
|
||||
$PAGE->assign('plugins_' . $name, $plugin_data);
|
||||
|
@ -686,7 +592,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
|
|||
if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=logout'))
|
||||
{
|
||||
invalidateCaches($conf->get('resource.page_cache'));
|
||||
logout();
|
||||
$sessionManager->logout();
|
||||
setcookie(LoginManager::$STAY_SIGNED_IN_COOKIE, 'false', 0, WEB_PATH);
|
||||
header('Location: ?');
|
||||
exit;
|
||||
}
|
||||
|
@ -713,7 +620,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
|
|||
$data = array(
|
||||
'linksToDisplay' => $linksToDisplay,
|
||||
);
|
||||
$pluginManager->executeHooks('render_picwall', $data, array('loggedin' => isLoggedIn()));
|
||||
$pluginManager->executeHooks('render_picwall', $data, array('loggedin' => $loginManager->isLoggedIn()));
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
$PAGE->assign($key, $value);
|
||||
|
@ -760,7 +667,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
|
|||
'search_tags' => $searchTags,
|
||||
'tags' => $tagList,
|
||||
);
|
||||
$pluginManager->executeHooks('render_tagcloud', $data, array('loggedin' => isLoggedIn()));
|
||||
$pluginManager->executeHooks('render_tagcloud', $data, array('loggedin' => $loginManager->isLoggedIn()));
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
$PAGE->assign($key, $value);
|
||||
|
@ -793,7 +700,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
|
|||
'search_tags' => $searchTags,
|
||||
'tags' => $tags,
|
||||
];
|
||||
$pluginManager->executeHooks('render_taglist', $data, ['loggedin' => isLoggedIn()]);
|
||||
$pluginManager->executeHooks('render_taglist', $data, ['loggedin' => $loginManager->isLoggedIn()]);
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
$PAGE->assign($key, $value);
|
||||
|
@ -807,7 +714,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
|
|||
|
||||
// Daily page.
|
||||
if ($targetPage == Router::$PAGE_DAILY) {
|
||||
showDaily($PAGE, $LINKSDB, $conf, $pluginManager);
|
||||
showDaily($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager);
|
||||
}
|
||||
|
||||
// ATOM and RSS feed.
|
||||
|
@ -820,7 +727,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
|
|||
$cache = new CachedPage(
|
||||
$conf->get('resource.page_cache'),
|
||||
page_url($_SERVER),
|
||||
startsWith($query,'do='. $targetPage) && !isLoggedIn()
|
||||
startsWith($query,'do='. $targetPage) && !$loginManager->isLoggedIn()
|
||||
);
|
||||
$cached = $cache->cachedVersion();
|
||||
if (!empty($cached)) {
|
||||
|
@ -829,15 +736,15 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
|
|||
}
|
||||
|
||||
// Generate data.
|
||||
$feedGenerator = new FeedBuilder($LINKSDB, $feedType, $_SERVER, $_GET, isLoggedIn());
|
||||
$feedGenerator = new FeedBuilder($LINKSDB, $feedType, $_SERVER, $_GET, $loginManager->isLoggedIn());
|
||||
$feedGenerator->setLocale(strtolower(setlocale(LC_COLLATE, 0)));
|
||||
$feedGenerator->setHideDates($conf->get('privacy.hide_timestamps') && !isLoggedIn());
|
||||
$feedGenerator->setHideDates($conf->get('privacy.hide_timestamps') && !$loginManager->isLoggedIn());
|
||||
$feedGenerator->setUsePermalinks(isset($_GET['permalinks']) || !$conf->get('feed.rss_permalinks'));
|
||||
$data = $feedGenerator->buildData();
|
||||
|
||||
// Process plugin hook.
|
||||
$pluginManager->executeHooks('render_feed', $data, array(
|
||||
'loggedin' => isLoggedIn(),
|
||||
'loggedin' => $loginManager->isLoggedIn(),
|
||||
'target' => $targetPage,
|
||||
));
|
||||
|
||||
|
@ -985,7 +892,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
|
|||
}
|
||||
|
||||
// -------- Handle other actions allowed for non-logged in users:
|
||||
if (!isLoggedIn())
|
||||
if (!$loginManager->isLoggedIn())
|
||||
{
|
||||
// User tries to post new link but is not logged in:
|
||||
// Show login screen, then redirect to ?post=...
|
||||
|
@ -1001,7 +908,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
|
|||
exit;
|
||||
}
|
||||
|
||||
showLinkList($PAGE, $LINKSDB, $conf, $pluginManager);
|
||||
showLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager);
|
||||
if (isset($_GET['edit_link'])) {
|
||||
header('Location: ?do=login&edit_link='. escape($_GET['edit_link']));
|
||||
exit;
|
||||
|
@ -1052,7 +959,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
|
|||
$conf->set('credentials.salt', sha1(uniqid('', true) .'_'. mt_rand()));
|
||||
$conf->set('credentials.hash', sha1($_POST['setpassword'] . $conf->get('credentials.login') . $conf->get('credentials.salt')));
|
||||
try {
|
||||
$conf->write(isLoggedIn());
|
||||
$conf->write($loginManager->isLoggedIn());
|
||||
}
|
||||
catch(Exception $e) {
|
||||
error_log(
|
||||
|
@ -1103,7 +1010,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
|
|||
$conf->set('translation.language', escape($_POST['language']));
|
||||
|
||||
try {
|
||||
$conf->write(isLoggedIn());
|
||||
$conf->write($loginManager->isLoggedIn());
|
||||
$history->updateSettings();
|
||||
invalidateCaches($conf->get('resource.page_cache'));
|
||||
}
|
||||
|
@ -1555,7 +1462,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
|
|||
else {
|
||||
$conf->set('general.enabled_plugins', save_plugin_config($_POST));
|
||||
}
|
||||
$conf->write(isLoggedIn());
|
||||
$conf->write($loginManager->isLoggedIn());
|
||||
$history->updateSettings();
|
||||
}
|
||||
catch (Exception $e) {
|
||||
|
@ -1580,7 +1487,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
|
|||
}
|
||||
|
||||
// -------- Otherwise, simply display search form and links:
|
||||
showLinkList($PAGE, $LINKSDB, $conf, $pluginManager);
|
||||
showLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
@ -1592,8 +1499,9 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
|
|||
* @param LinkDB $LINKSDB LinkDB instance.
|
||||
* @param ConfigManager $conf Configuration Manager instance.
|
||||
* @param PluginManager $pluginManager Plugin Manager instance.
|
||||
* @param LoginManager $loginManager LoginManager instance
|
||||
*/
|
||||
function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager)
|
||||
function buildLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager)
|
||||
{
|
||||
// Used in templates
|
||||
if (isset($_GET['searchtags'])) {
|
||||
|
@ -1632,8 +1540,6 @@ function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager)
|
|||
$keys[] = $key;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Select articles according to paging.
|
||||
$pagecount = ceil(count($keys) / $_SESSION['LINKS_PER_PAGE']);
|
||||
$pagecount = $pagecount == 0 ? 1 : $pagecount;
|
||||
|
@ -1714,7 +1620,7 @@ function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager)
|
|||
$data['pagetitle'] .= '- '. $conf->get('general.title');
|
||||
}
|
||||
|
||||
$pluginManager->executeHooks('render_linklist', $data, array('loggedin' => isLoggedIn()));
|
||||
$pluginManager->executeHooks('render_linklist', $data, array('loggedin' => $loginManager->isLoggedIn()));
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
$PAGE->assign($key, $value);
|
||||
|
@ -1985,7 +1891,7 @@ function install($conf, $sessionManager) {
|
|||
);
|
||||
try {
|
||||
// Everything is ok, let's create config file.
|
||||
$conf->write(isLoggedIn());
|
||||
$conf->write($loginManager->isLoggedIn());
|
||||
}
|
||||
catch(Exception $e) {
|
||||
error_log(
|
||||
|
@ -2249,7 +2155,7 @@ try {
|
|||
|
||||
$linkDb = new LinkDB(
|
||||
$conf->get('resource.datastore'),
|
||||
isLoggedIn(),
|
||||
$loginManager->isLoggedIn(),
|
||||
$conf->get('privacy.hide_public_links'),
|
||||
$conf->get('redirector.url'),
|
||||
$conf->get('redirector.encode_url')
|
||||
|
|
52
tests/HttpUtils/ClientIpIdTest.php
Normal file
52
tests/HttpUtils/ClientIpIdTest.php
Normal 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'
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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=')
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
273
tests/security/SessionManagerTest.php
Normal file
273
tests/security/SessionManagerTest.php
Normal 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'));
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"}·{/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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
·
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
<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}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}">
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<div class="paging">
|
||||
{if="isLoggedIn()"}
|
||||
{if="$is_logged_in"}
|
||||
<div class="paging_privatelinks">
|
||||
<a href="?visibility=private">
|
||||
{if="$visibility=='private'"}
|
||||
|
|
|
@ -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}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue