Add rate limit and don't use lock for vote

This commit is contained in:
cuom1999 2024-01-13 18:23:37 -06:00
parent 104cee9e81
commit 2cf386e8b5
5 changed files with 45 additions and 43 deletions

View file

@ -491,6 +491,11 @@ DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
# Chunk upload # Chunk upload
CHUNK_UPLOAD_DIR = "/tmp/chunk_upload_tmp" CHUNK_UPLOAD_DIR = "/tmp/chunk_upload_tmp"
# Rate limit
RL_VOTE = "200/h"
RL_COMMENT = "30/h"
try: try:
with open(os.path.join(os.path.dirname(__file__), "local_settings.py")) as f: with open(os.path.join(os.path.dirname(__file__), "local_settings.py")) as f:
exec(f.read(), globals()) exec(f.read(), globals())

View file

@ -21,6 +21,7 @@ from django.views.generic.base import TemplateResponseMixin
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
from reversion import revisions from reversion import revisions
from reversion.models import Revision, Version from reversion.models import Revision, Version
from django_ratelimit.decorators import ratelimit
from judge.dblock import LockModel from judge.dblock import LockModel
from judge.models import Comment, Notification from judge.models import Comment, Notification
@ -93,6 +94,7 @@ class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View):
and self.request.participation.contest.use_clarifications and self.request.participation.contest.use_clarifications
) )
@method_decorator(ratelimit(key="user", rate=settings.RL_COMMENT))
@method_decorator(login_required) @method_decorator(login_required)
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
self.object = self.get_object() self.object = self.get_object()
@ -115,9 +117,7 @@ class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View):
comment.author = request.profile comment.author = request.profile
comment.linked_object = self.object comment.linked_object = self.object
with LockModel( with revisions.create_revision():
write=(Comment, Revision, Version), read=(ContentType,)
), revisions.create_revision():
revisions.set_user(request.user) revisions.set_user(request.user)
revisions.set_comment(_("Posted comment")) revisions.set_comment(_("Posted comment"))
comment.save() comment.save()

View file

@ -22,6 +22,8 @@ from django.views.generic import DetailView, UpdateView
from django.urls import reverse_lazy from django.urls import reverse_lazy
from reversion import revisions from reversion import revisions
from reversion.models import Version from reversion.models import Version
from django.conf import settings
from django_ratelimit.decorators import ratelimit
from judge.dblock import LockModel from judge.dblock import LockModel
from judge.models import Comment, CommentVote, Notification, BlogPost from judge.models import Comment, CommentVote, Notification, BlogPost
@ -40,6 +42,7 @@ __all__ = [
] ]
@ratelimit(key="user", rate=settings.RL_VOTE)
@login_required @login_required
def vote_comment(request, delta): def vote_comment(request, delta):
if abs(delta) != 1: if abs(delta) != 1:
@ -77,27 +80,24 @@ def vote_comment(request, delta):
vote.voter = request.profile vote.voter = request.profile
vote.score = delta vote.score = delta
while True: try:
try: vote.save()
vote.save() except IntegrityError:
except IntegrityError: with LockModel(write=(CommentVote,)):
with LockModel(write=(CommentVote,)): try:
try: vote = CommentVote.objects.get(
vote = CommentVote.objects.get( comment_id=comment_id, voter=request.profile
comment_id=comment_id, voter=request.profile )
) except CommentVote.DoesNotExist:
except CommentVote.DoesNotExist: raise Http404()
# We must continue racing in case this is exploited to manipulate votes. if -vote.score != delta:
continue return HttpResponseBadRequest(
if -vote.score != delta: _("You already voted."), content_type="text/plain"
return HttpResponseBadRequest( )
_("You already voted."), content_type="text/plain" vote.delete()
) Comment.objects.filter(id=comment_id).update(score=F("score") - vote.score)
vote.delete() else:
Comment.objects.filter(id=comment_id).update(score=F("score") - vote.score) Comment.objects.filter(id=comment_id).update(score=F("score") + delta)
else:
Comment.objects.filter(id=comment_id).update(score=F("score") + delta)
break
return HttpResponse("success", content_type="text/plain") return HttpResponse("success", content_type="text/plain")

View file

@ -12,8 +12,9 @@ from judge.models.pagevote import PageVote, PageVoteVoter
from django.views.generic.base import TemplateResponseMixin from django.views.generic.base import TemplateResponseMixin
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
from judge.dblock import LockModel
from django.views.generic import View, ListView from django.views.generic import View, ListView
from django_ratelimit.decorators import ratelimit
from django.conf import settings
__all__ = [ __all__ = [
@ -24,6 +25,7 @@ __all__ = [
] ]
@ratelimit(key="user", rate=settings.RL_VOTE)
@login_required @login_required
def vote_page(request, delta): def vote_page(request, delta):
if abs(delta) != 1: if abs(delta) != 1:
@ -61,25 +63,19 @@ def vote_page(request, delta):
vote.voter = request.profile vote.voter = request.profile
vote.score = delta vote.score = delta
while True: try:
vote.save()
except IntegrityError:
try: try:
vote.save() vote = PageVoteVoter.objects.get(
except IntegrityError: pagevote_id=pagevote_id, voter=request.profile
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
) )
else: except PageVoteVoter.DoesNotExist:
PageVote.objects.filter(id=pagevote_id).update(score=F("score") + delta) raise Http404()
break 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) _dirty_vote_score(pagevote_id, request.profile)
return HttpResponse("success", content_type="text/plain") return HttpResponse("success", content_type="text/plain")

View file

@ -42,4 +42,5 @@ bleach
pymdown-extensions pymdown-extensions
mdx-breakless-lists mdx-breakless-lists
beautifulsoup4 beautifulsoup4
pre-commit pre-commit
django-ratelimit