elo-node 0.4.6__tar.gz → 0.4.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {elo_node-0.4.6 → elo_node-0.4.7}/PKG-INFO +1 -1
- {elo_node-0.4.6 → elo_node-0.4.7}/elo/__init__.py +1 -1
- {elo_node-0.4.6 → elo_node-0.4.7}/elo/node.py +75 -5
- {elo_node-0.4.6 → elo_node-0.4.7}/elo/transport/protocol.py +8 -3
- {elo_node-0.4.6 → elo_node-0.4.7}/elo_node.egg-info/PKG-INFO +1 -1
- {elo_node-0.4.6 → elo_node-0.4.7}/pyproject.toml +1 -1
- {elo_node-0.4.6 → elo_node-0.4.7}/tests/test_unit.py +1 -1
- {elo_node-0.4.6 → elo_node-0.4.7}/README.md +0 -0
- {elo_node-0.4.6 → elo_node-0.4.7}/elo/__main__.py +0 -0
- {elo_node-0.4.6 → elo_node-0.4.7}/elo/security.py +0 -0
- {elo_node-0.4.6 → elo_node-0.4.7}/elo/transport/__init__.py +0 -0
- {elo_node-0.4.6 → elo_node-0.4.7}/elo/transport/routing.py +0 -0
- {elo_node-0.4.6 → elo_node-0.4.7}/elo/transport/tcp.py +0 -0
- {elo_node-0.4.6 → elo_node-0.4.7}/elo/transport/tracker.py +0 -0
- {elo_node-0.4.6 → elo_node-0.4.7}/elo/types.py +0 -0
- {elo_node-0.4.6 → elo_node-0.4.7}/elo_node.egg-info/SOURCES.txt +0 -0
- {elo_node-0.4.6 → elo_node-0.4.7}/elo_node.egg-info/dependency_links.txt +0 -0
- {elo_node-0.4.6 → elo_node-0.4.7}/elo_node.egg-info/entry_points.txt +0 -0
- {elo_node-0.4.6 → elo_node-0.4.7}/elo_node.egg-info/requires.txt +0 -0
- {elo_node-0.4.6 → elo_node-0.4.7}/elo_node.egg-info/top_level.txt +0 -0
- {elo_node-0.4.6 → elo_node-0.4.7}/setup.cfg +0 -0
- {elo_node-0.4.6 → elo_node-0.4.7}/tests/test_security.py +0 -0
|
@@ -78,7 +78,7 @@ class Node:
|
|
|
78
78
|
peers: list[str] | None = None,
|
|
79
79
|
tracker: str = "public",
|
|
80
80
|
allowlist: list[str] | None = None,
|
|
81
|
-
version: str = "0.4.
|
|
81
|
+
version: str = "0.4.7",
|
|
82
82
|
identity: EphemeralIdentity | None = None,
|
|
83
83
|
verify_peers: bool = True,
|
|
84
84
|
heartbeat_interval_s: int = 30,
|
|
@@ -116,6 +116,14 @@ class Node:
|
|
|
116
116
|
self._pubkey_cache: dict[str, tuple[Any, float]] = {}
|
|
117
117
|
self._cache_ttl_s = heartbeat_interval_s * 5
|
|
118
118
|
|
|
119
|
+
# Rate limiting
|
|
120
|
+
self._msg_count: dict[str, tuple[int, float]] = {} # peer_addr → (count, window_start)
|
|
121
|
+
self._hello_count: dict[str, float] = {} # peer_addr → last_hello_timestamp
|
|
122
|
+
|
|
123
|
+
# Cache de query_ids para prevenir broadcast loop
|
|
124
|
+
self._seen_queries: dict[str, float] = {}
|
|
125
|
+
self._seen_query_ttl = 60 # segundos
|
|
126
|
+
|
|
119
127
|
# ── propriedades ──────────────────────────────────────────
|
|
120
128
|
|
|
121
129
|
@property
|
|
@@ -232,8 +240,13 @@ class Node:
|
|
|
232
240
|
task_dict.pop("signature", None)
|
|
233
241
|
task_dict["signature"] = self._identity.sign(task_dict)
|
|
234
242
|
|
|
235
|
-
# Tenta encontrar peer
|
|
236
|
-
peer =
|
|
243
|
+
# Tenta encontrar peer — matcha node_id contra addresses no formato "prefix@ip:port"
|
|
244
|
+
peer = None
|
|
245
|
+
if target_node:
|
|
246
|
+
for addr in self._tcp.peer_addresses:
|
|
247
|
+
if addr.startswith(target_node[:12] + "@"):
|
|
248
|
+
peer = addr
|
|
249
|
+
break
|
|
237
250
|
if not peer:
|
|
238
251
|
peer = self._routing.find_peer_for(capability)
|
|
239
252
|
|
|
@@ -343,7 +356,7 @@ class Node:
|
|
|
343
356
|
discovered: dict[str, dict[str, Any]] = {}
|
|
344
357
|
|
|
345
358
|
# Broadcast QUERY for any capability
|
|
346
|
-
await self._tcp.broadcast(query_msg("", query_id, ttl=3))
|
|
359
|
+
await self._tcp.broadcast(query_msg("", query_id, ttl=3, origin=self._node_id))
|
|
347
360
|
|
|
348
361
|
try:
|
|
349
362
|
await asyncio.wait_for(event.wait(), timeout=timeout)
|
|
@@ -413,6 +426,27 @@ class Node:
|
|
|
413
426
|
async def _handle_message(self, peer_addr: str, msg: dict[str, Any]) -> None:
|
|
414
427
|
msg_type = msg.get("type", "")
|
|
415
428
|
|
|
429
|
+
# ── Rate limiting ───────────────────────────────────────
|
|
430
|
+
now = time.monotonic()
|
|
431
|
+
# HELLO: max 1 per minute per peer
|
|
432
|
+
if msg_type == MessageType.HELLO:
|
|
433
|
+
last_hello = self._hello_count.get(peer_addr, 0.0)
|
|
434
|
+
if now - last_hello < 60:
|
|
435
|
+
logger.warning("[elo] rate limit: HELLO flood from %s", peer_addr[:20])
|
|
436
|
+
return
|
|
437
|
+
self._hello_count[peer_addr] = now
|
|
438
|
+
|
|
439
|
+
# General: max 100 messages per 60s window per peer
|
|
440
|
+
count, win_start = self._msg_count.get(peer_addr, (0, now))
|
|
441
|
+
if now - win_start > 60:
|
|
442
|
+
count, win_start = 0, now
|
|
443
|
+
count += 1
|
|
444
|
+
self._msg_count[peer_addr] = (count, win_start)
|
|
445
|
+
if count > 100:
|
|
446
|
+
logger.warning("[elo] rate limit: %d msgs in 60s from %s — dropping", count, peer_addr[:20])
|
|
447
|
+
return
|
|
448
|
+
# ── Fim rate limiting ───────────────────────────────────
|
|
449
|
+
|
|
416
450
|
if msg_type == MessageType.HELLO:
|
|
417
451
|
await self._on_hello(peer_addr, msg)
|
|
418
452
|
elif msg_type == MessageType.HELLO_ACK:
|
|
@@ -467,6 +501,22 @@ class Node:
|
|
|
467
501
|
capability = msg.get("capability", "")
|
|
468
502
|
query_id = msg.get("id", "")
|
|
469
503
|
ttl = msg.get("ttl", 5)
|
|
504
|
+
origin = msg.get("origin", "")
|
|
505
|
+
|
|
506
|
+
# Previne broadcast loop: não reencaminha se origin == self
|
|
507
|
+
if origin and origin == self._node_id:
|
|
508
|
+
return
|
|
509
|
+
|
|
510
|
+
# Previne duplicatas: cache de query_ids recentes
|
|
511
|
+
now = time.time()
|
|
512
|
+
# Limpa entradas expiradas
|
|
513
|
+
expired = [qid for qid, ts in self._seen_queries.items() if now - ts > self._seen_query_ttl]
|
|
514
|
+
for qid in expired:
|
|
515
|
+
self._seen_queries.pop(qid, None)
|
|
516
|
+
if query_id in self._seen_queries:
|
|
517
|
+
return
|
|
518
|
+
self._seen_queries[query_id] = now
|
|
519
|
+
|
|
470
520
|
nodes: list[dict[str, Any]] = []
|
|
471
521
|
|
|
472
522
|
# Modelo BitTorrent: tracker retorna SEMPRE todos os peers conhecidos,
|
|
@@ -490,7 +540,9 @@ class Node:
|
|
|
490
540
|
except Exception:
|
|
491
541
|
pass
|
|
492
542
|
elif ttl > 1:
|
|
493
|
-
|
|
543
|
+
# Propaga origin: usa a existente ou define self como origin
|
|
544
|
+
origin = origin or self._node_id
|
|
545
|
+
await self._tcp.broadcast(query_msg(capability, query_id, ttl=ttl - 1, origin=origin), exclude={peer_addr})
|
|
494
546
|
|
|
495
547
|
async def _on_query_resp(self, peer_addr: str, msg: dict) -> None:
|
|
496
548
|
query_id = msg.get("id", "")
|
|
@@ -543,6 +595,9 @@ class Node:
|
|
|
543
595
|
result_payload = {"message": "no handler registered"}
|
|
544
596
|
|
|
545
597
|
result = result_msg(task_id, "success", payload=result_payload)
|
|
598
|
+
# Assina resultado para garantir autenticidade
|
|
599
|
+
result_no_sig = {k: v for k, v in result.items() if k != "signature"}
|
|
600
|
+
result["signature"] = self._identity.sign(result_no_sig)
|
|
546
601
|
await self._tcp.send_to(peer_addr, result)
|
|
547
602
|
|
|
548
603
|
except Exception as e:
|
|
@@ -556,6 +611,21 @@ class Node:
|
|
|
556
611
|
|
|
557
612
|
async def _on_result(self, peer_addr: str, msg: dict) -> None:
|
|
558
613
|
task_id = msg.get("id", "")
|
|
614
|
+
|
|
615
|
+
# Verifica assinatura do resultado se verify_peers estiver ativo
|
|
616
|
+
if self._verify_peers and msg.get("signature"):
|
|
617
|
+
result_no_sig = {k: v for k, v in msg.items() if k != "signature"}
|
|
618
|
+
# Extrai caller_id do pending_result (já que o caller original sabe quem enviou)
|
|
619
|
+
# O peer que enviou é conhecido via peer_addr — extraímos node_id do prefixo
|
|
620
|
+
peer_node_id = peer_addr.split("@")[0] if "@" in peer_addr else ""
|
|
621
|
+
if peer_node_id:
|
|
622
|
+
caller_pub = await self._get_caller_pubkey(peer_node_id)
|
|
623
|
+
if caller_pub:
|
|
624
|
+
from elo.security import verify_signature
|
|
625
|
+
if not verify_signature(caller_pub, result_no_sig, msg["signature"]):
|
|
626
|
+
logger.warning("[elo] bad signature on result from %s", peer_node_id[:12])
|
|
627
|
+
return # descarta resultado com assinatura inválida
|
|
628
|
+
|
|
559
629
|
future = self._pending_results.get(task_id)
|
|
560
630
|
if future and not future.done():
|
|
561
631
|
status = msg.get("status", "error")
|
|
@@ -103,13 +103,16 @@ def hello_ack_msg(node_id: str, caps: dict, interests: list[str],
|
|
|
103
103
|
return msg
|
|
104
104
|
|
|
105
105
|
|
|
106
|
-
def query_msg(capability: str, query_id: str, ttl: int = 5) -> dict:
|
|
107
|
-
|
|
106
|
+
def query_msg(capability: str, query_id: str, ttl: int = 5, origin: str = "") -> dict:
|
|
107
|
+
msg: dict[str, Any] = {
|
|
108
108
|
"type": MessageType.QUERY,
|
|
109
109
|
"capability": capability,
|
|
110
110
|
"id": query_id,
|
|
111
111
|
"ttl": ttl,
|
|
112
112
|
}
|
|
113
|
+
if origin:
|
|
114
|
+
msg["origin"] = origin
|
|
115
|
+
return msg
|
|
113
116
|
|
|
114
117
|
|
|
115
118
|
def query_resp_msg(query_id: str, nodes: list[dict]) -> dict:
|
|
@@ -144,7 +147,7 @@ def task_msg(task_id: str, target: str, caller: str, capability: str,
|
|
|
144
147
|
|
|
145
148
|
|
|
146
149
|
def result_msg(task_id: str, status: str, payload: dict | None = None,
|
|
147
|
-
error: dict | None = None) -> dict:
|
|
150
|
+
error: dict | None = None, signature: str = "") -> dict:
|
|
148
151
|
msg = {
|
|
149
152
|
"type": MessageType.RESULT,
|
|
150
153
|
"id": task_id,
|
|
@@ -155,6 +158,8 @@ def result_msg(task_id: str, status: str, payload: dict | None = None,
|
|
|
155
158
|
msg["payload"] = payload
|
|
156
159
|
if error is not None:
|
|
157
160
|
msg["error"] = error
|
|
161
|
+
if signature:
|
|
162
|
+
msg["signature"] = signature
|
|
158
163
|
return msg
|
|
159
164
|
|
|
160
165
|
|
|
@@ -136,7 +136,7 @@ class TestNodeConstruction:
|
|
|
136
136
|
node = Node("test-node")
|
|
137
137
|
assert node._name == "test-node"
|
|
138
138
|
assert node._port == 7878
|
|
139
|
-
assert node._version == "0.4.
|
|
139
|
+
assert node._version == "0.4.7"
|
|
140
140
|
assert node.connected is False
|
|
141
141
|
assert node.node_id is not None
|
|
142
142
|
assert len(node.node_id) > 20
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|