Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Cloudflare Turnstile #52

Merged
merged 8 commits into from
Dec 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 23 additions & 23 deletions .github/workflows/publish-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -77,28 +77,28 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

eks-deployment-restart:
# Run job on branch dev only
if: github.ref == 'refs/heads/dev'
runs-on: ubuntu-latest
needs: build-and-push-image
permissions:
id-token: write # Required for the OIDC, see https://github.com/aws-actions/configure-aws-credentials?tab=readme-ov-file#OIDC
contents: read
steps:
- name: configure aws credentials
uses: aws-actions/[email protected]
with:
audience: sts.amazonaws.com
role-to-assume: ${{ secrets.DEV_AWS_EKS_ROLE }}
role-session-name: GitHub_to_AWS_via_FederatedOIDC
aws-region: ${{ secrets.DEV_AWS_REGION }}
# eks-deployment-restart:
# # Run job on branch dev only
# if: github.ref == 'refs/heads/dev'
# runs-on: ubuntu-latest
# needs: build-and-push-image
# permissions:
# id-token: write # Required for the OIDC, see https://github.com/aws-actions/configure-aws-credentials?tab=readme-ov-file#OIDC
# contents: read
# steps:
# - name: configure aws credentials
# uses: aws-actions/[email protected]
# with:
# audience: sts.amazonaws.com
# role-to-assume: ${{ secrets.DEV_AWS_EKS_ROLE }}
# role-session-name: GitHub_to_AWS_via_FederatedOIDC
# aws-region: ${{ secrets.DEV_AWS_REGION }}

- name: Configure kubectl for EKS
run: aws eks update-kubeconfig --name ${{ secrets.DEV_AWS_EKS_CLUSTER }} --region ${{ secrets.DEV_AWS_REGION }}
# - name: Configure kubectl for EKS
# run: aws eks update-kubeconfig --name ${{ secrets.DEV_AWS_EKS_CLUSTER }} --region ${{ secrets.DEV_AWS_REGION }}

- name: Restart Bridge Explorer Deployment
if: github.ref == 'refs/heads/dev'
run: |
kubectl config use-context arn:aws:eks:${{ secrets.DEV_AWS_REGION }}:${{ secrets.DEV_AWS_ACCOUNT_ID }}:cluster/${{ secrets.DEV_AWS_EKS_CLUSTER }}
kubectl rollout restart deploy/${{ secrets.DEV_AWS_EKS_DEPLOYMENT_API }} -n ${{ secrets.DEV_AWS_EKS_NAMESPACE }}
# - name: Restart Deployment
# if: github.ref == 'refs/heads/dev'
# run: |
# kubectl config use-context arn:aws:eks:${{ secrets.DEV_AWS_REGION }}:${{ secrets.DEV_AWS_ACCOUNT_ID }}:cluster/${{ secrets.DEV_AWS_EKS_CLUSTER }}
# kubectl rollout restart deploy/${{ secrets.DEV_AWS_EKS_DEPLOYMENT_API }} -n ${{ secrets.DEV_AWS_EKS_NAMESPACE }}
52 changes: 26 additions & 26 deletions .github/workflows/publish-ui.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
"REACT_APP_HCAPTCHA_SITE_KEY=${{ secrets.DEV_REACT_APP_HCAPTCHA_SITE_KEY }}"
"REACT_APP_CAPTCHA_SITE_KEY=${{ secrets.DEV_REACT_APP_CAPTCHA_SITE_KEY }}"
"REACT_APP_FAUCET_API_URL=${{ secrets.DEV_REACT_APP_FAUCET_API_URL}}"

- name: Gnosis Chain - Main branch / tags - Build and push Docker image
Expand All @@ -67,7 +67,7 @@ jobs:
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-gc
labels: ${{ steps.meta.outputs.labels }}
build-args: |
"REACT_APP_HCAPTCHA_SITE_KEY=${{ secrets.PROD_GC_REACT_APP_HCAPTCHA_SITE_KEY }}"
"REACT_APP_CAPTCHA_SITE_KEY=${{ secrets.PROD_GC_REACT_APP_CAPTCHA_SITE_KEY }}"
"REACT_APP_FAUCET_API_URL=${{ secrets.PROD_GC_REACT_APP_FAUCET_API_URL}}"

- name: Chiado Chain - Main branch / tags - Build and push Docker image
Expand All @@ -79,31 +79,31 @@ jobs:
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-chiado
labels: ${{ steps.meta.outputs.labels }}
build-args: |
"REACT_APP_HCAPTCHA_SITE_KEY=${{ secrets.PROD_CHIADO_REACT_APP_HCAPTCHA_SITE_KEY }}"
"REACT_APP_CAPTCHA_SITE_KEY=${{ secrets.PROD_CHIADO_REACT_APP_CAPTCHA_SITE_KEY }}"
"REACT_APP_FAUCET_API_URL=${{ secrets.PROD_CHIADO_REACT_APP_FAUCET_API_URL}}"

