<?php
namespace Shaarli\Security;
use Shaarli\FileUtils;
/**
* Class BanManager
*
* Failed login attempts will store the associated IP address.
* After N failed attempts, the IP will be prevented from log in for duration D.
* Both N and D can be set in the configuration file.
*
* @package Shaarli\Security
*/
class BanManager
{
/** @var array List of allowed proxies IP */
protected $trustedProxies;
/** @var int Number of allowed failed attempt before the ban */
protected $nbAttempts;
/** @var int Ban duration in seconds */
protected $banDuration;
/** @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 array List of IP with their associated number of failed attempts */
protected $failures = [];
/** @var array List of banned IP with their associated unban timestamp */
protected $bans = [];
/**
* 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
*/
public function __construct($trustedProxies, $nbAttempts, $banDuration, $banFile, $logFile) {
$this->trustedProxies = $trustedProxies;
$this->nbAttempts = $nbAttempts;
$this->banDuration = $banDuration;
$this->banFile = $banFile;
$this->logFile = $logFile;
$this->readBanFile();
}
/**
* Handle a failed login and ban the IP after too many failed attempts
*
* @param array $server The $_SERVER array
*/
public function handleFailedAttempt($server)
{
$ip = $this->getIp($server);
// the IP is behind a trusted forward proxy, but is not forwarded
// in the HTTP headers, so we do nothing
if (empty($ip)) {
return;
}
// increment the fail count for this IP
if (isset($this->failures[$ip])) {
$this->failures[$ip]++;
} else {
$this->failures[$ip] = 1;
}
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->writeBanFile();
}
/**
* Remove failed attempts for the provided client.
*
* @param array $server $_SERVER
*/
public function clearFailures($server)
{
$ip = $this->getIp($server);
// the IP is behind a trusted forward proxy, but is not forwarded
// in the HTTP headers, so we do nothing
if (empty($ip)) {
return;
}
if (isset($this->failures[$ip])) {
unset($this->failures[$ip]);
}
$this->writeBanFile();
}
/**
* Check whether the client IP is banned or not.
*
* @param array $server $_SERVER
*
* @return bool True if the IP is banned, false otherwise
*/
public function isBanned($server)
{
$ip = $this->getIp($server);
// the IP is behind a trusted forward proxy, but is not forwarded
// in the HTTP headers, so we allow the authentication attempt.
if (empty($ip)) {
return false;
}
// the user is not banned
if (! isset($this->bans[$ip])) {
return false;
}
// the user is still banned
if ($this->bans[$ip] > time()) {
return true;
}
// the ban has expired, the user can attempt to log in again
if (isset($this->failures[$ip])) {
unset($this->failures[$ip]);
}
unset($this->bans[$ip]);
logm($this->logFile, $server['REMOTE_ADDR'], 'Ban lifted for: '. $ip);
$this->writeBanFile();
return false;
}
/**
* Retrieve the IP from $_SERVER.
* If the actual IP is behind an allowed reverse proxy,
* we try to extract the forwarded IP from HTTP headers.
*
* @param array $server $_SERVER
*
* @return string|bool The IP or false if none could be extracted
*/
protected function getIp($server)
{
$ip = $server['REMOTE_ADDR'];
if (! in_array($ip, $this->trustedProxies)) {
return $ip;
}
return getIpAddressFromProxy($server, $this->trustedProxies);
}
/**
* Read a file containing banned IPs
*/
protected function readBanFile()
{
$data = FileUtils::readFlatDB($this->banFile);
if (isset($data['failures']) && is_array($data['failures'])) {
$this->failures = $data['failures'];
}
if (isset($data['bans']) && is_array($data['bans'])) {
$this->bans = $data['bans'];
}
}
/**
* Write the banned IPs to a file
*/
protected function writeBanFile()
{
return FileUtils::writeFlatDB(
$this->banFile,
[
'failures' => $this->failures,
'bans' => $this->bans,
]
);
}
/**
* Get the Failures (for UT purpose).
*
* @return array
*/
public function getFailures()
{
return $this->failures;
}
/**
* Get the Bans (for UT purpose).
*
* @return array
*/
public function getBans()
{
return $this->bans;
}
}