1
0

TrafficLimiter.php 5.9 KB

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