mirror of
https://github.com/openvk/openvk
synced 2024-11-13 10:39:24 +03:00
Add rate limiting feature
Co-authored-by: Rempai <kitsuruko@gmail.com>
This commit is contained in:
parent
6d389537e3
commit
2f09e32b1e
16 changed files with 224 additions and 2 deletions
115
Web/Models/Entities/IP.php
Normal file
115
Web/Models/Entities/IP.php
Normal file
|
@ -0,0 +1,115 @@
|
|||
<?php declare(strict_types=1);
|
||||
namespace openvk\Web\Models\Entities;
|
||||
use openvk\Web\Models\RowModel;
|
||||
use openvk\Web\Util\DateTime;
|
||||
|
||||
class IP extends RowModel
|
||||
{
|
||||
protected $tableName = "ip";
|
||||
|
||||
const RL_RESET = 0;
|
||||
const RL_CANEXEC = 1;
|
||||
const RL_VIOLATION = 2;
|
||||
const RL_BANNED = 3;
|
||||
|
||||
function getIp(): string
|
||||
{
|
||||
return inet_ntop($this->getRecord()->ip);
|
||||
}
|
||||
|
||||
function getDiscoveryDate(): DateTime
|
||||
{
|
||||
return new DateTime($this->getRecord()->first_seen);
|
||||
}
|
||||
|
||||
function isBanned(): bool
|
||||
{
|
||||
return (bool) $this->getRecord()->banned;
|
||||
}
|
||||
|
||||
function ban(): void
|
||||
{
|
||||
$this->stateChanges("banned", true);
|
||||
$this->save();
|
||||
}
|
||||
|
||||
function pardon(): void
|
||||
{
|
||||
$this->stateChanges("banned", false);
|
||||
$this->save();
|
||||
}
|
||||
|
||||
function clear(): void
|
||||
{
|
||||
$this->stateChanges("rate_limit_counter_start", 0);
|
||||
$this->stateChanges("rate_limit_counter", 0);
|
||||
$this->stateChanges("rate_limit_violation_counter_start", 0);
|
||||
$this->stateChanges("rate_limit_violation_counter", 0);
|
||||
$this->save();
|
||||
}
|
||||
|
||||
function rateLimit(int $actionComplexity = 1): int
|
||||
{
|
||||
$counterSessionStart = $this->getRecord()->rate_limit_counter_start;
|
||||
$vCounterSessionStart = $this->getRecord()->rate_limit_violation_counter_start;
|
||||
|
||||
$aCounter = $this->getRecord()->rate_limit_counter;
|
||||
$vCounter = $this->getRecord()->rate_limit_violation_counter;
|
||||
|
||||
$config = (object) OPENVK_ROOT_CONF["openvk"]["preferences"]["security"]["rateLimits"];
|
||||
|
||||
try {
|
||||
if((time() - $config->time) > $counterSessionStart) {
|
||||
$counterSessionStart = time();
|
||||
$aCounter = $actionComplexity;
|
||||
|
||||
return static::RL_RESET;
|
||||
}
|
||||
|
||||
if(($aCounter + $actionComplexity) <= $config->actions) {
|
||||
$aCounter += $actionComplexity;
|
||||
|
||||
return static::RL_CANEXEC;
|
||||
}
|
||||
|
||||
if((time() - $config->maxViolationsAge) > $vCounterSessionStart) {
|
||||
$vCounterSessionStart = time();
|
||||
$vCounter = 1;
|
||||
|
||||
return static::RL_VIOLATION;
|
||||
}
|
||||
|
||||
$vCounter += 1;
|
||||
if($vCounter >= $config->maxViolations) {
|
||||
$this->stateChanges("banned", true);
|
||||
|
||||
return static::RL_BANNED;
|
||||
}
|
||||
|
||||
return static::RL_VIOLATION;
|
||||
} finally {
|
||||
$this->stateChanges("rate_limit_counter_start", $counterSessionStart);
|
||||
$this->stateChanges("rate_limit_counter", $aCounter);
|
||||
$this->stateChanges("rate_limit_violation_counter_start", $vCounterSessionStart);
|
||||
$this->stateChanges("rate_limit_violation_counter", $vCounter);
|
||||
$this->save();
|
||||
}
|
||||
}
|
||||
|
||||
function setIp(string $ip): void
|
||||
{
|
||||
$ip = inet_pton($ip);
|
||||
if(!$ip)
|
||||
throw new \UnexpectedValueException("Malformed IP address");
|
||||
|
||||
$this->stateChanges("ip", $ip);
|
||||
}
|
||||
|
||||
function save(): void
|
||||
{
|
||||
if(is_null($this->getRecord()))
|
||||
$this->stateChanges("first_seen", time());
|
||||
|
||||
parent::save();
|
||||
}
|
||||
}
|
34
Web/Models/Repositories/IPs.php
Normal file
34
Web/Models/Repositories/IPs.php
Normal file
|
@ -0,0 +1,34 @@
|
|||
<?php declare(strict_types=1);
|
||||
namespace openvk\Web\Models\Repositories;
|
||||
use Chandler\Database\DatabaseConnection;
|
||||
use openvk\Web\Models\Entities\IP;
|
||||
|
||||
class IPs
|
||||
{
|
||||
private $context;
|
||||
private $ips;
|
||||
|
||||
function __construct()
|
||||
{
|
||||
$this->context = DatabaseConnection::i()->getContext();
|
||||
$this->ips = $this->context->table("ip");
|
||||
}
|
||||
|
||||
function get(string $ip): ?IP
|
||||
{
|
||||
$bip = inet_pton($ip);
|
||||
if(!$bip)
|
||||
throw new \UnexpectedValueException("Malformed IP address");
|
||||
|
||||
$res = $this->ips->get($bip);
|
||||
if(!$res) {
|
||||
$res = new IP;
|
||||
$res->setIp($ip);
|
||||
$res->save();
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
return new IP($res);
|
||||
}
|
||||
}
|
|
@ -30,6 +30,7 @@ class AudiosPresenter extends OpenVKPresenter
|
|||
function renderUpload(): void
|
||||
{
|
||||
$this->assertUserLoggedIn();
|
||||
$this->willExecuteWriteAction();
|
||||
|
||||
if($_SERVER["REQUEST_METHOD"] === "POST") {
|
||||
if(!isset($_FILES["blob"]))
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
<?php declare(strict_types=1);
|
||||
namespace openvk\Web\Presenters;
|
||||
use openvk\Web\Models\Entities\IP;
|
||||
use openvk\Web\Models\Entities\User;
|
||||
use openvk\Web\Models\Entities\PasswordReset;
|
||||
use openvk\Web\Models\Repositories\IPs;
|
||||
use openvk\Web\Models\Repositories\Users;
|
||||
use openvk\Web\Models\Repositories\Restores;
|
||||
use Chandler\Session\Session;
|
||||
|
@ -40,6 +42,14 @@ final class AuthPresenter extends OpenVKPresenter
|
|||
return checkdnsrr($domain, "MX");
|
||||
}
|
||||
|
||||
private function ipValid(): bool
|
||||
{
|
||||
$ip = (new IPs)->get(CONNECTING_IP);
|
||||
$res = $ip->rateLimit(0);
|
||||
|
||||
return $res === IP::RL_RESET || $res === IP::RL_CANEXEC;
|
||||
}
|
||||
|
||||
function renderRegister(): void
|
||||
{
|
||||
if(!is_null($this->user))
|
||||
|
@ -50,6 +60,9 @@ final class AuthPresenter extends OpenVKPresenter
|
|||
if($_SERVER["REQUEST_METHOD"] === "POST") {
|
||||
$this->assertCaptchaCheckPassed();
|
||||
|
||||
if(!$this->ipValid())
|
||||
$this->flashFail("err", "Подозрительная попытка регистрации", "Вы пытались зарегистрироваться из подозрительного места.");
|
||||
|
||||
if(!$this->emailValid($this->postParam("email")))
|
||||
$this->flashFail("err", "Неверный email адрес", "Email, который вы ввели, не является корректным.");
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ final class CommentPresenter extends OpenVKPresenter
|
|||
function renderLike(int $id): void
|
||||
{
|
||||
$this->assertUserLoggedIn();
|
||||
$this->willExecuteWriteAction();
|
||||
|
||||
$comment = (new Comments)->get($id);
|
||||
if(!$comment || $comment->isDeleted()) $this->notFound();
|
||||
|
@ -28,6 +29,7 @@ final class CommentPresenter extends OpenVKPresenter
|
|||
function renderMakeComment(string $repo, int $eId): void
|
||||
{
|
||||
$this->assertUserLoggedIn();
|
||||
$this->willExecuteWriteAction();
|
||||
|
||||
$repoClass = $this->models[$repo] ?? NULL;
|
||||
if(!$repoClass) chandler_http_panic(400, "Bad Request", "Unexpected $repo.");
|
||||
|
@ -58,6 +60,7 @@ final class CommentPresenter extends OpenVKPresenter
|
|||
function renderDeleteComment(int $id): void
|
||||
{
|
||||
$this->assertUserLoggedIn();
|
||||
$this->willExecuteWriteAction();
|
||||
|
||||
$comment = (new Comments)->get($id);
|
||||
if(!$comment) $this->notFound();
|
||||
|
|
|
@ -34,6 +34,7 @@ final class GroupPresenter extends OpenVKPresenter
|
|||
function renderCreate(): void
|
||||
{
|
||||
$this->assertUserLoggedIn();
|
||||
$this->willExecuteWriteAction();
|
||||
|
||||
if($_SERVER["REQUEST_METHOD"] === "POST") {
|
||||
if(!empty($this->postParam("name")))
|
||||
|
@ -64,6 +65,7 @@ final class GroupPresenter extends OpenVKPresenter
|
|||
function renderSub(): void
|
||||
{
|
||||
$this->assertUserLoggedIn();
|
||||
$this->willExecuteWriteAction();
|
||||
|
||||
if($_SERVER["REQUEST_METHOD"] !== "POST") exit("Invalid state");
|
||||
|
||||
|
@ -135,6 +137,7 @@ final class GroupPresenter extends OpenVKPresenter
|
|||
function renderEdit(int $id): void
|
||||
{
|
||||
$this->assertUserLoggedIn();
|
||||
$this->willExecuteWriteAction();
|
||||
|
||||
$club = $this->clubs->get($id);
|
||||
if(!$club->canBeModifiedBy($this->user->identity))
|
||||
|
|
|
@ -116,6 +116,7 @@ final class MessengerPresenter extends OpenVKPresenter
|
|||
function renderApiWriteMessage(int $sel): void
|
||||
{
|
||||
$this->assertUserLoggedIn();
|
||||
$this->willExecuteWriteAction();
|
||||
|
||||
if(empty($this->postParam("content"))) {
|
||||
header("HTTP/1.1 400 Bad Request");
|
||||
|
|
|
@ -46,6 +46,7 @@ final class NotesPresenter extends OpenVKPresenter
|
|||
function renderCreate(): void
|
||||
{
|
||||
$this->assertUserLoggedIn();
|
||||
$this->willExecuteWriteAction();
|
||||
|
||||
$id = $this->user->id; #TODO: when ACL'll be done, allow admins to edit users via ?GUID=(chandler guid)
|
||||
|
||||
|
|
|
@ -5,6 +5,8 @@ use Chandler\MVC\SimplePresenter;
|
|||
use Chandler\Session\Session;
|
||||
use Chandler\Security\Authenticator;
|
||||
use Latte\Engine as TemplatingEngine;
|
||||
use openvk\Web\Models\Entities\IP;
|
||||
use openvk\Web\Models\Repositories\IPs;
|
||||
use openvk\Web\Models\Repositories\Users;
|
||||
|
||||
abstract class OpenVKPresenter extends SimplePresenter
|
||||
|
@ -87,10 +89,25 @@ abstract class OpenVKPresenter extends SimplePresenter
|
|||
|
||||
protected function assertCaptchaCheckPassed(): void
|
||||
{
|
||||
if(!check_captcha($_POST["captcha"]))
|
||||
if(!check_captcha())
|
||||
$this->flashFail("err", "Неправильно введены символы", "Пожалуйста, убедитесь, что вы правильно заполнили поле с капчей.");
|
||||
}
|
||||
|
||||
protected function willExecuteWriteAction(): void
|
||||
{
|
||||
$ip = (new IPs)->get(CONNECTING_IP);
|
||||
$res = $ip->rateLimit();
|
||||
|
||||
if(!($res === IP::RL_RESET || $res === IP::RL_CANEXEC)) {
|
||||
if($res === IP::RL_BANNED && OPENVK_ROOT_CONF["openvk"]["preferences"]["security"]["rateLimits"]["autoban"]) {
|
||||
$this->user->identity->ban("Account has possibly been stolen");
|
||||
exit("Хакеры? Интересно...");
|
||||
}
|
||||
|
||||
$this->flashFail("err", "Чумба, ты совсем ёбнутый?", "Сходи к мозгоправу, попей колёсики. В OpenVK нельзя вбрасывать щитпосты так часто. Код исключения: $res.");
|
||||
}
|
||||
}
|
||||
|
||||
protected function signal(object $event): bool
|
||||
{
|
||||
return (SignalManager::i())->triggerEvent($event, $this->user->id);
|
||||
|
|
|
@ -57,6 +57,7 @@ final class PhotosPresenter extends OpenVKPresenter
|
|||
function renderCreateAlbum(): void
|
||||
{
|
||||
$this->assertUserLoggedIn();
|
||||
$this->willExecuteWriteAction();
|
||||
|
||||
if(!is_null($gpid = $this->queryParam("gpid"))) {
|
||||
$club = (new Clubs)->get((int) $gpid);
|
||||
|
@ -81,6 +82,7 @@ final class PhotosPresenter extends OpenVKPresenter
|
|||
function renderEditAlbum(int $owner, int $id): void
|
||||
{
|
||||
$this->assertUserLoggedIn();
|
||||
$this->willExecuteWriteAction();
|
||||
|
||||
$album = $this->albums->get($id);
|
||||
if(!$album) $this->notFound();
|
||||
|
@ -102,6 +104,7 @@ final class PhotosPresenter extends OpenVKPresenter
|
|||
function renderDeleteAlbum(int $owner, int $id): void
|
||||
{
|
||||
$this->assertUserLoggedIn();
|
||||
$this->willExecuteWriteAction();
|
||||
$this->assertNoCSRF();
|
||||
|
||||
$album = $this->albums->get($id);
|
||||
|
@ -156,6 +159,7 @@ final class PhotosPresenter extends OpenVKPresenter
|
|||
function renderEditPhoto(int $ownerId, int $photoId): void
|
||||
{
|
||||
$this->assertUserLoggedIn();
|
||||
$this->willExecuteWriteAction();
|
||||
|
||||
$photo = $this->photos->getByOwnerAndVID($ownerId, $photoId);
|
||||
if(!$photo) $this->notFound();
|
||||
|
@ -176,6 +180,7 @@ final class PhotosPresenter extends OpenVKPresenter
|
|||
function renderUploadPhoto(): void
|
||||
{
|
||||
$this->assertUserLoggedIn();
|
||||
$this->willExecuteWriteAction();
|
||||
|
||||
if(is_null($this->queryParam("album")))
|
||||
$this->flashFail("err", "Неизвестная ошибка", "Не удалось сохранить фотографию в <b>DELETED</b>.");
|
||||
|
@ -213,6 +218,7 @@ final class PhotosPresenter extends OpenVKPresenter
|
|||
function renderUnlinkPhoto(int $owner, int $albumId, int $photoId): void
|
||||
{
|
||||
$this->assertUserLoggedIn();
|
||||
$this->willExecuteWriteAction();
|
||||
|
||||
$album = $this->albums->get($albumId);
|
||||
$photo = $this->photos->get($photoId);
|
||||
|
@ -233,6 +239,7 @@ final class PhotosPresenter extends OpenVKPresenter
|
|||
function renderDeletePhoto(int $ownerId, int $photoId): void
|
||||
{
|
||||
$this->assertUserLoggedIn();
|
||||
$this->willExecuteWriteAction();
|
||||
$this->assertNoCSRF();
|
||||
|
||||
$photo = $this->photos->getByOwnerAndVID($ownerId, $photoId);
|
||||
|
|
|
@ -40,10 +40,11 @@ final class SupportPresenter extends OpenVKPresenter
|
|||
|
||||
if($_SERVER["REQUEST_METHOD"] === "POST")
|
||||
{
|
||||
|
||||
if(!empty($this->postParam("name")) && !empty($this->postParam("text")))
|
||||
{
|
||||
$this->assertNoCSRF();
|
||||
$this->willExecuteWriteAction();
|
||||
|
||||
$ticket = new Ticket;
|
||||
$ticket->setType(0);
|
||||
$ticket->setUser_id($this->user->id);
|
||||
|
@ -101,6 +102,7 @@ final class SupportPresenter extends OpenVKPresenter
|
|||
function renderDelete(int $id): void
|
||||
{
|
||||
$this->assertUserLoggedIn();
|
||||
$this->willExecuteWriteAction();
|
||||
if (!empty($id)) {
|
||||
$ticket = $this->tickets->get($id);
|
||||
if (!$ticket || $ticket->isDeleted() != 0 || $ticket->authorId() !== $this->user->id)
|
||||
|
@ -132,6 +134,8 @@ final class SupportPresenter extends OpenVKPresenter
|
|||
$ticket->save();
|
||||
|
||||
$this->assertNoCSRF();
|
||||
$this->willExecuteWriteAction();
|
||||
|
||||
$comment = new TicketComment;
|
||||
$comment->setUser_id($this->user->id);
|
||||
$comment->setUser_type(0);
|
||||
|
@ -166,6 +170,8 @@ final class SupportPresenter extends OpenVKPresenter
|
|||
|
||||
if($_SERVER["REQUEST_METHOD"] === "POST")
|
||||
{
|
||||
$this->willExecuteWriteAction();
|
||||
|
||||
if(!empty($this->postParam("text")) && !empty($this->postParam("status")))
|
||||
{
|
||||
$ticket->setType($this->postParam("status"));
|
||||
|
|
|
@ -95,6 +95,8 @@ final class UserPresenter extends OpenVKPresenter
|
|||
|
||||
$user = $this->users->get($id);
|
||||
if($_SERVER["REQUEST_METHOD"] === "POST") {
|
||||
$this->willExecuteWriteAction();
|
||||
|
||||
if($_GET['act'] === "main" || $_GET['act'] == NULL) {
|
||||
$user->setFirst_Name(empty($this->postParam("first_name")) ? $user->getFirstName() : $this->postParam("first_name"));
|
||||
$user->setLast_Name(empty($this->postParam("last_name")) ? "" : $this->postParam("last_name"));
|
||||
|
@ -157,6 +159,7 @@ final class UserPresenter extends OpenVKPresenter
|
|||
function renderVerifyPhone(): void
|
||||
{
|
||||
$this->assertUserLoggedIn();
|
||||
$this->willExecuteWriteAction();
|
||||
|
||||
$user = $this->user->identity;
|
||||
if(!$user->hasPendingNumberChange())
|
||||
|
@ -175,6 +178,7 @@ final class UserPresenter extends OpenVKPresenter
|
|||
function renderSub(): void
|
||||
{
|
||||
$this->assertUserLoggedIn();
|
||||
$this->willExecuteWriteAction();
|
||||
|
||||
if($_SERVER["REQUEST_METHOD"] !== "POST") exit("Invalid state");
|
||||
|
||||
|
@ -191,6 +195,7 @@ final class UserPresenter extends OpenVKPresenter
|
|||
function renderSetAvatar(): void
|
||||
{
|
||||
$this->assertUserLoggedIn();
|
||||
$this->willExecuteWriteAction();
|
||||
|
||||
$photo = new Photo;
|
||||
try {
|
||||
|
@ -219,6 +224,8 @@ final class UserPresenter extends OpenVKPresenter
|
|||
|
||||
$user = $this->users->get($id);
|
||||
if($_SERVER["REQUEST_METHOD"] === "POST") {
|
||||
$this->willExecuteWriteAction();
|
||||
|
||||
if($_GET['act'] === "main" || $_GET['act'] == NULL) {
|
||||
if($this->postParam("old_pass") && $this->postParam("new_pass") && $this->postParam("repeat_pass")) {
|
||||
if($this->postParam("new_pass") === $this->postParam("repeat_pass")) {
|
||||
|
|
|
@ -51,6 +51,7 @@ final class VideosPresenter extends OpenVKPresenter
|
|||
function renderUpload(): void
|
||||
{
|
||||
$this->assertUserLoggedIn();
|
||||
$this->willExecuteWriteAction();
|
||||
|
||||
if($_SERVER["REQUEST_METHOD"] === "POST") {
|
||||
if(!empty($this->postParam("name"))) {
|
||||
|
@ -83,6 +84,7 @@ final class VideosPresenter extends OpenVKPresenter
|
|||
function renderEdit(int $owner, int $vId): void
|
||||
{
|
||||
$this->assertUserLoggedIn();
|
||||
$this->willExecuteWriteAction();
|
||||
|
||||
$video = $this->videos->getByOwnerAndVID($owner, $vId);
|
||||
if(!$video)
|
||||
|
@ -105,6 +107,7 @@ final class VideosPresenter extends OpenVKPresenter
|
|||
function renderRemove(int $owner, int $vid): void
|
||||
{
|
||||
$this->assertUserLoggedIn();
|
||||
$this->willExecuteWriteAction();
|
||||
|
||||
$video = $this->videos->getByOwnerAndVID($owner, $vid);
|
||||
if(!$video)
|
||||
|
|
|
@ -150,6 +150,7 @@ final class WallPresenter extends OpenVKPresenter
|
|||
function renderMakePost(int $wall): void
|
||||
{
|
||||
$this->assertUserLoggedIn();
|
||||
$this->willExecuteWriteAction();
|
||||
|
||||
$wallOwner = ($wall > 0 ? (new Users)->get($wall) : (new Clubs)->get($wall * -1))
|
||||
?? $this->flashFail("err", "Не удалось опубликовать пост", "Такого пользователя не существует.");
|
||||
|
@ -247,6 +248,7 @@ final class WallPresenter extends OpenVKPresenter
|
|||
function renderLike(int $wall, int $post_id): void
|
||||
{
|
||||
$this->assertUserLoggedIn();
|
||||
$this->willExecuteWriteAction();
|
||||
$this->assertNoCSRF();
|
||||
|
||||
$post = $this->posts->getPostById($wall, $post_id);
|
||||
|
@ -268,6 +270,7 @@ final class WallPresenter extends OpenVKPresenter
|
|||
function renderShare(int $wall, int $post_id): void
|
||||
{
|
||||
$this->assertUserLoggedIn();
|
||||
$this->willExecuteWriteAction();
|
||||
$this->assertNoCSRF();
|
||||
|
||||
$post = $this->posts->getPostById($wall, $post_id);
|
||||
|
@ -292,6 +295,7 @@ final class WallPresenter extends OpenVKPresenter
|
|||
function renderDelete(int $wall, int $post_id): void
|
||||
{
|
||||
$this->assertUserLoggedIn();
|
||||
$this->willExecuteWriteAction();
|
||||
|
||||
$post = $this->posts->getPostById($wall, $post_id);
|
||||
if(!$post)
|
||||
|
|
|
@ -34,4 +34,5 @@ services:
|
|||
- openvk\Web\Models\Repositories\Restores
|
||||
- openvk\Web\Models\Repositories\Notifications
|
||||
- openvk\Web\Models\Repositories\TicketComments
|
||||
- openvk\Web\Models\Repositories\IPs
|
||||
- openvk\Web\Models\Repositories\ContentSearchRepository
|
||||
|
|
|
@ -17,6 +17,12 @@ openvk:
|
|||
forcePhoneVerification: false
|
||||
forceEmailVerification: false
|
||||
enableSu: true
|
||||
rateLimits:
|
||||
actions: 5
|
||||
time: 20
|
||||
maxViolations: 50
|
||||
maxViolationsAge: 120
|
||||
autoban: true
|
||||
support:
|
||||
supportName: "Moderator"
|
||||
adminAccount: 1 # Change this ok
|
||||
|
|
Loading…
Reference in a new issue