This commit is contained in:
mr❤️🤢 2025-06-25 20:02:40 +00:00 committed by GitHub
commit 55455c21c5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 213 additions and 1 deletions

View file

@ -98,6 +98,10 @@ final class Friends extends VKAPIRequestHandler
switch ($user->getSubscriptionStatus($this->getUser())) {
case 0:
if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->getUser(), "friends.outgoing_sub")) {
$this->failTooOften();
}
$user->toggleSubscription($this->getUser());
return 1;

View file

@ -61,6 +61,10 @@ final class Gifts extends VKAPIRequestHandler
$this->fail(-105, "Commerce is disabled on this instance");
}
if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->getUser(), "gifts.send", false)) {
$this->failTooOften();
}
$user = (new UsersRepo())->get((int) $user_ids); # FAKE прогноз погоды (в данном случае user_ids)
if (!$user || $user->isDeleted()) {

View file

@ -312,6 +312,10 @@ final class Groups extends VKAPIRequestHandler
$isMember = !is_null($this->getUser()) ? (int) $club->getSubscriptionStatus($this->getUser()) : 0;
if ($isMember == 0) {
if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->getUser(), "groups.sub")) {
$this->failTooOften();
}
$club->toggleSubscription($this->getUser());
}

View file

@ -25,6 +25,11 @@ abstract class VKAPIRequestHandler
throw new APIErrorException($message, $code);
}
protected function failTooOften(): never
{
$this->fail(9, "Rate limited");
}
protected function getUser(): ?User
{
return $this->user;

View file

@ -620,6 +620,10 @@ final class Wall extends VKAPIRequestHandler
return (object) ["post_id" => $post->getVirtualId()];
}
if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->getUser(), "wall.post", false)) {
$this->failTooOften();
}
$anon = OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["anonymousPosting"]["enable"];
if ($wallOwner instanceof Club && $from_group == 1 && $signed != 1 && $anon) {
$manager = $wallOwner->getManager($this->getUser());
@ -723,9 +727,11 @@ final class Wall extends VKAPIRequestHandler
}
if ($owner_id > 0 && $owner_id !== $this->getUser()->getId()) {
(new WallPostNotification($wallOwner, $post, $this->user->identity))->emit();
(new WallPostNotification($wallOwner, $post, $this->getUser()))->emit();
}
\openvk\Web\Util\EventRateLimiter::i()->writeEvent("wall.post", $this->getUser(), $wallOwner);
return (object) ["post_id" => $post->getVirtualId()];
}

View file

@ -37,6 +37,7 @@ trait TSubscribable
if (!($sub->fetch())) {
$ctx->table("subscriptions")->insert($data);
return true;
}

View file

@ -114,6 +114,7 @@ class User extends RowModel
public function getChandlerUser(): ChandlerUser
{
# TODO cache this function
return new ChandlerUser($this->getRecord()->ref("ChandlerUsers", "user"));
}
@ -1738,4 +1739,42 @@ class User extends RowModel
{
return DatabaseConnection::i()->getContext()->table("blacklist_relations")->where("author", $this->getId())->count();
}
public function recieveEventsData(array $list): array
{
$ev_str = $this->getRecord()->events_counters;
$values = [];
if (!$ev_str) {
for ($i = 0; $i < sizeof(array_keys($list)); $i++) {
$values[] = 0;
}
} else {
$keys = array_keys($list);
$values = unpack("S*", base64_decode($ev_str));
}
return [
'counters' => $values,
'refresh_time' => $this->getRecord()->events_refresh_time,
];
}
public function stateEvents(array $list): void
{
$this->stateChanges("events_counters", base64_encode(pack("S*", array_values($list))));
}
public function resetEvents(array $list, int $restriction_length)
{
$values = [];
for ($i = 0; $i < sizeof(array_keys($list)); $i++) {
$values[] = 0;
}
$this->stateEvents($values);
$this->stateChanges("events_refresh_time", $restriction_length + time());
$this->save();
}
}

View file

@ -106,6 +106,10 @@ final class GiftsPresenter extends OpenVKPresenter
return;
}
if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->user->identity, "gifts.send", false)) {
$this->flashFail("err", tr("error"), tr("limit_exceed_exception"));
}
$comment = empty($c = $this->postParam("comment")) ? null : $c;
$notification = new GiftNotification($user, $this->user->identity, $gift, $comment);
$notification->emit();

View file

