def test_different_key_prefixes(): """ Test using different key_prefixes """ limiter1 = Limiter( key_func=get_remote_addr, default_limits=["10 per hour", "1 per second"], config={ 'RATELIMIT_KEY_PREFIX': 'app1' } ) @limiter1.limit() class ThingsResource: def on_get(self, req, resp): resp.body = 'Hello world!' app1 = API(middleware=limiter1.middleware) app1.add_route('/things', ThingsResource()) client1 = testing.TestClient(app1) r = client1.simulate_get('/things') assert r.status == HTTP_200 # due to the 1 second default limit on the default limiter r = client1.simulate_get('/things') assert r.status == HTTP_429 ##### # app2 - with a different key limiter2 = Limiter( key_func=get_remote_addr, default_limits=["10 per hour", "1 per second"], config={ 'RATELIMIT_KEY_PREFIX': 'app2' } ) @limiter2.limit() class ThingsResource: def on_get(self, req, resp): resp.body = 'Hello world!' app2 = API(middleware=limiter2.middleware) app2.add_route('/things', ThingsResource()) client2 = testing.TestClient(app2) r = client2.simulate_get('/things') assert r.status == HTTP_200 # due to the 1 second default limit on the default limiter r = client2.simulate_get('/things') assert r.status == HTTP_429
def test_redis(redis_server): """ Test using the redis backend """ limiter = Limiter( key_func=get_remote_addr, default_limits=["10 per hour", "1 per second"], config={ 'RATELIMIT_KEY_PREFIX': 'myapp', 'RATELIMIT_STORAGE_URL': f'redis://@localhost:{REDIS_PORT}' } ) @limiter.limit() class ThingsResource: def on_get(self, req, resp): resp.body = 'Hello world!' app = API(middleware=limiter.middleware) app.add_route('/things', ThingsResource()) client = testing.TestClient(app) r = client.simulate_get('/things') assert r.status == HTTP_200 # due to the 1 second default limit on the default limiter r = client.simulate_get('/things') assert r.status == HTTP_429 sleep(1) r = client.simulate_get('/things') assert r.status == HTTP_200
def test_limit_by_resource_and_method(): """ Test using a custom key_func - one which creates different buckets by resource and method """ def get_key(req, resp, resource, params) -> str: user_key = get_remote_addr(req, resp, resource, params) return f"{user_key}:{resource.__class__.__name__}:{req.method}" limiter = Limiter( key_func=get_key, default_limits=["10 per hour", "1 per second"] ) @limiter.limit() class ThingsResource: def on_get(self, req, resp): resp.body = 'Hello world!' def on_post(self, req, resp): resp.body = 'Hello world!' app = API(middleware=limiter.middleware) app.add_route('/things', ThingsResource()) client = testing.TestClient(app) r = client.simulate_get('/things') assert r.status == HTTP_200 r = client.simulate_get('/things') assert r.status == HTTP_429 # but a different endpoint can still be hit r = client.simulate_post('/things') assert r.status == HTTP_200
def test_get_remote_addr(): """ Test using the get_remote_addr() key function as default """ limiter = Limiter( key_func=get_remote_addr, default_limits=["10 per hour", "1 per second"] ) @limiter.limit() class ThingsResource: def on_get(self, req, resp): resp.body = 'Hello world!' app = API(middleware=limiter.middleware) app.add_route('/things', ThingsResource()) client = testing.TestClient(app) r = client.simulate_get('/things') assert r.status == HTTP_200 # due to the 1 second default limit on the default limiter r = client.simulate_get('/things') assert r.status == HTTP_429 sleep(1) r = client.simulate_get('/things') assert r.status == HTTP_200
def test_dynamic_limits_on_method2(): """ Test using the dynamic_limits param of the method decorators to change the limit per user Overwriting the default limits from the class level decortor """ limiter = Limiter(key_func=get_remote_addr, default_limits=["10 per hour", "1 per second"]) @limiter.limit() class ThingsResource: @limiter.limit(dynamic_limits=lambda req, resp, resource, req_succeeded: '5/second' if req.get_header('APIUSER') == 'admin' else '2/second') def on_get(self, req, resp): resp.body = 'Hello world!' def on_post(self, req, resp): resp.body = 'Hello world!' app = API(middleware=limiter.middleware) app.add_route('/things', ThingsResource()) client = testing.TestClient(app) #### # 'normal' user - errors after more than 2 calls per sec r = client.simulate_get('/things') assert r.status == HTTP_200 r = client.simulate_get('/things') assert r.status == HTTP_200 # due to the 2/second limit r = client.simulate_get('/things') assert r.status == HTTP_429 ######### # 'admin' user should be able to make 5 calls admin_header = {"APIUSER": "******"} for i in range(5): r = client.simulate_get('/things', headers=admin_header) assert r.status == HTTP_200 # at the 6th hit even the admin user will error: r = client.simulate_get('/things', headers=admin_header) assert r.status == HTTP_429 sleep(1) r = client.simulate_get('/things', headers=admin_header) assert r.status == HTTP_200 ######## # the on_post() method gets the default limit - 1 per second r = client.simulate_post('/things', headers=admin_header) assert r.status == HTTP_200 r = client.simulate_post('/things', headers=admin_header) assert r.status == HTTP_429
def test_default_dynamic_limits(): """ Test using the default_dynamic_limits option to change the limit per user """ limiter = Limiter(key_func=get_remote_addr, default_dynamic_limits=lambda req, resp, resource, req_succeeded: '5/second' if req.get_header('APIUSER') == 'admin' else '2/second') @limiter.limit() class ThingsResource: def on_get(self, req, resp): resp.body = 'Hello world!' app = API(middleware=limiter.middleware) app.add_route('/things', ThingsResource()) client = testing.TestClient(app) #### # 'normal' user - errors after more than 2 calls per sec r = client.simulate_get('/things') assert r.status == HTTP_200 r = client.simulate_get('/things') assert r.status == HTTP_200 # due to the 2/second limit r = client.simulate_get('/things') assert r.status == HTTP_429 ######### # 'admin' user should be able to make 5 calls admin_header = {"APIUSER": "******"} for i in range(5): r = client.simulate_get('/things', headers=admin_header) assert r.status == HTTP_200 # at the 6th hit even the admin user will error: r = client.simulate_get('/things', headers=admin_header) assert r.status == HTTP_429 sleep(1) r = client.simulate_get('/things', headers=admin_header) assert r.status == HTTP_200
def test_empy_limits(): """ Test incorrect setup - empty limits! In this case there are NO limits applied """ limiter = Limiter(key_func=get_remote_addr) @limiter.limit() class ThingsResource: def on_get(self, req, resp): resp.body = 'Hello world!' app = API(middleware=limiter.middleware) app.add_route('/things', ThingsResource()) client = testing.TestClient(app) for i in range(5): r = client.simulate_get('/things') assert r.status == HTTP_200
def test_deduct_when_http200_as_method_decorator(): """ Test using the deduct_when option on a method decorator to deduct only when the response is 200 """ limiter = Limiter( key_func=get_remote_addr, default_limits=["10 per hour", "1 per second"] ) @limiter.limit() class ThingsResource: def on_get(self, req, resp): resp.body = 'Hello world!' @limiter.limit(deduct_when=lambda req, resp, resource, req_succeeded: resp.status == HTTP_200) def on_post(self, req, resp): resp.body = 'Hello world!' resp.status = HTTP_500 app = API(middleware=limiter.middleware) app.add_route('/things', ThingsResource()) client = testing.TestClient(app) # this is a 500 response, so it should NOT count! r = client.simulate_post('/things') assert r.status == HTTP_500 r = client.simulate_get('/things') assert r.status == HTTP_200 # due to the 1 second default limit on the default limiter r = client.simulate_get('/things') assert r.status == HTTP_429 sleep(1) r = client.simulate_get('/things') assert r.status == HTTP_200
def test_reverse_proxies(): """ Test using a custom key_func - one which you would use to handle reverse proxies """ def get_access_route_addr(req, resp, resource, params) -> str: """ Returns the remote address from the access_routes list discounting 1 reverse proxy """ return req.access_route[-3] limiter = Limiter( key_func=get_access_route_addr, default_limits=["10 per hour", "1 per second"] ) @limiter.limit() class ThingsResource: def on_get(self, req, resp): resp.body = 'Hello world!' # two different source IPs through 1 reverse proxy: ip1_header = {"X-FORWARDED-FOR": "10.0.0.1, 1.2.3.4"} ip2_header = {"X-FORWARDED-FOR": "10.0.0.2, 1.2.3.4"} app = API(middleware=limiter.middleware) app.add_route('/things', ThingsResource()) client = testing.TestClient(app) r = client.simulate_get('/things', headers=ip1_header) assert r.status == HTTP_200 # the same IP is denied r = client.simulate_get('/things', headers=ip1_header) assert r.status == HTTP_429 # but a different IP can still access it r = client.simulate_get('/things', headers=ip2_header) assert r.status == HTTP_200
@property def middleware(self): return middlewares.FalconCacheMiddleware(self.cache, self.config) app_cache = Cache( config={ 'CACHE_TYPE': 'redis', 'CACHE_EVICTION_STRATEGY': 'time-based', 'CACHE_KEY_PREFIX': 'falcon-cache', 'CACHE_REDIS_URL': settings.REDIS_URL, }) # https://falcon-caching.readthedocs.io/en/stable/ limiter = Limiter(key_func=get_limiter_key, default_limits=settings.FALCON_LIMITER_DEFAULT_LIMITS, config={ 'RATELIMIT_KEY_PREFIX': 'falcon-limiter', 'RATELIMIT_STORAGE_URL': settings.REDIS_URL, }) def get_api_app(): from mcod.routes import routes os.environ.setdefault("COMPONENT", "api") _middlewares = [ middlewares.ContentTypeMiddleware(), middlewares.DebugMiddleware(), middlewares.LocaleMiddleware(), middlewares.ApiVersionMiddleware(), middlewares.CounterMiddleware(),
def limiter(request): """ Create a basic limiter """ limiter = Limiter(key_func=get_remote_addr, default_limits=["10 per hour", "1 per second"]) return limiter