aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/captcha.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-02 11:54:22 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-02 11:54:22 +0200
commit2d8c6bc449e343a80569e45d4ceae0423616cedd (patch)
tree09a1ee29c2a39db6189ba80315cfa0a1c463bcf3 /packages/meshbay-hub/src/meshbay_hub/captcha.py
parentbee5901f7a04d17c40e2c8d077437f10e59c8d81 (diff)
downloadmeshbay-2d8c6bc449e343a80569e45d4ceae0423616cedd.tar.gz
fix(hub): a desktop solve reports no hostname at all, not "meshbay"
Registering from the native client failed with `captcha_failed` while the checkbox was green — a worse symptom than the one being fixed, because the widget now looked fine and only the hub's own log said otherwise: captcha solved on an unexpected host ''; allowed: ['localhost', 'meshbay', 'meshbay.org'] The previous commit assumed Google would report the host component of the origin, so `app://meshbay` would come back as `meshbay` and could sit in `allowed_hosts`. It does not. A solve Google cannot attribute to a domain reports an **empty** hostname, and no allowlist entry can match that. An empty entry is not the answer either: a blank in a TOML list is a typo far more often than an intention, and `load_config` drops blanks for that reason — `captcha.allow_unattributed_host` is a named flag instead, so the trade is stated where it is made. What it admits, plainly: every non-web client, not only ours. A file:// page or somebody else's Electron application look identical from here. That is the same bar the client's own origin would have been — main.js already records that `app://meshbay` is not a credential — and it is a bar: the captcha still has to be solved, per token, in something that can render it. What is given up is the origin restriction for non-web clients, not the captcha. Off by default, and a hub without the desktop client should leave it off. The refusal now names which of the two it is, since they need different answers: an unexpected host names the host, an unattributed one says to set the flag. docs/captcha.md §6 said `meshbay` was the value and told operators to add it; it now records what was measured and why the guess was wrong. The packaged example config carries the flag with the same warning. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014UtzVrzM7e2tG9fSpkR9ML
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/captcha.py')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/captcha.py28
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))