214 lines
		
	
	
		
			8.6 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			214 lines
		
	
	
		
			8.6 KiB
		
	
	
	
		
			Python
		
	
	
# 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.types import Requester
 | 
						|
from synapse.util import Clock
 | 
						|
 | 
						|
 | 
						|
class Ratelimiter:
 | 
						|
    """
 | 
						|
    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_requester_do_action(
 | 
						|
        self,
 | 
						|
        requester: Requester,
 | 
						|
        rate_hz: Optional[float] = None,
 | 
						|
        burst_count: Optional[int] = None,
 | 
						|
        update: bool = True,
 | 
						|
        _time_now_s: Optional[int] = None,
 | 
						|
    ) -> Tuple[bool, float]:
 | 
						|
        """Can the requester perform the action?
 | 
						|
 | 
						|
        Args:
 | 
						|
            requester: The requester to key off when rate limiting. The user property
 | 
						|
                will be used.
 | 
						|
            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
 | 
						|
        """
 | 
						|
        # Disable rate limiting of users belonging to any AS that is configured
 | 
						|
        # not to be rate limited in its registration file (rate_limited: true|false).
 | 
						|
        if requester.app_service and not requester.app_service.is_rate_limited():
 | 
						|
            return True, -1.0
 | 
						|
 | 
						|
        return self.can_do_action(
 | 
						|
            requester.user.to_string(), rate_hz, burst_count, update, _time_now_s
 | 
						|
        )
 | 
						|
 | 
						|
    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))
 | 
						|
            )
 |