diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-07 17:45:54 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-07 17:45:54 +0200 |
| commit | 77dd077491aea50e71e21e0d17555a2f91cf818b (patch) | |
| tree | 87b88674b3210541f413b4992750dcb2807b8f12 /packages/meshbay-node/tests | |
| parent | d1f998b42137465b610667439527917a00030b4d (diff) | |
| download | meshbay-77dd077491aea50e71e21e0d17555a2f91cf818b.tar.gz | |
refactor(mnp)!: remove stream_seg, the last unencrypted content message
`stream_seg` answered with an MPEG-TS segment as base64 with no encryption at
all — the one message on the content plane that never went through a
GEK-derived key. Live on both transports, answering any authenticated member.
It predates `stream_data`, which does the same job properly (`chunk_ciphertext`,
keyed per segment, AES-256-GCM) and has since Phase 12. Its only browser caller,
`fetchStreamSegment`, was defined and never once invoked — a plaintext media
endpoint with no client. Removed rather than repaired.
Gone with it: `_extract_segment` and the ffmpeg semaphore in quic_server, the
`fetch_stream_segment` QUIC client method, and `_b64decode` in transport.js,
which had no other caller.
The H6 regression test lived on this handler — it pinned `_do_stream_segment_async`
to a coroutine so `subprocess.run` could not stall the event loop for thirty
seconds per request. It is replaced by the property that outlives the handler:
no transport carries media outside an AEAD, asserted on `stream_seg` and
`data_b64` across all three transport modules. The half of H6 that survives —
the live streaming path still spawns ffmpeg — keeps its own test.
BREAKING CHANGE: `stream_seg` is no longer answered on either transport. No
shipping client sends it. Recorded as part of MNP 2.0, whose other half — the
sealed upload — carries the version bump.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AsoWC3GmhNdwVFomW3QjH3
Diffstat (limited to 'packages/meshbay-node/tests')
| -rw-r--r-- | packages/meshbay-node/tests/test_security_regressions.py | 48 | ||||
| -rw-r--r-- | packages/meshbay-node/tests/test_task_lifetime.py | 9 | ||||
| -rw-r--r-- | packages/meshbay-node/tests/test_webrtc_transport.py | 30 |
3 files changed, 45 insertions, 42 deletions
diff --git a/packages/meshbay-node/tests/test_security_regressions.py b/packages/meshbay-node/tests/test_security_regressions.py index 1a318f7..fa821d9 100644 --- a/packages/meshbay-node/tests/test_security_regressions.py +++ b/packages/meshbay-node/tests/test_security_regressions.py @@ -743,20 +743,48 @@ def test_pre_handshake_message_budget_is_small(): list(buf.messages()) -def test_stream_segment_is_not_synchronous(): +def test_no_transport_ships_media_outside_the_aead(): """ - H6: _do_stream_segment ran subprocess.run(timeout=30) inside the event loop, - stalling every peer on the node for up to thirty seconds per request. + `stream_seg` served an MPEG-TS segment as base64 with no encryption at all + — the one content-plane message that never went through a GEK-derived key, + on both transports, answering any authenticated member. Its browser caller + was defined and never invoked. Removed in MNP 2.0 rather than repaired: + `stream_data` already does the job under `chunk_ciphertext`. - Asserts the property (the worker is a coroutine, ffmpeg is spawned through - asyncio) rather than grepping for "subprocess.run" — which also matches the - comment that documents the old behaviour. + Asserted as the property, not as "the function is gone": what matters is + that no transport has a field carrying media bytes past the AEAD. The old + H6 test lived here — it pinned `_do_stream_segment_async` to a coroutine so + ffmpeg could not block the event loop — and the handler outliving that + concern is exactly what this replaces. """ - import ast - import inspect - from meshbay_node.transport.webrtc_server import WebRTCPeerSession + import re + + from meshbay_common.protocol import MNP + + assert not hasattr(MNP, "STREAM_SEGMENT"), ( + "the constant outliving the handlers is how a deleted endpoint keeps " + "looking like part of the wire contract") + + root = Path(__file__).parent.parent / "src" / "meshbay_node" / "transport" + for name in ("webrtc_server.py", "quic_server.py", "quic_client.py"): + source = (root / name).read_text(encoding="utf-8") + # Word boundaries: `_stream_segments` and `STREAM_SEGMENT_SIZE` belong + # to the live `stream_data` path, which is encrypted and stays. + assert not re.search(r"\bstream_seg\b", source), ( + f"{name} still speaks stream_seg") + assert not re.search(r"\bSTREAM_SEGMENT\b", source), ( + f"{name} still names the removed type") + assert "data_b64" not in source, ( + f"{name} carries a base64 media field — media leaves this node " + "encrypted or not at all") - assert inspect.iscoroutinefunction(WebRTCPeerSession._do_stream_segment_async) + +def test_ffmpeg_never_blocks_the_event_loop(): + """ + H6, the half that survives `stream_seg`: the live streaming path still + spawns ffmpeg, and a synchronous spawn stalls every peer on the node. + """ + import ast source = (Path(__file__).parent.parent / "src" / "meshbay_node" / "transport" / "webrtc_server.py").read_text(encoding="utf-8") diff --git a/packages/meshbay-node/tests/test_task_lifetime.py b/packages/meshbay-node/tests/test_task_lifetime.py index 3a5b8a5..9dffedb 100644 --- a/packages/meshbay-node/tests/test_task_lifetime.py +++ b/packages/meshbay-node/tests/test_task_lifetime.py @@ -245,8 +245,13 @@ def test_chunks_wait_for_room_on_the_channel(session): work through the rest — which is what "stuck at 1 MB" looks like, one chunk being exactly one megabyte. """ - fn = session[session.index("async def _do_file_request"):] - fn = fn[:fn.index("\n def _do_stream_segment")] + start = session.index("async def _do_file_request") + # Up to whatever the next member is. This used to end at + # "\n def _do_stream_segment" — a neighbour removed in MNP 2.0 — and an + # `index()` on a name that no longer exists fails the test for a reason + # that has nothing to do with what it is about. + nxt = re.search(r"\n (?:@|(?:async )?def )", session[start:]) + fn = session[start:start + nxt.start()] if nxt else session[start:] assert "DOWNLOAD_BUFFER_HIGH" in fn, "the send buffer has to be watched" assert "await asyncio.sleep" in fn, "waiting for room is the point" assert 'readyState != "open"' in fn, ( diff --git a/packages/meshbay-node/tests/test_webrtc_transport.py b/packages/meshbay-node/tests/test_webrtc_transport.py index dc74752..88b6f63 100644 --- a/packages/meshbay-node/tests/test_webrtc_transport.py +++ b/packages/meshbay-node/tests/test_webrtc_transport.py @@ -743,36 +743,6 @@ async def test_webrtc_peer_cleanup_on_close(sk_node, sk_hub, gek, shared_dir): @pytest.mark.asyncio -async def test_webrtc_stream_segment_missing_file(sk_node, sk_hub, gek, shared_dir): - """WebRTC DataChannel: stream_segment for non-existent file returns error.""" - hub_pk_pem = _hub_pk_pem(sk_hub) - indexer = DirectoryIndexer(roots=one_root(shared_dir), group_id="g", sk_node=sk_node, gek=gek) - await indexer.initial_scan() - - transport = WebRTCTransport( - sk_node=sk_node, hub_pk_pem=hub_pk_pem, gek=gek, - roots=one_root(shared_dir), index=indexer.index, - stun_servers=[], - ) - - browser_pc, channel, received = await _setup_peer( - transport, sk_hub, gek, "peer-stream") - - channel.send(_pack({ - "type": MNP.STREAM_SEGMENT, "v": MNP_VERSION, - "file_id": "nonexistent-file-id", - "segment_index": 0, "segment_duration": 4, - })) - - msg = await asyncio.wait_for(received.get(), timeout=5.0) - assert msg["type"] == "error" - assert "not found" in msg["detail"].lower() - - await browser_pc.close() - await transport.close_all() - - -@pytest.mark.asyncio async def test_webrtc_wrong_gek_proof_rejected(sk_node, sk_hub, gek, shared_dir): """WebRTC DataChannel: wrong GEK proof is rejected — hub admin can't fake membership.""" hub_pk_pem = _hub_pk_pem(sk_hub) |