Skip to content

Commit

Permalink
API: add rate-limits by IP, add configurable rate-limit strategy
Browse files Browse the repository at this point in the history
  • Loading branch information
Giacomo Licari committed Nov 25, 2023
1 parent 96314f3 commit d67c6d2
Show file tree
Hide file tree
Showing 8 changed files with 228 additions and 130 deletions.
20 changes: 14 additions & 6 deletions api/api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from web3 import Web3
from web3.middleware import construct_sign_and_send_raw_middleware

from .services import Token, Cache, claim_native, claim_token, captcha_verify
from .services import Token, Cache, Strategy, claim_native, claim_token, captcha_verify


def is_token_enabled(address, tokens_list):
Expand All @@ -31,7 +31,7 @@ def create_app():
w3 = Web3(Web3.HTTPProvider(app.config['FAUCET_RPC_URL']))
w3.middleware_onion.add(construct_sign_and_send_raw_middleware(app.config['FAUCET_PRIVATE_KEY']))

cache = Cache(app.config['FAUCET_TIME_LIMIT_SECONDS'])
cache = Cache(app.config['FAUCET_RATE_LIMIT_TIME_LIMIT_SECONDS'])

# Set logger
logging.basicConfig(level=logging.INFO)
Expand Down Expand Up @@ -81,7 +81,7 @@ def ask():
if not w3.is_address(recipient):
validation_errors.append('recipient: A valid recipient address must be specified')

if recipient.lower() == app.config['FAUCET_ADDRESS']:
if not recipient or recipient.lower() == app.config['FAUCET_ADDRESS']:
validation_errors.append('recipient: address cant\'t be the Faucet address itself')

token_address = request_data.get('tokenAddress', None)
Expand All @@ -101,9 +101,17 @@ def ask():
if len(validation_errors) > 0:
return jsonify(errors=validation_errors), 400

# Check last claim
if cache.limit_by_address(recipient):
return jsonify(errors=['recipient: you have exceeded the limit for today. Try again in %s hours' % cache.ttl(hours=True)]), 429
if app.config['FAUCET_RATE_LIMIT_STRATEGY'].strategy == Strategy.address.value:
# Check last claim
if cache.limit_by_address(recipient):
return jsonify(errors=['recipient: you have exceeded the limit for today. Try again in %s hours' % cache.ttl(hours=True)]), 429
elif app.config['FAUCET_RATE_LIMIT_STRATEGY'].strategy == Strategy.ip.value:
ip_address = request.environ.get('HTTP_X_FORWARDED_FOR', request.remote_addr)
# Check last claim for the IP address
if cache.limit_by_ip(ip_address):
return jsonify(errors=['recipient: you have exceeded the limit for today. Try again in %s hours' % cache.ttl(hours=True)]), 429
elif app.config['FAUCET_RATE_LIMIT_STRATEGY'].strategy == Strategy.ip_and_address:
raise NotImplemented

amount_wei = w3.to_wei(amount, 'ether')
try:
Expand Down
3 changes: 2 additions & 1 deletion api/api/services/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .token import Token
from .cache import Cache
from .transaction import claim_native, claim_token
from .captcha import captcha_verify
from .captcha import captcha_verify
from .rate_limit import RateLimitStrategy, Strategy
6 changes: 6 additions & 0 deletions api/api/services/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ def limit_by_address(self, address):
self.cache[address] = datetime.now()
return cached

def limit_by_ip(self, ip):
cached = self.cache.get(ip, False)
if not cached:
self.cache[ip] = datetime.now()
return cached

def delete(self, attr):
self.cache.pop(attr)

Expand Down
27 changes: 27 additions & 0 deletions api/api/services/rate_limit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from enum import Enum

class Strategy(Enum):
ip = 'IP'
address = 'ADDRESS'
ip_and_address = 'IP_AND_ADDRESS'


class RateLimitStrategy:
_strategies = set([Strategy.ip.value, Strategy.address.value, Strategy.ip_and_address.value])
_strategy = None
_default_strategy = Strategy.address.value


