Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
85.71% covered (success)
85.71%
6 / 7
CRAP
98.15% covered (success)
98.15%
53 / 54
TrafficLimiter
0.00% covered (danger)
0.00%
0 / 1
85.71% covered (success)
85.71%
6 / 7
24
98.15% covered (success)
98.15%
53 / 54
 setConfiguration
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
8 / 8
 setCreators
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 setExempted
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 setLimit
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 getHash
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 matchIp
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
11 / 11
 canPass
0.00% covered (danger)
0.00%
0 / 1
12
96.43% covered (success)
96.43%
27 / 28
<?php
/**
 * PrivateBin
 *
 * a zero-knowledge paste bin
 *
 * @link      https://github.com/PrivateBin/PrivateBin
 * @copyright 2012 S├ębastien SAUVAGE (sebsauvage.net)
 * @license   https://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
 * @version   1.5.1
 */
namespace PrivateBin\Persistence;
use Exception;
use IPLib\Factory;
use IPLib\ParseStringFlag;
use PrivateBin\Configuration;
use PrivateBin\I18n;
/**
 * TrafficLimiter
 *
 * Handles traffic limiting, so no user does more than one call per 10 seconds.
 */
class TrafficLimiter extends AbstractPersistence
{
    /**
     * listed IPs are the only ones allowed to create, defaults to null
     *
     * @access private
     * @static
     * @var    string|null
     */
    private static $_creators = null;
    /**
     * listed IPs are exempted from limits, defaults to null
     *
     * @access private
     * @static
     * @var    string|null
     */
    private static $_exempted = null;
    /**
     * key to fetch IP address
     *
     * @access private
     * @static
     * @var    string
     */
    private static $_ipKey = 'REMOTE_ADDR';
    /**
     * time limit in seconds, defaults to 10s
     *
     * @access private
     * @static
     * @var    int
     */
    private static $_limit = 10;
    /**
     * set configuration options of the traffic limiter
     *
     * @access public
     * @static
     * @param Configuration $conf
     */
    public static function setConfiguration(Configuration $conf)
    {
        self::setCreators($conf->getKey('creators', 'traffic'));
        self::setExempted($conf->getKey('exempted', 'traffic'));
        self::setLimit($conf->getKey('limit', 'traffic'));
        if (($option = $conf->getKey('header', 'traffic')) !== '') {
            $httpHeader = 'HTTP_' . $option;
            if (array_key_exists($httpHeader, $_SERVER) && !empty($_SERVER[$httpHeader])) {
                self::$_ipKey = $httpHeader;
            }
        }
    }
    /**
     * set a list of creator IP(-ranges) as string
     *
     * @access public
     * @static
     * @param string $creators
     */
    public static function setCreators($creators)
    {
        self::$_creators = $creators;
    }
    /**
     * set a list of exempted IP(-ranges) as string
     *
     * @access public
     * @static
     * @param string $exempted
     */
    public static function setExempted($exempted)
    {
        self::$_exempted = $exempted;
    }
    /**
     * set the time limit in seconds
     *
     * @access public
     * @static
     * @param  int $limit
     */
    public static function setLimit($limit)
    {
        self::$_limit = $limit;
    }
    /**
     * get a HMAC of the current visitors IP address
     *
     * @access public
     * @static
     * @param  string $algo
     * @return string
     */
    public static function getHash($algo = 'sha512')
    {
        return hash_hmac($algo, $_SERVER[self::$_ipKey], ServerSalt::get());
    }
    /**
     * validate $_ipKey against configured ipranges. If matched we will ignore the ip
     *
     * @access private
     * @static
     * @param  string $ipRange
     * @return bool
     */
    private static function matchIp($ipRange = null)
    {
        if (is_string($ipRange)) {
            $ipRange = trim($ipRange);
        }
        $address = Factory::parseAddressString($_SERVER[self::$_ipKey]);
        $range   = Factory::parseRangeString(
            $ipRange,
            ParseStringFlag::IPV4_MAYBE_NON_DECIMAL | ParseStringFlag::IPV4SUBNET_MAYBE_COMPACT | ParseStringFlag::IPV4ADDRESS_MAYBE_NON_QUAD_DOTTED
        );
        // address could not be parsed, we might not be in IP space and try a string comparison instead
        if (is_null($address)) {
            return $_SERVER[self::$_ipKey] === $ipRange;
        }
        // range could not be parsed, possibly an invalid ip range given in config
        if (is_null($range)) {
            return false;
        }
        return $address->matches($range);
    }
    /**
     * make sure the IP address is allowed to perfom a request
     *
     * @access public
     * @static
     * @throws Exception
     * @return true
     */
    public static function canPass()
    {
        // if creators are defined, the traffic limiter will only allow creation
        // for these, with no limits, and skip any other rules
        if (!empty(self::$_creators)) {
            $creatorIps = explode(',', self::$_creators);
            foreach ($creatorIps as $ipRange) {
                if (self::matchIp($ipRange) === true) {
                    return true;
                }
            }
            throw new Exception(I18n::_('Your IP is not authorized to create pastes.'));
        }
        // disable limits if set to less then 1
        if (self::$_limit < 1) {
            return true;
        }
        // check if $_ipKey is exempted from ratelimiting
        if (!empty(self::$_exempted)) {
            $exIp_array = explode(',', self::$_exempted);
            foreach ($exIp_array as $ipRange) {
                if (self::matchIp($ipRange) === true) {
                    return true;
                }
            }
        }
        // used as array key, which are limited in length, hence using algo with shorter range
        $hash = self::getHash('sha256');
        $now  = time();
        $tl   = (int) self::$_store->getValue('traffic_limiter', $hash);
        self::$_store->purgeValues('traffic_limiter', $now - self::$_limit);
        if ($tl > 0 && ($tl + self::$_limit >= $now)) {
            $result = false;
        } else {
            $tl     = time();
            $result = true;
        }
        if (!self::$_store->setValue((string) $tl, 'traffic_limiter', $hash)) {
            error_log('failed to store the traffic limiter, it probably contains outdated information');
        }
        if ($result) {
            return true;
        }
        throw new Exception(I18n::_(
            'Please wait %d seconds between each post.',
            self::$_limit
        ));
    }
}