initiales bauen mit mistral
This commit is contained in:
1 parent
7e2e8bdf59
commit
c74bca1e13
22 files changed
+5642
No files matched your search
@@ -0,0 +1,225 @@
|
||||
# ==============================================================================
|
||||
# Formeleon - Multi-tenant Form Generator
|
||||
# Author: Björn Runden <br@runden.net>
|
||||
# Module: services/recaptcha.py
|
||||
# Description: reCAPTCHA v3 validation service with development mode mocking.
|
||||
# Provides score-based spam protection for form submissions.
|
||||
# ==============================================================================
|
||||
|
||||
"""
|
||||
reCAPTCHA v3 validation service for Formeleon.
|
||||
|
||||
This module provides:
|
||||
- reCAPTCHA token verification against Google's API
|
||||
- Development mode mocking with configurable scores
|
||||
- Score threshold configuration
|
||||
- Integration with Flask application context
|
||||
|
||||
In development mode (DEV_MODE=true), the service returns mock scores
|
||||
without contacting Google's API, allowing local testing without API keys.
|
||||
|
||||
Usage:
|
||||
from services.recaptcha import RecaptchaService
|
||||
|
||||
# In a route handler:
|
||||
recaptcha_service = RecaptchaService(current_app._get_current_object())
|
||||
is_valid, score = recaptcha_service.verify(token)
|
||||
|
||||
if is_valid:
|
||||
# Process form submission
|
||||
pass
|
||||
else:
|
||||
# Reject as spam
|
||||
pass
|
||||
"""
|
||||
|
||||
import requests
|
||||
import logging
|
||||
from typing import Optional, Tuple
|
||||
from flask import current_app
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RecaptchaService:
|
||||
"""
|
||||
Service for validating reCAPTCHA v3 tokens.
|
||||
|
||||
This service handles:
|
||||
- Token verification with Google's reCAPTCHA API
|
||||
- Development mode mocking
|
||||
- Score threshold evaluation
|
||||
- Error handling and logging
|
||||
|
||||
Attributes:
|
||||
app: Flask application instance
|
||||
secret_key: reCAPTCHA secret key from configuration
|
||||
site_key: reCAPTCHA site key from configuration
|
||||
threshold: Minimum score threshold for passing validation
|
||||
mock_score: Score to return in development mode
|
||||
verify_url: Google reCAPTCHA verification endpoint
|
||||
"""
|
||||
|
||||
def __init__(self, app=None):
|
||||
"""
|
||||
Initialize the reCAPTCHA service.
|
||||
|
||||
:param app: Flask application instance (optional, can use current_app)
|
||||
"""
|
||||
self.app = app or current_app._get_current_object()
|
||||
self.secret_key = self.app.config.get('RECAPTCHA_SECRET_KEY')
|
||||
self.site_key = self.app.config.get('RECAPTCHA_SITE_KEY')
|
||||
self.threshold = float(self.app.config.get('RECAPTCHA_THRESHOLD', 0.5))
|
||||
self.mock_score = float(self.app.config.get('MOCK_RECAPTCHA_SCORE', 0.9))
|
||||
self.verify_url = self.app.config.get(
|
||||
'RECAPTCHA_VERIFY_URL',
|
||||
'https://www.google.com/recaptcha/api/siteverify'
|
||||
)
|
||||
self.timeout = 5 # seconds for API requests
|
||||
|
||||
def is_enabled(self) -> bool:
|
||||
"""
|
||||
Check if reCAPTCHA is enabled (keys configured).
|
||||
|
||||
:return: True if both secret and site keys are configured
|
||||
"""
|
||||
return (self.secret_key is not None and
|
||||
self.site_key is not None)
|
||||
|
||||
def is_dev_mode(self) -> bool:
|
||||
"""
|
||||
Check if application is in development mode.
|
||||
|
||||
:return: True if DEV_MODE is enabled in configuration
|
||||
"""
|
||||
return self.app.config.get('DEV_MODE', False)
|
||||
|
||||
def verify(self, token: str, remote_ip: Optional[str] = None) -> Tuple[bool, Optional[float]]:
|
||||
"""
|
||||
Verify a reCAPTCHA v3 token.
|
||||
|
||||
:param token: reCAPTCHA response token from client
|
||||
:param remote_ip: Optional client IP address for verification
|
||||
:return: Tuple of (is_valid, score) where is_valid indicates if score >= threshold
|
||||
"""
|
||||
if not token:
|
||||
logger.warning('reCAPTCHA verification: No token provided')
|
||||
return False, None
|
||||
|
||||
# Development mode: return mock score
|
||||
if self.is_dev_mode():
|
||||
logger.debug(f'reCAPTCHA dev mode: Returning mock score {self.mock_score}')
|
||||
return self.mock_score >= self.threshold, self.mock_score
|
||||
|
||||
# Production mode: verify with Google API
|
||||
if not self.is_enabled():
|
||||
logger.warning('reCAPTCHA verification: Service not configured (missing keys)')
|
||||
# In production, if not configured, we might want to fail-open or fail-closed
|
||||
# For security, we fail-closed (treat as spam)
|
||||
return False, None
|
||||
|
||||
return self._verify_with_google(token, remote_ip)
|
||||
|
||||
def _verify_with_google(self, token: str, remote_ip: Optional[str] = None) -> Tuple[bool, Optional[float]]:
|
||||
"""
|
||||
Verify token with Google's reCAPTCHA API.
|
||||
|
||||
:param token: reCAPTCHA response token
|
||||
:param remote_ip: Client IP address
|
||||
:return: Tuple of (is_valid, score)
|
||||
"""
|
||||
try:
|
||||
data = {
|
||||
'secret': self.secret_key,
|
||||
'response': token
|
||||
}
|
||||
if remote_ip:
|
||||
data['remoteip'] = remote_ip
|
||||
|
||||
logger.debug(f'reCAPTCHA: Verifying token with Google API')
|
||||
|
||||
response = requests.post(
|
||||
self.verify_url,
|
||||
data=data,
|
||||
timeout=self.timeout
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
result = response.json()
|
||||
|
||||
if not result.get('success', False):
|
||||
error_codes = result.get('error-codes', [])
|
||||
logger.warning(f'reCAPTCHA verification failed: {error_codes}')
|
||||
return False, None
|
||||
|
||||
score = float(result.get('score', 0))
|
||||
action = result.get('action', 'unknown')
|
||||
|
||||
logger.debug(f'reCAPTCHA: Score={score}, Action={action}')
|
||||
|
||||
return score >= self.threshold, score
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
logger.error('reCAPTCHA: Verification request timed out')
|
||||
# Fail-safe: allow submission on timeout to avoid blocking legitimate users
|
||||
return True, None
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f'reCAPTCHA: Verification request failed: {e}')
|
||||
# Fail-safe: allow submission on network errors
|
||||
return True, None
|
||||
except (ValueError, TypeError, KeyError) as e:
|
||||
logger.error(f'reCAPTCHA: Invalid response format: {e}')
|
||||
return False, None
|
||||
|
||||
def get_site_key(self) -> str:
|
||||
"""
|
||||
Get the reCAPTCHA site key for client-side rendering.
|
||||
|
||||
:return: Site key string
|
||||
"""
|
||||
return self.site_key or ''
|
||||
|
||||
def get_threshold(self) -> float:
|
||||
"""
|
||||
Get the current score threshold.
|
||||
|
||||
:return: Threshold value
|
||||
"""
|
||||
return self.threshold
|
||||
|
||||
def get_dev_mode_score(self) -> float:
|
||||
"""
|
||||
Get the mock score used in development mode.
|
||||
|
||||
:return: Mock score value
|
||||
"""
|
||||
return self.mock_score
|
||||
|
||||
|
||||
# Singleton instance for convenience (can be used when app context is available)
|
||||
_recaptcha_service: Optional[RecaptchaService] = None
|
||||
|
||||
|
||||
def get_recaptcha_service() -> RecaptchaService:
|
||||
"""
|
||||
Get the reCAPTCHA service instance.
|
||||
|
||||
Creates a new instance on first call, then returns the same instance.
|
||||
|
||||
:return: RecaptchaService instance
|
||||
"""
|
||||
global _recaptcha_service
|
||||
if _recaptcha_service is None:
|
||||
_recaptcha_service = RecaptchaService()
|
||||
return _recaptcha_service
|
||||
|
||||
|
||||
def reset_recaptcha_service() -> None:
|
||||
"""
|
||||
Reset the reCAPTCHA service singleton.
|
||||
|
||||
Useful for testing or when configuration changes.
|
||||
"""
|
||||
global _recaptcha_service
|
||||
_recaptcha_service = None
|
||||
Reference in new issue
Block a user