eks-deployment-restart:
# Run job on branch dev only
if: github.ref == 'refs/heads/dev'
runs-on: ubuntu-latest
needs: build-and-push-image
permissions:
id-token: write # Required for the OIDC, see https://github.com/aws-actions/configure-aws-credentials?tab=readme-ov-file#OIDC
contents: read
steps:
- name: configure aws credentials
uses: aws-actions/[email protected]
with:
audience: sts.amazonaws.com
role-to-assume: ${{ secrets.DEV_AWS_EKS_ROLE }}
role-session-name: GitHub_to_AWS_via_FederatedOIDC
aws-region: ${{ secrets.DEV_AWS_REGION }}
# eks-deployment-restart:
# # Run job on branch dev only
# if: github.ref == 'refs/heads/dev'
# runs-on: ubuntu-latest
# needs: build-and-push-image
# permissions:
# id-token: write # Required for the OIDC, see https://github.com/aws-actions/configure-aws-credentials?tab=readme-ov-file#OIDC
# contents: read
# steps:
# - name: configure aws credentials
# uses: aws-actions/[email protected]
# with:
# audience: sts.amazonaws.com
# role-to-assume: ${{ secrets.DEV_AWS_EKS_ROLE }}
# role-session-name: GitHub_to_AWS_via_FederatedOIDC
# aws-region: ${{ secrets.DEV_AWS_REGION }}

- name: Configure kubectl for EKS
run: aws eks update-kubeconfig --name ${{ secrets.DEV_AWS_EKS_CLUSTER }} --region ${{ secrets.DEV_AWS_REGION }}
# - name: Configure kubectl for EKS
# run: aws eks update-kubeconfig --name ${{ secrets.DEV_AWS_EKS_CLUSTER }} --region ${{ secrets.DEV_AWS_REGION }}

- name: Restart Bridge Explorer Deployment
if: github.ref == 'refs/heads/dev'
run: |
kubectl config use-context arn:aws:eks:${{ secrets.DEV_AWS_REGION }}:${{ secrets.DEV_AWS_ACCOUNT_ID }}:cluster/${{ secrets.DEV_AWS_EKS_CLUSTER }}
kubectl rollout restart deploy/${{ secrets.DEV_AWS_EKS_DEPLOYMENT_UI }} -n ${{ secrets.DEV_AWS_EKS_NAMESPACE }}
# - name: Restart Deployment
# if: github.ref == 'refs/heads/dev'
# run: |
# kubectl config use-context arn:aws:eks:${{ secrets.DEV_AWS_REGION }}:${{ secrets.DEV_AWS_ACCOUNT_ID }}:cluster/${{ secrets.DEV_AWS_EKS_CLUSTER }}
# kubectl rollout restart deploy/${{ secrets.DEV_AWS_EKS_DEPLOYMENT_UI }} -n ${{ secrets.DEV_AWS_EKS_NAMESPACE }}
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ python3 -m flake8
#### Add enabled tokens

To enable tokens on the API just run the command `create_enabled_token`.
The environment variable `FAUCET_ENABLE_CLI_API` must be set to `True`.
Accepted parameters: token name, chain ID, token address, maximum amount per day per user, whether native or erc20

Samples below:
Expand Down
6 changes: 4 additions & 2 deletions api/.env.example
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
FAUCET_AMOUNT=0.1
FAUCET_AMOUNT=0.001
FAUCET_PRIVATE_KEY=0x0000000000000000000000000000000000000000000000000000000000000000
FAUCET_RPC_URL=https://rpc.chiadochain.net
FAUCET_CHAIN_ID=10200
FAUCET_DATABASE_URI=sqlite://
CAPTCHA_VERIFY_ENDPOINT=https://api.hcaptcha.com/siteverify
CAPTCHA_SECRET_KEY=0x0000000000000000000000000000000000000000
CAPTCHA_SITE_KEY=xxxxx-xxxxx-xxxxx-xxxxx
CAPTCHA_SITE_KEY=xxxxx-xxxxx-xxxxx-xxxxx
CSRF_PRIVATE_KEY="!!CREATE_YOUR_RSA_PRIVATE_KEY!!"
CSRF_SECRET_SALT="test-salt"
4 changes: 4 additions & 0 deletions api/api/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
claim_token)
from .services.database import AccessKey, Token, Transaction


apiv1 = Blueprint("version1", "version1")


Expand Down Expand Up @@ -116,6 +117,9 @@ def ask():

@apiv1.route("/cli/ask", methods=["POST"])
def cli_ask():
if not current_app.config['FAUCET_ENABLE_CLI_API']:
return jsonify(errors=['Endpoint disabled']), 403

