Add rate limit and don't use lock for vote
This commit is contained in:
parent
104cee9e81
commit
2cf386e8b5
5 changed files with 45 additions and 43 deletions
|
@ -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())
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
@ -42,4 +42,5 @@ bleach
|
||||||
pymdown-extensions
|
pymdown-extensions
|
||||||
mdx-breakless-lists
|
mdx-breakless-lists
|
||||||
beautifulsoup4
|
beautifulsoup4
|
||||||
pre-commit
|
pre-commit
|
||||||
|
django-ratelimit
|
Loading…
Reference in a new issue