mirror of
https://github.com/openvk/openvk
synced 2025-07-07 00:09:48 +03:00
Merge 2333924089
into a3634c19cf
This commit is contained in:
commit
52d3ff13a5
15 changed files with 173 additions and 1 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;
|
||||||
|
|
|
@ -620,6 +620,10 @@ final class Wall extends VKAPIRequestHandler
|
||||||
return (object) ["post_id" => $post->getVirtualId()];
|
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"];
|
$anon = OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["anonymousPosting"]["enable"];
|
||||||
if ($wallOwner instanceof Club && $from_group == 1 && $signed != 1 && $anon) {
|
if ($wallOwner instanceof Club && $from_group == 1 && $signed != 1 && $anon) {
|
||||||
$manager = $wallOwner->getManager($this->getUser());
|
$manager = $wallOwner->getManager($this->getUser());
|
||||||
|
@ -723,9 +727,11 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
\openvk\Web\Util\EventRateLimiter::i()->writeEvent("wall.post", $this->getUser(), $wallOwner);
|
||||||
|
|
||||||
return (object) ["post_id" => $post->getVirtualId()];
|
return (object) ["post_id" => $post->getVirtualId()];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,15 @@ trait TSubscribable
|
||||||
|
|
||||||
if (!($sub->fetch())) {
|
if (!($sub->fetch())) {
|
||||||
$ctx->table("subscriptions")->insert($data);
|
$ctx->table("subscriptions")->insert($data);
|
||||||
|
|
||||||
|
# todo change
|
||||||
|
|
||||||
|
if (str_contains(static::class, "Club")) {
|
||||||
|
\openvk\Web\Util\EventRateLimiter::i()->writeEvent("groups.sub", $user, $this);
|
||||||
|
} else {
|
||||||
|
\openvk\Web\Util\EventRateLimiter::i()->writeEvent("friends.outgoing_sub", $user, $this);
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -114,6 +114,7 @@ class User extends RowModel
|
||||||
|
|
||||||
public function getChandlerUser(): ChandlerUser
|
public function getChandlerUser(): ChandlerUser
|
||||||
{
|
{
|
||||||
|
# TODO cache this function
|
||||||
return new ChandlerUser($this->getRecord()->ref("ChandlerUsers", "user"));
|
return new ChandlerUser($this->getRecord()->ref("ChandlerUsers", "user"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1043,6 +1044,8 @@ class User extends RowModel
|
||||||
"anonymous" => $anonymous,
|
"anonymous" => $anonymous,
|
||||||
"sent" => time(),
|
"sent" => time(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
\openvk\Web\Util\EventRateLimiter::i()->writeEvent("gifts.send", $sender, $this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function ban(string $reason, bool $deleteSubscriptions = true, $unban_time = null, ?int $initiator = null): void
|
public function ban(string $reason, bool $deleteSubscriptions = true, $unban_time = null, ?int $initiator = null): void
|
||||||
|
|
|
@ -106,6 +106,10 @@ final class GiftsPresenter extends OpenVKPresenter
|
||||||
return;
|
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;
|
$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();
|
||||||
|
|
|
@ -63,6 +63,10 @@ final class GroupPresenter extends OpenVKPresenter
|
||||||
|
|
||||||
if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
||||||
if (!empty($this->postParam("name")) && mb_strlen(trim($this->postParam("name"))) > 0) {
|
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 = new Club();
|
||||||
$club->setName($this->postParam("name"));
|
$club->setName($this->postParam("name"));
|
||||||
$club->setAbout(empty($this->postParam("about")) ? null : $this->postParam("about"));
|
$club->setAbout(empty($this->postParam("about")) ? null : $this->postParam("about"));
|
||||||
|
@ -79,6 +83,9 @@ final class GroupPresenter extends OpenVKPresenter
|
||||||
}
|
}
|
||||||
|
|
||||||
$club->toggleSubscription($this->user->identity);
|
$club->toggleSubscription($this->user->identity);
|
||||||
|
|
||||||
|
\openvk\Web\Util\EventRateLimiter::i()->writeEvent("groups.create", $this->user->identity, $club);
|
||||||
|
|
||||||
$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"));
|
||||||
|
|
|
@ -432,6 +432,8 @@ final class WallPresenter extends OpenVKPresenter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
\openvk\Web\Util\EventRateLimiter::i()->writeEvent("wall.post", $this->user->identity, $wallOwner);
|
||||||
|
|
||||||
if ($should_be_suggested) {
|
if ($should_be_suggested) {
|
||||||
$this->redirect("/club" . $wallOwner->getId() . "/suggested");
|
$this->redirect("/club" . $wallOwner->getId() . "/suggested");
|
||||||
} else {
|
} else {
|
||||||
|
|
102
Web/Util/EventRateLimiter.php
Normal file
102
Web/Util/EventRateLimiter.php
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
<?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 UserEvent
|
||||||
|
{
|
||||||
|
private $data;
|
||||||
|
|
||||||
|
public function __construct($data)
|
||||||
|
{
|
||||||
|
$this->data = $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function write($edb): bool
|
||||||
|
{
|
||||||
|
$edb->getConnection()->query("INSERT INTO `user-events` VALUES (?, ?, ?, ?, ?)", ...array_values($this->data));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 x seconds, returns true if limit has exceed
|
||||||
|
|
||||||
|
x is OPENVK_ROOT_CONF["openvk"]["preferences"]["security"]["rateLimits"]["eventsLimit"]["restrictionTime"]
|
||||||
|
*/
|
||||||
|
public function tryToLimit(?User $user, string $event_type, bool $distinct = true): bool
|
||||||
|
{
|
||||||
|
if (!$this->config['enable']) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!($e = eventdb())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->config['ignoreForAdmins'] && $user->isAdmin()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$limitForThisEvent = $this->config['list'][$event_type];
|
||||||
|
$compareTime = time() - $this->config['restrictionTime'];
|
||||||
|
|
||||||
|
$query = "SELECT COUNT(".($distinct ? "DISTINCT(`receiverId`)" : "*").") as `cnt` FROM `user-events` WHERE `initiatorId` = ? AND `eventType` = ? AND `eventTime` > ?";
|
||||||
|
|
||||||
|
$result = $e->getConnection()->query($query, ...[$user->getId(), $event_type, $compareTime]);
|
||||||
|
$count = $result->fetch()->cnt;
|
||||||
|
#bdump($count); exit();
|
||||||
|
|
||||||
|
return $count > $limitForThisEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Writes new event to `openvk-eventdb`.`user-events`
|
||||||
|
*/
|
||||||
|
public function writeEvent(string $event_type, User $initiator, ?RowModel $reciever = null): bool
|
||||||
|
{
|
||||||
|
if (!$this->config['enable']) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!($e = eventdb())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'initiatorId' => $initiator->getId(),
|
||||||
|
'initiatorIp' => null,
|
||||||
|
'receiverId' => null,
|
||||||
|
'eventType' => $event_type,
|
||||||
|
'eventTime' => time()
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($reciever) {
|
||||||
|
$data['receiverId'] = $reciever->getRealId();
|
||||||
|
}
|
||||||
|
|
||||||
|
$newEvent = new UserEvent($data);
|
||||||
|
$newEvent->write($e);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
8
install/sqls/eventdb/00001-events-log.sql
Normal file
8
install/sqls/eventdb/00001-events-log.sql
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
CREATE TABLE `user-events`
|
||||||
|
(
|
||||||
|
`initiatorId` BIGINT(20) UNSIGNED NOT NULL,
|
||||||
|
`initiatorIp` VARBINARY(16) NULL DEFAULT NULL,
|
||||||
|
`receiverId` BIGINT(20) NULL DEFAULT NULL,
|
||||||
|
`eventType` CHAR(25) NOT NULL,
|
||||||
|
`eventTime` BIGINT(20) UNSIGNED NOT NULL
|
||||||
|
) ENGINE = InnoDB;
|
|
@ -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: 20
|
||||||
blacklists:
|
blacklists:
|
||||||
limit: 100
|
limit: 100
|
||||||
applyToAdmins: true
|
applyToAdmins: true
|
||||||
|
|
Loading…
Reference in a new issue