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