aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--packages/meshbay-node/src/meshbay_node/transport/webrtc/chat.py15
-rw-r--r--packages/meshbay-node/tests/test_chat_is_bounded.py35
2 files changed, 48 insertions, 2 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc/chat.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc/chat.py
index 5ef153e..493d7f0 100644
--- a/packages/meshbay-node/src/meshbay_node/transport/webrtc/chat.py
+++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc/chat.py
@@ -283,7 +283,18 @@ class ChatMixin:
# anyone on the node.
gctx = self._group_ctx()
chat_store = gctx.get("chat_store")
+ # The two fields that travel in clear beside the ciphertext (the sealed
+ # envelope carries its own). Stored and relayed to every member, so they
+ # are what they claim to be and no larger: a name as long as a username,
+ # a thread id as long as a message id. Anything else is dropped.
sender_name = msg.get("sender_name", "")
+ if not isinstance(sender_name, str) or len(sender_name) > 64:
+ sender_name = ""
+ thread_id = msg.get("thread_id")
+ id_like = (isinstance(thread_id, int) and not isinstance(thread_id, bool)
+ or isinstance(thread_id, str) and len(thread_id) <= 64)
+ if thread_id is not None and not id_like:
+ thread_id = None
# Two shapes, and keeping them apart is what makes this deployable.
#
@@ -329,7 +340,7 @@ class ChatMixin:
self._spawn(self._store_chat_message(
chat_store,
iteration=msg.get("iteration", 0), payload=raw,
- thread_id=msg.get("thread_id"), sender_name=sender_name,
+ thread_id=thread_id, sender_name=sender_name,
format=fmt, epoch=epoch, device=device, nonce=nonce, sig=sig,
))
@@ -340,7 +351,7 @@ class ChatMixin:
"sender_id": self._user_id,
"sender_name": sender_name,
"payload": payload,
- "thread_id": msg.get("thread_id"),
+ "thread_id": thread_id,
"timestamp": time.time(),
"format": fmt,
"epoch": epoch,
diff --git a/packages/meshbay-node/tests/test_chat_is_bounded.py b/packages/meshbay-node/tests/test_chat_is_bounded.py
index 3332601..c48f26d 100644
--- a/packages/meshbay-node/tests/test_chat_is_bounded.py
+++ b/packages/meshbay-node/tests/test_chat_is_bounded.py
@@ -204,3 +204,38 @@ async def test_one_member_at_their_limit_has_not_spent_anyone_elses(ctx, store):
await _flood(bob, ctx, 1)
assert not _errors(bob), "one member's flood silenced another"
assert _acks(bob)
+
+
+# ── the fields beside the ciphertext ─────────────────────────────────────────
+
+async def test_the_clear_fields_are_what_they_claim_and_no_larger(ctx, store):
+ """
+ `sender_name` and `thread_id` travel in clear beside the sealed envelope,
+ which carries its own. They are stored on the operator's disk and relayed
+ to every member, so a megabyte of name or a list for a thread id is dropped,
+ not kept.
+ """
+ alice = _session(ctx, store, user="alice", conn="c1")
+ bob = _session(ctx, store, user="bob", conn="c2")
+ msg = _message(alice, size=64)
+ msg["sender_name"] = "x" * (1024 * 1024)
+ msg["thread_id"] = list(range(10_000))
+ alice._do_chat_message(msg)
+ await _drain(ctx)
+
+ stored = (await store.get_recent(limit=1))[0]
+ assert stored.sender_name in ("", None)
+ assert stored.thread_id is None
+ relayed = [m for m in bob.sent if m.get("type") == "chat_msg"]
+ assert relayed and relayed[-1]["sender_name"] == "" and relayed[-1]["thread_id"] is None
+
+
+async def test_ordinary_clear_fields_pass_unchanged(ctx, store):
+ alice = _session(ctx, store, user="alice", conn="c1")
+ msg = _message(alice, size=64)
+ msg["sender_name"] = "Alice"
+ msg["thread_id"] = "42"
+ alice._do_chat_message(msg)
+ await _drain(ctx)
+ stored = (await store.get_recent(limit=1))[0]
+ assert (stored.sender_name, stored.thread_id) == ("Alice", "42")