aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/api
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/api')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/hub.py10
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/users.py24
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/webapp.py23
3 files changed, 26 insertions, 31 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/hub.py b/packages/meshbay-hub/src/meshbay_hub/api/hub.py
index 0d2d39c..b47f076 100644
--- a/packages/meshbay-hub/src/meshbay_hub/api/hub.py
+++ b/packages/meshbay-hub/src/meshbay_hub/api/hub.py
@@ -7,19 +7,10 @@ from sqlalchemy.ext.asyncio import AsyncSession
from meshbay_hub import __version__, hub_settings
from meshbay_hub.api import federation
from meshbay_hub.auth import hub_public_key_pem
-from meshbay_hub.config import HubConfig
from meshbay_hub.db.engine import get_db, get_engine
router = APIRouter(prefix="/v1/hub", tags=["hub"])
-_cfg: HubConfig | None = None
-
-
-def set_config(cfg: HubConfig) -> None:
- global _cfg
- _cfg = cfg
-
-
@router.get("/info")
async def hub_info(db: AsyncSession = Depends(get_db)):
"""Versions and the instance policy a client needs before signing in."""
@@ -40,7 +31,6 @@ async def hub_info(db: AsyncSession = Depends(get_db)):
# which answers 503 — this is the same answer somewhere an operator can
# read it without opening the source.
"federation": federation.FEDERATION_ENABLED,
- "captcha_site_key": _cfg.captcha.site_key if _cfg and _cfg.captcha.enabled else "",
# How long a browser tab stays signed in with nobody at it. The page
# measures it (static/idle.js); the hub only says how long.
"browser_idle_hours":
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/users.py b/packages/meshbay-hub/src/meshbay_hub/api/users.py
index 130240e..fbba6d7 100644
--- a/packages/meshbay-hub/src/meshbay_hub/api/users.py
+++ b/packages/meshbay-hub/src/meshbay_hub/api/users.py
@@ -100,18 +100,24 @@ def _generate_code() -> str:
async def _verify_captcha_or_raise(token: str | None, request: Request) -> None:
if not token:
raise HTTPException(status_code=400, detail="captcha_required")
- from meshbay_hub.captcha import verify_captcha
- ok = await verify_captcha(
- _cfg.captcha.secret_key, # type: ignore[union-attr]
- token,
- request.client.host if request.client else None,
- _cfg.captcha.host_check, # type: ignore[union-attr]
- _cfg.captcha.allow_unattributed_host, # type: ignore[union-attr]
- )
- if not ok:
+ from meshbay_hub.captcha import verify
+ if not verify(token):
raise HTTPException(status_code=400, detail="captcha_failed")
+@router.get("/captcha")
+@limiter.limit("30/minute")
+async def captcha_challenge(request: Request):
+ """A proof-of-work challenge for the register and reset-request forms.
+
+ 404 when this hub has the check turned off, so the page sends no answer.
+ """
+ if not (_cfg and _cfg.captcha.enabled):
+ raise HTTPException(status_code=404, detail="captcha_disabled")
+ from meshbay_hub.captcha import issue
+ return issue(_cfg.captcha.max_number)
+
+
# ── Models ────────────────────────────────────────────────────────────────────
# Mirrored by USERNAME_MIN_LEN in static/auth-page.js; test_username_floor.py
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/webapp.py b/packages/meshbay-hub/src/meshbay_hub/api/webapp.py
index badbdda..adbd522 100644
--- a/packages/meshbay-hub/src/meshbay_hub/api/webapp.py
+++ b/packages/meshbay-hub/src/meshbay_hub/api/webapp.py
@@ -82,21 +82,19 @@ _NO_STORE = {"Cache-Control": "no-store"}
# `<script>`. `'wasm-unsafe-eval'` is required for the Argon2id WASM. The hub's
# own origin is deliberately absent from `script-src`: a response it returns is
# never executed, which is the point of T3.
-_RECAPTCHA_SRC = "https://www.google.com https://www.gstatic.com"
CSP = "; ".join([
"default-src 'none'",
- f"script-src 'self' 'wasm-unsafe-eval' {_RECAPTCHA_SRC}",
+ "script-src 'self' 'wasm-unsafe-eval'",
"style-src 'self' 'unsafe-inline'",
- f"img-src 'self' data: blob: {_RECAPTCHA_SRC}",
+ "img-src 'self' data: blob:",
"media-src 'self' blob:",
"font-src 'self'",
# The page talks to its own origin and nothing else — the hub, the node
- # arrives over WebRTC, which this directive does not govern. reCAPTCHA's
- # two origins are the only others, kept so its script may call home from
- # this page; the register and reset views were driven in Chrome and Firefox
- # against Google's test keys with this line and reported no violation.
- # `https: wss:` let an injected script post anything anywhere.
- f"connect-src 'self' {_RECAPTCHA_SRC}",
+ # arrives over WebRTC, which this directive does not govern. No third
+ # party has an origin here: the sign-up check is a proof of work the hub
+ # issues itself (captcha.py). `https: wss:` let an injected script post
+ # anything anywhere.
+ "connect-src 'self'",
"worker-src 'self'",
# `object-src` exists for one thing, and `frame-src`'s `blob:` for the same
# thing: previewing a PDF without writing it anywhere.
@@ -123,9 +121,10 @@ CSP = "; ".join([
# response it is holding. Without it Chrome refuses the frame, the worker is
# never asked, and the page waits out its timeout for a download that cannot
# happen — on Firefox and Safari that is the *only* way to write a large
- # file to disk, so the whole path was dead. Added when reCAPTCHA needed a
- # frame, which is why nobody connected the two.
- f"frame-src 'self' blob: {_RECAPTCHA_SRC}",
+ # file to disk, so the whole path was dead. It was missing because the
+ # directive had been written for a third-party captcha frame, and nobody
+ # connected the two.
+ "frame-src 'self' blob:",
# `'self'`, not `'none'`, and the difference is one same-origin iframe.
#
# The threat frame-ancestors answers is clickjacking: a *foreign* page