aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests/test_webrtc_transport.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/tests/test_webrtc_transport.py')
-rw-r--r--packages/meshbay-node/tests/test_webrtc_transport.py71
1 files changed, 69 insertions, 2 deletions
diff --git a/packages/meshbay-node/tests/test_webrtc_transport.py b/packages/meshbay-node/tests/test_webrtc_transport.py
index a4c7f61..dc74752 100644
--- a/packages/meshbay-node/tests/test_webrtc_transport.py
+++ b/packages/meshbay-node/tests/test_webrtc_transport.py
@@ -33,6 +33,7 @@ from meshbay_common.crypto import (
unwrap_gek,
unwrap_gek_aes,
)
+from meshbay_common.groupbox import PURPOSE_ACK, PURPOSE_INDEX, unseal
from meshbay_common.webcrypto import chunk_key_aes, decrypt_chunk_aes
from meshbay_common.protocol import MNP
TEST_GROUP = "g"
@@ -358,13 +359,20 @@ async def test_webrtc_datachannel_file_transfer(sk_node, sk_hub, gek, shared_dir
ack = await _handshake_with_gek_proof(channel, received, sk_hub, gek,
browser_pc=browser_pc)
assert ack["type"] == MNP.HANDSHAKE_ACK
+ # 1b) The ack's configuration is sealed under the group key (MNP 1.0), and the
+ # signed handshake transcript names no ack field — so this envelope is the only
+ # thing authenticating `is_node_admin` and the rest.
+ config = unseal(gek, PURPOSE_ACK, MNP.HANDSHAKE_ACK, TEST_GROUP, ack)
+ assert "is_node_admin" in config
+ assert "is_node_admin" not in ack
# 2) Request index
channel.send(_pack({"type": MNP.INDEX_SYNC, "v": MNP_VERSION}))
idx_msg = await asyncio.wait_for(received.get(), timeout=5.0)
assert idx_msg["type"] == MNP.INDEX_SYNC
- assert "entries" in idx_msg
- assert len(idx_msg["entries"]) > 0
+ assert "entries" not in idx_msg, "the index travels in the clear"
+ payload = unseal(gek, PURPOSE_INDEX, MNP.INDEX_SYNC, TEST_GROUP, idx_msg)
+ assert len(payload["entries"]) > 0
# 3) Request file chunk
entry = next(e for e in indexer.index.entries if e.name == "test.bin")
@@ -442,6 +450,65 @@ async def test_webrtc_invalid_jwt_rejected(sk_node, sk_hub, gek, shared_dir):
@pytest.mark.asyncio
+async def test_webrtc_old_client_is_refused_with_a_code(sk_node, sk_hub, gek, shared_dir):
+ """
+ A version mismatch must present as a refusal, not as a missing field.
+
+ An 0.x client reaching a 1.0 node would otherwise get a `handshake_ack` with
+ no `enabled_apps` and apply its documented fallback — show every app — and an
+ `index_sync` with no `entries` it would read as an empty group. Both are
+ confident wrong answers. The check runs *before* the token, so it costs
+ nothing and reports the real reason (L2).
+ """
+ hub_pk_pem = _hub_pk_pem(sk_hub)
+ indexer = DirectoryIndexer(roots=one_root(shared_dir), group_id=TEST_GROUP,
+ 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 = RTCPeerConnection()
+ received = asyncio.Queue()
+ channel = browser_pc.createDataChannel("mnp")
+
+ @channel.on("message")
+ def on_msg(message):
+ if isinstance(message, str):
+ message = message.encode()
+ received.put_nowait(_unpack(message))
+
+ offer = await browser_pc.createOffer()
+ await browser_pc.setLocalDescription(offer)
+ answer_sdp, _ = await transport.handle_offer(
+ browser_pc.localDescription.sdp, "peer-old")
+ await browser_pc.setRemoteDescription(
+ RTCSessionDescription(sdp=answer_sdp, type="answer"))
+ await asyncio.sleep(0.5)
+
+ # A perfectly valid token — the refusal must not depend on it, and must not
+ # be reported as an authorization problem either.
+ channel.send(_pack({
+ "type": MNP.HANDSHAKE,
+ "v": "0.15",
+ "token": _make_jwt(sk_hub, groups=[TEST_GROUP]),
+ "group_id": TEST_GROUP,
+ "nonce": base64.b64encode(os.urandom(32)).decode(),
+ }))
+
+ msg = await asyncio.wait_for(received.get(), timeout=5.0)
+ assert msg["type"] == "error"
+ # The client matches on the code; the text may be reworded.
+ assert msg["code"] == "version_too_old"
+ assert "0.15" in msg["detail"]
+
+ await browser_pc.close()
+ await transport.close_all()
+
+
+@pytest.mark.asyncio
async def test_webrtc_request_before_handshake_rejected(sk_node, sk_hub, gek, shared_dir):
"""WebRTC DataChannel: request without handshake is rejected."""
hub_pk_pem = _hub_pk_pem(sk_hub)