*/
/**
- * Logs a message to a text file
+ * Format log using provided data.
*
- * The log format is compatible with fail2ban.
+ * @param string $message the message to log
+ * @param string|null $clientIp the client's remote IPv4/IPv6 address
*
- * @param string $logFile where to write the logs
- * @param string $clientIp the client's remote IPv4/IPv6 address
- * @param string $message the message to log
+ * @return string Formatted message to log
*/
-function logm($logFile, $clientIp, $message)
+function format_log(string $message, string $clientIp = null): string
{
- file_put_contents(
- $logFile,
- date('Y/m/d H:i:s').' - '.$clientIp.' - '.strval($message).PHP_EOL,
- FILE_APPEND
- );
+ $out = $message;
+
+ if (!empty($clientIp)) {
+ // Note: we keep the first dash to avoid breaking fail2ban configs
+ $out = '- ' . $clientIp . ' - ' . $out;
+ }
+
+ return $out;
}
/**
namespace Shaarli\Container;
use malkusch\lock\mutex\FlockMutex;
+use Psr\Log\LoggerInterface;
use Shaarli\Bookmark\BookmarkFileService;
use Shaarli\Bookmark\BookmarkServiceInterface;
use Shaarli\Config\ConfigManager;
/** @var LoginManager */
protected $login;
+ /** @var LoggerInterface */
+ protected $logger;
+
/** @var string|null */
protected $basePath = null;
ConfigManager $conf,
SessionManager $session,
CookieManager $cookieManager,
- LoginManager $login
+ LoginManager $login,
+ LoggerInterface $logger
) {
$this->conf = $conf;
$this->session = $session;
$this->login = $login;
$this->cookieManager = $cookieManager;
+ $this->logger = $logger;
}
public function build(): ShaarliContainer
$container['sessionManager'] = $this->session;
$container['cookieManager'] = $this->cookieManager;
$container['loginManager'] = $this->login;
+ $container['logger'] = $this->logger;
$container['basePath'] = $this->basePath;
$container['plugins'] = function (ShaarliContainer $container): PluginManager {
return new PageBuilder(
$container->conf,
$container->sessionManager->getSession(),
+ $container->logger,
$container->bookmarkService,
$container->sessionManager->generateToken(),
$container->loginManager->isLoggedIn()
namespace Shaarli\Container;
+use Psr\Log\LoggerInterface;
use Shaarli\Bookmark\BookmarkServiceInterface;
use Shaarli\Config\ConfigManager;
use Shaarli\Feed\FeedBuilder;
* @property History $history
* @property HttpAccess $httpAccess
* @property LoginManager $loginManager
+ * @property LoggerInterface $logger
* @property MetadataRetriever $metadataRetriever
* @property NetscapeBookmarkUtils $netscapeBookmarkUtils
* @property callable $notFoundHandler Overrides default Slim exception display
}
if (!$this->container->loginManager->checkCredentials(
- $this->container->environment['REMOTE_ADDR'],
client_ip_id($this->container->environment),
$request->getParam('login'),
$request->getParam('password')
namespace Shaarli\Render;
use Exception;
-use exceptions\MissingBasePathException;
+use Psr\Log\LoggerInterface;
use RainTPL;
use Shaarli\ApplicationUtils;
use Shaarli\Bookmark\BookmarkServiceInterface;
*/
protected $session;
+ /** @var LoggerInterface */
+ protected $logger;
+
/**
* @var BookmarkServiceInterface $bookmarkService instance.
*/
* PageBuilder constructor.
* $tpl is initialized at false for lazy loading.
*
- * @param ConfigManager $conf Configuration Manager instance (reference).
- * @param array $session $_SESSION array
- * @param BookmarkServiceInterface $linkDB instance.
- * @param string $token Session token
- * @param bool $isLoggedIn
+ * @param ConfigManager $conf Configuration Manager instance (reference).
+ * @param array $session $_SESSION array
+ * @param LoggerInterface $logger
+ * @param null $linkDB instance.
+ * @param null $token Session token
+ * @param bool $isLoggedIn
*/
- public function __construct(&$conf, $session, $linkDB = null, $token = null, $isLoggedIn = false)
- {
+ public function __construct(
+ ConfigManager &$conf,
+ array $session,
+ LoggerInterface $logger,
+ $linkDB = null,
+ $token = null,
+ $isLoggedIn = false
+ ) {
$this->tpl = false;
$this->conf = $conf;
$this->session = $session;
+ $this->logger = $logger;
$this->bookmarkService = $linkDB;
$this->token = $token;
$this->isLoggedIn = $isLoggedIn;
$this->tpl->assign('newVersion', escape($version));
$this->tpl->assign('versionError', '');
} catch (Exception $exc) {
- logm($this->conf->get('resource.log'), $_SERVER['REMOTE_ADDR'], $exc->getMessage());
+ $this->logger->error(format_log('Error: ' . $exc->getMessage(), client_ip_id($_SERVER)));
$this->tpl->assign('newVersion', '');
$this->tpl->assign('versionError', escape($exc->getMessage()));
}
namespace Shaarli\Security;
+use Psr\Log\LoggerInterface;
use Shaarli\FileUtils;
/**
/** @var string Path to the file containing IP bans and failures */
protected $banFile;
- /** @var string Path to the log file, used to log bans */
- protected $logFile;
+ /** @var LoggerInterface Path to the log file, used to log bans */
+ protected $logger;
/** @var array List of IP with their associated number of failed attempts */
protected $failures = [];
/**
* BanManager constructor.
*
- * @param array $trustedProxies List of allowed proxies IP
- * @param int $nbAttempts Number of allowed failed attempt before the ban
- * @param int $banDuration Ban duration in seconds
- * @param string $banFile Path to the file containing IP bans and failures
- * @param string $logFile Path to the log file, used to log bans
+ * @param array $trustedProxies List of allowed proxies IP
+ * @param int $nbAttempts Number of allowed failed attempt before the ban
+ * @param int $banDuration Ban duration in seconds
+ * @param string $banFile Path to the file containing IP bans and failures
+ * @param LoggerInterface $logger PSR-3 logger to save login attempts in log directory
*/
- public function __construct($trustedProxies, $nbAttempts, $banDuration, $banFile, $logFile) {
+ public function __construct($trustedProxies, $nbAttempts, $banDuration, $banFile, LoggerInterface $logger) {
$this->trustedProxies = $trustedProxies;
$this->nbAttempts = $nbAttempts;
$this->banDuration = $banDuration;
$this->banFile = $banFile;
- $this->logFile = $logFile;
+ $this->logger = $logger;
+
$this->readBanFile();
}
if ($this->failures[$ip] >= $this->nbAttempts) {
$this->bans[$ip] = time() + $this->banDuration;
- logm(
- $this->logFile,
- $server['REMOTE_ADDR'],
- 'IP address banned from login: '. $ip
- );
+ $this->logger->info(format_log('IP address banned from login: '. $ip, $ip));
}
$this->writeBanFile();
}
unset($this->failures[$ip]);
}
unset($this->bans[$ip]);
- logm($this->logFile, $server['REMOTE_ADDR'], 'Ban lifted for: '. $ip);
+ $this->logger->info(format_log('Ban lifted for: '. $ip, $ip));
$this->writeBanFile();
return false;
namespace Shaarli\Security;
use Exception;
+use Psr\Log\LoggerInterface;
use Shaarli\Config\ConfigManager;
/**
protected $staySignedInToken = '';
/** @var CookieManager */
protected $cookieManager;
+ /** @var LoggerInterface */
+ protected $logger;
/**
* Constructor
*
- * @param ConfigManager $configManager Configuration Manager instance
+ * @param ConfigManager $configManager Configuration Manager instance
* @param SessionManager $sessionManager SessionManager instance
- * @param CookieManager $cookieManager CookieManager instance
+ * @param CookieManager $cookieManager CookieManager instance
+ * @param BanManager $banManager
+ * @param LoggerInterface $logger Used to log login attempts
*/
- public function __construct($configManager, $sessionManager, $cookieManager)
- {
+ public function __construct(
+ ConfigManager $configManager,
+ SessionManager $sessionManager,
+ CookieManager $cookieManager,
+ BanManager $banManager,
+ LoggerInterface $logger
+ ) {
$this->configManager = $configManager;
$this->sessionManager = $sessionManager;
$this->cookieManager = $cookieManager;
- $this->banManager = new BanManager(
- $this->configManager->get('security.trusted_proxies', []),
- $this->configManager->get('security.ban_after'),
- $this->configManager->get('security.ban_duration'),
- $this->configManager->get('resource.ban_file', 'data/ipbans.php'),
- $this->configManager->get('resource.log')
- );
+ $this->banManager = $banManager;
+ $this->logger = $logger;
if ($this->configManager->get('security.open_shaarli') === true) {
$this->openShaarli = true;
/**
* 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)
+ public function checkCredentials($clientIpId, $login, $password)
{
- // Check login matches config
- if ($login !== $this->configManager->get('credentials.login')) {
- return false;
- }
-
// Check credentials
try {
$useLdapLogin = !empty($this->configManager->get('ldap.host'));
- if ((false === $useLdapLogin && $this->checkCredentialsFromLocalConfig($login, $password))
- || (true === $useLdapLogin && $this->checkCredentialsFromLdap($login, $password))
+ if ($login === $this->configManager->get('credentials.login')
+ && (
+ (false === $useLdapLogin && $this->checkCredentialsFromLocalConfig($login, $password))
+ || (true === $useLdapLogin && $this->checkCredentialsFromLdap($login, $password))
+ )
) {
- $this->sessionManager->storeLoginInfo($clientIpId);
- logm(
- $this->configManager->get('resource.log'),
- $remoteIp,
- 'Login successful'
- );
- return true;
+ $this->sessionManager->storeLoginInfo($clientIpId);
+ $this->logger->info(format_log('Login successful', $clientIpId));
+
+ return true;
}
- }
- catch(Exception $exception) {
- logm(
- $this->configManager->get('resource.log'),
- $remoteIp,
- 'Exception while checking credentials: ' . $exception
- );
+ } catch(Exception $exception) {
+ $this->logger->info(format_log('Exception while checking credentials: ' . $exception, $clientIpId));
}
- logm(
- $this->configManager->get('resource.log'),
- $remoteIp,
- 'Login failed for user ' . $login
- );
+ $this->logger->info(format_log('Login failed for user ' . $login, $clientIpId));
+
return false;
}
require_once __DIR__ . '/init.php';
+use Katzgrau\KLogger\Logger;
+use Psr\Log\LogLevel;
use Shaarli\Config\ConfigManager;
use Shaarli\Container\ContainerBuilder;
use Shaarli\Languages;
+use Shaarli\Security\BanManager;
use Shaarli\Security\CookieManager;
use Shaarli\Security\LoginManager;
use Shaarli\Security\SessionManager;
});
}
+$logger = new Logger(
+ dirname($conf->get('resource.log')),
+ !$conf->get('dev.debug') ? LogLevel::INFO : LogLevel::DEBUG,
+ ['filename' => basename($conf->get('resource.log'))]
+);
$sessionManager = new SessionManager($_SESSION, $conf, session_save_path());
$sessionManager->initialize();
$cookieManager = new CookieManager($_COOKIE);
-$loginManager = new LoginManager($conf, $sessionManager, $cookieManager);
+$banManager = new BanManager(
+ $conf->get('security.trusted_proxies', []),
+ $conf->get('security.ban_after'),
+ $conf->get('security.ban_duration'),
+ $conf->get('resource.ban_file', 'data/ipbans.php'),
+ $logger
+);
+$loginManager = new LoginManager($conf, $sessionManager, $cookieManager, $banManager, $logger);
$loginManager->generateStaySignedInToken($_SERVER['REMOTE_ADDR']);
// Sniff browser language and set date format accordingly.
$loginManager->checkLoginState(client_ip_id($_SERVER));
-$containerBuilder = new ContainerBuilder($conf, $sessionManager, $cookieManager, $loginManager);
+$containerBuilder = new ContainerBuilder($conf, $sessionManager, $cookieManager, $loginManager, $logger);
$container = $containerBuilder->build();
$app = new App($container);
}
/**
- * Log a message to a file - IPv4 client address
+ * Format a log a message - IPv4 client address
*/
- public function testLogmIp4()
+ public function testFormatLogIp4()
{
- $logMessage = 'IPv4 client connected';
- logm(self::$testLogFile, '127.0.0.1', $logMessage);
- list($date, $ip, $message) = $this->getLastLogEntry();
+ $message = 'IPv4 client connected';
+ $log = format_log($message, '127.0.0.1');
- $this->assertInstanceOf(
- 'DateTime',
- DateTime::createFromFormat(self::$dateFormat, $date)
- );
- $this->assertTrue(
- filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false
- );
- $this->assertEquals($logMessage, $message);
+ static::assertSame('- 127.0.0.1 - IPv4 client connected', $log);
}
/**
- * Log a message to a file - IPv6 client address
+ * Format a log a message - IPv6 client address
*/
- public function testLogmIp6()
+ public function testFormatLogIp6()
{
- $logMessage = 'IPv6 client connected';
- logm(self::$testLogFile, '2001:db8::ff00:42:8329', $logMessage);
- list($date, $ip, $message) = $this->getLastLogEntry();
+ $message = 'IPv6 client connected';
+ $log = format_log($message, '2001:db8::ff00:42:8329');
- $this->assertInstanceOf(
- 'DateTime',
- DateTime::createFromFormat(self::$dateFormat, $date)
- );
- $this->assertTrue(
- filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false
- );
- $this->assertEquals($logMessage, $message);
+ static::assertSame('- 2001:db8::ff00:42:8329 - IPv6 client connected', $log);
}
/**
namespace Shaarli\Container;
+use Psr\Log\LoggerInterface;
use Shaarli\Bookmark\BookmarkServiceInterface;
use Shaarli\Config\ConfigManager;
use Shaarli\Feed\FeedBuilder;
$this->conf,
$this->sessionManager,
$this->cookieManager,
- $this->loginManager
+ $this->loginManager,
+ $this->createMock(LoggerInterface::class)
);
}
static::assertInstanceOf(History::class, $container->history);
static::assertInstanceOf(HttpAccess::class, $container->httpAccess);
static::assertInstanceOf(LoginManager::class, $container->loginManager);
+ static::assertInstanceOf(LoggerInterface::class, $container->logger);
static::assertInstanceOf(MetadataRetriever::class, $container->metadataRetriever);
static::assertInstanceOf(NetscapeBookmarkUtils::class, $container->netscapeBookmarkUtils);
static::assertInstanceOf(PageBuilder::class, $container->pageBuilder);
$this->container->loginManager
->expects(static::once())
->method('checkCredentials')
- ->with('1.2.3.4', '1.2.3.4', 'bob', 'pass')
+ ->with('1.2.3.4', 'bob', 'pass')
->willReturn(true)
;
$this->container->loginManager->method('getStaySignedInToken')->willReturn(bin2hex(random_bytes(8)));
namespace Shaarli\Security;
+use Psr\Log\LoggerInterface;
use Shaarli\FileUtils;
use Shaarli\TestCase;
3,
1800,
$this->banFile,
- $this->logFile
+ $this->createMock(LoggerInterface::class)
);
}
}
namespace Shaarli\Security;
+use Psr\Log\LoggerInterface;
+use Shaarli\FakeConfigManager;
use Shaarli\TestCase;
/**
*/
class LoginManagerTest extends TestCase
{
- /** @var \FakeConfigManager Configuration Manager instance */
+ /** @var FakeConfigManager Configuration Manager instance */
protected $configManager = null;
/** @var LoginManager Login Manager instance */
/** @var CookieManager */
protected $cookieManager;
+ /** @var BanManager */
+ protected $banManager;
+
/**
* Prepare or reset test resources
*/
$this->passwordHash = sha1($this->password . $this->login . $this->salt);
- $this->configManager = new \FakeConfigManager([
+ $this->configManager = new FakeConfigManager([
'credentials.login' => $this->login,
'credentials.hash' => $this->passwordHash,
'credentials.salt' => $this->salt,
return $this->cookie[$key] ?? null;
});
$this->sessionManager = new SessionManager($this->session, $this->configManager, 'session_path');
- $this->loginManager = new LoginManager($this->configManager, $this->sessionManager, $this->cookieManager);
+ $this->banManager = $this->createMock(BanManager::class);
+ $this->loginManager = new LoginManager(
+ $this->configManager,
+ $this->sessionManager,
+ $this->cookieManager,
+ $this->banManager,
+ $this->createMock(LoggerInterface::class)
+ );
$this->server['REMOTE_ADDR'] = $this->ipAddr;
}
/**
* Record a failed login attempt
*/
- public function testHandleFailedLogin()
+ public function testHandleFailedLogin(): void
{
+ $this->banManager->expects(static::exactly(2))->method('handleFailedAttempt');
+ $this->banManager->method('isBanned')->willReturn(true);
+
$this->loginManager->handleFailedLogin($this->server);
$this->loginManager->handleFailedLogin($this->server);
- $this->assertFalse($this->loginManager->canLogin($this->server));
+
+ static::assertFalse($this->loginManager->canLogin($this->server));
}
/**
'REMOTE_ADDR' => $this->trustedProxy,
'HTTP_X_FORWARDED_FOR' => $this->ipAddr,
];
+
+ $this->banManager->expects(static::exactly(2))->method('handleFailedAttempt');
+ $this->banManager->method('isBanned')->willReturn(true);
+
$this->loginManager->handleFailedLogin($server);
$this->loginManager->handleFailedLogin($server);
+
$this->assertFalse($this->loginManager->canLogin($server));
}
*/
public function testCheckLoginStateNotConfigured()
{
- $configManager = new \FakeConfigManager([
+ $configManager = new FakeConfigManager([
'resource.ban_file' => $this->banFile,
]);
- $loginManager = new LoginManager($configManager, null, $this->cookieManager);
+ $loginManager = new LoginManager(
+ $configManager,
+ $this->sessionManager,
+ $this->cookieManager,
+ $this->banManager,
+ $this->createMock(LoggerInterface::class)
+ );
$loginManager->checkLoginState('');
$this->assertFalse($loginManager->isLoggedIn());
public function testCheckCredentialsWrongLogin()
{
$this->assertFalse(
- $this->loginManager->checkCredentials('', '', 'b4dl0g1n', $this->password)
+ $this->loginManager->checkCredentials('', 'b4dl0g1n', $this->password)
);
}
public function testCheckCredentialsWrongPassword()
{
$this->assertFalse(
- $this->loginManager->checkCredentials('', '', $this->login, 'b4dp455wd')
+ $this->loginManager->checkCredentials('', $this->login, 'b4dp455wd')
);
}
public function testCheckCredentialsWrongLoginAndPassword()
{
$this->assertFalse(
- $this->loginManager->checkCredentials('', '', 'b4dl0g1n', 'b4dp455wd')
+ $this->loginManager->checkCredentials('', 'b4dl0g1n', 'b4dp455wd')
);
}
public function testCheckCredentialsGoodLoginAndPassword()
{
$this->assertTrue(
- $this->loginManager->checkCredentials('', '', $this->login, $this->password)
+ $this->loginManager->checkCredentials('', $this->login, $this->password)
);
}
{
$this->configManager->set('ldap.host', 'dummy');
$this->assertFalse(
- $this->loginManager->checkCredentials('', '', $this->login, $this->password)
+ $this->loginManager->checkCredentials('', $this->login, $this->password)
);
}
namespace Shaarli\Security;
+use Shaarli\FakeConfigManager;
use Shaarli\TestCase;
/**
/** @var array Session ID hashes */
protected static $sidHashes = null;
- /** @var \FakeConfigManager ConfigManager substitute for testing */
+ /** @var FakeConfigManager ConfigManager substitute for testing */
protected $conf = null;
/** @var array $_SESSION array for testing */
*/
protected function setUp(): void
{
- $this->conf = new \FakeConfigManager([
+ $this->conf = new FakeConfigManager([
'credentials.login' => 'johndoe',
'credentials.salt' => 'salt',
'security.session_protection_disabled' => false,
<?php
+namespace Shaarli;
+
+use Shaarli\Config\ConfigManager;
+
/**
* Fake ConfigManager
*/
-class FakeConfigManager
+class FakeConfigManager extends ConfigManager
{
protected $values = [];
* @param string $key Key of the value to set
* @param mixed $value Value to set
*/
- public function set($key, $value)
+ public function set($key, $value, $write = false, $isLoggedIn = false)
{
$this->values[$key] = $value;
}
*
* @return mixed The value if set, else the name of the key
*/
- public function get($key)
+ public function get($key, $default = '')
{
if (isset($this->values[$key])) {
return $this->values[$key];