Add rate limiting feature

Co-authored-by: Rempai <kitsuruko@gmail.com>
This commit is contained in:
Alma Armas 2020-12-31 21:18:53 +00:00
parent 6d389537e3
commit 2f09e32b1e
16 changed files with 224 additions and 2 deletions

115
Web/Models/Entities/IP.php Normal file
View 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();
}
}

View 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);
}
}

View file

@ -30,6 +30,7 @@ class AudiosPresenter extends OpenVKPresenter
function renderUpload(): void
{
$this->assertUserLoggedIn();
$this->willExecuteWriteAction();
if($_SERVER["REQUEST_METHOD"] === "POST") {
if(!isset($_FILES["blob"]))

View file

@ -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, который вы ввели, не является корректным.");

View file

@ -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();

View file

@ -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))

View file

@ -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");

View file

@ -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)

View file

@ -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);

View file

@ -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);

View file

@ -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"));

View file

@ -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")) {

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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