From 2cf386e8b5342e6f1d727e5f1b831716ba7c8130 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Sat, 13 Jan 2024 18:23:37 -0600 Subject: [PATCH] Add rate limit and don't use lock for vote --- dmoj/settings.py | 5 +++++ judge/comments.py | 6 +++--- judge/views/comment.py | 42 ++++++++++++++++++++--------------------- judge/views/pagevote.py | 32 ++++++++++++++----------------- requirements.txt | 3 ++- 5 files changed, 45 insertions(+), 43 deletions(-) diff --git a/dmoj/settings.py b/dmoj/settings.py index 43c7527..00db9a1 100644 --- a/dmoj/settings.py +++ b/dmoj/settings.py @@ -491,6 +491,11 @@ DEFAULT_AUTO_FIELD = "django.db.models.AutoField" # Chunk upload CHUNK_UPLOAD_DIR = "/tmp/chunk_upload_tmp" +# Rate limit +RL_VOTE = "200/h" +RL_COMMENT = "30/h" + + try: with open(os.path.join(os.path.dirname(__file__), "local_settings.py")) as f: exec(f.read(), globals()) diff --git a/judge/comments.py b/judge/comments.py index b4de658..58fdcd1 100644 --- a/judge/comments.py +++ b/judge/comments.py @@ -21,6 +21,7 @@ from django.views.generic.base import TemplateResponseMixin from django.views.generic.detail import SingleObjectMixin from reversion import revisions from reversion.models import Revision, Version +from django_ratelimit.decorators import ratelimit from judge.dblock import LockModel from judge.models import Comment, Notification @@ -93,6 +94,7 @@ class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View): and self.request.participation.contest.use_clarifications ) + @method_decorator(ratelimit(key="user", rate=settings.RL_COMMENT)) @method_decorator(login_required) def post(self, request, *args, **kwargs): self.object = self.get_object() @@ -115,9 +117,7 @@ class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View): comment.author = request.profile comment.linked_object = self.object - with LockModel( - write=(Comment, Revision, Version), read=(ContentType,) - ), revisions.create_revision(): + with revisions.create_revision(): revisions.set_user(request.user) revisions.set_comment(_("Posted comment")) comment.save() diff --git a/judge/views/comment.py b/judge/views/comment.py index b4808a7..cfe9adf 100644 --- a/judge/views/comment.py +++ b/judge/views/comment.py @@ -22,6 +22,8 @@ from django.views.generic import DetailView, UpdateView from django.urls import reverse_lazy from reversion import revisions from reversion.models import Version +from django.conf import settings +from django_ratelimit.decorators import ratelimit from judge.dblock import LockModel from judge.models import Comment, CommentVote, Notification, BlogPost @@ -40,6 +42,7 @@ __all__ = [ ] +@ratelimit(key="user", rate=settings.RL_VOTE) @login_required def vote_comment(request, delta): if abs(delta) != 1: @@ -77,27 +80,24 @@ def vote_comment(request, delta): vote.voter = request.profile vote.score = delta - while True: - try: - vote.save() - except IntegrityError: - with LockModel(write=(CommentVote,)): - try: - vote = CommentVote.objects.get( - comment_id=comment_id, voter=request.profile - ) - except CommentVote.DoesNotExist: - # We must continue racing in case this is exploited to manipulate votes. - continue - if -vote.score != delta: - return HttpResponseBadRequest( - _("You already voted."), content_type="text/plain" - ) - vote.delete() - Comment.objects.filter(id=comment_id).update(score=F("score") - vote.score) - else: - Comment.objects.filter(id=comment_id).update(score=F("score") + delta) - break + try: + vote.save() + except IntegrityError: + with LockModel(write=(CommentVote,)): + try: + vote = CommentVote.objects.get( + comment_id=comment_id, voter=request.profile + ) + except CommentVote.DoesNotExist: + raise Http404() + if -vote.score != delta: + return HttpResponseBadRequest( + _("You already voted."), content_type="text/plain" + ) + vote.delete() + Comment.objects.filter(id=comment_id).update(score=F("score") - vote.score) + else: + Comment.objects.filter(id=comment_id).update(score=F("score") + delta) return HttpResponse("success", content_type="text/plain") diff --git a/judge/views/pagevote.py b/judge/views/pagevote.py index 988d355..6816ebb 100644 --- a/judge/views/pagevote.py +++ b/judge/views/pagevote.py @@ -12,8 +12,9 @@ from judge.models.pagevote import PageVote, PageVoteVoter from django.views.generic.base import TemplateResponseMixin from django.views.generic.detail import SingleObjectMixin -from judge.dblock import LockModel from django.views.generic import View, ListView +from django_ratelimit.decorators import ratelimit +from django.conf import settings __all__ = [ @@ -24,6 +25,7 @@ __all__ = [ ] +@ratelimit(key="user", rate=settings.RL_VOTE) @login_required def vote_page(request, delta): if abs(delta) != 1: @@ -61,25 +63,19 @@ def vote_page(request, delta): vote.voter = request.profile vote.score = delta - while True: + try: + vote.save() + except IntegrityError: try: - vote.save() - except IntegrityError: - with LockModel(write=(PageVoteVoter,)): - try: - vote = PageVoteVoter.objects.get( - pagevote_id=pagevote_id, voter=request.profile - ) - except PageVoteVoter.DoesNotExist: - # We must continue racing in case this is exploited to manipulate votes. - continue - vote.delete() - PageVote.objects.filter(id=pagevote_id).update( - score=F("score") - vote.score + vote = PageVoteVoter.objects.get( + pagevote_id=pagevote_id, voter=request.profile ) - else: - PageVote.objects.filter(id=pagevote_id).update(score=F("score") + delta) - break + except PageVoteVoter.DoesNotExist: + raise Http404() + vote.delete() + PageVote.objects.filter(id=pagevote_id).update(score=F("score") - vote.score) + else: + PageVote.objects.filter(id=pagevote_id).update(score=F("score") + delta) _dirty_vote_score(pagevote_id, request.profile) return HttpResponse("success", content_type="text/plain") diff --git a/requirements.txt b/requirements.txt index 53228e7..052b752 100644 --- a/requirements.txt +++ b/requirements.txt @@ -42,4 +42,5 @@ bleach pymdown-extensions mdx-breakless-lists beautifulsoup4 -pre-commit \ No newline at end of file +pre-commit +django-ratelimit \ No newline at end of file