mirror of https://github.com/zulip/zulip.git
rate_limit: Add a flag to lump all TOR exit node IPs together.
TOR users are legitimate users of the system; however, that system can also be used for abuse -- specifically, by evading IP-based rate-limiting. For the purposes of IP-based rate-limiting, add a RATE_LIMIT_TOR_TOGETHER flag, defaulting to false, which lumps all requests from TOR exit nodes into the same bucket. This may allow a TOR user to deny other TOR users access to the find-my-account and new-realm endpoints, but this is a low cost for cutting off a significant potential abuse vector. If enabled, the list of TOR exit nodes is fetched from their public endpoint once per hour, via a cron job, and cached on disk. Django processes load this data from disk, and cache it in memcached. Requests are spared from the burden of checking disk on failure via a circuitbreaker, which trips of there are two failures in a row, and only begins trying again after 10 minutes.
This commit is contained in:
parent
2d3d0f862a
commit
49ad188449
|
@ -0,0 +1,5 @@
|
||||||
|
SHELL=/bin/bash
|
||||||
|
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
|
||||||
|
USER=zulip
|
||||||
|
|
||||||
|
0 * * * * zulip /home/zulip/deployments/current/manage.py fetch_tor_exit_nodes
|
|
@ -199,4 +199,13 @@ class zulip::app_frontend_base {
|
||||||
mode => '0755',
|
mode => '0755',
|
||||||
source => 'puppet:///modules/zulip/nagios_plugins/zulip_app_frontend',
|
source => 'puppet:///modules/zulip/nagios_plugins/zulip_app_frontend',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# This cron job does nothing unless RATE_LIMIT_TOR_TOGETHER is enabled.
|
||||||
|
file { '/etc/cron.d/fetch-for-exit-nodes':
|
||||||
|
ensure => file,
|
||||||
|
owner => 'root',
|
||||||
|
group => 'root',
|
||||||
|
mode => '0644',
|
||||||
|
source => 'puppet:///modules/zulip/cron.d/fetch-tor-exit-nodes',
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,6 +54,7 @@ module = [
|
||||||
"bs4.*",
|
"bs4.*",
|
||||||
"bson.*",
|
"bson.*",
|
||||||
"cairosvg.*",
|
"cairosvg.*",
|
||||||
|
"circuitbreaker.*",
|
||||||
"coverage.*",
|
"coverage.*",
|
||||||
"cssutils.*",
|
"cssutils.*",
|
||||||
"defusedxml.*",
|
"defusedxml.*",
|
||||||
|
|
|
@ -194,3 +194,6 @@ django-scim2
|
||||||
|
|
||||||
# CSS manipulation
|
# CSS manipulation
|
||||||
soupsieve
|
soupsieve
|
||||||
|
|
||||||
|
# Circuit-breaking for outgoing services
|
||||||
|
circuitbreaker
|
||||||
|
|
|
@ -214,6 +214,9 @@ charset-normalizer==2.0.7 \
|
||||||
--hash=sha256:e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0 \
|
--hash=sha256:e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0 \
|
||||||
--hash=sha256:f7af805c321bfa1ce6714c51f254e0d5bb5e5834039bc17db7ebe3a4cec9492b
|
--hash=sha256:f7af805c321bfa1ce6714c51f254e0d5bb5e5834039bc17db7ebe3a4cec9492b
|
||||||
# via requests
|
# via requests
|
||||||
|
circuitbreaker==1.3.2 \
|
||||||
|
--hash=sha256:747d4ced5c0797e2ab1d3e00a03b312db23e7ec65106148fc63beec25bbba50f
|
||||||
|
# via -r requirements/common.in
|
||||||
click==8.0.3 \
|
click==8.0.3 \
|
||||||
--hash=sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3 \
|
--hash=sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3 \
|
||||||
--hash=sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b
|
--hash=sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b
|
||||||
|
|
|
@ -161,6 +161,9 @@ charset-normalizer==2.0.7 \
|
||||||
--hash=sha256:e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0 \
|
--hash=sha256:e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0 \
|
||||||
--hash=sha256:f7af805c321bfa1ce6714c51f254e0d5bb5e5834039bc17db7ebe3a4cec9492b
|
--hash=sha256:f7af805c321bfa1ce6714c51f254e0d5bb5e5834039bc17db7ebe3a4cec9492b
|
||||||
# via requests
|
# via requests
|
||||||
|
circuitbreaker==1.3.2 \
|
||||||
|
--hash=sha256:747d4ced5c0797e2ab1d3e00a03b312db23e7ec65106148fc63beec25bbba50f
|
||||||
|
# via -r requirements/common.in
|
||||||
click==8.0.3 \
|
click==8.0.3 \
|
||||||
--hash=sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3 \
|
--hash=sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3 \
|
||||||
--hash=sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b
|
--hash=sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b
|
||||||
|
|
|
@ -48,4 +48,4 @@ API_FEATURE_LEVEL = 107
|
||||||
# historical commits sharing the same major version, in which case a
|
# historical commits sharing the same major version, in which case a
|
||||||
# minor version bump suffices.
|
# minor version bump suffices.
|
||||||
|
|
||||||
PROVISION_VERSION = "165.2"
|
PROVISION_VERSION = "165.3"
|
||||||
|
|
|
@ -4,9 +4,11 @@ import logging
|
||||||
import urllib
|
import urllib
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from typing import Callable, Dict, Optional, Sequence, Tuple, TypeVar, Union, cast, overload
|
from typing import Callable, Dict, Optional, Sequence, Set, Tuple, TypeVar, Union, cast, overload
|
||||||
|
|
||||||
import django_otp
|
import django_otp
|
||||||
|
import orjson
|
||||||
|
from circuitbreaker import CircuitBreakerError, circuit
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import REDIRECT_FIELD_NAME
|
from django.contrib.auth import REDIRECT_FIELD_NAME
|
||||||
from django.contrib.auth import login as django_login
|
from django.contrib.auth import login as django_login
|
||||||
|
@ -23,6 +25,7 @@ from django.views.decorators.csrf import csrf_exempt
|
||||||
from django_otp import user_has_device
|
from django_otp import user_has_device
|
||||||
from two_factor.utils import default_device
|
from two_factor.utils import default_device
|
||||||
|
|
||||||
|
from zerver.lib.cache import cache_with_key
|
||||||
from zerver.lib.exceptions import (
|
from zerver.lib.exceptions import (
|
||||||
AccessDeniedError,
|
AccessDeniedError,
|
||||||
ErrorCode,
|
ErrorCode,
|
||||||
|
@ -884,6 +887,30 @@ def rate_limit_user(request: HttpRequest, user: UserProfile, domain: str) -> Non
|
||||||
RateLimitedUser(user, domain=domain).rate_limit_request(request)
|
RateLimitedUser(user, domain=domain).rate_limit_request(request)
|
||||||
|
|
||||||
|
|
||||||
|
@cache_with_key(lambda: "tor_ip_addresses:", timeout=60 * 60)
|
||||||
|
@circuit(failure_threshold=2, recovery_timeout=60 * 10)
|
||||||
|
def get_tor_ips() -> Set[str]:
|
||||||
|
if not settings.RATE_LIMIT_TOR_TOGETHER:
|
||||||
|
return set()
|
||||||
|
|
||||||
|
# Cron job in /etc/cron.d/fetch-for-exit-nodes fetches this
|
||||||
|
# hourly; we cache it in memcached to prevent going to disk on
|
||||||
|
# every unauth'd request. In case of failures to read, we
|
||||||
|
# circuit-break so 2 failures cause a 10-minute backoff.
|
||||||
|
|
||||||
|
with open(settings.TOR_EXIT_NODE_FILE_PATH, "rb") as f:
|
||||||
|
exit_node_list = orjson.loads(f.read())
|
||||||
|
|
||||||
|
# This should always be non-empty; if it's empty, assume something
|
||||||
|
# went wrong with writing and treat it as a non-existent file.
|
||||||
|
# Circuit-breaking will ensure that we back off on re-reading the
|
||||||
|
# file.
|
||||||
|
if len(exit_node_list) == 0:
|
||||||
|
raise IOError("File is empty")
|
||||||
|
|
||||||
|
return set(exit_node_list)
|
||||||
|
|
||||||
|
|
||||||
def rate_limit_ip(request: HttpRequest, ip_addr: str, domain: str) -> None:
|
def rate_limit_ip(request: HttpRequest, ip_addr: str, domain: str) -> None:
|
||||||
RateLimitedIPAddr(ip_addr, domain=domain).rate_limit_request(request)
|
RateLimitedIPAddr(ip_addr, domain=domain).rate_limit_request(request)
|
||||||
|
|
||||||
|
@ -894,6 +921,24 @@ def rate_limit_request_by_ip(request: HttpRequest, domain: str) -> None:
|
||||||
# IP address to use - without worrying we'll grab the IP of a proxy.
|
# IP address to use - without worrying we'll grab the IP of a proxy.
|
||||||
ip_addr = request.META["REMOTE_ADDR"]
|
ip_addr = request.META["REMOTE_ADDR"]
|
||||||
assert ip_addr
|
assert ip_addr
|
||||||
|
|
||||||
|
try:
|
||||||
|
# We lump all TOR exit nodes into one bucket; this prevents
|
||||||
|
# abuse from TOR, while still allowing some access to these
|
||||||
|
# endpoints for legitimate users. Checking for local
|
||||||
|
# addresses is a shortcut somewhat for ease of testing without
|
||||||
|
# mocking the TOR endpoint in every test.
|
||||||
|
if is_local_addr(ip_addr):
|
||||||
|
pass
|
||||||
|
elif ip_addr in get_tor_ips():
|
||||||
|
ip_addr = "tor-exit-node"
|
||||||
|
except (IOError, CircuitBreakerError) as err:
|
||||||
|
# In the event that we can't get an updated list of TOR exit
|
||||||
|
# nodes, assume the IP is _not_ one, and leave it unchanged.
|
||||||
|
# We log a warning so that this endpoint being taken out of
|
||||||
|
# service doesn't silently remove this functionality.
|
||||||
|
rate_limiter_logger.warning("Failed to fetch TOR exit node list: %s", err)
|
||||||
|
pass
|
||||||
rate_limit_ip(request, ip_addr, domain=domain)
|
rate_limit_ip(request, ip_addr, domain=domain)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
import os
|
||||||
|
from argparse import ArgumentParser
|
||||||
|
from typing import Any, Set
|
||||||
|
|
||||||
|
import orjson
|
||||||
|
from django.conf import settings
|
||||||
|
from requests.packages.urllib3.util.retry import Retry
|
||||||
|
|
||||||
|
from zerver.lib.management import ZulipBaseCommand
|
||||||
|
from zerver.lib.outgoing_http import OutgoingSession
|
||||||
|
|
||||||
|
|
||||||
|
class TorDataSession(OutgoingSession):
|
||||||
|
def __init__(self, max_retries: int) -> None:
|
||||||
|
Retry.BACKOFF_MAX = 64
|
||||||
|
retry = Retry(
|
||||||
|
total=max_retries,
|
||||||
|
backoff_factor=2.0,
|
||||||
|
status_forcelist={ # Retry on these
|
||||||
|
429, # The formal rate-limiting response code
|
||||||
|
500, # Server error
|
||||||
|
502, # Bad gateway
|
||||||
|
503, # Service unavailable
|
||||||
|
},
|
||||||
|
)
|
||||||
|
super().__init__(role="tor_data", timeout=3, max_retries=retry)
|
||||||
|
|
||||||
|
|
||||||
|
class Command(ZulipBaseCommand):
|
||||||
|
help = """Fetch the list of TOR exit nodes, and write the list of IP addresses
|
||||||
|
to a file for access from Django for rate-limiting purposes.
|
||||||
|
|
||||||
|
Does nothing unless RATE_LIMIT_TOR_TOGETHER is enabled.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def add_arguments(self, parser: ArgumentParser) -> None:
|
||||||
|
parser.add_argument(
|
||||||
|
"--max-retries",
|
||||||
|
type=int,
|
||||||
|
default=10,
|
||||||
|
help="Number of times to retry fetching data from TOR",
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(*args: Any, **options: Any) -> None:
|
||||||
|
if not settings.RATE_LIMIT_TOR_TOGETHER:
|
||||||
|
return
|
||||||
|
|
||||||
|
certificates = os.environ.get("CUSTOM_CA_CERTIFICATES")
|
||||||
|
session = TorDataSession(max_retries=options["max_retries"])
|
||||||
|
response = session.get(
|
||||||
|
"https://check.torproject.org/exit-addresses",
|
||||||
|
verify=certificates,
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
# Format:
|
||||||
|
# ExitNode 4273E6D162ED2717A1CF4207A254004CD3F5307B
|
||||||
|
# Published 2021-11-02 11:01:07
|
||||||
|
# LastStatus 2021-11-02 23:00:00
|
||||||
|
# ExitAddress 176.10.99.200 2021-11-02 23:17:02
|
||||||
|
exit_nodes: Set[str] = set()
|
||||||
|
for line in response.text.splitlines():
|
||||||
|
if line.startswith("ExitAddress "):
|
||||||
|
exit_nodes.add(line.split()[1])
|
||||||
|
|
||||||
|
# Write to a tmpfile to ensure we can't read a partially-written file
|
||||||
|
with open(settings.TOR_EXIT_NODE_FILE_PATH + ".tmp", "wb") as f:
|
||||||
|
f.write(orjson.dumps(list(exit_nodes)))
|
||||||
|
|
||||||
|
# Do an atomic rename into place
|
||||||
|
os.rename(
|
||||||
|
settings.TOR_EXIT_NODE_FILE_PATH + ".tmp",
|
||||||
|
settings.TOR_EXIT_NODE_FILE_PATH,
|
||||||
|
)
|
|
@ -1,15 +1,20 @@
|
||||||
import time
|
import time
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from typing import Any, Callable, Iterator
|
from typing import IO, Any, Callable, Iterator, Optional, Sequence
|
||||||
from unittest import mock, skipUnless
|
from unittest import mock, skipUnless
|
||||||
|
|
||||||
import DNS
|
import DNS
|
||||||
|
import orjson
|
||||||
|
from circuitbreaker import CircuitBreakerMonitor
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
|
from django.test import override_settings
|
||||||
from django.utils.timezone import now as timezone_now
|
from django.utils.timezone import now as timezone_now
|
||||||
|
|
||||||
|
from zerver import decorator
|
||||||
from zerver.forms import email_is_not_mit_mailing_list
|
from zerver.forms import email_is_not_mit_mailing_list
|
||||||
|
from zerver.lib.cache import cache_delete
|
||||||
from zerver.lib.rate_limiter import (
|
from zerver.lib.rate_limiter import (
|
||||||
RateLimitedIPAddr,
|
RateLimitedIPAddr,
|
||||||
RateLimitedUser,
|
RateLimitedUser,
|
||||||
|
@ -287,6 +292,122 @@ class RateLimitTests(ZulipTestCase):
|
||||||
|
|
||||||
self.do_test_hit_ratelimits(alternate_requests, is_json=False)
|
self.do_test_hit_ratelimits(alternate_requests, is_json=False)
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def tor_mock(
|
||||||
|
self,
|
||||||
|
side_effect: Optional[Exception] = None,
|
||||||
|
read_data: Sequence[str] = ["1.2.3.4", "5.6.7.8"],
|
||||||
|
) -> Iterator[mock.Mock]:
|
||||||
|
# We need to reset the circuitbreaker before starting. We
|
||||||
|
# patch the .opened property to be false, then call the
|
||||||
|
# function, so it resets to closed.
|
||||||
|
with mock.patch("builtins.open", mock.mock_open(read_data=orjson.dumps(["1.2.3.4"]))):
|
||||||
|
with mock.patch(
|
||||||
|
"circuitbreaker.CircuitBreaker.opened", new_callable=mock.PropertyMock
|
||||||
|
) as mock_opened:
|
||||||
|
mock_opened.return_value = False
|
||||||
|
decorator.get_tor_ips()
|
||||||
|
|
||||||
|
# Having closed it, it's now cached. Clear the cache.
|
||||||
|
assert CircuitBreakerMonitor.get("get_tor_ips").closed
|
||||||
|
cache_delete("tor_ip_addresses:")
|
||||||
|
|
||||||
|
builtin_open = open
|
||||||
|
if side_effect:
|
||||||
|
tor_open = mock.MagicMock(side_effect=side_effect)
|
||||||
|
else:
|
||||||
|
tor_open = mock.mock_open(read_data=orjson.dumps(read_data))
|
||||||
|
|
||||||
|
def selective_mock_open(*args: Any, **kwargs: Any) -> IO[Any]:
|
||||||
|
if args[0] == settings.TOR_EXIT_NODE_FILE_PATH:
|
||||||
|
return tor_open(*args, **kwargs)
|
||||||
|
return builtin_open(*args, **kwargs)
|
||||||
|
|
||||||
|
with mock.patch("builtins.open", selective_mock_open):
|
||||||
|
yield tor_open
|
||||||
|
|
||||||
|
@rate_limit_rule(1, 5, domain="api_by_ip")
|
||||||
|
@override_settings(RATE_LIMIT_TOR_TOGETHER=True)
|
||||||
|
def test_tor_ip_limits(self) -> None:
|
||||||
|
request_count = 0
|
||||||
|
for ip in ["1.2.3.4", "5.6.7.8", "tor-exit-node"]:
|
||||||
|
RateLimitedIPAddr(ip, domain="api_by_ip").clear_history()
|
||||||
|
|
||||||
|
def alternate_requests() -> HttpResponse:
|
||||||
|
nonlocal request_count
|
||||||
|
request_count += 1
|
||||||
|
if request_count % 2 == 1:
|
||||||
|
return self.send_unauthed_api_request(REMOTE_ADDR="1.2.3.4")
|
||||||
|
else:
|
||||||
|
return self.send_unauthed_api_request(REMOTE_ADDR="5.6.7.8")
|
||||||
|
|
||||||
|
with self.tor_mock(read_data=["1.2.3.4", "5.6.7.8"]) as tor_open:
|
||||||
|
self.do_test_hit_ratelimits(alternate_requests)
|
||||||
|
|
||||||
|
# This is only read once, despite being used on each request
|
||||||
|
tor_open.assert_called_once_with(settings.TOR_EXIT_NODE_FILE_PATH, "rb")
|
||||||
|
tor_open().read.assert_called_once()
|
||||||
|
|
||||||
|
@rate_limit_rule(1, 5, domain="api_by_ip")
|
||||||
|
@override_settings(RATE_LIMIT_TOR_TOGETHER=True)
|
||||||
|
def test_tor_file_empty(self) -> None:
|
||||||
|
for ip in ["1.2.3.4", "5.6.7.8", "tor-exit-node"]:
|
||||||
|
RateLimitedIPAddr(ip, domain="api_by_ip").clear_history()
|
||||||
|
|
||||||
|
# An empty list of IPs is treated as some error in parsing the
|
||||||
|
# input, and as such should not be cached; rate-limiting
|
||||||
|
# should work as normal, per-IP
|
||||||
|
with self.tor_mock(read_data=[]) as tor_open:
|
||||||
|
with self.assertLogs("zerver.lib.rate_limiter", level="WARNING"):
|
||||||
|
self.do_test_hit_ratelimits(
|
||||||
|
lambda: self.send_unauthed_api_request(REMOTE_ADDR="1.2.3.4")
|
||||||
|
)
|
||||||
|
resp = self.send_unauthed_api_request(REMOTE_ADDR="5.6.7.8")
|
||||||
|
self.assertNotEqual(resp.status_code, 429)
|
||||||
|
|
||||||
|
# Was not cached, so tried to read twice before hitting the
|
||||||
|
# circuit-breaker, and stopping trying
|
||||||
|
tor_open().read.assert_has_calls([mock.call(), mock.call()])
|
||||||
|
|
||||||
|
@rate_limit_rule(1, 5, domain="api_by_ip")
|
||||||
|
@override_settings(RATE_LIMIT_TOR_TOGETHER=True)
|
||||||
|
def test_tor_file_not_found(self) -> None:
|
||||||
|
for ip in ["1.2.3.4", "5.6.7.8", "tor-exit-node"]:
|
||||||
|
RateLimitedIPAddr(ip, domain="api_by_ip").clear_history()
|
||||||
|
|
||||||
|
with self.tor_mock(side_effect=FileNotFoundError("File not found")) as tor_open:
|
||||||
|
# If we cannot get a list of TOR exit nodes, then
|
||||||
|
# rate-limiting works as normal, per-IP
|
||||||
|
with self.assertLogs("zerver.lib.rate_limiter", level="WARNING") as log_mock:
|
||||||
|
self.do_test_hit_ratelimits(
|
||||||
|
lambda: self.send_unauthed_api_request(REMOTE_ADDR="1.2.3.4")
|
||||||
|
)
|
||||||
|
resp = self.send_unauthed_api_request(REMOTE_ADDR="5.6.7.8")
|
||||||
|
self.assertNotEqual(resp.status_code, 429)
|
||||||
|
|
||||||
|
# Tries twice before hitting the circuit-breaker, and stopping trying
|
||||||
|
tor_open.assert_has_calls(
|
||||||
|
[
|
||||||
|
mock.call(settings.TOR_EXIT_NODE_FILE_PATH, "rb"),
|
||||||
|
mock.call(settings.TOR_EXIT_NODE_FILE_PATH, "rb"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assert_length(log_mock.output, 8)
|
||||||
|
self.assertEqual(
|
||||||
|
log_mock.output[0:2],
|
||||||
|
[
|
||||||
|
"WARNING:zerver.lib.rate_limiter:Failed to fetch TOR exit node list: {}".format(
|
||||||
|
"File not found"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
* 2,
|
||||||
|
)
|
||||||
|
self.assertIn(
|
||||||
|
'Failed to fetch TOR exit node list: Circuit "get_tor_ips" OPEN',
|
||||||
|
log_mock.output[3],
|
||||||
|
)
|
||||||
|
|
||||||
@skipUnless(settings.ZILENCER_ENABLED, "requires zilencer")
|
@skipUnless(settings.ZILENCER_ENABLED, "requires zilencer")
|
||||||
@rate_limit_rule(1, 5, domain="api_by_remote_server")
|
@rate_limit_rule(1, 5, domain="api_by_remote_server")
|
||||||
def test_hit_ratelimits_as_remote_server(self) -> None:
|
def test_hit_ratelimits_as_remote_server(self) -> None:
|
||||||
|
|
|
@ -416,6 +416,12 @@ RATE_LIMITING_MIRROR_REALM_RULES = [
|
||||||
DEBUG_RATE_LIMITING = DEBUG
|
DEBUG_RATE_LIMITING = DEBUG
|
||||||
REDIS_PASSWORD = get_secret("redis_password")
|
REDIS_PASSWORD = get_secret("redis_password")
|
||||||
|
|
||||||
|
# See RATE_LIMIT_TOR_TOGETHER
|
||||||
|
if DEVELOPMENT:
|
||||||
|
TOR_EXIT_NODE_FILE_PATH = os.path.join(DEPLOY_ROOT, "var/tor-exit-nodes.json")
|
||||||
|
else:
|
||||||
|
TOR_EXIT_NODE_FILE_PATH = "/var/lib/zulip/tor-exit-nodes.json"
|
||||||
|
|
||||||
########################################################################
|
########################################################################
|
||||||
# SECURITY SETTINGS
|
# SECURITY SETTINGS
|
||||||
########################################################################
|
########################################################################
|
||||||
|
|
|
@ -195,6 +195,7 @@ SUBMIT_USAGE_STATISTICS = True
|
||||||
PROMOTE_SPONSORING_ZULIP = True
|
PROMOTE_SPONSORING_ZULIP = True
|
||||||
RATE_LIMITING = True
|
RATE_LIMITING = True
|
||||||
RATE_LIMITING_AUTHENTICATE = True
|
RATE_LIMITING_AUTHENTICATE = True
|
||||||
|
RATE_LIMIT_TOR_TOGETHER = False
|
||||||
SEND_LOGIN_EMAILS = True
|
SEND_LOGIN_EMAILS = True
|
||||||
EMBEDDED_BOTS_ENABLED = False
|
EMBEDDED_BOTS_ENABLED = False
|
||||||
|
|
||||||
|
|
|
@ -756,6 +756,10 @@ CAMO_URI = "/external_content/"
|
||||||
## Controls whether Zulip will rate-limit user requests.
|
## Controls whether Zulip will rate-limit user requests.
|
||||||
# RATE_LIMITING = True
|
# RATE_LIMITING = True
|
||||||
|
|
||||||
|
## Fetch TOR exit node list every hour, and group all TOR exit nodes
|
||||||
|
## together into one bucket when applying rate-limiting.
|
||||||
|
# RATE_LIMIT_TOR_TOGETHER = False
|
||||||
|
|
||||||
## If you want to set a Terms of Service for your server, set the path
|
## If you want to set a Terms of Service for your server, set the path
|
||||||
## to your Markdown file, and uncomment the following line.
|
## to your Markdown file, and uncomment the following line.
|
||||||
# TERMS_OF_SERVICE = '/etc/zulip/terms.md'
|
# TERMS_OF_SERVICE = '/etc/zulip/terms.md'
|
||||||
|
|
Loading…
Reference in New Issue