Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
98.08% |
51 / 52 |
|
85.71% |
6 / 7 |
CRAP | |
0.00% |
0 / 1 |
TrafficLimiter | |
98.08% |
51 / 52 |
|
85.71% |
6 / 7 |
24 | |
0.00% |
0 / 1 |
setConfiguration | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
4 | |||
setCreators | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setExempted | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setLimit | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getHash | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
matchIp | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
4 | |||
canPass | |
96.55% |
28 / 29 |
|
0.00% |
0 / 1 |
12 |
1 | <?php declare(strict_types=1); |
2 | |
3 | /** |
4 | * PrivateBin |
5 | * |
6 | * a zero-knowledge paste bin |
7 | * |
8 | * @link https://github.com/PrivateBin/PrivateBin |
9 | * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net) |
10 | * @license https://www.opensource.org/licenses/zlib-license.php The zlib/libpng License |
11 | */ |
12 | |
13 | namespace PrivateBin\Persistence; |
14 | |
15 | use Exception; |
16 | use IPLib\Factory; |
17 | use IPLib\ParseStringFlag; |
18 | use PrivateBin\Configuration; |
19 | use PrivateBin\I18n; |
20 | |
21 | /** |
22 | * TrafficLimiter |
23 | * |
24 | * Handles traffic limiting, so no user does more than one call per 10 seconds. |
25 | */ |
26 | class TrafficLimiter extends AbstractPersistence |
27 | { |
28 | /** |
29 | * listed IPs are the only ones allowed to create, defaults to null |
30 | * |
31 | * @access private |
32 | * @static |
33 | * @var string|null |
34 | */ |
35 | private static $_creators = null; |
36 | |
37 | /** |
38 | * listed IPs are exempted from limits, defaults to null |
39 | * |
40 | * @access private |
41 | * @static |
42 | * @var string|null |
43 | */ |
44 | private static $_exempted = null; |
45 | |
46 | /** |
47 | * key to fetch IP address |
48 | * |
49 | * @access private |
50 | * @static |
51 | * @var string |
52 | */ |
53 | private static $_ipKey = 'REMOTE_ADDR'; |
54 | |
55 | /** |
56 | * time limit in seconds, defaults to 10s |
57 | * |
58 | * @access private |
59 | * @static |
60 | * @var int |
61 | */ |
62 | private static $_limit = 10; |
63 | |
64 | /** |
65 | * set configuration options of the traffic limiter |
66 | * |
67 | * @access public |
68 | * @static |
69 | * @param Configuration $conf |
70 | */ |
71 | public static function setConfiguration(Configuration $conf) |
72 | { |
73 | self::setCreators($conf->getKey('creators', 'traffic')); |
74 | self::setExempted($conf->getKey('exempted', 'traffic')); |
75 | self::setLimit($conf->getKey('limit', 'traffic')); |
76 | |
77 | if (($option = $conf->getKey('header', 'traffic')) !== '') { |
78 | $httpHeader = 'HTTP_' . $option; |
79 | if (array_key_exists($httpHeader, $_SERVER) && !empty($_SERVER[$httpHeader])) { |
80 | self::$_ipKey = $httpHeader; |
81 | } |
82 | } |
83 | } |
84 | |
85 | /** |
86 | * set a list of creator IP(-ranges) as string |
87 | * |
88 | * @access public |
89 | * @static |
90 | * @param string $creators |
91 | */ |
92 | public static function setCreators($creators) |
93 | { |
94 | self::$_creators = $creators; |
95 | } |
96 | |
97 | /** |
98 | * set a list of exempted IP(-ranges) as string |
99 | * |
100 | * @access public |
101 | * @static |
102 | * @param string $exempted |
103 | */ |
104 | public static function setExempted($exempted) |
105 | { |
106 | self::$_exempted = $exempted; |
107 | } |
108 | |
109 | /** |
110 | * set the time limit in seconds |
111 | * |
112 | * @access public |
113 | * @static |
114 | * @param int $limit |
115 | */ |
116 | public static function setLimit($limit) |
117 | { |
118 | self::$_limit = $limit; |
119 | } |
120 | |
121 | /** |
122 | * get a HMAC of the current visitors IP address |
123 | * |
124 | * @access public |
125 | * @static |
126 | * @param string $algo |
127 | * @return string |
128 | */ |
129 | public static function getHash($algo = 'sha512') |
130 | { |
131 | return hash_hmac($algo, $_SERVER[self::$_ipKey], ServerSalt::get()); |
132 | } |
133 | |
134 | /** |
135 | * validate $_ipKey against configured ipranges. If matched we will ignore the ip |
136 | * |
137 | * @access private |
138 | * @static |
139 | * @param string $ipRange |
140 | * @return bool |
141 | */ |
142 | private static function matchIp($ipRange = null) |
143 | { |
144 | if (is_string($ipRange)) { |
145 | $ipRange = trim($ipRange); |
146 | } |
147 | $address = Factory::parseAddressString($_SERVER[self::$_ipKey]); |
148 | $range = Factory::parseRangeString( |
149 | $ipRange, |
150 | ParseStringFlag::IPV4_MAYBE_NON_DECIMAL | ParseStringFlag::IPV4SUBNET_MAYBE_COMPACT | ParseStringFlag::IPV4ADDRESS_MAYBE_NON_QUAD_DOTTED |
151 | ); |
152 | |
153 | // address could not be parsed, we might not be in IP space and try a string comparison instead |
154 | if (is_null($address)) { |
155 | return $_SERVER[self::$_ipKey] === $ipRange; |
156 | } |
157 | // range could not be parsed, possibly an invalid ip range given in config |
158 | if (is_null($range)) { |
159 | return false; |
160 | } |
161 | |
162 | return $address->matches($range); |
163 | } |
164 | |
165 | /** |
166 | * make sure the IP address is allowed to perfom a request |
167 | * |
168 | * @access public |
169 | * @static |
170 | * @throws Exception |
171 | * @return true |
172 | */ |
173 | public static function canPass() |
174 | { |
175 | // if creators are defined, the traffic limiter will only allow creation |
176 | // for these, with no limits, and skip any other rules |
177 | if (!empty(self::$_creators)) { |
178 | $creatorIps = explode(',', self::$_creators); |
179 | foreach ($creatorIps as $ipRange) { |
180 | if (self::matchIp($ipRange) === true) { |
181 | return true; |
182 | } |
183 | } |
184 | throw new Exception(I18n::_('Your IP is not authorized to create pastes.')); |
185 | } |
186 | |
187 | // disable limits if set to less then 1 |
188 | if (self::$_limit < 1) { |
189 | return true; |
190 | } |
191 | |
192 | // check if $_ipKey is exempted from ratelimiting |
193 | if (!empty(self::$_exempted)) { |
194 | $exIp_array = explode(',', self::$_exempted); |
195 | foreach ($exIp_array as $ipRange) { |
196 | if (self::matchIp($ipRange) === true) { |
197 | return true; |
198 | } |
199 | } |
200 | } |
201 | |
202 | // used as array key, which are limited in length, hence using algo with shorter range |
203 | $hash = self::getHash('sha256'); |
204 | $now = time(); |
205 | $tl = (int) self::$_store->getValue('traffic_limiter', $hash); |
206 | self::$_store->purgeValues('traffic_limiter', $now - self::$_limit); |
207 | if ($tl > 0 && ($tl + self::$_limit >= $now)) { |
208 | $result = false; |
209 | } else { |
210 | $tl = time(); |
211 | $result = true; |
212 | } |
213 | if (!self::$_store->setValue((string) $tl, 'traffic_limiter', $hash)) { |
214 | error_log('failed to store the traffic limiter, it probably contains outdated information'); |
215 | } |
216 | if ($result) { |
217 | return true; |
218 | } |
219 | throw new Exception(I18n::_( |
220 | 'Please wait %d seconds between each post.', |
221 | self::$_limit |
222 | )); |
223 | } |
224 | } |