19ed332c7SGreg Roach<?php 29ed332c7SGreg Roach 39ed332c7SGreg Roach/** 49ed332c7SGreg Roach * webtrees: online genealogy 5*d11be702SGreg Roach * Copyright (C) 2023 webtrees development team 69ed332c7SGreg Roach * This program is free software: you can redistribute it and/or modify 79ed332c7SGreg Roach * it under the terms of the GNU General Public License as published by 89ed332c7SGreg Roach * the Free Software Foundation, either version 3 of the License, or 99ed332c7SGreg Roach * (at your option) any later version. 109ed332c7SGreg Roach * This program is distributed in the hope that it will be useful, 119ed332c7SGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of 129ed332c7SGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 139ed332c7SGreg Roach * GNU General Public License for more details. 149ed332c7SGreg Roach * You should have received a copy of the GNU General Public License 159ed332c7SGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>. 169ed332c7SGreg Roach */ 179ed332c7SGreg Roach 189ed332c7SGreg Roachdeclare(strict_types=1); 199ed332c7SGreg Roach 209ed332c7SGreg Roachnamespace Fisharebest\Webtrees\Services; 219ed332c7SGreg Roach 229ed332c7SGreg Roachuse Fisharebest\Webtrees\Contracts\UserInterface; 239ed332c7SGreg Roachuse Fisharebest\Webtrees\Http\Exceptions\HttpTooManyRequestsException; 249ed332c7SGreg Roachuse Fisharebest\Webtrees\Site; 259ed332c7SGreg Roachuse LogicException; 269ed332c7SGreg Roach 279ed332c7SGreg Roachuse function array_filter; 2810e06497SGreg Roachuse function count; 299ed332c7SGreg Roachuse function explode; 309ed332c7SGreg Roachuse function intdiv; 319ed332c7SGreg Roachuse function strlen; 329ed332c7SGreg Roachuse function time; 339ed332c7SGreg Roach 349ed332c7SGreg Roach/** 359ed332c7SGreg Roach * Throttle events to prevent abuse. 369ed332c7SGreg Roach */ 379ed332c7SGreg Roachclass RateLimitService 389ed332c7SGreg Roach{ 399ed332c7SGreg Roach private int $now; 409ed332c7SGreg Roach 419ed332c7SGreg Roach /** 429ed332c7SGreg Roach * 439ed332c7SGreg Roach */ 449ed332c7SGreg Roach public function __construct() 459ed332c7SGreg Roach { 469ed332c7SGreg Roach $this->now = time(); 479ed332c7SGreg Roach } 489ed332c7SGreg Roach 499ed332c7SGreg Roach /** 509ed332c7SGreg Roach * Rate limit for actions related to a user, such as password reset request. 519ed332c7SGreg Roach * Allow $num requests every $seconds 529ed332c7SGreg Roach * 539ed332c7SGreg Roach * @param int $num allow this number of events 549ed332c7SGreg Roach * @param int $seconds in a rolling window of this number of seconds 559ed332c7SGreg Roach * @param string $limit name of limit to enforce 569ed332c7SGreg Roach * 579ed332c7SGreg Roach * @return void 589ed332c7SGreg Roach */ 599ed332c7SGreg Roach public function limitRateForSite(int $num, int $seconds, string $limit): void 609ed332c7SGreg Roach { 619ed332c7SGreg Roach $history = Site::getPreference($limit); 629ed332c7SGreg Roach 639ed332c7SGreg Roach $history = $this->checkLimitReached($num, $seconds, $history); 649ed332c7SGreg Roach 659ed332c7SGreg Roach Site::setPreference($limit, $history); 669ed332c7SGreg Roach } 679ed332c7SGreg Roach 689ed332c7SGreg Roach /** 699ed332c7SGreg Roach * Rate limit for actions related to a user, such as password reset request. 709ed332c7SGreg Roach * Allow $num requests every $seconds 719ed332c7SGreg Roach * 729ed332c7SGreg Roach * @param UserInterface $user limit events for this user 739ed332c7SGreg Roach * @param int $num allow this number of events 749ed332c7SGreg Roach * @param int $seconds in a rolling window of this number of seconds 759ed332c7SGreg Roach * @param string $limit name of limit to enforce 769ed332c7SGreg Roach * 779ed332c7SGreg Roach * @return void 789ed332c7SGreg Roach */ 799ed332c7SGreg Roach public function limitRateForUser(UserInterface $user, int $num, int $seconds, string $limit): void 809ed332c7SGreg Roach { 819ed332c7SGreg Roach $history = $user->getPreference($limit); 829ed332c7SGreg Roach 839ed332c7SGreg Roach $history = $this->checkLimitReached($num, $seconds, $history); 849ed332c7SGreg Roach 859ed332c7SGreg Roach $user->setPreference($limit, $history); 869ed332c7SGreg Roach } 879ed332c7SGreg Roach 889ed332c7SGreg Roach /** 899ed332c7SGreg Roach * Rate limit - allow $num requests every $seconds 909ed332c7SGreg Roach * 919ed332c7SGreg Roach * @param int $num allow this number of events 929ed332c7SGreg Roach * @param int $seconds in a rolling window of this number of seconds 939ed332c7SGreg Roach * @param string $history comma-separated list of previous timestamps 949ed332c7SGreg Roach * 959ed332c7SGreg Roach * @return string updated list of timestamps 969ed332c7SGreg Roach * @throws HttpTooManyRequestsException 979ed332c7SGreg Roach */ 989ed332c7SGreg Roach private function checkLimitReached(int $num, int $seconds, string $history): string 999ed332c7SGreg Roach { 1009ed332c7SGreg Roach // Make sure we can store enough previous timestamps in a database field. 1019ed332c7SGreg Roach $max = intdiv(256, strlen($this->now . ',')); 1029ed332c7SGreg Roach if ($num > $max) { 1039ed332c7SGreg Roach throw new LogicException('Cannot store ' . $num . ' previous events in the database'); 1049ed332c7SGreg Roach } 1059ed332c7SGreg Roach 1069ed332c7SGreg Roach // Extract the timestamps. 1079ed332c7SGreg Roach $timestamps = array_filter(explode(',', $history)); 1089ed332c7SGreg Roach 1099ed332c7SGreg Roach // Filter events within our time window. 1109ed332c7SGreg Roach $filter = fn (string $x): bool => (int) $x >= $this->now - $seconds && (int) $x <= $this->now; 1119ed332c7SGreg Roach $in_window = array_filter($timestamps, $filter); 1129ed332c7SGreg Roach 1139ed332c7SGreg Roach if (count($in_window) >= $num) { 1149ed332c7SGreg Roach throw new HttpTooManyRequestsException(); 1159ed332c7SGreg Roach } 1169ed332c7SGreg Roach 1179ed332c7SGreg Roach $timestamps[] = (string) $this->now; 1189ed332c7SGreg Roach 1199ed332c7SGreg Roach while (count($timestamps) > $max) { 1209ed332c7SGreg Roach array_shift($timestamps); 1219ed332c7SGreg Roach } 1229ed332c7SGreg Roach 1239ed332c7SGreg Roach return implode(',', $timestamps); 1249ed332c7SGreg Roach } 1259ed332c7SGreg Roach} 126