@property
def default_strategy(self):
return self._default_strategy

@property
def strategy(self):
return self._strategy

@strategy.setter
def strategy(self, value):
if value not in self._strategies:
raise ValueError('Invalid strategy value', value, 'Expected one of', self._strategies)
self._strategy = value
13 changes: 11 additions & 2 deletions api/api/settings.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
import os
import json

from .services import RateLimitStrategy

from dotenv import load_dotenv
from eth_account import Account
from eth_account.signers.local import LocalAccount


load_dotenv()

rate_limit_strategy = RateLimitStrategy()
rate_limit_strategy.strategy = os.getenv('FAUCET_RATE_LIMIT_STRATEGY', default=rate_limit_strategy.default_strategy)

FAUCET_RPC_URL = os.getenv("FAUCET_RPC_URL")
FAUCET_PRIVATE_KEY = os.environ.get("FAUCET_PRIVATE_KEY")
FAUCET_CHAIN_ID=os.getenv('FAUCET_CHAIN_ID')
FAUCET_CHAIN_NATIVE_TOKEN_SYMBOL=os.getenv('FAUCET_CHAIN_NATIVE_TOKEN_SYMBOL', 'xDAI')
FAUCET_CHAIN_NATIVE_TOKEN_SYMBOL=os.getenv('FAUCET_CHAIN_NATIVE_TOKEN_SYMBOL', default='xDAI')
FAUCET_ENABLED_TOKENS=json.loads(os.getenv('FAUCET_ENABLED_TOKENS', default='[]'))
FAUCET_AMOUNT=float(os.getenv('FAUCET_AMOUNT'))
FAUCET_ADDRESS: LocalAccount = Account.from_key(FAUCET_PRIVATE_KEY).address
FAUCET_TIME_LIMIT_SECONDS=seconds=os.getenv('FAUCET_TIME_LIMIT_SECONDS', 86400) # 86400 = 24h
FAUCET_RATE_LIMIT_STRATEGY=rate_limit_strategy
FAUCET_RATE_LIMIT_TIME_LIMIT_SECONDS=seconds=os.getenv('FAUCET_RATE_LIMIT_TIME_LIMIT_SECONDS', 86400) # 86400 = 24h

CORS_ALLOWED_ORIGINS=os.getenv('CORS_ALLOWED_ORIGINS', '*')

CAPTCHA_VERIFY_ENDPOINT=os.getenv('CAPTCHA_VERIFY_ENDPOINT')
CAPTCHA_SECRET_KEY=os.getenv('CAPTCHA_SECRET_KEY')
23 changes: 1 addition & 22 deletions api/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,22 +1 @@
import pytest
from api import create_app
from temp_env_var import TEMP_ENV_VARS, NATIVE_TRANSFER_TX_HASH, TOKEN_TRANSFER_TX_HASH

api_prefix = '/api/v1'


@pytest.fixture
def app(mocker):
# Mock values
mocker.patch('api.api.claim_native', return_value=NATIVE_TRANSFER_TX_HASH)
mocker.patch('api.api.claim_token', return_value=TOKEN_TRANSFER_TX_HASH)
# Instantiate app
app = create_app()
# Override configs
app.config.update(TEMP_ENV_VARS)

yield app

@pytest.fixture
def client(app):
return app.test_client()
api_prefix = '/api/v1'
2 changes: 1 addition & 1 deletion api/tests/temp_env_var.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
'FAUCET_CHAIN_ID': '100000',
'FAUCET_PRIVATE_KEY': token_bytes(32).hex(),
'FAUCET_AMOUNT': 0.1,
'FAUCET_TIME_LIMIT_SECONDS': '1',
'FAUCET_RATE_LIMIT_TIME_LIMIT_SECONDS': '1',
'FAUCET_ENABLED_TOKENS': json.loads('[{"address":"' + ZERO_ADDRESS + '", "name": "TestToken"}]'),
'CAPTCHA_SECRET_KEY': CAPTCHA_TEST_SECRET_KEY
}
Expand Down
Loading

0 comments on commit d67c6d2

Please sign in to comment.