access_key_id = request.headers.get('X-faucet-access-key-id', None)
secret_access_key = request.headers.get('X-faucet-secret-access-key', None)

Expand Down
1 change: 1 addition & 0 deletions api/api/services/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
from .token import Token
from .transaction import Web3Singleton, claim_native, claim_token
from .validator import AskEndpointValidator
from .captcha import CaptchaSingleton
72 changes: 59 additions & 13 deletions api/api/services/captcha.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,62 @@
logging.basicConfig(level=logging.INFO)


def captcha_verify(client_response, catpcha_api_url, secret_key, remote_ip, site_key):
request = requests.post(catpcha_api_url, data={
'response': client_response,
'secret': secret_key,
'remoteip': remote_ip,
'sitekey': site_key
})

logging.info('Captcha verify response: %s' % request.json())

if request.status_code != 200:
return False
return request.json()['success'] == True
class Captcha:
def __init__(self, provider):
self.provider = provider

def verify(self, client_response, catpcha_api_url, secret_key, remote_ip, site_key=None):
logging.info('Captcha: Remote IP %s' % remote_ip)

if self.provider == 'HCAPTCHA':
request = requests.post(catpcha_api_url, data={
'response': client_response,
'secret': secret_key,
'remoteip': remote_ip,
'sitekey': site_key
})

logging.info('Captcha: verify response %s' % request.json())

if request.status_code != 200:
return False
return request.json()['success'] is True
elif self.provider == 'CLOUDFLARE':
request = requests.post(catpcha_api_url, data={
'response': client_response,
'secret': secret_key,
'remoteip': remote_ip
})

logging.info('Captcha: verify response %s' % request.json())

if request.status_code != 200:
return False
return request.json()['success'] is True
else:
raise NotImplementedError


class CaptchaSingleton:
_instance = None

def __new__(cls, provider):
if not hasattr(cls, 'instance'):
cls.instance = Captcha(provider)
return cls.instance


# def captcha_verify(client_response, catpcha_api_url, secret_key, remote_ip, site_key):
# logging.info('Captcha: Remote IP %s' % remote_ip)
# request = requests.post(catpcha_api_url, data={
# 'response': client_response,
# 'secret': secret_key,
# 'remoteip': remote_ip,
# 'sitekey': site_key
# })

# logging.info('Captcha: verify response %s' % request.json())

# if request.status_code != 200:
# return False
# return request.json()['success'] == True
13 changes: 7 additions & 6 deletions api/api/services/csrf.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
from Crypto.Cipher import PKCS1_OAEP
from Crypto.PublicKey import RSA

# Waiting period: the minimum time interval between UI asks for the CSFR token
# and the time it asks for funds.
CSRF_TIMESTAMP_MIN_SECONDS = 15
# Waiting period: the minimum time interval between the UI asks
# for the CSFR token to /api/v1/info and the time the UI can ask for funds.
# This check aims to block any bots that could be triggering actions through the UI.
CSRF_TIMESTAMP_MIN_SECONDS = 5


class CSRFTokenItem:
Expand Down Expand Up @@ -45,9 +46,9 @@ def validate_token(self, request_id, token, timestamp):
decrypted_text = cipher_rsa.decrypt(bytes.fromhex(token)).decode()
expected_text = '%s%s%f' % (request_id, self._salt, timestamp)
if decrypted_text == expected_text:
# Check that timestamp is OK, the diff between now() and creation time in seconds
# must be greater than min. waiting period.
# Waiting period: the minimum time interval between UI asks for the CSFR token and the time it asks for funds.
# Check that the timestamp is OK, the diff between now() and creation time in seconds
# must be greater than the minimum waiting period.
# Waiting period: the minimum time interval between UI asks for the CSFR token and the time the UI can ask for funds.
seconds_diff = (datetime.now()-datetime.fromtimestamp(timestamp)).total_seconds()
if seconds_diff > CSRF_TIMESTAMP_MIN_SECONDS:
return True
Expand Down
15 changes: 12 additions & 3 deletions api/api/services/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from flask import current_app, request
from web3 import Web3

from .captcha import captcha_verify
from .captcha import CaptchaSingleton
from .csrf import CSRF
from .database import AccessKeyConfig, BlockedUsers, Token, Transaction
from .rate_limit import Strategy
Expand Down Expand Up @@ -140,14 +140,23 @@ def data_validation(self):

