diff --git a/synapse/api/errors.py b/synapse/api/errors.py
index 578e798773..fdb2955be8 100644
--- a/synapse/api/errors.py
+++ b/synapse/api/errors.py
@@ -211,6 +211,11 @@ class SynapseError(CodeMessageException):
def error_dict(self, config: Optional["HomeServerConfig"]) -> "JsonDict":
return cs_error(self.msg, self.errcode, **self._additional_fields)
+ @property
+ def debug_context(self) -> Optional[str]:
+ """Override this to add debugging context that shouldn't be sent to clients."""
+ return None
+
class InvalidAPICallError(SynapseError):
"""You called an existing API endpoint, but fed that endpoint
@@ -508,8 +513,8 @@ class LimitExceededError(SynapseError):
def __init__(
self,
+ limiter_name: str,
code: int = 429,
- msg: str = "Too Many Requests",
retry_after_ms: Optional[int] = None,
errcode: str = Codes.LIMIT_EXCEEDED,
):
@@ -518,12 +523,17 @@ class LimitExceededError(SynapseError):
if self.include_retry_after_header and retry_after_ms is not None
else None
)
- super().__init__(code, msg, errcode, headers=headers)
+ super().__init__(code, "Too Many Requests", errcode, headers=headers)
self.retry_after_ms = retry_after_ms
+ self.limiter_name = limiter_name
def error_dict(self, config: Optional["HomeServerConfig"]) -> "JsonDict":
return cs_error(self.msg, self.errcode, retry_after_ms=self.retry_after_ms)
+ @property
+ def debug_context(self) -> Optional[str]:
+ return self.limiter_name
+
class RoomKeysVersionError(SynapseError):
"""A client has tried to upload to a non-current version of the room_keys store"""
diff --git a/synapse/api/ratelimiting.py b/synapse/api/ratelimiting.py
index 511790c7c5..887b214d64 100644
--- a/synapse/api/ratelimiting.py
+++ b/synapse/api/ratelimiting.py
@@ -61,12 +61,16 @@ class Ratelimiter:
"""
def __init__(
- self, store: DataStore, clock: Clock, rate_hz: float, burst_count: int
+ self,
+ store: DataStore,
+ clock: Clock,
+ cfg: RatelimitSettings,
):
self.clock = clock
- self.rate_hz = rate_hz
- self.burst_count = burst_count
+ self.rate_hz = cfg.per_second
+ self.burst_count = cfg.burst_count
self.store = store
+ self._limiter_name = cfg.key
# An ordered dictionary representing the token buckets tracked by this rate
# limiter. Each entry maps a key of arbitrary type to a tuple representing:
@@ -305,7 +309,8 @@ class Ratelimiter:
if not allowed:
raise LimitExceededError(
- retry_after_ms=int(1000 * (time_allowed - time_now_s))
+ limiter_name=self._limiter_name,
+ retry_after_ms=int(1000 * (time_allowed - time_now_s)),
)
@@ -322,7 +327,9 @@ class RequestRatelimiter:
# The rate_hz and burst_count are overridden on a per-user basis
self.request_ratelimiter = Ratelimiter(
- store=self.store, clock=self.clock, rate_hz=0, burst_count=0
+ store=self.store,
+ clock=self.clock,
+ cfg=RatelimitSettings(key=rc_message.key, per_second=0, burst_count=0),
)
self._rc_message = rc_message
@@ -332,8 +339,7 @@ class RequestRatelimiter:
self.admin_redaction_ratelimiter: Optional[Ratelimiter] = Ratelimiter(
store=self.store,
clock=self.clock,
- rate_hz=rc_admin_redaction.per_second,
- burst_count=rc_admin_redaction.burst_count,
+ cfg=rc_admin_redaction,
)
else:
self.admin_redaction_ratelimiter = None
diff --git a/synapse/config/ratelimiting.py b/synapse/config/ratelimiting.py
index a5514e70a2..4efbaeac0d 100644
--- a/synapse/config/ratelimiting.py
+++ b/synapse/config/ratelimiting.py
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-from typing import Any, Dict, Optional
+from typing import Any, Dict, Optional, cast
import attr
@@ -21,16 +21,47 @@ from synapse.types import JsonDict
from ._base import Config
+@attr.s(slots=True, frozen=True, auto_attribs=True)
class RatelimitSettings:
- def __init__(
- self,
- config: Dict[str, float],
+ key: str
+ per_second: float
+ burst_count: int
+
+ @classmethod
+ def parse(
+ cls,
+ config: Dict[str, Any],
+ key: str,
defaults: Optional[Dict[str, float]] = None,
- ):
+ ) -> "RatelimitSettings":
+ """Parse config[key] as a new-style rate limiter config.
+
+ The key may refer to a nested dictionary using a full stop (.) to separate
+ each nested key. For example, use the key "a.b.c" to parse the following:
+
+ a:
+ b:
+ c:
+ per_second: 10
+ burst_count: 200
+
+ If this lookup fails, we'll fallback to the defaults.
+ """
defaults = defaults or {"per_second": 0.17, "burst_count": 3.0}
- self.per_second = config.get("per_second", defaults["per_second"])
- self.burst_count = int(config.get("burst_count", defaults["burst_count"]))
+ rl_config = config
+ for part in key.split("."):
+ rl_config = rl_config.get(part, {})
+
+ # By this point we should have hit the rate limiter parameters.
+ # We don't actually check this though!
+ rl_config = cast(Dict[str, float], rl_config)
+
+ return cls(
+ key=key,
+ per_second=rl_config.get("per_second", defaults["per_second"]),
+ burst_count=int(rl_config.get("burst_count", defaults["burst_count"])),
+ )
@attr.s(auto_attribs=True)
@@ -49,15 +80,14 @@ class RatelimitConfig(Config):
# Load the new-style messages config if it exists. Otherwise fall back
# to the old method.
if "rc_message" in config:
- self.rc_message = RatelimitSettings(
- config["rc_message"], defaults={"per_second": 0.2, "burst_count": 10.0}
+ self.rc_message = RatelimitSettings.parse(
+ config, "rc_message", defaults={"per_second": 0.2, "burst_count": 10.0}
)
else:
self.rc_message = RatelimitSettings(
- {
- "per_second": config.get("rc_messages_per_second", 0.2),
- "burst_count": config.get("rc_message_burst_count", 10.0),
- }
+ key="rc_messages",
+ per_second=config.get("rc_messages_per_second", 0.2),
+ burst_count=config.get("rc_message_burst_count", 10.0),
)
# Load the new-style federation config, if it exists. Otherwise, fall
@@ -79,51 +109,59 @@ class RatelimitConfig(Config):
}
)
- self.rc_registration = RatelimitSettings(config.get("rc_registration", {}))
+ self.rc_registration = RatelimitSettings.parse(config, "rc_registration", {})
- self.rc_registration_token_validity = RatelimitSettings(
- config.get("rc_registration_token_validity", {}),
+ self.rc_registration_token_validity = RatelimitSettings.parse(
+ config,
+ "rc_registration_token_validity",
defaults={"per_second": 0.1, "burst_count": 5},
)
# It is reasonable to login with a bunch of devices at once (i.e. when
# setting up an account), but it is *not* valid to continually be
# logging into new devices.
- rc_login_config = config.get("rc_login", {})
- self.rc_login_address = RatelimitSettings(
- rc_login_config.get("address", {}),
+ self.rc_login_address = RatelimitSettings.parse(
+ config,
+ "rc_login.address",
defaults={"per_second": 0.003, "burst_count": 5},
)
- self.rc_login_account = RatelimitSettings(
- rc_login_config.get("account", {}),
+ self.rc_login_account = RatelimitSettings.parse(
+ config,
+ "rc_login.account",
defaults={"per_second": 0.003, "burst_count": 5},
)
- self.rc_login_failed_attempts = RatelimitSettings(
- rc_login_config.get("failed_attempts", {})
+ self.rc_login_failed_attempts = RatelimitSettings.parse(
+ config,
+ "rc_login.failed_attempts",
+ {},
)
self.federation_rr_transactions_per_room_per_second = config.get(
"federation_rr_transactions_per_room_per_second", 50
)
- rc_admin_redaction = config.get("rc_admin_redaction")
self.rc_admin_redaction = None
- if rc_admin_redaction:
- self.rc_admin_redaction = RatelimitSettings(rc_admin_redaction)
+ if "rc_admin_redaction" in config:
+ self.rc_admin_redaction = RatelimitSettings.parse(
+ config, "rc_admin_redaction", {}
+ )
- self.rc_joins_local = RatelimitSettings(
- config.get("rc_joins", {}).get("local", {}),
+ self.rc_joins_local = RatelimitSettings.parse(
+ config,
+ "rc_joins.local",
defaults={"per_second": 0.1, "burst_count": 10},
)
- self.rc_joins_remote = RatelimitSettings(
- config.get("rc_joins", {}).get("remote", {}),
+ self.rc_joins_remote = RatelimitSettings.parse(
+ config,
+ "rc_joins.remote",
defaults={"per_second": 0.01, "burst_count": 10},
)
# Track the rate of joins to a given room. If there are too many, temporarily
# prevent local joins and remote joins via this server.
- self.rc_joins_per_room = RatelimitSettings(
- config.get("rc_joins_per_room", {}),
+ self.rc_joins_per_room = RatelimitSettings.parse(
+ config,
+ "rc_joins_per_room",
defaults={"per_second": 1, "burst_count": 10},
)
@@ -132,31 +170,37 @@ class RatelimitConfig(Config):
# * For requests received over federation this is keyed by the origin.
#
# Note that this isn't exposed in the configuration as it is obscure.
- self.rc_key_requests = RatelimitSettings(
- config.get("rc_key_requests", {}),
+ self.rc_key_requests = RatelimitSettings.parse(
+ config,
+ "rc_key_requests",
defaults={"per_second": 20, "burst_count": 100},
)
- self.rc_3pid_validation = RatelimitSettings(
- config.get("rc_3pid_validation") or {},
+ self.rc_3pid_validation = RatelimitSettings.parse(
+ config,
+ "rc_3pid_validation",
defaults={"per_second": 0.003, "burst_count": 5},
)
- self.rc_invites_per_room = RatelimitSettings(
- config.get("rc_invites", {}).get("per_room", {}),
+ self.rc_invites_per_room = RatelimitSettings.parse(
+ config,
+ "rc_invites.per_room",
defaults={"per_second": 0.3, "burst_count": 10},
)
- self.rc_invites_per_user = RatelimitSettings(
- config.get("rc_invites", {}).get("per_user", {}),
+ self.rc_invites_per_user = RatelimitSettings.parse(
+ config,
+ "rc_invites.per_user",
defaults={"per_second": 0.003, "burst_count": 5},
)
- self.rc_invites_per_issuer = RatelimitSettings(
- config.get("rc_invites", {}).get("per_issuer", {}),
+ self.rc_invites_per_issuer = RatelimitSettings.parse(
+ config,
+ "rc_invites.per_issuer",
defaults={"per_second": 0.3, "burst_count": 10},
)
- self.rc_third_party_invite = RatelimitSettings(
- config.get("rc_third_party_invite", {}),
+ self.rc_third_party_invite = RatelimitSettings.parse(
+ config,
+ "rc_third_party_invite",
defaults={"per_second": 0.0025, "burst_count": 5},
)
diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py
index 59ecafa6a0..2b0c505130 100644
--- a/synapse/handlers/auth.py
+++ b/synapse/handlers/auth.py
@@ -218,19 +218,17 @@ class AuthHandler:
self._failed_uia_attempts_ratelimiter = Ratelimiter(
store=self.store,
clock=self.clock,
- rate_hz=self.hs.config.ratelimiting.rc_login_failed_attempts.per_second,
- burst_count=self.hs.config.ratelimiting.rc_login_failed_attempts.burst_count,
+ cfg=self.hs.config.ratelimiting.rc_login_failed_attempts,
)
# The number of seconds to keep a UI auth session active.
self._ui_auth_session_timeout = hs.config.auth.ui_auth_session_timeout
- # Ratelimitier for failed /login attempts
+ # Ratelimiter for failed /login attempts
self._failed_login_attempts_ratelimiter = Ratelimiter(
store=self.store,
clock=hs.get_clock(),
- rate_hz=self.hs.config.ratelimiting.rc_login_failed_attempts.per_second,
- burst_count=self.hs.config.ratelimiting.rc_login_failed_attempts.burst_count,
+ cfg=self.hs.config.ratelimiting.rc_login_failed_attempts,
)
self._clock = self.hs.get_clock()
diff --git a/synapse/handlers/devicemessage.py b/synapse/handlers/devicemessage.py
index 17ff8821d9..798c7039f9 100644
--- a/synapse/handlers/devicemessage.py
+++ b/synapse/handlers/devicemessage.py
@@ -90,8 +90,7 @@ class DeviceMessageHandler:
self._ratelimiter = Ratelimiter(
store=self.store,
clock=hs.get_clock(),
- rate_hz=hs.config.ratelimiting.rc_key_requests.per_second,
- burst_count=hs.config.ratelimiting.rc_key_requests.burst_count,
+ cfg=hs.config.ratelimiting.rc_key_requests,
)
async def on_direct_to_device_edu(self, origin: str, content: JsonDict) -> None:
diff --git a/synapse/handlers/identity.py b/synapse/handlers/identity.py
index 3031384d25..472879c964 100644
--- a/synapse/handlers/identity.py
+++ b/synapse/handlers/identity.py
@@ -66,14 +66,12 @@ class IdentityHandler:
self._3pid_validation_ratelimiter_ip = Ratelimiter(
store=self.store,
clock=hs.get_clock(),
- rate_hz=hs.config.ratelimiting.rc_3pid_validation.per_second,
- burst_count=hs.config.ratelimiting.rc_3pid_validation.burst_count,
+ cfg=hs.config.ratelimiting.rc_3pid_validation,
)
self._3pid_validation_ratelimiter_address = Ratelimiter(
store=self.store,
clock=hs.get_clock(),
- rate_hz=hs.config.ratelimiting.rc_3pid_validation.per_second,
- burst_count=hs.config.ratelimiting.rc_3pid_validation.burst_count,
+ cfg=hs.config.ratelimiting.rc_3pid_validation,
)
async def ratelimit_request_token_requests(
diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py
index 1d8d4a72e7..de0f04e3fe 100644
--- a/synapse/handlers/room_member.py
+++ b/synapse/handlers/room_member.py
@@ -112,8 +112,7 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
self._join_rate_limiter_local = Ratelimiter(
store=self.store,
clock=self.clock,
- rate_hz=hs.config.ratelimiting.rc_joins_local.per_second,
- burst_count=hs.config.ratelimiting.rc_joins_local.burst_count,
+ cfg=hs.config.ratelimiting.rc_joins_local,
)
# Tracks joins from local users to rooms this server isn't a member of.
# I.e. joins this server makes by requesting /make_join /send_join from
@@ -121,8 +120,7 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
self._join_rate_limiter_remote = Ratelimiter(
store=self.store,
clock=self.clock,
- rate_hz=hs.config.ratelimiting.rc_joins_remote.per_second,
- burst_count=hs.config.ratelimiting.rc_joins_remote.burst_count,
+ cfg=hs.config.ratelimiting.rc_joins_remote,
)
# TODO: find a better place to keep this Ratelimiter.
# It needs to be
@@ -135,8 +133,7 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
self._join_rate_per_room_limiter = Ratelimiter(
store=self.store,
clock=self.clock,
- rate_hz=hs.config.ratelimiting.rc_joins_per_room.per_second,
- burst_count=hs.config.ratelimiting.rc_joins_per_room.burst_count,
+ cfg=hs.config.ratelimiting.rc_joins_per_room,
)
# Ratelimiter for invites, keyed by room (across all issuers, all
@@ -144,8 +141,7 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
self._invites_per_room_limiter = Ratelimiter(
store=self.store,
clock=self.clock,
- rate_hz=hs.config.ratelimiting.rc_invites_per_room.per_second,
- burst_count=hs.config.ratelimiting.rc_invites_per_room.burst_count,
+ cfg=hs.config.ratelimiting.rc_invites_per_room,
)
# Ratelimiter for invites, keyed by recipient (across all rooms, all
@@ -153,8 +149,7 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
self._invites_per_recipient_limiter = Ratelimiter(
store=self.store,
clock=self.clock,
- rate_hz=hs.config.ratelimiting.rc_invites_per_user.per_second,
- burst_count=hs.config.ratelimiting.rc_invites_per_user.burst_count,
+ cfg=hs.config.ratelimiting.rc_invites_per_user,
)
# Ratelimiter for invites, keyed by issuer (across all rooms, all
@@ -162,15 +157,13 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
self._invites_per_issuer_limiter = Ratelimiter(
store=self.store,
clock=self.clock,
- rate_hz=hs.config.ratelimiting.rc_invites_per_issuer.per_second,
- burst_count=hs.config.ratelimiting.rc_invites_per_issuer.burst_count,
+ cfg=hs.config.ratelimiting.rc_invites_per_issuer,
)
self._third_party_invite_limiter = Ratelimiter(
store=self.store,
clock=self.clock,
- rate_hz=hs.config.ratelimiting.rc_third_party_invite.per_second,
- burst_count=hs.config.ratelimiting.rc_third_party_invite.burst_count,
+ cfg=hs.config.ratelimiting.rc_third_party_invite,
)
self.request_ratelimiter = hs.get_request_ratelimiter()
diff --git a/synapse/handlers/room_summary.py b/synapse/handlers/room_summary.py
index dad3e23470..dd559b4c45 100644
--- a/synapse/handlers/room_summary.py
+++ b/synapse/handlers/room_summary.py
@@ -35,6 +35,7 @@ from synapse.api.errors import (
UnsupportedRoomVersionError,
)
from synapse.api.ratelimiting import Ratelimiter
+from synapse.config.ratelimiting import RatelimitSettings
from synapse.events import EventBase
from synapse.types import JsonDict, Requester, StrCollection
from synapse.util.caches.response_cache import ResponseCache
@@ -94,7 +95,9 @@ class RoomSummaryHandler:
self._server_name = hs.hostname
self._federation_client = hs.get_federation_client()
self._ratelimiter = Ratelimiter(
- store=self._store, clock=hs.get_clock(), rate_hz=5, burst_count=10
+ store=self._store,
+ clock=hs.get_clock(),
+ cfg=RatelimitSettings("<room summary>", per_second=5, burst_count=10),
)
# If a user tries to fetch the same page multiple times in quick succession,
diff --git a/synapse/http/server.py b/synapse/http/server.py
index 5109cec983..3bbf91298e 100644
--- a/synapse/http/server.py
+++ b/synapse/http/server.py
@@ -115,7 +115,13 @@ def return_json_error(
if exc.headers is not None:
for header, value in exc.headers.items():
request.setHeader(header, value)
- logger.info("%s SynapseError: %s - %s", request, error_code, exc.msg)
+ error_ctx = exc.debug_context
+ if error_ctx:
+ logger.info(
+ "%s SynapseError: %s - %s (%s)", request, error_code, exc.msg, error_ctx
+ )
+ else:
+ logger.info("%s SynapseError: %s - %s", request, error_code, exc.msg)
elif f.check(CancelledError):
error_code = HTTP_STATUS_REQUEST_CANCELLED
error_dict = {"error": "Request cancelled", "errcode": Codes.UNKNOWN}
diff --git a/synapse/rest/client/login.py b/synapse/rest/client/login.py
index d724c68920..7be327e26f 100644
--- a/synapse/rest/client/login.py
+++ b/synapse/rest/client/login.py
@@ -120,14 +120,12 @@ class LoginRestServlet(RestServlet):
self._address_ratelimiter = Ratelimiter(
store=self._main_store,
clock=hs.get_clock(),
- rate_hz=self.hs.config.ratelimiting.rc_login_address.per_second,
- burst_count=self.hs.config.ratelimiting.rc_login_address.burst_count,
+ cfg=self.hs.config.ratelimiting.rc_login_address,
)
self._account_ratelimiter = Ratelimiter(
store=self._main_store,
clock=hs.get_clock(),
- rate_hz=self.hs.config.ratelimiting.rc_login_account.per_second,
- burst_count=self.hs.config.ratelimiting.rc_login_account.burst_count,
+ cfg=self.hs.config.ratelimiting.rc_login_account,
)
# ensure the CAS/SAML/OIDC handlers are loaded on this worker instance.
diff --git a/synapse/rest/client/login_token_request.py b/synapse/rest/client/login_token_request.py
index b1629f94a5..d189a923b5 100644
--- a/synapse/rest/client/login_token_request.py
+++ b/synapse/rest/client/login_token_request.py
@@ -16,6 +16,7 @@ import logging
from typing import TYPE_CHECKING, Tuple
from synapse.api.ratelimiting import Ratelimiter
+from synapse.config.ratelimiting import RatelimitSettings
from synapse.http.server import HttpServer
from synapse.http.servlet import RestServlet, parse_json_object_from_request
from synapse.http.site import SynapseRequest
@@ -66,15 +67,18 @@ class LoginTokenRequestServlet(RestServlet):
self.token_timeout = hs.config.auth.login_via_existing_token_timeout
self._require_ui_auth = hs.config.auth.login_via_existing_require_ui_auth
- # Ratelimit aggressively to a maxmimum of 1 request per minute.
+ # Ratelimit aggressively to a maximum of 1 request per minute.
#
# This endpoint can be used to spawn additional sessions and could be
# abused by a malicious client to create many sessions.
self._ratelimiter = Ratelimiter(
store=self._main_store,
clock=hs.get_clock(),
- rate_hz=1 / 60,
- burst_count=1,
+ cfg=RatelimitSettings(
+ key="<login token request>",
+ per_second=1 / 60,
+ burst_count=1,
+ ),
)
@interactive_auth_handler
diff --git a/synapse/rest/client/register.py b/synapse/rest/client/register.py
index 77e3b91b79..132623462a 100644
--- a/synapse/rest/client/register.py
+++ b/synapse/rest/client/register.py
@@ -376,8 +376,7 @@ class RegistrationTokenValidityRestServlet(RestServlet):
self.ratelimiter = Ratelimiter(
store=self.store,
clock=hs.get_clock(),
- rate_hz=hs.config.ratelimiting.rc_registration_token_validity.per_second,
- burst_count=hs.config.ratelimiting.rc_registration_token_validity.burst_count,
+ cfg=hs.config.ratelimiting.rc_registration_token_validity,
)
async def on_GET(self, request: Request) -> Tuple[int, JsonDict]:
diff --git a/synapse/server.py b/synapse/server.py
index 7cdd3ea3c2..fd16dacd0d 100644
--- a/synapse/server.py
+++ b/synapse/server.py
@@ -408,8 +408,7 @@ class HomeServer(metaclass=abc.ABCMeta):
return Ratelimiter(
store=self.get_datastores().main,
clock=self.get_clock(),
- rate_hz=self.config.ratelimiting.rc_registration.per_second,
- burst_count=self.config.ratelimiting.rc_registration.burst_count,
+ cfg=self.config.ratelimiting.rc_registration,
)
@cache_in_self
diff --git a/synapse/util/ratelimitutils.py b/synapse/util/ratelimitutils.py
index cde4a0780f..f693ba2a8c 100644
--- a/synapse/util/ratelimitutils.py
+++ b/synapse/util/ratelimitutils.py
@@ -291,7 +291,8 @@ class _PerHostRatelimiter:
if self.metrics_name:
rate_limit_reject_counter.labels(self.metrics_name).inc()
raise LimitExceededError(
- retry_after_ms=int(self.window_size / self.sleep_limit)
+ limiter_name="rc_federation",
+ retry_after_ms=int(self.window_size / self.sleep_limit),
)
self.request_times.append(time_now)
|