PrivateBin/lib/Persistence/TrafficLimiter.php
El RIDO ddd2d72064
replaced the term "paste" with the more generic "document"
Some of the references to "paste" in code or comments got changed as well, but to clarify the intended usage of the terms:

- A PrivateBin document can consist of a paste text (key "paste" in the encrypted payload) and one or several attachments and discussion entries.
- Internally the root document is called a "Paste" and each discussion entry is called a "Discussion".
- When referring to a whole document with one paste and optional discussion(s), we call it just "document".
- When talking about a particular JSON payload type in the internal logic, i.e. during storage or transmission, we call them a paste or discussion to distinguish which type we refer to.

closes #397
2025-07-24 10:46:31 +02:00

224 lines
6 KiB
PHP

<?php declare(strict_types=1);
/**
* 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
*/
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 documents.'));
}
// 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
));
}
}