def test_storage_check(self): self.assertTrue(storage_from_string("memory://").check()) self.assertTrue(storage_from_string("redis://localhost:6379").check()) self.assertTrue(storage_from_string("memcached://localhost:11211").check()) self.assertTrue( storage_from_string("redis+sentinel://localhost:26379", service_name="localhost-redis-sentinel").check() ) self.assertTrue(storage_from_string("redis+cluster://localhost:7000").check())
def test_storage_string(self): self.assertTrue(isinstance(storage_from_string("memory://"), MemoryStorage)) self.assertTrue(isinstance(storage_from_string("redis://localhost:6379"), RedisStorage)) self.assertTrue(isinstance(storage_from_string("memcached://localhost:11211"), MemcachedStorage)) self.assertTrue(isinstance(storage_from_string("redis+sentinel://localhost:26379", service_name="localhost-redis-sentinel"), RedisSentinelStorage)) self.assertTrue(isinstance(storage_from_string("redis+sentinel://localhost:26379/localhost-redis-sentinel"), RedisSentinelStorage)) self.assertRaises(ConfigurationError, storage_from_string, "blah://") self.assertRaises(ConfigurationError, storage_from_string, "redis+sentinel://localhost:26379")
def init_app(self, app): """ :param app: :class:`flask.Flask` instance to rate limit. """ self.enabled = app.config.setdefault(C.ENABLED, True) self._swallow_errors = app.config.setdefault( C.SWALLOW_ERRORS, self._swallow_errors ) self._headers_enabled = ( self._headers_enabled or app.config.setdefault(C.HEADERS_ENABLED, False) ) self._storage_options.update( app.config.get(C.STORAGE_OPTIONS, {}) ) self._storage = storage_from_string( self._storage_uri or app.config.setdefault(C.STORAGE_URL, 'memory://'), ** self._storage_options ) strategy = ( self._strategy or app.config.setdefault(C.STRATEGY, 'fixed-window') ) if strategy not in STRATEGIES: raise ConfigurationError("Invalid rate limiting strategy %s" % strategy) self._limiter = STRATEGIES[strategy](self._storage) self._header_mapping.update({ HEADERS.RESET : self._header_mapping.get(HEADERS.RESET,None) or app.config.setdefault(C.HEADER_RESET, "X-RateLimit-Reset"), HEADERS.REMAINING : self._header_mapping.get(HEADERS.REMAINING,None) or app.config.setdefault(C.HEADER_REMAINING, "X-RateLimit-Remaining"), HEADERS.LIMIT : self._header_mapping.get(HEADERS.LIMIT,None) or app.config.setdefault(C.HEADER_LIMIT, "X-RateLimit-Limit"), }) conf_limits = app.config.get(C.GLOBAL_LIMITS, None) if not self._global_limits and conf_limits: self._global_limits = [ ExtLimit( limit, self._key_func, None, False, None, None, None ) for limit in parse_many(conf_limits) ] fallback_limits = app.config.get(C.IN_MEMORY_FALLBACK, None) if not self._in_memory_fallback and fallback_limits: self._in_memory_fallback = [ ExtLimit( limit, self._key_func, None, False, None, None, None ) for limit in parse_many(fallback_limits) ] if self._auto_check: app.before_request(self.__check_request_limit) app.after_request(self.__inject_headers) if self._in_memory_fallback: self._fallback_storage = MemoryStorage() self._fallback_limiter = STRATEGIES[strategy](self._fallback_storage) # purely for backward compatibility as stated in flask documentation if not hasattr(app, 'extensions'): app.extensions = {} # pragma: no cover app.extensions['limiter'] = self
def test_storage_string(self): self.assertTrue(isinstance(storage_from_string("memory://"), MemoryStorage)) self.assertTrue(isinstance(storage_from_string("redis://*****:*****@localhost:26379/localhost-redis-sentinel"), RedisSentinelStorage, ) ) self.assertEqual(get_dependency().Sentinel.call_args[1]["password"], "foobared")
def test_pluggable_storage_no_moving_window(self): class MyStorage(Storage): STORAGE_SCHEME = "mystorage" def incr(self, key, expiry, elastic_expiry=False): return def get(self, key): return 0 def get_expiry(self, key): return time.time() storage = storage_from_string("mystorage://") self.assertTrue(isinstance(storage, MyStorage)) self.assertRaises(NotImplementedError, MovingWindowRateLimiter, storage)
def test_storage_check(self): self.assertTrue(storage_from_string("memory://").check()) self.assertTrue(storage_from_string("redis://localhost:6379").check()) self.assertTrue( storage_from_string( "redis+unix:///var/tmp/limits.redis.sock").check()) self.assertTrue( storage_from_string("memcached://localhost:11211").check()) self.assertTrue( storage_from_string( "redis+sentinel://localhost:26379", service_name="localhost-redis-sentinel").check()) self.assertTrue( storage_from_string("redis+cluster://localhost:7000").check()) if RUN_GAE: self.assertTrue(storage_from_string("gaememcached://").check())
def init_app(self, app): """ :param app: :class:`flask.Flask` instance to rate limit. """ self.enabled = app.config.setdefault(C.ENABLED, True) self.headers_enabled = (self.headers_enabled or app.config.setdefault( C.HEADERS_ENABLED, False)) self.storage_options.update(app.config.get(C.STORAGE_OPTIONS, {})) self.storage = storage_from_string( self.storage_uri or app.config.setdefault(C.STORAGE_URL, 'memory://'), **self.storage_options) strategy = (self.strategy or app.config.setdefault(C.STRATEGY, 'fixed-window')) if strategy not in STRATEGIES: raise ConfigurationError("Invalid rate limiting strategy %s" % strategy) self.limiter = STRATEGIES[strategy](self.storage) self.header_mapping.update({ HEADERS.RESET: self.header_mapping.get(HEADERS.RESET, None) or app.config.setdefault(C.HEADER_RESET, "X-RateLimit-Reset"), HEADERS.REMAINING: self.header_mapping.get(HEADERS.REMAINING, None) or app.config.setdefault(C.HEADER_REMAINING, "X-RateLimit-Remaining"), HEADERS.LIMIT: self.header_mapping.get(HEADERS.LIMIT, None) or app.config.setdefault(C.HEADER_LIMIT, "X-RateLimit-Limit"), }) conf_limits = app.config.get(C.GLOBAL_LIMITS, None) if not self.global_limits and conf_limits: self.global_limits = [ ExtLimit(limit, self.key_func, None, False, None, None) for limit in parse_many(conf_limits) ] if self.auto_check: app.before_request(self.__check_request_limit) app.after_request(self.__inject_headers) # purely for backward compatibility as stated in flask documentation if not hasattr(app, 'extensions'): app.extensions = {} # pragma: no cover app.extensions['limiter'] = self
def __init__(self): conf_limits = getattr(settings, C.GLOBAL_LIMITS, "") callback = getattr(settings, C.CALLBACK, self.__raise_exceeded) self.enabled = getattr(settings, C.ENABLED, True) self.headers_enabled = getattr(settings, C.HEADERS_ENABLED, False) self.strategy = getattr(settings, C.STRATEGY, 'fixed-window') if self.strategy not in STRATEGIES: raise ConfigurationError("Invalid rate limiting strategy %s" % self.strategy) self.storage = storage_from_string( getattr(settings, C.STORAGE_URL, "memory://")) self.limiter = STRATEGIES[self.strategy](self.storage) self.key_function = getattr(settings, C.DEFAULT_KEY_FUNCTION, get_ipaddr) self.global_limits = [] if conf_limits: self.global_limits = [ LimitWrapper(list(parse_many(conf_limits)), self.key_function, None, False) ] self.header_mapping = { HEADERS.RESET: getattr(settings, C.HEADER_RESET, "X-RateLimit-Reset"), HEADERS.REMAINING: getattr(settings, C.HEADER_REMAINING, "X-RateLimit-Remaining"), HEADERS.LIMIT: getattr(settings, C.HEADER_LIMIT, "X-RateLimit-Limit"), } self.logger = logging.getLogger("djlimiter") self.logger.addHandler(BlackHoleHandler()) if isinstance(callback, six.string_types): mod, _, name = callback.rpartition(".") try: self.callback = getattr(importlib.import_module(mod), name) except AttributeError: self.logger.error( "Unable to load callback function %s. Rate limiting disabled", callback) self.enabled = False else: self.callback = callback
def test_pluggable_storage_moving_window(self): class MyStorage(Storage): STORAGE_SCHEME = "mystorage" def incr(self, key, expiry, elastic_expiry=False): return def get(self, key): return 0 def get_expiry(self, key): return time.time() def acquire_entry(self, *a, **k): return True def get_moving_window(self, *a, **k): return (time.time(), 1) storage = storage_from_string("mystorage://") self.assertTrue(isinstance(storage, MyStorage)) MovingWindowRateLimiter(storage)
def test_storage_string(self): self.assertTrue( isinstance(storage_from_string("memory://"), MemoryStorage)) self.assertTrue( isinstance(storage_from_string("redis://*****:*****@localhost:26379/localhost-redis-sentinel" ), RedisSentinelStorage)) self.assertEqual( get_dependency().Sentinel.call_args[1]['password'], 'foobared')
def __init__(self): conf_limits = getattr(settings, C.GLOBAL_LIMITS, "") callback = getattr(settings, C.CALLBACK, self.__raise_exceeded ) self.enabled = getattr(settings, C.ENABLED, True) self.headers_enabled = getattr(settings, C.HEADERS_ENABLED, False) self.strategy = getattr(settings, C.STRATEGY, 'fixed-window') if self.strategy not in STRATEGIES: raise ConfigurationError("Invalid rate limiting strategy %s" % self.strategy) self.storage = storage_from_string(getattr(settings, C.STORAGE_URL, "memory://")) self.limiter = STRATEGIES[self.strategy](self.storage) self.key_function = getattr(settings, C.DEFAULT_KEY_FUNCTION, get_ipaddr) self.global_limits = [] if conf_limits: self.global_limits = [ LimitWrapper( list(parse_many(conf_limits)), self.key_function, None, False ) ] self.header_mapping = { HEADERS.RESET : getattr(settings,C.HEADER_RESET, "X-RateLimit-Reset"), HEADERS.REMAINING : getattr(settings,C.HEADER_REMAINING, "X-RateLimit-Remaining"), HEADERS.LIMIT : getattr(settings,C.HEADER_LIMIT, "X-RateLimit-Limit"), } self.logger = logging.getLogger("djlimiter") self.logger.addHandler(BlackHoleHandler()) if isinstance(callback, six.string_types): mod, _, name = callback.rpartition(".") try: self.callback = getattr(importlib.import_module(mod), name) except AttributeError: self.logger.error( "Unable to load callback function %s. Rate limiting disabled", callback ) self.enabled = False else: self.callback = callback
def test_init_options(self): with mock.patch("limits.storage.get_dependency") as get_dependency: storage_from_string(self.storage_url, connection_timeout=1) self.assertEqual( get_dependency().from_url.call_args[1]['connection_timeout'], 1)
def __init__( self, # app: Starlette = None, key_func: Callable[..., str], default_limits: List[StrOrCallableStr] = [], application_limits: List[StrOrCallableStr] = [], headers_enabled: bool = False, strategy: Optional[str] = None, storage_uri: Optional[str] = None, storage_options: Dict[str, str] = {}, auto_check: bool = True, swallow_errors: bool = False, in_memory_fallback: List[StrOrCallableStr] = [], in_memory_fallback_enabled: bool = False, retry_after: Optional[str] = None, key_prefix: str = "", enabled: bool = True, config_filename: Optional[str] = None, ) -> None: """ Configure the rate limiter at app level """ # assert app is not None, "Passing the app instance to the limiter is required" # self.app = app # app.state.limiter = self self.logger = logging.getLogger("slowapi") self.app_config = Config( config_filename if config_filename is not None else ".env") self.enabled = enabled self._default_limits = [] self._application_limits = [] self._in_memory_fallback: List[LimitGroup] = [] self._in_memory_fallback_enabled = (in_memory_fallback_enabled or len(in_memory_fallback) > 0) self._exempt_routes: Set[str] = set() self._request_filters: List[Callable[..., bool]] = [] self._headers_enabled = headers_enabled self._header_mapping: Dict[int, str] = {} self._retry_after: Optional[str] = retry_after self._strategy = strategy self._storage_uri = storage_uri self._storage_options = storage_options self._auto_check = auto_check self._swallow_errors = swallow_errors self._key_func = key_func self._key_prefix = key_prefix for limit in set(default_limits): self._default_limits.extend([ LimitGroup(limit, self._key_func, None, False, None, None, None, False) ]) for limit in application_limits: self._application_limits.extend([ LimitGroup(limit, self._key_func, "global", False, None, None, None, False) ]) for limit in in_memory_fallback: self._in_memory_fallback.extend([ LimitGroup(limit, self._key_func, None, False, None, None, None, False) ]) self._route_limits: Dict[str, List[Limit]] = {} self._dynamic_route_limits: Dict[str, List[LimitGroup]] = {} # a flag to note if the storage backend is dead (not available) self._storage_dead: bool = False self._fallback_limiter = None self.__check_backend_count = 0 self.__last_check_backend = time.time() self.__marked_for_limiting: Dict[str, List[Callable]] = {} class BlackHoleHandler(logging.StreamHandler): def emit(*_): return self.logger.addHandler(BlackHoleHandler()) self.enabled = self.get_app_config(C.ENABLED, self.enabled) self._swallow_errors = self.get_app_config(C.SWALLOW_ERRORS, self._swallow_errors) self._headers_enabled = self._headers_enabled or self.get_app_config( C.HEADERS_ENABLED, False) self._storage_options.update(self.get_app_config( C.STORAGE_OPTIONS, {})) self._storage: Storage = storage_from_string( self._storage_uri or self.get_app_config(C.STORAGE_URL, "memory://"), **self._storage_options, ) strategy = self._strategy or self.get_app_config( C.STRATEGY, "fixed-window") if strategy not in STRATEGIES: raise ConfigurationError("Invalid rate limiting strategy %s" % strategy) self._limiter: RateLimiter = STRATEGIES[strategy](self._storage) self._header_mapping.update({ HEADERS.RESET: self._header_mapping.get( HEADERS.RESET, self.get_app_config(C.HEADER_RESET, "X-RateLimit-Reset"), ), HEADERS.REMAINING: self._header_mapping.get( HEADERS.REMAINING, self.get_app_config(C.HEADER_REMAINING, "X-RateLimit-Remaining"), ), HEADERS.LIMIT: self._header_mapping.get( HEADERS.LIMIT, self.get_app_config(C.HEADER_LIMIT, "X-RateLimit-Limit"), ), HEADERS.RETRY_AFTER: self._header_mapping.get( HEADERS.RETRY_AFTER, self.get_app_config(C.HEADER_RETRY_AFTER, "Retry-After"), ), }) self._retry_after = self._retry_after or self.get_app_config( C.HEADER_RETRY_AFTER_VALUE) self._key_prefix = self._key_prefix or self.get_app_config( C.KEY_PREFIX) app_limits: Optional[StrOrCallableStr] = self.get_app_config( C.APPLICATION_LIMITS, None) if not self._application_limits and app_limits: self._application_limits = [ LimitGroup(app_limits, self._key_func, "global", False, None, None, None, False) ] conf_limits: Optional[StrOrCallableStr] = self.get_app_config( C.DEFAULT_LIMITS, None) if not self._default_limits and conf_limits: self._default_limits = [ LimitGroup(conf_limits, self._key_func, None, False, None, None, None, False) ] fallback_enabled = self.get_app_config(C.IN_MEMORY_FALLBACK_ENABLED, False) fallback_limits: Optional[StrOrCallableStr] = self.get_app_config( C.IN_MEMORY_FALLBACK, None) if not self._in_memory_fallback and fallback_limits: self._in_memory_fallback = [ LimitGroup( fallback_limits, self._key_func, None, False, None, None, None, False, ) ] if not self._in_memory_fallback_enabled: self._in_memory_fallback_enabled = ( fallback_enabled or len(self._in_memory_fallback) > 0) if self._in_memory_fallback_enabled: self._fallback_storage = MemoryStorage() self._fallback_limiter = STRATEGIES[strategy]( self._fallback_storage)
def includeme(config): config.registry["ratelimiter.storage"] = storage_from_string( config.registry.settings["ratelimit.url"] )
def test_init_options(self): with mock.patch("limits.storage.redis_cluster.get_dependency" ) as get_dependency: storage_from_string(self.storage_url, connection_timeout=1) call_args = get_dependency().RedisCluster.call_args self.assertEqual(call_args[1]['connection_timeout'], 1)
def init_app(self, app): """ :param app: :class:`flask.Flask` instance to rate limit. """ config = app.config self.enabled = config.setdefault(C.ENABLED, self.enabled) if not self.enabled: return self._default_limits_per_method = config.setdefault( C.DEFAULT_LIMITS_PER_METHOD, self._default_limits_per_method ) self._default_limits_exempt_when = config.setdefault( C.DEFAULT_LIMITS_EXEMPT_WHEN, self._default_limits_exempt_when ) self._default_limits_deduct_when = config.setdefault( C.DEFAULT_LIMITS_DEDUCT_WHEN, self._default_limits_deduct_when ) self._swallow_errors = config.setdefault( C.SWALLOW_ERRORS, self._swallow_errors ) self._headers_enabled = ( self._headers_enabled or config.setdefault(C.HEADERS_ENABLED, False) ) self._storage_options.update(config.get(C.STORAGE_OPTIONS, {})) self._storage = storage_from_string( self._storage_uri or config.setdefault(C.STORAGE_URL, 'memory://'), **self._storage_options ) strategy = ( self._strategy or config.setdefault(C.STRATEGY, 'fixed-window') ) if strategy not in STRATEGIES: raise ConfigurationError( "Invalid rate limiting strategy %s" % strategy ) self._limiter = STRATEGIES[strategy](self._storage) # TODO: this should be made consistent with the rest of the # configuration self._header_mapping = { HEADERS.RESET: self._header_mapping.get( HEADERS.RESET, config.get( C.HEADER_RESET, "X-RateLimit-Reset" ) ), HEADERS.REMAINING: self._header_mapping.get( HEADERS.REMAINING, config.get( C.HEADER_REMAINING, "X-RateLimit-Remaining" ) ), HEADERS.LIMIT: self._header_mapping.get( HEADERS.LIMIT, config.get( C.HEADER_LIMIT, "X-RateLimit-Limit" ) ), HEADERS.RETRY_AFTER: self._header_mapping.get( HEADERS.RETRY_AFTER, config.get( C.HEADER_RETRY_AFTER, "Retry-After" ) ), } self._retry_after = ( self._retry_after or config.get(C.HEADER_RETRY_AFTER_VALUE) ) self._key_prefix = (self._key_prefix or config.get(C.KEY_PREFIX)) app_limits = config.get(C.APPLICATION_LIMITS, None) if not self._application_limits and app_limits: self._application_limits = [ LimitGroup( app_limits, self._key_func, "global", False, None, None, None, None, None ) ] if config.get(C.GLOBAL_LIMITS, None): self.__raise_global_limits_warning() conf_limits = config.get( C.GLOBAL_LIMITS, config.get(C.DEFAULT_LIMITS, None) ) if not self._default_limits and conf_limits: self._default_limits = [ LimitGroup( conf_limits, self._key_func, None, False, None, None, None, None, None ) ] for limit in self._default_limits: limit.per_method = self._default_limits_per_method limit.exempt_when = self._default_limits_exempt_when limit.deduct_when = self._default_limits_deduct_when self.__configure_fallbacks(app, strategy) # purely for backward compatibility as stated in flask documentation if not hasattr(app, 'extensions'): app.extensions = {} # pragma: no cover if not app.extensions.get('limiter'): if self._auto_check: app.before_request(self.__check_request_limit) app.after_request(self.__inject_headers) app.extensions['limiter'] = self self.initialized = True
def test_options(self): with mock.patch( "limits.storage.memcached.get_dependency") as get_dependency: storage_from_string(self.storage_url, connect_timeout=1).check() self.assertEqual( get_dependency().Client.call_args[1]['connect_timeout'], 1)
def test_storage_string(self): self.assertTrue(isinstance(storage_from_string("memory://"), MemoryStorage)) self.assertTrue(isinstance(storage_from_string("redis://localhost:6379"), RedisStorage)) self.assertTrue(isinstance(storage_from_string("memcached://localhost:11211"), MemcachedStorage)) self.assertRaises(ConfigurationError, storage_from_string, "blah://")
def test_storage_check(self): self.assertTrue(storage_from_string("memory://").check()) self.assertTrue(storage_from_string("redis://localhost:6379").check()) self.assertTrue(storage_from_string("memcached://localhost:11211").check())
def init_app(self, app): """ :param app: :class:`flask.Flask` instance to rate limit. """ self.enabled = app.config.setdefault(C.ENABLED, True) self._swallow_errors = app.config.setdefault(C.SWALLOW_ERRORS, self._swallow_errors) self._headers_enabled = (self._headers_enabled or app.config.setdefault( C.HEADERS_ENABLED, False)) self._storage_options.update(app.config.get(C.STORAGE_OPTIONS, {})) self._storage = storage_from_string( self._storage_uri or app.config.setdefault(C.STORAGE_URL, 'memory://'), **self._storage_options) strategy = (self._strategy or app.config.setdefault(C.STRATEGY, 'fixed-window')) if strategy not in STRATEGIES: raise ConfigurationError("Invalid rate limiting strategy %s" % strategy) self._limiter = STRATEGIES[strategy](self._storage) self._header_mapping.update({ HEADERS.RESET: self._header_mapping.get(HEADERS.RESET, None) or app.config.setdefault(C.HEADER_RESET, "X-RateLimit-Reset"), HEADERS.REMAINING: self._header_mapping.get(HEADERS.REMAINING, None) or app.config.setdefault(C.HEADER_REMAINING, "X-RateLimit-Remaining"), HEADERS.LIMIT: self._header_mapping.get(HEADERS.LIMIT, None) or app.config.setdefault(C.HEADER_LIMIT, "X-RateLimit-Limit"), HEADERS.RETRY_AFTER: self._header_mapping.get(HEADERS.RETRY_AFTER, None) or app.config.setdefault(C.HEADER_RETRY_AFTER, "Retry-After"), }) self._retry_after = (self._retry_after or app.config.get(C.HEADER_RETRY_AFTER_VALUE)) app_limits = app.config.get(C.APPLICATION_LIMITS, None) if not self._application_limits and app_limits: self._application_limits = [ ExtLimit(limit, self._key_func, "global", False, None, None, None) for limit in parse_many(app_limits) ] if app.config.get(C.GLOBAL_LIMITS, None): self.raise_global_limits_warning() conf_limits = app.config.get(C.GLOBAL_LIMITS, app.config.get(C.DEFAULT_LIMITS, None)) if not self._default_limits and conf_limits: self._default_limits = [ ExtLimit(limit, self._key_func, None, False, None, None, None) for limit in parse_many(conf_limits) ] fallback_limits = app.config.get(C.IN_MEMORY_FALLBACK, None) if not self._in_memory_fallback and fallback_limits: self._in_memory_fallback = [ ExtLimit(limit, self._key_func, None, False, None, None, None) for limit in parse_many(fallback_limits) ] if self._auto_check: app.before_request(self.__check_request_limit) app.after_request(self.__inject_headers) if self._in_memory_fallback: self._fallback_storage = MemoryStorage() self._fallback_limiter = STRATEGIES[strategy]( self._fallback_storage) # purely for backward compatibility as stated in flask documentation if not hasattr(app, 'extensions'): app.extensions = {} # pragma: no cover app.extensions['limiter'] = self
def includeme(config): config.registry["ratelimiter.storage"] = storage_from_string( config.registry.settings["ratelimit.url"])