# # This file is part of pretix (Community Edition). # # Copyright (C) 2014-2020 Raphael Michel and contributors # Copyright (C) 2020-today pretix GmbH and contributors # # This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General # Public License as published by the Free Software Foundation in version 3 of the License. # # ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are # applicable granting you additional permissions and placing additional restrictions on your usage of this software. # Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive # this file, see . # # This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied # warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more # details. # # You should have received a copy of the GNU Affero General Public License along with this program. If not, see # . # import hashlib import ipaddress import logging from django.conf import settings from django.http import HttpRequest from pretix.helpers.http import get_client_ip logger = logging.getLogger(__name__) def _get_key(key, parameters): return f'pretix:ratelimit:{key}:' + hashlib.sha256(','.join(str(p) for p in parameters).encode()).hexdigest() def _get_ip(request): client_ip = get_client_ip(request) if not client_ip: return None try: client_ip = ipaddress.ip_address(client_ip) except ValueError: # Web server not set up correctly return None if client_ip.is_private and not settings.DEBUG: # This is the private IP of the server, web server not set up correctly return None return str(client_ip) def rate_limit(key: str, *parameters, include_ip_from_request: HttpRequest=None, max_num: int, expire_time: int, increase: bool = True): """ This is a shared utility to implement simple rate limiting in operations like password resets. :param key: The key referring to the feature like "pwreset" :param parameters: Any number of things to be hashed as the bucket key :param include_ip_from_request: Add IP address from request to the bucket key. If IP address cannot be determined, rate limit is not applied. :param max_num: The maximum number of actions to performed within expire_time of the first action :param expire_time: The length of the time window in seconds :param increase: Whether to count the call as an event counted towards the rate, or just check :return: """ if not settings.HAS_REDIS: # No rate limiting return False from django_redis import get_redis_connection rc = get_redis_connection("redis") if include_ip_from_request: ip = _get_ip(include_ip_from_request) if not ip: # IP not discovered, can't rate limit return False parameters = (*parameters, ip) redis_key = _get_key(key, parameters) if increase: p = rc.pipeline() p.set(redis_key, 0, nx=True, ex=expire_time) # Start a rate limit window if none is running p.incr(redis_key) new_counter = p.execute()[1] else: new_counter = int(rc.get(redis_key) or 0) if new_counter > max_num: return True return False def rate_limit_reset(key: str, *parameters, include_ip_from_request: HttpRequest=None): """ Reset a rate limit bucket. """ if not settings.HAS_REDIS: # No rate limiting return from django_redis import get_redis_connection rc = get_redis_connection("redis") if include_ip_from_request: ip = _get_ip(include_ip_from_request) if not ip: # IP not discovered, can't rate limit return False parameters = (*parameters, ip) redis_key = _get_key(key, parameters) rc.delete(redis_key)