1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
"""reCAPTCHA v2 server-side verification."""
import logging
import httpx
log = logging.getLogger(__name__)
VERIFY_URL = "https://www.google.com/recaptcha/api/siteverify"
async def verify_captcha(
secret_key: str,
token: str,
remote_ip: str | None = None,
allowed_hosts: frozenset[str] | set[str] | None = None,
) -> bool:
"""True when Google accepts the token, and it came from a host we expect.
`allowed_hosts` is the answer to a reCAPTCHA key whose own origin check is
turned off. That check compares the *page's hostname* against the key's
domain list, and the desktop client's page is served from `app://meshbay`
— Chromium refuses a service worker on a custom scheme and the interface
ships inside the package, so it cannot be `https://<hub>` and the hostname
cannot be the hub's. Google answers "Invalid domain for site key" inside
its own widget, which is not something the client can configure away.
Turning the key's origin check off moves that decision here, where the hub
knows which hosts are its own. `siteverify` reports the hostname it
observed, not one the caller asserts, so this is a real check and not a
formality — a bot embedding the site key on a page of its own is refused
on the hostname it actually served from.
`None` (the default) skips it, which is what a deployment leaving the
origin check with Google wants: it is then already done, one layer up.
"""
payload: dict[str, str] = {"secret": secret_key, "response": token}
if remote_ip:
payload["remoteip"] = remote_ip
try:
async with httpx.AsyncClient(timeout=5) as client:
resp = await client.post(VERIFY_URL, data=payload)
resp.raise_for_status()
result = resp.json()
if not result.get("success"):
log.info("captcha rejected: %s", result.get("error-codes", []))
return False
if allowed_hosts is not None:
# Logged at warning with the hostname spelled out: this is also
# how an operator finds what to allow after adding a client
# whose origin they have not seen before.
host = result.get("hostname") or ""
if host not in allowed_hosts:
log.warning(
"captcha solved on an unexpected host %r; allowed: %s",
host, sorted(allowed_hosts))
return False
return True
except Exception:
log.exception("captcha verification request failed")
return False
|