mirror of https://github.com/zulip/zulip.git
rate_limit: Show html page when rate limited at /new/ endpoint.
Previously this showed a json error, but this is an endpoint that human users use in the browser, so a proper HTML page is more appropriate.
This commit is contained in:
parent
3ec55e7976
commit
4161e0caeb
|
@ -0,0 +1,23 @@
|
||||||
|
{% extends "zerver/portico.html" %}
|
||||||
|
|
||||||
|
{% block portico_content %}
|
||||||
|
|
||||||
|
<div class="error_page">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row-fluid">
|
||||||
|
<img src="/static/images/500art.svg" alt=""/>
|
||||||
|
<div class="errorbox">
|
||||||
|
<div class="errorcontent">
|
||||||
|
<h1 class="lead">{{ _("Rate limit exceeded.") }}</h1>
|
||||||
|
<p>
|
||||||
|
{% trans %}You have exceeded the limit for how
|
||||||
|
often a user can perform this action.{% endtrans %}
|
||||||
|
{% trans %}You can try again in {{retry_after}} seconds.{% endtrans %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
|
@ -1,5 +1,5 @@
|
||||||
import time
|
import time
|
||||||
from typing import Callable
|
from typing import Callable, Optional
|
||||||
from unittest import mock, skipUnless
|
from unittest import mock, skipUnless
|
||||||
|
|
||||||
import DNS
|
import DNS
|
||||||
|
@ -134,12 +134,12 @@ class RateLimitTests(ZulipTestCase):
|
||||||
newlimit = int(result["X-RateLimit-Remaining"])
|
newlimit = int(result["X-RateLimit-Remaining"])
|
||||||
self.assertEqual(limit, newlimit + 1)
|
self.assertEqual(limit, newlimit + 1)
|
||||||
|
|
||||||
def do_test_hit_ratelimits(self, request_func: Callable[[], HttpResponse]) -> HttpResponse:
|
def do_test_hit_ratelimits(
|
||||||
start_time = time.time()
|
self,
|
||||||
for i in range(6):
|
request_func: Callable[[], HttpResponse],
|
||||||
with mock.patch("time.time", return_value=(start_time + i * 0.1)):
|
assert_func: Optional[Callable[[HttpResponse], None]] = None,
|
||||||
result = request_func()
|
) -> HttpResponse:
|
||||||
|
def default_assert_func(result: HttpResponse) -> None:
|
||||||
self.assertEqual(result.status_code, 429)
|
self.assertEqual(result.status_code, 429)
|
||||||
json = result.json()
|
json = result.json()
|
||||||
self.assertEqual(json.get("result"), "error")
|
self.assertEqual(json.get("result"), "error")
|
||||||
|
@ -148,6 +148,16 @@ class RateLimitTests(ZulipTestCase):
|
||||||
self.assertTrue("Retry-After" in result)
|
self.assertTrue("Retry-After" in result)
|
||||||
self.assertEqual(result["Retry-After"], "0.5")
|
self.assertEqual(result["Retry-After"], "0.5")
|
||||||
|
|
||||||
|
if assert_func is None:
|
||||||
|
assert_func = default_assert_func
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
for i in range(6):
|
||||||
|
with mock.patch("time.time", return_value=(start_time + i * 0.1)):
|
||||||
|
result = request_func()
|
||||||
|
|
||||||
|
assert_func(result)
|
||||||
|
|
||||||
# We simulate waiting a second here, rather than force-clearing our history,
|
# We simulate waiting a second here, rather than force-clearing our history,
|
||||||
# to make sure the rate-limiting code automatically forgives a user
|
# to make sure the rate-limiting code automatically forgives a user
|
||||||
# after some time has passed.
|
# after some time has passed.
|
||||||
|
@ -173,12 +183,17 @@ class RateLimitTests(ZulipTestCase):
|
||||||
remove_ratelimit_rule(1, 5, domain="api_by_ip")
|
remove_ratelimit_rule(1, 5, domain="api_by_ip")
|
||||||
|
|
||||||
def test_create_realm_rate_limiting(self) -> None:
|
def test_create_realm_rate_limiting(self) -> None:
|
||||||
|
def assert_func(result: HttpResponse) -> None:
|
||||||
|
self.assertEqual(result.status_code, 429)
|
||||||
|
self.assert_in_response("Rate limit exceeded.", result)
|
||||||
|
|
||||||
with self.settings(OPEN_REALM_CREATION=True):
|
with self.settings(OPEN_REALM_CREATION=True):
|
||||||
add_ratelimit_rule(1, 5, domain="create_realm_by_ip")
|
add_ratelimit_rule(1, 5, domain="create_realm_by_ip")
|
||||||
try:
|
try:
|
||||||
RateLimitedIPAddr("127.0.0.1").clear_history()
|
RateLimitedIPAddr("127.0.0.1").clear_history()
|
||||||
self.do_test_hit_ratelimits(
|
self.do_test_hit_ratelimits(
|
||||||
lambda: self.client_post("/new/", {"email": "new@zulip.com"})
|
lambda: self.client_post("/new/", {"email": "new@zulip.com"}),
|
||||||
|
assert_func=assert_func,
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
remove_ratelimit_rule(1, 5, domain="create_realm_by_ip")
|
remove_ratelimit_rule(1, 5, domain="create_realm_by_ip")
|
||||||
|
|
|
@ -45,6 +45,7 @@ from zerver.lib.actions import (
|
||||||
lookup_default_stream_groups,
|
lookup_default_stream_groups,
|
||||||
)
|
)
|
||||||
from zerver.lib.email_validation import email_allowed_for_realm, validate_email_not_already_in_realm
|
from zerver.lib.email_validation import email_allowed_for_realm, validate_email_not_already_in_realm
|
||||||
|
from zerver.lib.exceptions import RateLimited
|
||||||
from zerver.lib.onboarding import send_initial_realm_messages, setup_realm_internal_bots
|
from zerver.lib.onboarding import send_initial_realm_messages, setup_realm_internal_bots
|
||||||
from zerver.lib.pysa import mark_sanitized
|
from zerver.lib.pysa import mark_sanitized
|
||||||
from zerver.lib.send_email import EmailNotDeliveredException, FromAddress, send_email
|
from zerver.lib.send_email import EmailNotDeliveredException, FromAddress, send_email
|
||||||
|
@ -590,7 +591,16 @@ def create_realm(request: HttpRequest, creation_key: Optional[str] = None) -> Ht
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = RealmCreationForm(request.POST)
|
form = RealmCreationForm(request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
|
try:
|
||||||
rate_limit_request_by_ip(request, domain="create_realm_by_ip")
|
rate_limit_request_by_ip(request, domain="create_realm_by_ip")
|
||||||
|
except RateLimited as e:
|
||||||
|
assert e.secs_to_freedom is not None
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"zerver/rate_limit_exceeded.html",
|
||||||
|
context={"retry_after": int(e.secs_to_freedom)},
|
||||||
|
status=429,
|
||||||
|
)
|
||||||
|
|
||||||
email = form.cleaned_data["email"]
|
email = form.cleaned_data["email"]
|
||||||
activation_url = prepare_activation_url(email, request, realm_creation=True)
|
activation_url = prepare_activation_url(email, request, realm_creation=True)
|
||||||
|
|
Loading…
Reference in New Issue