diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/captcha.py')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/captcha.py | 28 |
1 files changed, 26 insertions, 2 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/captcha.py b/packages/meshbay-hub/src/meshbay_hub/captcha.py index 74a77b6..cf33d52 100644 --- a/packages/meshbay-hub/src/meshbay_hub/captcha.py +++ b/packages/meshbay-hub/src/meshbay_hub/captcha.py @@ -14,6 +14,7 @@ async def verify_captcha( token: str, remote_ip: str | None = None, allowed_hosts: frozenset[str] | set[str] | None = None, + allow_unattributed: bool = False, ) -> bool: """True when Google accepts the token, and it came from a host we expect. @@ -33,6 +34,21 @@ async def verify_captcha( `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. + + `allow_unattributed` covers the case that made the desktop client fail + anyway. A solve from a page Google cannot attribute to a domain comes back + with an **empty** hostname, not the host part of the origin — measured + live: `app://meshbay` reports `''`, never `'meshbay'`. So an allowlist + entry can never match it, and an empty string cannot be an allowlist entry + either: a blank in a TOML list is a typo far more often than it is an + intention, and the config parser drops blanks for that reason. + + What it admits is every non-web client, not only ours — a `file://` page or + somebody else's Electron application report the same nothing. That is the + same bar the desktop client's own origin would have been (`app://meshbay` + is not a credential; any application can claim it), and it is a bar: the + captcha still has to be *solved*, per token. What is given up is the origin + restriction for non-web clients, not the captcha. """ payload: dict[str, str] = {"secret": secret_key, "response": token} if remote_ip: @@ -48,9 +64,17 @@ async def verify_captcha( 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. + # whose origin they have not seen before. It is how the empty + # one was found. host = result.get("hostname") or "" - if host not in allowed_hosts: + if not host: + if not allow_unattributed: + log.warning( + "captcha solved on a host Google did not attribute; " + "set captcha.allow_unattributed_host to admit " + "non-web clients such as the desktop application") + return False + elif host not in allowed_hosts: log.warning( "captcha solved on an unexpected host %r; allowed: %s", host, sorted(allowed_hosts)) |