]> git.immae.eu Git - github/shaarli/Shaarli.git/blob - application/security/BanManager.php
288cbde05fd2e77009b6622b8669729d02903ba6
[github/shaarli/Shaarli.git] / application / security / BanManager.php
1 <?php
2
3
4 namespace Shaarli\Security;
5
6 use Psr\Log\LoggerInterface;
7 use Shaarli\Helper\FileUtils;
8
9 /**
10 * Class BanManager
11 *
12 * Failed login attempts will store the associated IP address.
13 * After N failed attempts, the IP will be prevented from log in for duration D.
14 * Both N and D can be set in the configuration file.
15 *
16 * @package Shaarli\Security
17 */
18 class BanManager
19 {
20 /** @var array List of allowed proxies IP */
21 protected $trustedProxies;
22
23 /** @var int Number of allowed failed attempt before the ban */
24 protected $nbAttempts;
25
26 /** @var int Ban duration in seconds */
27 protected $banDuration;
28
29 /** @var string Path to the file containing IP bans and failures */
30 protected $banFile;
31
32 /** @var LoggerInterface Path to the log file, used to log bans */
33 protected $logger;
34
35 /** @var array List of IP with their associated number of failed attempts */
36 protected $failures = [];
37
38 /** @var array List of banned IP with their associated unban timestamp */
39 protected $bans = [];
40
41 /**
42 * BanManager constructor.
43 *
44 * @param array $trustedProxies List of allowed proxies IP
45 * @param int $nbAttempts Number of allowed failed attempt before the ban
46 * @param int $banDuration Ban duration in seconds
47 * @param string $banFile Path to the file containing IP bans and failures
48 * @param LoggerInterface $logger PSR-3 logger to save login attempts in log directory
49 */
50 public function __construct($trustedProxies, $nbAttempts, $banDuration, $banFile, LoggerInterface $logger) {
51 $this->trustedProxies = $trustedProxies;
52 $this->nbAttempts = $nbAttempts;
53 $this->banDuration = $banDuration;
54 $this->banFile = $banFile;
55 $this->logger = $logger;
56
57 $this->readBanFile();
58 }
59
60 /**
61 * Handle a failed login and ban the IP after too many failed attempts
62 *
63 * @param array $server The $_SERVER array
64 */
65 public function handleFailedAttempt($server)
66 {
67 $ip = $this->getIp($server);
68 // the IP is behind a trusted forward proxy, but is not forwarded
69 // in the HTTP headers, so we do nothing
70 if (empty($ip)) {
71 return;
72 }
73
74 // increment the fail count for this IP
75 if (isset($this->failures[$ip])) {
76 $this->failures[$ip]++;
77 } else {
78 $this->failures[$ip] = 1;
79 }
80
81 if ($this->failures[$ip] >= $this->nbAttempts) {
82 $this->bans[$ip] = time() + $this->banDuration;
83 $this->logger->info(format_log('IP address banned from login: '. $ip, $ip));
84 }
85 $this->writeBanFile();
86 }
87
88 /**
89 * Remove failed attempts for the provided client.
90 *
91 * @param array $server $_SERVER
92 */
93 public function clearFailures($server)
94 {
95 $ip = $this->getIp($server);
96 // the IP is behind a trusted forward proxy, but is not forwarded
97 // in the HTTP headers, so we do nothing
98 if (empty($ip)) {
99 return;
100 }
101
102 if (isset($this->failures[$ip])) {
103 unset($this->failures[$ip]);
104 }
105 $this->writeBanFile();
106 }
107
108 /**
109 * Check whether the client IP is banned or not.
110 *
111 * @param array $server $_SERVER
112 *
113 * @return bool True if the IP is banned, false otherwise
114 */
115 public function isBanned($server)
116 {
117 $ip = $this->getIp($server);
118 // the IP is behind a trusted forward proxy, but is not forwarded
119 // in the HTTP headers, so we allow the authentication attempt.
120 if (empty($ip)) {
121 return false;
122 }
123
124 // the user is not banned
125 if (! isset($this->bans[$ip])) {
126 return false;
127 }
128
129 // the user is still banned
130 if ($this->bans[$ip] > time()) {
131 return true;
132 }
133
134 // the ban has expired, the user can attempt to log in again
135 if (isset($this->failures[$ip])) {
136 unset($this->failures[$ip]);
137 }
138 unset($this->bans[$ip]);
139 $this->logger->info(format_log('Ban lifted for: '. $ip, $ip));
140
141 $this->writeBanFile();
142 return false;
143 }
144
145 /**
146 * Retrieve the IP from $_SERVER.
147 * If the actual IP is behind an allowed reverse proxy,
148 * we try to extract the forwarded IP from HTTP headers.
149 *
150 * @param array $server $_SERVER
151 *
152 * @return string|bool The IP or false if none could be extracted
153 */
154 protected function getIp($server)
155 {
156 $ip = $server['REMOTE_ADDR'];
157 if (! in_array($ip, $this->trustedProxies)) {
158 return $ip;
159 }
160 return getIpAddressFromProxy($server, $this->trustedProxies);
161 }
162
163 /**
164 * Read a file containing banned IPs
165 */
166 protected function readBanFile()
167 {
168 $data = FileUtils::readFlatDB($this->banFile);
169 if (isset($data['failures']) && is_array($data['failures'])) {
170 $this->failures = $data['failures'];
171 }
172
173 if (isset($data['bans']) && is_array($data['bans'])) {
174 $this->bans = $data['bans'];
175 }
176 }
177
178 /**
179 * Write the banned IPs to a file
180 */
181 protected function writeBanFile()
182 {
183 return FileUtils::writeFlatDB(
184 $this->banFile,
185 [
186 'failures' => $this->failures,
187 'bans' => $this->bans,
188 ]
189 );
190 }
191
192 /**
193 * Get the Failures (for UT purpose).
194 *
195 * @return array
196 */
197 public function getFailures()
198 {
199 return $this->failures;
200 }
201
202 /**
203 * Get the Bans (for UT purpose).
204 *
205 * @return array
206 */
207 public function getBans()
208 {
209 return $this->bans;
210 }
211 }