summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/transport
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/transport')
-rw-r--r--packages/meshbay-node/src/meshbay_node/transport/quic_server.py58
-rw-r--r--packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py9
2 files changed, 59 insertions, 8 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/transport/quic_server.py b/packages/meshbay-node/src/meshbay_node/transport/quic_server.py
index ede756e..7bb6ce5 100644
--- a/packages/meshbay-node/src/meshbay_node/transport/quic_server.py
+++ b/packages/meshbay-node/src/meshbay_node/transport/quic_server.py
@@ -50,22 +50,69 @@ ALPN = ["meshbay-mnp"]
class Denylist:
- """Shared denylist for revoked users and invalidated JWTs."""
+ """
+ Denylist for revoked users, groups and invalidated JWTs.
- def __init__(self):
+ Finding H4: revocations used to live only in memory, so a node restart silently
+ un-revoked everyone, and group revocations were dropped entirely — the hub
+ signed and broadcast them but the node's handler only understood "user" and
+ "jti". Now persisted to disk and group targets are honoured.
+ """
+
+ def __init__(self, path: Path | None = None):
self.user_ids: set[str] = set()
+ self.group_ids: set[str] = set()
self.jtis: set[str] = set()
+ self._path = path
+ self._load()
- def is_denied(self, user_id: str, jti: str) -> bool:
- return user_id in self.user_ids or jti in self.jtis
+ def is_denied(self, user_id: str, jti: str, group_id: str = "") -> bool:
+ return (user_id in self.user_ids
+ or jti in self.jtis
+ or (bool(group_id) and group_id in self.group_ids))
def deny_user(self, user_id: str) -> None:
self.user_ids.add(user_id)
log.info("Denied user: %s", user_id[:8])
+ self._save()
+
+ def deny_group(self, group_id: str) -> None:
+ self.group_ids.add(group_id)
+ log.info("Denied group: %s", group_id[:8])
+ self._save()
def deny_jti(self, jti: str) -> None:
self.jtis.add(jti)
log.info("Denied jti: %s", jti[:8])
+ self._save()
+
+ def _load(self) -> None:
+ if not self._path or not self._path.exists():
+ return
+ try:
+ import json
+ data = json.loads(self._path.read_text())
+ self.user_ids = set(data.get("users", []))
+ self.group_ids = set(data.get("groups", []))
+ self.jtis = set(data.get("jtis", []))
+ log.info("Denylist loaded: %d users, %d groups, %d jtis",
+ len(self.user_ids), len(self.group_ids), len(self.jtis))
+ except Exception as e:
+ log.warning("Could not load denylist from %s: %s", self._path, e)
+
+ def _save(self) -> None:
+ if not self._path:
+ return
+ try:
+ import json
+ self._path.parent.mkdir(parents=True, exist_ok=True)
+ self._path.write_text(json.dumps({
+ "users": sorted(self.user_ids),
+ "groups": sorted(self.group_ids),
+ "jtis": sorted(self.jtis),
+ }))
+ except Exception as e:
+ log.warning("Could not persist denylist to %s: %s", self._path, e)
# ── Wire helpers ──────────────────────────────────────────────────────────────
@@ -156,7 +203,8 @@ class _MNPServerProtocol(QuicConnectionProtocol):
return
denylist = self._ctx.get("denylist")
- if denylist and denylist.is_denied(decoded.get("sub", ""), decoded.get("jti", "")):
+ if denylist and denylist.is_denied(
+ decoded.get("sub", ""), decoded.get("jti", ""), group_id):
self._send(stream_id, {"type": "error", "detail": "Token revoked"})
self._quic.close()
return
diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
index 9fb9ef2..43e6fdc 100644
--- a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
+++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
@@ -241,8 +241,10 @@ class WebRTCPeerSession:
else:
log.warning("Unknown MNP message type on DataChannel: %s", mtype)
except Exception as e:
- log.error("Error handling %s on DataChannel: %s", mtype, e)
- self._send({"type": "error", "detail": str(e)})
+ # Log the detail locally; send the peer a generic message. Exception
+ # text here carries filesystem paths and internal state (finding L3).
+ log.error("Error handling %s on DataChannel: %s", mtype, e, exc_info=True)
+ self._send({"type": "error", "detail": "Request failed"})
def _audit(self, event: str, detail: str = "") -> None:
audit = self._ctx.get("audit_store")
@@ -269,7 +271,8 @@ class WebRTCPeerSession:
return
denylist = self._ctx.get("denylist")
- if denylist and denylist.is_denied(decoded.get("sub", ""), decoded.get("jti", "")):
+ if denylist and denylist.is_denied(
+ decoded.get("sub", ""), decoded.get("jti", ""), group_id):
self._send({"type": "error", "detail": "Token revoked"})
return