1import json
2from urllib import request as http
3from urllib.parse import urlencode
4
5from flask import current_app
6from flask import request
7from wtforms import ValidationError
8
9RECAPTCHA_VERIFY_SERVER_DEFAULT = "https://www.google.com/recaptcha/api/siteverify"
10RECAPTCHA_ERROR_CODES = {
11 "missing-input-secret": "The secret parameter is missing.",
12 "invalid-input-secret": "The secret parameter is invalid or malformed.",
13 "missing-input-response": "The response parameter is missing.",
14 "invalid-input-response": "The response parameter is invalid or malformed.",
15}
16
17
18__all__ = ["Recaptcha"]
19
20
21class Recaptcha:
22 """Validates a ReCaptcha.
23
24 Verification is skipped and the field is considered valid whenever
25 ``current_app.testing`` is ``True`` or ``RECAPTCHA_ENABLED`` is
26 ``False``, so tests and offline development don't need a real
27 reCAPTCHA token.
28
29 .. versionchanged:: 1.3.0
30 Verification is also skipped when ``RECAPTCHA_ENABLED`` is
31 ``False``.
32 """
33
34 def __init__(self, message=None):
35 if message is None:
36 message = RECAPTCHA_ERROR_CODES["missing-input-response"]
37 self.message = message
38
39 def __call__(self, form, field):
40 if current_app.testing or not current_app.config.get("RECAPTCHA_ENABLED", True):
41 return True
42
43 if request.is_json:
44 response = request.json.get("g-recaptcha-response", "")
45 else:
46 response = request.form.get("g-recaptcha-response", "")
47 remote_ip = request.remote_addr
48
49 if not response:
50 raise ValidationError(field.gettext(self.message))
51
52 if not self._validate_recaptcha(response, remote_ip):
53 field.recaptcha_error = "incorrect-captcha-sol"
54 raise ValidationError(field.gettext(self.message))
55
56 def _validate_recaptcha(self, response, remote_addr):
57 """Performs the actual validation."""
58 try:
59 private_key = current_app.config["RECAPTCHA_PRIVATE_KEY"]
60 except KeyError:
61 raise RuntimeError("No RECAPTCHA_PRIVATE_KEY config set") from None
62
63 verify_server = current_app.config.get("RECAPTCHA_VERIFY_SERVER")
64 if not verify_server:
65 verify_server = RECAPTCHA_VERIFY_SERVER_DEFAULT
66
67 data = urlencode(
68 {"secret": private_key, "remoteip": remote_addr, "response": response}
69 )
70
71 http_response = http.urlopen(verify_server, data.encode("utf-8"))
72
73 if http_response.code != 200:
74 return False
75
76 json_resp = json.loads(http_response.read())
77
78 if json_resp["success"]:
79 return True
80
81 for error in json_resp.get("error-codes", []):
82 if error in RECAPTCHA_ERROR_CODES:
83 raise ValidationError(RECAPTCHA_ERROR_CODES[error])
84
85 return False