Feat: Update CAPTCHA service to ALTCHA self-hosted.

This commit is contained in:
2025-03-15 17:47:36 +00:00
parent b843849af9
commit bade1f11dd
10 changed files with 3823 additions and 153 deletions

View File

@@ -14,65 +14,66 @@ Defines Flask-WTF form for handling user input on Contact Us page.
# internal
# from business_objects.store.product_category import Filters_Product_Category # circular
# from models.model_view_store import Model_View_Store # circular
from models.model_view_base import Model_View_Base
from forms.base import Form_Base
# external
from flask import Flask, render_template, request, flash, redirect, url_for, current_app
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, SubmitField, HiddenField, BooleanField
from wtforms import StringField, TextAreaField, SubmitField, HiddenField, BooleanField, Field
from wtforms.validators import DataRequired, Email, ValidationError
import markupsafe
from flask_wtf.recaptcha import RecaptchaField
from abc import ABCMeta, abstractmethod
import requests
import json
import hmac
import hashlib
from altcha import verify_solution
import base64
import urllib.parse
"""
def validate_altcha(form, field):
if not field.data:
raise ValidationError('Please complete the ALTCHA challenge')
try:
# Decode the base64-encoded payload
payload_json = base64.b64decode(field.data).decode('utf-8')
payload = json.loads(payload_json)
class ALTCHAValidator:
def __init__(self, message=None):
self.message = message or 'ALTCHA verification failed'
# Verify ALTCHA response
if not payload.get('verified', False):
raise ValidationError('ALTCHA verification failed')
def __call__(self, form, field):
altcha_data = field.data
# Verify signature
verification_data = payload.get('verificationData', '')
received_signature = payload.get('signature', '')
algorithm = payload.get('algorithm', 'SHA-256')
if not altcha_data:
raise ValidationError(self.message)
# Calculate the hash of verification data
verification_hash = hashlib.sha256(verification_data.encode()).digest()
# Calculate HMAC signature
hmac_key = current_app.config['ALTCHA_SECRET_KEY'].encode()
calculated_signature = hmac.new(
hmac_key,
verification_hash,
getattr(hashlib, algorithm.lower().replace('-', ''))
).hexdigest()
if calculated_signature != received_signature:
raise ValidationError('Invalid ALTCHA signature')
try:
# The data is base64 encoded JSON
try:
# First try to decode it as JSON directly (if it's not base64 encoded)
altcha_payload = json.loads(altcha_data)
except json.JSONDecodeError:
# If direct JSON decoding fails, try base64 decoding first
decoded_data = base64.b64decode(altcha_data).decode('utf-8')
altcha_payload = json.loads(decoded_data)
ok, err = verify_solution(altcha_payload, current_app.config["ALTCHA_SECRET_KEY"], check_expires=True)
# Optional: If using the spam filter, you could parse verification_data
# and reject submissions classified as spam
# Example:
parsed_data = dict(urllib.parse.parse_qsl(verification_data))
if parsed_data.get('classification') == 'BAD':
raise ValidationError('This submission was classified as spam')
except Exception as e:
current_app.logger.error(f"ALTCHA validation error: {str(e)}")
raise ValidationError('ALTCHA validation failed')
"""
if err or not ok:
raise ValidationError(self.message + ': ' + (err or 'Invalid solution'))
except Exception as e:
raise ValidationError(f'Invalid ALTCHA data: {str(e)}')
class ALTCHAField(Field):
def __init__(self, label='', validators=None, **kwargs):
validators = validators or []
validators.append(ALTCHAValidator())
super(ALTCHAField, self).__init__(label, validators, **kwargs)
def __call__(self, **kwargs):
html = f"""
<altcha-widget
challengeurl="/get-challenge"
auto="onload"
id="{self.id}"
name="{self.name}">
</altcha-widget>
"""
return markupsafe.Markup(html)
class Form_Contact(FlaskForm):
email = StringField('Email')
@@ -81,5 +82,6 @@ class Form_Contact(FlaskForm):
message = TextAreaField('Message')
receive_marketing = BooleanField('I would like to receive marketing emails.')
# recaptcha = RecaptchaField()
altcha = HiddenField('ALTCHA') # , validators=[validate_altcha]
# altcha = HiddenField('ALTCHA') # , validators=[validate_altcha]
altcha = ALTCHAField('Verification')
submit = SubmitField('Send Message')