def captcha_validation(self):
error_key = 'captcha'
# check hcatpcha
catpcha_verified = captcha_verify(

captcha = CaptchaSingleton(current_app.config['CAPTCHA_PROVIDER'])
catpcha_verified = captcha.verify(
self.request_data.get('captcha'),
current_app.config['CAPTCHA_VERIFY_ENDPOINT'],
current_app.config['CAPTCHA_SECRET_KEY'],
self.ip_address,
current_app.config['CAPTCHA_SITE_KEY']
)
# check hcatpcha
# catpcha_verified = captcha_verify(
# self.request_data.get('captcha'),
# current_app.config['CAPTCHA_VERIFY_ENDPOINT'],
# current_app.config['CAPTCHA_SECRET_KEY'],
# self.ip_address,
# current_app.config['CAPTCHA_SITE_KEY']
# )

if not catpcha_verified:
self.errors.append('%s: validation failed' % error_key)
Expand Down
6 changes: 5 additions & 1 deletion api/api/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,18 @@
FAUCET_ADDRESS: LocalAccount = Account.from_key(FAUCET_PRIVATE_KEY).address
FAUCET_RATE_LIMIT_STRATEGY = rate_limit_strategy
FAUCET_RATE_LIMIT_TIME_LIMIT_SECONDS = int(os.getenv('FAUCET_RATE_LIMIT_TIME_LIMIT_SECONDS', 86400)) # 86400 = 24h
FAUCET_ENABLE_CLI_API = os.getenv('FAUCET_ENABLE_CLI_API', "False") == "True"

SQLALCHEMY_DATABASE_URI = os.getenv('FAUCET_DATABASE_URI')

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

CAPTCHA_PROVIDER = os.getenv('CAPTCHA_PROVIDER', 'CLOUDFLARE')
CAPTCHA_VERIFY_ENDPOINT = os.getenv('CAPTCHA_VERIFY_ENDPOINT')
CAPTCHA_SECRET_KEY = os.getenv('CAPTCHA_SECRET_KEY')
CAPTCHA_SITE_KEY = os.getenv('CAPTCHA_SITE_KEY')
CAPTCHA_SITE_KEY = os.getenv('CAPTCHA_SITE_KEY', None) # It's mandatory for HCAPTCHA
if CAPTCHA_PROVIDER == 'HCAPTCHA' and CAPTCHA_SITE_KEY is None:
raise ValueError('CAPTCHA_SITE_KEY is mandatory for HCAPTCHA')

CSRF_PRIVATE_KEY = os.getenv('CSRF_PRIVATE_KEY')
CSRF_SECRET_SALT = os.getenv('CSRF_SECRET_SALT')
11 changes: 8 additions & 3 deletions api/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

from api.services import CSRF, Strategy
from api.services.database import Token, db
from flask.testing import FlaskClient

from api import create_app

Expand All @@ -25,13 +24,19 @@ def mock_claim_erc20(self, *args):
tx_hash = '0x1' + '%d' % self.erc20_tx_counter * 63
self.erc20_tx_counter += 1
return tx_hash

def mock_captcha_verify(self, *args):
class Test:
def verify(self, *args):
return True
return Test

def _mock(self, env_variables=None):
# Mock values
self.patchers = [
mock.patch('api.routes.claim_native', self.mock_claim_native),
mock.patch('api.routes.claim_token', self.mock_claim_erc20),
mock.patch('api.services.validator.captcha_verify', return_value=True),
mock.patch('api.services.validator.CaptchaSingleton', self.mock_captcha_verify),
mock.patch('api.api.print_info', return_value=None)
]
if env_variables:
Expand Down Expand Up @@ -133,4 +138,4 @@ def setUp(self):
self.csrf = CSRF.instance
# use same token for the whole test
# use a timestamp that would be actually validated by the CSRF class.
self.csrf_token = self.csrf.generate_token(timestamp=self.valid_csrf_timestamp)
self.csrf_token = self.csrf.generate_token(timestamp=self.valid_csrf_timestamp)
1 change: 1 addition & 0 deletions api/tests/temp_env_var.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
'FAUCET_RATE_LIMIT_TIME_LIMIT_SECONDS': '10',
'FAUCET_DATABASE_URI': 'sqlite://', # run in-memory
# 'FAUCET_DATABASE_URI': 'sqlite:///test.db',
'FAUCET_ENABLE_CLI_API': 'True',
'CAPTCHA_SECRET_KEY': CAPTCHA_TEST_SECRET_KEY,
'CSRF_PRIVATE_KEY': privatekey.export_key().decode(),
'CSRF_SECRET_SALT': 'testsalt'
Expand Down
2 changes: 1 addition & 1 deletion api/tests/test_api_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
ERC20_TOKEN_ADDRESS, FAUCET_CHAIN_ID)


class TestAPICli(BaseTest):
class TestAPICliEnabledEndpoints(BaseTest):
def test_ask_route_parameters(self):
access_key_id, secret_access_key = generate_access_key()
http_headers = {
Expand Down
2 changes: 1 addition & 1 deletion app/.env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
REACT_APP_HCAPTCHA_SITE_KEY=
REACT_APP_CAPTCHA_SITE_KEY=
REACT_APP_FAUCET_API_URL=http://localhost:8000/api/v1
Loading
Loading