aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests/test_linkpreview.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-30 11:22:24 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-30 11:22:24 +0200
commit69554fac7eba6eef7eb8a1c0111c5b92e7f21256 (patch)
tree87ae908d008159c4237b31dade3f385a629c3c7b /packages/meshbay-node/tests/test_linkpreview.py
parente2a487c3cd24589b9d9bd298b79d8efaa9914480 (diff)
downloadmeshbay-69554fac7eba6eef7eb8a1c0111c5b92e7f21256.tar.gz
fix: a member can no longer lock a node, crash it with a link, or stop hub cleanup
- node: only a wrong code counts towards the join lock, now per account (5) as well as node-wide (20), and it is consulted only when a code is tried. Every member reconnecting gets the group key through join_request, so a lock checked before recognition let one member refuse it to everyone. - node: link previews read the body as a stream and stop at the cap, counted on decoded bytes; a declared oversized image is not read; 15 s total deadline; image decoding off the loop. `client.get` had buffered the whole (decompressed) response before the caps looked at it. - hub: the daily purge of never-verified accounts detaches their IP-log rows (keeping the name) and clears every other reference first, and each cleanup step runs on its own. On PostgreSQL the bare DELETE violated the ip_logs foreign key and stopped every purge behind it for good. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-node/tests/test_linkpreview.py')
-rw-r--r--packages/meshbay-node/tests/test_linkpreview.py80
1 files changed, 80 insertions, 0 deletions
diff --git a/packages/meshbay-node/tests/test_linkpreview.py b/packages/meshbay-node/tests/test_linkpreview.py
index a14b173..9fca186 100644
--- a/packages/meshbay-node/tests/test_linkpreview.py
+++ b/packages/meshbay-node/tests/test_linkpreview.py
@@ -193,3 +193,83 @@ async def test_fetch_image_refuses_a_decompression_bomb(resolves_public, monkeyp
return httpx.Response(200, headers={"content-type": "image/png"}, content=bomb)
async with _client(handler) as c:
assert await linkpreview.fetch_image("https://example.com/x.png", client=c) is None
+
+
+# ── Size caps bind while reading, not after ─────────────────────────────────
+#
+# `client.get` read and decoded the whole body before the caps looked at it, so
+# a link posted in chat could make the node hold any amount of data. These
+# count what the server actually had to hand over.
+
+_CHUNK = 64 * 1024
+
+
+def _endless(counter, head=b""):
+ async def body():
+ if head:
+ counter["sent"] += len(head)
+ yield head
+ for _ in range(2000): # 125 MiB if nobody stops
+ counter["sent"] += _CHUNK
+ yield b"x" * _CHUNK
+ return body()
+
+
+async def test_a_huge_page_is_not_read_past_the_cap(resolves_public):
+ counter = {"sent": 0}
+ head = b"<html><head><title>T</title></head><body>"
+
+ def handler(request):
+ return httpx.Response(200, headers={"content-type": "text/html"},
+ content=_endless(counter, head))
+ async with _client(handler) as c:
+ meta = await linkpreview.fetch_preview("https://example.com/", client=c)
+ assert meta is not None and meta["title"] == "T"
+ assert counter["sent"] <= linkpreview._MAX_HTML_BYTES + 2 * _CHUNK
+
+
+async def test_a_huge_image_is_refused_without_being_read(resolves_public):
+ counter = {"sent": 0}
+
+ def handler(request):
+ return httpx.Response(200, headers={"content-type": "image/png"},
+ content=_endless(counter))
+ async with _client(handler) as c:
+ assert await linkpreview.fetch_image("https://example.com/x.png", client=c) is None
+ assert counter["sent"] <= linkpreview._MAX_IMAGE_BYTES + 2 * _CHUNK
+
+
+async def test_a_compressed_body_is_capped_after_decoding(resolves_public):
+ import zlib
+ comp = zlib.compressobj(9, zlib.DEFLATED, 31) # gzip
+ counter = {"inflated": 0}
+
+ async def body():
+ yield comp.compress(b"<html><head><title>T</title></head><body>")
+ for _ in range(2000): # 125 MiB inflated
+ counter["inflated"] += _CHUNK
+ # Flushed per chunk, so what is counted is what went on the wire.
+ yield comp.compress(b"x" * _CHUNK) + comp.flush(zlib.Z_SYNC_FLUSH)
+ yield comp.flush()
+
+ def handler(request):
+ return httpx.Response(200, headers={"content-type": "text/html",
+ "content-encoding": "gzip"},
+ content=body())
+ async with _client(handler) as c:
+ meta = await linkpreview.fetch_preview("https://example.com/", client=c)
+ assert meta is not None
+ assert counter["inflated"] < 50 * _CHUNK # stopped early, not at 125 MiB
+
+
+async def test_a_declared_oversized_image_is_not_read(resolves_public):
+ counter = {"sent": 0}
+
+ def handler(request):
+ return httpx.Response(
+ 200, headers={"content-type": "image/png",
+ "content-length": str(linkpreview._MAX_IMAGE_BYTES + 1)},
+ content=_endless(counter))
+ async with _client(handler) as c:
+ assert await linkpreview.fetch_image("https://example.com/x.png", client=c) is None
+ assert counter["sent"] <= _CHUNK