diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/captcha.py')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/captcha.py | 37 |
1 files changed, 35 insertions, 2 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/captcha.py b/packages/meshbay-hub/src/meshbay_hub/captcha.py index faf7907..74a77b6 100644 --- a/packages/meshbay-hub/src/meshbay_hub/captcha.py +++ b/packages/meshbay-hub/src/meshbay_hub/captcha.py @@ -10,8 +10,30 @@ VERIFY_URL = "https://www.google.com/recaptcha/api/siteverify" async def verify_captcha( - secret_key: str, token: str, remote_ip: str | None = None, + 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 @@ -22,7 +44,18 @@ async def verify_captcha( result = resp.json() if not result.get("success"): log.info("captcha rejected: %s", result.get("error-codes", [])) - return result.get("success", False) + 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 |