]>
Commit | Line | Data |
---|---|---|
1 | <?php | |
2 | ||
3 | namespace Shaarli\Security; | |
4 | ||
5 | use Psr\Log\LoggerInterface; | |
6 | use Shaarli\Helper\FileUtils; | |
7 | ||
8 | /** | |
9 | * Class BanManager | |
10 | * | |
11 | * Failed login attempts will store the associated IP address. | |
12 | * After N failed attempts, the IP will be prevented from log in for duration D. | |
13 | * Both N and D can be set in the configuration file. | |
14 | * | |
15 | * @package Shaarli\Security | |
16 | */ | |
17 | class BanManager | |
18 | { | |
19 | /** @var array List of allowed proxies IP */ | |
20 | protected $trustedProxies; | |
21 | ||
22 | /** @var int Number of allowed failed attempt before the ban */ | |
23 | protected $nbAttempts; | |
24 | ||
25 | /** @var int Ban duration in seconds */ | |
26 | protected $banDuration; | |
27 | ||
28 | /** @var string Path to the file containing IP bans and failures */ | |
29 | protected $banFile; | |
30 | ||
31 | /** @var LoggerInterface Path to the log file, used to log bans */ | |
32 | protected $logger; | |
33 | ||
34 | /** @var array List of IP with their associated number of failed attempts */ | |
35 | protected $failures = []; | |
36 | ||
37 | /** @var array List of banned IP with their associated unban timestamp */ | |
38 | protected $bans = []; | |
39 | ||
40 | /** | |
41 | * BanManager constructor. | |
42 | * | |
43 | * @param array $trustedProxies List of allowed proxies IP | |
44 | * @param int $nbAttempts Number of allowed failed attempt before the ban | |
45 | * @param int $banDuration Ban duration in seconds | |
46 | * @param string $banFile Path to the file containing IP bans and failures | |
47 | * @param LoggerInterface $logger PSR-3 logger to save login attempts in log directory | |
48 | */ | |
49 | public function __construct($trustedProxies, $nbAttempts, $banDuration, $banFile, LoggerInterface $logger) | |
50 | { | |
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 | } |