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