mirror of
https://github.com/openvk/openvk
synced 2025-07-01 21:48:17 +03:00
feat(rate_limits) (#1353)
Добавляет возможность ограничить такие действия как отправка подарка, заявка в друзья, джойн в группу, создание группы на время, чтобы можно было создать только 5 групп за день итд. Находится в ветке конфига preferences>security>rateLimits>eventsLimit. На момент написания этого текста регулирование постинга и отправки заявки в друзья не было продублировано в презентеры, мб так и оставить.
This commit is contained in:
parent
4f126d1b05
commit
93b1202a13
18 changed files with 213 additions and 3 deletions
|
@ -98,6 +98,10 @@ final class Friends extends VKAPIRequestHandler
|
||||||
|
|
||||||
switch ($user->getSubscriptionStatus($this->getUser())) {
|
switch ($user->getSubscriptionStatus($this->getUser())) {
|
||||||
case 0:
|
case 0:
|
||||||
|
if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->getUser(), "friends.outgoing_sub")) {
|
||||||
|
$this->failTooOften();
|
||||||
|
}
|
||||||
|
|
||||||
$user->toggleSubscription($this->getUser());
|
$user->toggleSubscription($this->getUser());
|
||||||
return 1;
|
return 1;
|
||||||
|
|
||||||
|
|
|
@ -61,6 +61,10 @@ final class Gifts extends VKAPIRequestHandler
|
||||||
$this->fail(-105, "Commerce is disabled on this instance");
|
$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)
|
$user = (new UsersRepo())->get((int) $user_ids); # FAKE прогноз погоды (в данном случае user_ids)
|
||||||
|
|
||||||
if (!$user || $user->isDeleted()) {
|
if (!$user || $user->isDeleted()) {
|
||||||
|
|
|
@ -312,6 +312,10 @@ final class Groups extends VKAPIRequestHandler
|
||||||
$isMember = !is_null($this->getUser()) ? (int) $club->getSubscriptionStatus($this->getUser()) : 0;
|
$isMember = !is_null($this->getUser()) ? (int) $club->getSubscriptionStatus($this->getUser()) : 0;
|
||||||
|
|
||||||
if ($isMember == 0) {
|
if ($isMember == 0) {
|
||||||
|
if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->getUser(), "groups.sub")) {
|
||||||
|
$this->failTooOften();
|
||||||
|
}
|
||||||
|
|
||||||
$club->toggleSubscription($this->getUser());
|
$club->toggleSubscription($this->getUser());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,11 @@ abstract class VKAPIRequestHandler
|
||||||
throw new APIErrorException($message, $code);
|
throw new APIErrorException($message, $code);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function failTooOften(): never
|
||||||
|
{
|
||||||
|
$this->fail(9, "Rate limited");
|
||||||
|
}
|
||||||
|
|
||||||
protected function getUser(): ?User
|
protected function getUser(): ?User
|
||||||
{
|
{
|
||||||
return $this->user;
|
return $this->user;
|
||||||
|
|
|
@ -713,6 +713,10 @@ final class Wall extends VKAPIRequestHandler
|
||||||
$post->setSuggested(1);
|
$post->setSuggested(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->getUser(), "wall.post")) {
|
||||||
|
$this->failTooOften();
|
||||||
|
}
|
||||||
|
|
||||||
$post->save();
|
$post->save();
|
||||||
} catch (\LogicException $ex) {
|
} catch (\LogicException $ex) {
|
||||||
$this->fail(100, "One of the parameters specified was missing or invalid");
|
$this->fail(100, "One of the parameters specified was missing or invalid");
|
||||||
|
@ -723,7 +727,7 @@ final class Wall extends VKAPIRequestHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($owner_id > 0 && $owner_id !== $this->getUser()->getId()) {
|
if ($owner_id > 0 && $owner_id !== $this->getUser()->getId()) {
|
||||||
(new WallPostNotification($wallOwner, $post, $this->user->identity))->emit();
|
(new WallPostNotification($wallOwner, $post, $this->getUser()))->emit();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (object) ["post_id" => $post->getVirtualId()];
|
return (object) ["post_id" => $post->getVirtualId()];
|
||||||
|
|
|
@ -34,9 +34,9 @@ trait TSubscribable
|
||||||
"target" => $this->getId(),
|
"target" => $this->getId(),
|
||||||
];
|
];
|
||||||
$sub = $ctx->table("subscriptions")->where($data);
|
$sub = $ctx->table("subscriptions")->where($data);
|
||||||
|
|
||||||
if (!($sub->fetch())) {
|
if (!($sub->fetch())) {
|
||||||
$ctx->table("subscriptions")->insert($data);
|
$ctx->table("subscriptions")->insert($data);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1738,4 +1738,52 @@ class User extends RowModel
|
||||||
{
|
{
|
||||||
return DatabaseConnection::i()->getContext()->table("blacklist_relations")->where("author", $this->getId())->count();
|
return DatabaseConnection::i()->getContext()->table("blacklist_relations")->where("author", $this->getId())->count();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getEventCounters(array $list): array
|
||||||
|
{
|
||||||
|
$count_of_keys = sizeof(array_keys($list));
|
||||||
|
$ev_str = $this->getRecord()->events_counters;
|
||||||
|
$counters = [];
|
||||||
|
|
||||||
|
if (!$ev_str) {
|
||||||
|
for ($i = 0; $i < sizeof(array_keys($list)); $i++) {
|
||||||
|
$counters[] = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$counters = unpack("S" . $count_of_keys, base64_decode($ev_str, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'counters' => array_combine(array_keys($list), $counters),
|
||||||
|
'refresh_time' => $this->getRecord()->events_refresh_time,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stateEvents(array $state_list): void
|
||||||
|
{
|
||||||
|
$pack_str = "";
|
||||||
|
|
||||||
|
foreach ($state_list as $item => $id) {
|
||||||
|
$pack_str .= "S";
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->stateChanges("events_counters", base64_encode(pack($pack_str, ...array_values($state_list))));
|
||||||
|
|
||||||
|
if (!$this->getRecord()->events_refresh_time) {
|
||||||
|
$this->stateChanges("events_refresh_time", time());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resetEvents(array $list): void
|
||||||
|
{
|
||||||
|
$values = [];
|
||||||
|
|
||||||
|
foreach ($list as $key => $val) {
|
||||||
|
$values[$key] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->stateEvents($values);
|
||||||
|
$this->stateChanges("events_refresh_time", time());
|
||||||
|
$this->save();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -106,6 +106,10 @@ final class GiftsPresenter extends OpenVKPresenter
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->user->identity, "gifts.send")) {
|
||||||
|
$this->flashFail("err", tr("error"), tr("limit_exceed_exception"));
|
||||||
|
}
|
||||||
|
|
||||||
$comment = empty($c = $this->postParam("comment")) ? null : $c;
|
$comment = empty($c = $this->postParam("comment")) ? null : $c;
|
||||||
$notification = new GiftNotification($user, $this->user->identity, $gift, $comment);
|
$notification = new GiftNotification($user, $this->user->identity, $gift, $comment);
|
||||||
$notification->emit();
|
$notification->emit();
|
||||||
|
|
|
@ -68,6 +68,10 @@ final class GroupPresenter extends OpenVKPresenter
|
||||||
$club->setAbout(empty($this->postParam("about")) ? null : $this->postParam("about"));
|
$club->setAbout(empty($this->postParam("about")) ? null : $this->postParam("about"));
|
||||||
$club->setOwner($this->user->id);
|
$club->setOwner($this->user->id);
|
||||||
|
|
||||||
|
if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->user->identity, "groups.create")) {
|
||||||
|
$this->flashFail("err", tr("error"), tr("limit_exceed_exception"));
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$club->save();
|
$club->save();
|
||||||
} catch (\PDOException $ex) {
|
} catch (\PDOException $ex) {
|
||||||
|
@ -79,6 +83,7 @@ final class GroupPresenter extends OpenVKPresenter
|
||||||
}
|
}
|
||||||
|
|
||||||
$club->toggleSubscription($this->user->identity);
|
$club->toggleSubscription($this->user->identity);
|
||||||
|
|
||||||
$this->redirect("/club" . $club->getId());
|
$this->redirect("/club" . $club->getId());
|
||||||
} else {
|
} else {
|
||||||
$this->flashFail("err", tr("error"), tr("error_no_group_name"));
|
$this->flashFail("err", tr("error"), tr("error_no_group_name"));
|
||||||
|
@ -103,6 +108,12 @@ final class GroupPresenter extends OpenVKPresenter
|
||||||
$this->flashFail("err", tr("error"), tr("forbidden"));
|
$this->flashFail("err", tr("error"), tr("forbidden"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!$club->getSubscriptionStatus($this->user->identity)) {
|
||||||
|
if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->user->identity, "groups.sub")) {
|
||||||
|
$this->flashFail("err", tr("error"), tr("limit_exceed_exception"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$club->toggleSubscription($this->user->identity);
|
$club->toggleSubscription($this->user->identity);
|
||||||
|
|
||||||
$this->redirect($club->getURL());
|
$this->redirect($club->getURL());
|
||||||
|
|
|
@ -103,6 +103,10 @@ final class ReportPresenter extends OpenVKPresenter
|
||||||
exit(json_encode([ "error" => "You can't report yourself" ]));
|
exit(json_encode([ "error" => "You can't report yourself" ]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($this->user->identity->isBannedInSupport()) {
|
||||||
|
exit(json_encode([ "reason" => $this->queryParam("reason") ]));
|
||||||
|
}
|
||||||
|
|
||||||
if (in_array($this->queryParam("type"), ["post", "photo", "video", "group", "comment", "note", "app", "user", "audio", "doc"])) {
|
if (in_array($this->queryParam("type"), ["post", "photo", "video", "group", "comment", "note", "app", "user", "audio", "doc"])) {
|
||||||
if (count(iterator_to_array($this->reports->getDuplicates($this->queryParam("type"), $id, null, $this->user->id))) <= 0) {
|
if (count(iterator_to_array($this->reports->getDuplicates($this->queryParam("type"), $id, null, $this->user->id))) <= 0) {
|
||||||
$report = new Report();
|
$report = new Report();
|
||||||
|
|
|
@ -418,6 +418,12 @@ final class UserPresenter extends OpenVKPresenter
|
||||||
if ($this->postParam("act") == "rej") {
|
if ($this->postParam("act") == "rej") {
|
||||||
$user->changeFlags($this->user->identity, 0b10000000, true);
|
$user->changeFlags($this->user->identity, 0b10000000, true);
|
||||||
} else {
|
} else {
|
||||||
|
if ($user->getSubscriptionStatus($this->user->identity) == \openvk\Web\Models\Entities\User::SUBSCRIPTION_ABSENT) {
|
||||||
|
if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->user->identity, "friends.outgoing_sub")) {
|
||||||
|
$this->flashFail("err", tr("error"), tr("limit_exceed_exception"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$user->toggleSubscription($this->user->identity);
|
$user->toggleSubscription($this->user->identity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -356,6 +356,10 @@ final class WallPresenter extends OpenVKPresenter
|
||||||
$this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_empty_or_too_big"));
|
$this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_empty_or_too_big"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->user->identity, "wall.post")) {
|
||||||
|
$this->flashFail("err", tr("error"), tr("limit_exceed_exception"));
|
||||||
|
}
|
||||||
|
|
||||||
$should_be_suggested = $wall < 0 && !$wallOwner->canBeModifiedBy($this->user->identity) && $wallOwner->getWallType() == 2;
|
$should_be_suggested = $wall < 0 && !$wallOwner->canBeModifiedBy($this->user->identity) && $wallOwner->getWallType() == 2;
|
||||||
try {
|
try {
|
||||||
$post = new Post();
|
$post = new Post();
|
||||||
|
|
|
@ -67,7 +67,7 @@
|
||||||
},
|
},
|
||||||
success: (response) => {
|
success: (response) => {
|
||||||
if (response?.reports?.length != _content) {
|
if (response?.reports?.length != _content) {
|
||||||
NewNotification("Обратите внимание", "В списке появились новые жалобы. Работа ждёт :)");
|
// NewNotification("Обратите внимание", "В списке появились новые жалобы. Работа ждёт :)");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.reports.length > 0) {
|
if (response.reports.length > 0) {
|
||||||
|
|
95
Web/Util/EventRateLimiter.php
Normal file
95
Web/Util/EventRateLimiter.php
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
<?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;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->config = OPENVK_ROOT_CONF["openvk"]["preferences"]["security"]["rateLimits"]["eventsLimit"];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function tryToLimit(?User $user, string $event_type, bool $is_update = true): bool
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
Checks count of actions for last x seconds
|
||||||
|
|
||||||
|
Uses OPENVK_ROOT_CONF["openvk"]["preferences"]["security"]["rateLimits"]["eventsLimit"]
|
||||||
|
|
||||||
|
This check should be peformed only after checking other conditions cuz by default it increments counter
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
true — limit has exceed and the action must be restricted
|
||||||
|
|
||||||
|
false — the action can be performed
|
||||||
|
|
||||||
|
Also returns "true" if this option is disabled
|
||||||
|
*/
|
||||||
|
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$eventsStats = $user->getEventCounters($eventsList);
|
||||||
|
$limitForThatEvent = $eventsList[$event_type];
|
||||||
|
|
||||||
|
$counters = $eventsStats["counters"];
|
||||||
|
$refresh_time = $eventsStats["refresh_time"];
|
||||||
|
$is_restrict_over = $refresh_time < (time() - $restrictionTime);
|
||||||
|
$event_counter = $counters[$event_type];
|
||||||
|
|
||||||
|
if ($refresh_time && $is_restrict_over) {
|
||||||
|
$user->resetEvents($eventsList);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$is_limit_exceed = $event_counter >= $limitForThatEvent;
|
||||||
|
|
||||||
|
if (!$is_limit_exceed && $is_update) {
|
||||||
|
$this->incrementEvent($counters, $event_type, $user);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $is_limit_exceed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function incrementEvent(array $old_values, string $event_type, User $initiator): bool
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
Updates counter for user
|
||||||
|
*/
|
||||||
|
$isEnabled = $this->config['enable'];
|
||||||
|
$eventsList = $this->config['list'];
|
||||||
|
|
||||||
|
if (!$isEnabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$old_values[$event_type] += 1;
|
||||||
|
|
||||||
|
$initiator->stateEvents($old_values);
|
||||||
|
$initiator->save();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
3
install/sqls/00057-event-limiting.sql
Normal file
3
install/sqls/00057-event-limiting.sql
Normal 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`;
|
|
@ -1657,6 +1657,8 @@
|
||||||
"error_geolocation" = "Error while trying to pin geolocation";
|
"error_geolocation" = "Error while trying to pin geolocation";
|
||||||
"error_no_geotag" = "There is no geo-tag pinned in this post";
|
"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 */
|
/* Admin actions */
|
||||||
|
|
||||||
"login_as" = "Login as $1";
|
"login_as" = "Login as $1";
|
||||||
|
|
|
@ -1561,6 +1561,8 @@
|
||||||
"error_geolocation" = "Ошибка при прикреплении геометки";
|
"error_geolocation" = "Ошибка при прикреплении геометки";
|
||||||
"error_no_geotag" = "У поста не указана гео-метка";
|
"error_no_geotag" = "У поста не указана гео-метка";
|
||||||
|
|
||||||
|
"limit_exceed_exception" = "Вы совершаете это действие слишком часто. Повторите позже.";
|
||||||
|
|
||||||
/* Admin actions */
|
/* Admin actions */
|
||||||
|
|
||||||
"login_as" = "Войти как $1";
|
"login_as" = "Войти как $1";
|
||||||
|
|
|
@ -41,6 +41,16 @@ openvk:
|
||||||
maxViolations: 50
|
maxViolations: 50
|
||||||
maxViolationsAge: 120
|
maxViolationsAge: 120
|
||||||
autoban: true
|
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: 30
|
||||||
blacklists:
|
blacklists:
|
||||||
limit: 100
|
limit: 100
|
||||||
applyToAdmins: true
|
applyToAdmins: true
|
||||||
|
|
Loading…
Reference in a new issue