@ -63,6 +63,10 @@ final class GroupPresenter extends OpenVKPresenter
if ($_SERVER["REQUEST_METHOD"] === "POST") {
if (!empty($this->postParam("name")) && mb_strlen(trim($this->postParam("name"))) > 0) {
if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->user->identity, "groups.create")) {
$this->flashFail("err", tr("error"), tr("limit_exceed_exception"));
}
$club = new Club();
$club->setName($this->postParam("name"));
$club->setAbout(empty($this->postParam("about")) ? null : $this->postParam("about"));
@ -79,6 +83,7 @@ final class GroupPresenter extends OpenVKPresenter
}
$club->toggleSubscription($this->user->identity);
$this->redirect("/club" . $club->getId());
} else {
$this->flashFail("err", tr("error"), tr("error_no_group_name"));

View file

@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
namespace openvk\Web\Util;
use openvk\Web\Models\Entities\User;
use openvk\Web\Models\RowModel;
use Chandler\Patterns\TSimpleSingleton;
class EventRateLimiter
{
use TSimpleSingleton;
private $config;
private $availableFields;
public function __construct()
{
$this->config = OPENVK_ROOT_CONF["openvk"]["preferences"]["security"]["rateLimits"]["eventsLimit"];
$this->availableFields = array_keys($this->config['list']);
}
/*
Checks count of actions for last hours
Uses config path OPENVK_ROOT_CONF["openvk"]["preferences"]["security"]["rateLimits"]["eventsLimit"]
Returns:
true limit has exceed and the action must be restricted
false the action can be performed
*/
public function tryToLimit(?User $user, string $event_type, bool $distinct = true): bool
{
$isEnabled = $this->config['enable'];
$isIgnoreForAdmins = $this->config['ignoreForAdmins'];
$restrictionTime = $this->config['restrictionTime'];
$eventsList = $this->config['list'];
if (!$isEnabled) {
return false;
}
if ($isIgnoreForAdmins && $user->isAdmin()) {
return false;
}
$limitForThatEvent = $eventsList[$event_type];
$stat = $this->getEvent($event_type, $user);
bdump($stat);
$is_restrict_over = $stat["refresh_time"] < time() - $restrictionTime;
if ($is_restrict_over) {
$user->resetEvents($eventsList, $restrictionTime);
return false;
}
$is = $stat["compared"] > $limitForThatEvent;
if ($is === false) {
$this->incrementEvent($event_type, $user);
}
return $is;
}
public function getEvent(string $event_type, User $by_user): array
{
$ev_data = $by_user->recieveEventsData($this->config['list']);
$values = $ev_data['counters'];
$i = 0;
$compared = [];
bdump($values);
foreach ($this->config['list'] as $name => $value) {
bdump($value);
$compared[$name] = $values[$i];
$i += 1;
}
return [
"compared" => $compared,
"refresh_time" => $ev_data["refresh_time"]
];
}
/*
Updates counter for user
*/
public function incrementEvent(string $event_type, User $initiator): bool
{
$isEnabled = $this->config['enable'];
$eventsList = OPENVK_ROOT_CONF["openvk"]["preferences"]["security"]["rateLimits"]["eventsLimit"];
if (!$isEnabled) {
return false;
}
$ev_data = $initiator->recieveEventsData($eventsList);
$values = $ev_data['counters'];
$i = 0;
$compared = [];
foreach ($eventsList as $name => $value) {
$compared[$name] = $values[$i];
$i += 1;
}
$compared[$event_type] += 1;
bdump($compared);
$initiator->stateEvents($compared);
return true;
}
}

View file

@ -0,0 +1,3 @@
ALTER TABLE `profiles`
ADD `events_counters` VARCHAR(299) NULL DEFAULT NULL AFTER `audio_broadcast_enabled`,
ADD `events_refresh_time` BIGINT(20) UNSIGNED NULL DEFAULT NULL AFTER `events_counters`;

View file

@ -1657,6 +1657,8 @@
"error_geolocation" = "Error while trying to pin geolocation";
"error_no_geotag" = "There is no geo-tag pinned in this post";
"limit_exceed_exception" = "You're doing this action too often. Try again later.";
/* Admin actions */
"login_as" = "Login as $1";

View file

@ -1561,6 +1561,8 @@
"error_geolocation" = "Ошибка при прикреплении геометки";
"error_no_geotag" = "У поста не указана гео-метка";
"limit_exceed_exception" = "Вы совершаете это действие слишком часто. Повторите позже.";
/* Admin actions */
"login_as" = "Войти как $1";

View file

@ -41,6 +41,16 @@ openvk:
maxViolations: 50
maxViolationsAge: 120
autoban: true
eventsLimit:
enable: true
ignoreForAdmins: true
restrictionTime: 86400
list:
groups.create: 5
groups.sub: 50
friends.outgoing_sub: 25
wall.post: 5000
gifts.send: 20
blacklists:
limit: 100
applyToAdmins: true