Changeset View
Changeset View
Standalone View
Standalone View
bid_main/recaptcha.py
- This file was added.
| """Utilities for handling reCAPTCHA.""" | |||||
| from typing import Tuple, Optional | |||||
| import logging | |||||
| from django.conf import settings | |||||
| from django.http.request import validate_host | |||||
| import requests | |||||
| logger = logging.getLogger(__name__) | |||||
| recaptcha_session = requests.Session() | |||||
| recaptcha_session.mount( | |||||
| 'https://', | |||||
| requests.adapters.HTTPAdapter( | |||||
| max_retries=requests.adapters.Retry(total=10, backoff_factor=0.2), | |||||
| ), | |||||
| ) | |||||
| VERIFY_URL = 'https://www.google.com/recaptcha/api/siteverify' | |||||
| MIN_SCORE = 0.5 | |||||
| msg_recaptcha_unavailable = ( | |||||
| 'There was a communication error checking reCAPTCHA. Please try again later.' | |||||
| ) | |||||
| def check_recaptcha(request, recaptcha_response) -> Tuple[Optional[bool], str]: | |||||
| """Handles recaptcha verification and sets request.recaptcha_is_valid. | |||||
| Be sure to set settings.GOOGLE_RECAPTCHA_SECRET_KEY to a non-empty string, | |||||
| otherwise this function does nothing. | |||||
| :returns: None when no check was performed, or True/False indicating a | |||||
| successful resp. unsuccessful check. | |||||
| """ | |||||
| if not recaptcha_response: | |||||
| logger.warning('reCAPTCHA response not included in request') | |||||
| return ( | |||||
| False, | |||||
| 'reCAPTCHA failed to check your request. Please disable script/ad ' | |||||
| 'blockers and try again.', | |||||
| ) | |||||
| data = {'secret': settings.GOOGLE_RECAPTCHA_SECRET_KEY, 'response': recaptcha_response} | |||||
| full_url = request.build_absolute_uri() | |||||
| try: | |||||
| r = recaptcha_session.post(VERIFY_URL, data=data) | |||||
| except IOError: | |||||
| logger.exception('Error communicating with Google reCAPTCHA service at %s', full_url) | |||||
| return None, msg_recaptcha_unavailable | |||||
| if r.status_code != 200: | |||||
| logger.error("Error code %d verifying reCAPTCHA at %s: %s", r.status_code, full_url, r.text) | |||||
| return None, msg_recaptcha_unavailable | |||||
| result = r.json() | |||||
| if not result.get('success', False): | |||||
| logger.warning("reCAPTCHA failed to verify at %s: %s", full_url, r.text) | |||||
| message = 'reCAPTCHA failed to verify that you are human being. Please try again.' | |||||
| error_codes = result.get('error-codes') or [] | |||||
| if 'timeout-or-duplicate' in error_codes: | |||||
| message = ( | |||||
| 'reCAPTCHA token expired or was already used.' | |||||
| ' Please reload the page and try again.' | |||||
| ) | |||||
| return False, message | |||||
| # Check that nobody is trying to fake the response via some other website. | |||||
| hostname = result.get('hostname') | |||||
| if not validate_host(hostname, settings.ALLOWED_HOSTS): | |||||
| logger.error( | |||||
| "reCAPTCHA verified but for an unexpected hostname %r at %s", | |||||
| hostname, | |||||
| full_url, | |||||
| ) | |||||
| return ( | |||||
| False, | |||||
| 'reCAPTCHA verified, but for an unexpected hostname. Please try again. If ' | |||||
| 'this keeps happening, send an email to production@blender.org.', | |||||
| ) | |||||
| score = result.get('score', 0.0) | |||||
| logger.warning('score: %s, expected minimum: %s, %s', score, MIN_SCORE, result) | |||||
| if not score or score < MIN_SCORE: | |||||
| return False, 'reCAPTCHA failed to verify that you are human being. Please try again.' | |||||
| return True, '' | |||||