]>
Commit | Line | Data |
---|---|---|
b49a04f7 A |
1 | <?php |
2 | ||
3 | ||
4 | namespace Shaarli\Security; | |
5 | ||
6 | use Shaarli\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 string Path to the log file, used to log bans */ | |
32 | protected $logFile; | |
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 string $logFile Path to the log file, used to log bans | |
48 | */ | |
49 | public function __construct($trustedProxies, $nbAttempts, $banDuration, $banFile, $logFile) { | |
50 | $this->trustedProxies = $trustedProxies; | |
51 | $this->nbAttempts = $nbAttempts; | |
52 | $this->banDuration = $banDuration; | |
53 | $this->banFile = $banFile; | |
54 | $this->logFile = $logFile; | |
55 | $this->readBanFile(); | |
56 | } | |
57 | ||
58 | /** | |
59 | * Handle a failed login and ban the IP after too many failed attempts | |
60 | * | |
61 | * @param array $server The $_SERVER array | |
62 | */ | |
63 | public function handleFailedAttempt($server) | |
64 | { | |
65 | $ip = $this->getIp($server); | |
66 | // the IP is behind a trusted forward proxy, but is not forwarded | |
67 | // in the HTTP headers, so we do nothing | |
68 | if (empty($ip)) { | |
69 | return; | |
70 | } | |
71 | ||
72 | // increment the fail count for this IP | |
73 | if (isset($this->failures[$ip])) { | |
74 | $this->failures[$ip]++; | |
75 | } else { | |
76 | $this->failures[$ip] = 1; | |
77 | } | |
78 | ||
79 | if ($this->failures[$ip] >= $this->nbAttempts) { | |
80 | $this->bans[$ip] = time() + $this->banDuration; | |
81 | logm( | |
82 | $this->logFile, | |
83 | $server['REMOTE_ADDR'], | |
84 | 'IP address banned from login: '. $ip | |
85 | ); | |
86 | } | |
87 | $this->writeBanFile(); | |
88 | } | |
89 | ||
90 | /** | |
91 | * Remove failed attempts for the provided client. | |
92 | * | |
93 | * @param array $server $_SERVER | |
94 | */ | |
95 | public function clearFailures($server) | |
96 | { | |
97 | $ip = $this->getIp($server); | |
98 | // the IP is behind a trusted forward proxy, but is not forwarded | |
99 | // in the HTTP headers, so we do nothing | |
100 | if (empty($ip)) { | |
101 | return; | |
102 | } | |
103 | ||
104 | if (isset($this->failures[$ip])) { | |
105 | unset($this->failures[$ip]); | |
106 | } | |
107 | $this->writeBanFile(); | |
108 | } | |
109 | ||
110 | /** | |
111 | * Check whether the client IP is banned or not. | |
112 | * | |
113 | * @param array $server $_SERVER | |
114 | * | |
115 | * @return bool True if the IP is banned, false otherwise | |
116 | */ | |
117 | public function isBanned($server) | |
118 | { | |
119 | $ip = $this->getIp($server); | |
120 | // the IP is behind a trusted forward proxy, but is not forwarded | |
121 | // in the HTTP headers, so we allow the authentication attempt. | |
122 | if (empty($ip)) { | |
123 | return false; | |
124 | } | |
125 | ||
126 | // the user is not banned | |
127 | if (! isset($this->bans[$ip])) { | |
128 | return false; | |
129 | } | |
130 | ||
131 | // the user is still banned | |
132 | if ($this->bans[$ip] > time()) { | |
133 | return true; | |
134 | } | |
135 | ||
136 | // the ban has expired, the user can attempt to log in again | |
137 | if (isset($this->failures[$ip])) { | |
138 | unset($this->failures[$ip]); | |
139 | } | |
140 | unset($this->bans[$ip]); | |
141 | logm($this->logFile, $server['REMOTE_ADDR'], 'Ban lifted for: '. $ip); | |
142 | ||
143 | $this->writeBanFile(); | |
144 | return false; | |
145 | } | |
146 | ||
147 | /** | |
148 | * Retrieve the IP from $_SERVER. | |
149 | * If the actual IP is behind an allowed reverse proxy, | |
150 | * we try to extract the forwarded IP from HTTP headers. | |
151 | * | |
152 | * @param array $server $_SERVER | |
153 | * | |
154 | * @return string|bool The IP or false if none could be extracted | |
155 | */ | |
156 | protected function getIp($server) | |
157 | { | |
158 | $ip = $server['REMOTE_ADDR']; | |
159 | if (! in_array($ip, $this->trustedProxies)) { | |
160 | return $ip; | |
161 | } | |
162 | return getIpAddressFromProxy($server, $this->trustedProxies); | |
163 | } | |
164 | ||
165 | /** | |
166 | * Read a file containing banned IPs | |
167 | */ | |
168 | protected function readBanFile() | |
169 | { | |
170 | $data = FileUtils::readFlatDB($this->banFile); | |
171 | if (isset($data['failures']) && is_array($data['failures'])) { | |
172 | $this->failures = $data['failures']; | |
173 | } | |
174 | ||
175 | if (isset($data['bans']) && is_array($data['bans'])) { | |
176 | $this->bans = $data['bans']; | |
177 | } | |
178 | } | |
179 | ||
180 | /** | |
181 | * Write the banned IPs to a file | |
182 | */ | |
183 | protected function writeBanFile() | |
184 | { | |
185 | return FileUtils::writeFlatDB( | |
186 | $this->banFile, | |
187 | [ | |
188 | 'failures' => $this->failures, | |
189 | 'bans' => $this->bans, | |
190 | ] | |
191 | ); | |
192 | } | |
193 | ||
194 | /** | |
195 | * Get the Failures (for UT purpose). | |
196 | * | |
197 | * @return array | |
198 | */ | |
199 | public function getFailures() | |
200 | { | |
201 | return $this->failures; | |
202 | } | |
203 | ||
204 | /** | |
205 | * Get the Bans (for UT purpose). | |
206 | * | |
207 | * @return array | |
208 | */ | |
209 | public function getBans() | |
210 | { | |
211 | return $this->bans; | |
212 | } | |
213 | } |