123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176 |
- # Copyright 2014-2016 OpenMarket Ltd
- # Copyright 2020 The Matrix.org Foundation C.I.C.
- #
- # Licensed under the Apache License, Version 2.0 (the "License");
- # you may not use this file except in compliance with the License.
- # You may obtain a copy of the License at
- #
- # http://www.apache.org/licenses/LICENSE-2.0
- #
- # Unless required by applicable law or agreed to in writing, software
- # distributed under the License is distributed on an "AS IS" BASIS,
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- # See the License for the specific language governing permissions and
- # limitations under the License.
- from collections import OrderedDict
- from typing import Any, Optional, Tuple
- from synapse.api.errors import LimitExceededError
- from synapse.util import Clock
- class Ratelimiter(object):
- """
- Ratelimit actions marked by arbitrary keys.
- Args:
- clock: A homeserver clock, for retrieving the current time
- rate_hz: The long term number of actions that can be performed in a second.
- burst_count: How many actions that can be performed before being limited.
- """
- def __init__(self, clock: Clock, rate_hz: float, burst_count: int):
- self.clock = clock
- self.rate_hz = rate_hz
- self.burst_count = burst_count
- # A ordered dictionary keeping track of actions, when they were last
- # performed and how often. Each entry is a mapping from a key of arbitrary type
- # to a tuple representing:
- # * How many times an action has occurred since a point in time
- # * The point in time
- # * The rate_hz of this particular entry. This can vary per request
- self.actions = OrderedDict() # type: OrderedDict[Any, Tuple[float, int, float]]
- def can_do_action(
- self,
- key: Any,
- rate_hz: Optional[float] = None,
- burst_count: Optional[int] = None,
- update: bool = True,
- _time_now_s: Optional[int] = None,
- ) -> Tuple[bool, float]:
- """Can the entity (e.g. user or IP address) perform the action?
- Args:
- key: The key we should use when rate limiting. Can be a user ID
- (when sending events), an IP address, etc.
- rate_hz: The long term number of actions that can be performed in a second.
- Overrides the value set during instantiation if set.
- burst_count: How many actions that can be performed before being limited.
- Overrides the value set during instantiation if set.
- update: Whether to count this check as performing the action
- _time_now_s: The current time. Optional, defaults to the current time according
- to self.clock. Only used by tests.
- Returns:
- A tuple containing:
- * A bool indicating if they can perform the action now
- * The reactor timestamp for when the action can be performed next.
- -1 if rate_hz is less than or equal to zero
- """
- # Override default values if set
- time_now_s = _time_now_s if _time_now_s is not None else self.clock.time()
- rate_hz = rate_hz if rate_hz is not None else self.rate_hz
- burst_count = burst_count if burst_count is not None else self.burst_count
- # Remove any expired entries
- self._prune_message_counts(time_now_s)
- # Check if there is an existing count entry for this key
- action_count, time_start, _ = self.actions.get(key, (0.0, time_now_s, 0.0))
- # Check whether performing another action is allowed
- time_delta = time_now_s - time_start
- performed_count = action_count - time_delta * rate_hz
- if performed_count < 0:
- # Allow, reset back to count 1
- allowed = True
- time_start = time_now_s
- action_count = 1.0
- elif performed_count > burst_count - 1.0:
- # Deny, we have exceeded our burst count
- allowed = False
- else:
- # We haven't reached our limit yet
- allowed = True
- action_count += 1.0
- if update:
- self.actions[key] = (action_count, time_start, rate_hz)
- if rate_hz > 0:
- # Find out when the count of existing actions expires
- time_allowed = time_start + (action_count - burst_count + 1) / rate_hz
- # Don't give back a time in the past
- if time_allowed < time_now_s:
- time_allowed = time_now_s
- else:
- # XXX: Why is this -1? This seems to only be used in
- # self.ratelimit. I guess so that clients get a time in the past and don't
- # feel afraid to try again immediately
- time_allowed = -1
- return allowed, time_allowed
- def _prune_message_counts(self, time_now_s: int):
- """Remove message count entries that have not exceeded their defined
- rate_hz limit
- Args:
- time_now_s: The current time
- """
- # We create a copy of the key list here as the dictionary is modified during
- # the loop
- for key in list(self.actions.keys()):
- action_count, time_start, rate_hz = self.actions[key]
- # Rate limit = "seconds since we started limiting this action" * rate_hz
- # If this limit has not been exceeded, wipe our record of this action
- time_delta = time_now_s - time_start
- if action_count - time_delta * rate_hz > 0:
- continue
- else:
- del self.actions[key]
- def ratelimit(
- self,
- key: Any,
- rate_hz: Optional[float] = None,
- burst_count: Optional[int] = None,
- update: bool = True,
- _time_now_s: Optional[int] = None,
- ):
- """Checks if an action can be performed. If not, raises a LimitExceededError
- Args:
- key: An arbitrary key used to classify an action
- rate_hz: The long term number of actions that can be performed in a second.
- Overrides the value set during instantiation if set.
- burst_count: How many actions that can be performed before being limited.
- Overrides the value set during instantiation if set.
- update: Whether to count this check as performing the action
- _time_now_s: The current time. Optional, defaults to the current time according
- to self.clock. Only used by tests.
- Raises:
- LimitExceededError: If an action could not be performed, along with the time in
- milliseconds until the action can be performed again
- """
- time_now_s = _time_now_s if _time_now_s is not None else self.clock.time()
- allowed, time_allowed = self.can_do_action(
- key,
- rate_hz=rate_hz,
- burst_count=burst_count,
- update=update,
- _time_now_s=time_now_s,
- )
- if not allowed:
- raise LimitExceededError(
- retry_after_ms=int(1000 * (time_allowed - time_now_s))
- )
|