mirror of
https://github.com/kc1awv/rrcd.git
synced 2026-06-08 14:11:53 -07:00
refactor trust management
This commit is contained in:
+13
-12
@@ -45,7 +45,7 @@ class CommandHandler:
|
||||
cmd = parts[0].lower()
|
||||
|
||||
if cmd == "reload":
|
||||
if not self.hub._is_server_op(peer_hash):
|
||||
if not self.hub.trust_manager.is_server_op(peer_hash):
|
||||
if self.hub.identity is not None:
|
||||
self._emit_error(
|
||||
outgoing,
|
||||
@@ -61,7 +61,7 @@ class CommandHandler:
|
||||
|
||||
# Global/server-operator commands
|
||||
if cmd == "stats":
|
||||
if not self.hub._is_server_op(peer_hash):
|
||||
if not self.hub.trust_manager.is_server_op(peer_hash):
|
||||
if self.hub.identity is not None:
|
||||
self._emit_error(
|
||||
outgoing,
|
||||
@@ -125,7 +125,7 @@ class CommandHandler:
|
||||
# Check if room is private - only server operators can see private rooms
|
||||
st = self.hub.room_manager._room_state_get(r)
|
||||
if st and st.get("private"):
|
||||
if not self.hub._is_server_op(peer_hash):
|
||||
if not self.hub.trust_manager.is_server_op(peer_hash):
|
||||
self._emit_notice(outgoing, link, None, f"room {r} is private")
|
||||
return True
|
||||
|
||||
@@ -207,7 +207,7 @@ class CommandHandler:
|
||||
return True
|
||||
|
||||
if cmd == "kline":
|
||||
if not self.hub._is_server_op(peer_hash):
|
||||
if not self.hub.trust_manager.is_server_op(peer_hash):
|
||||
if self.hub.identity is not None:
|
||||
self._emit_error(
|
||||
outgoing,
|
||||
@@ -230,7 +230,8 @@ class CommandHandler:
|
||||
|
||||
op = parts[1].strip().lower()
|
||||
if op == "list":
|
||||
items = sorted(h.hex() for h in self.hub._banned)
|
||||
with self.hub._state_lock:
|
||||
items = sorted(h.hex() for h in self.hub.trust_manager._banned)
|
||||
self._emit_notice(
|
||||
outgoing,
|
||||
link,
|
||||
@@ -261,8 +262,8 @@ class CommandHandler:
|
||||
tsess = self.hub.session_manager.sessions.get(target_link)
|
||||
ph = tsess.get("peer") if tsess else None
|
||||
if isinstance(ph, (bytes, bytearray)):
|
||||
self.hub._banned.add(bytes(ph))
|
||||
self.hub._persist_banned_identities_to_config(link, None, outgoing)
|
||||
self.hub.trust_manager.add_ban(bytes(ph))
|
||||
self.hub.trust_manager.persist_banned_identities_to_config(link, None, outgoing)
|
||||
try:
|
||||
target_link.teardown()
|
||||
except Exception:
|
||||
@@ -285,8 +286,8 @@ class CommandHandler:
|
||||
except Exception as e:
|
||||
self._emit_notice(outgoing, link, None, f"bad identity hash: {e}")
|
||||
return True
|
||||
self.hub._banned.add(h)
|
||||
self.hub._persist_banned_identities_to_config(link, None, outgoing)
|
||||
self.hub.trust_manager.add_ban(h)
|
||||
self.hub.trust_manager.persist_banned_identities_to_config(link, None, outgoing)
|
||||
self._emit_notice(outgoing, link, None, f"kline added for {h.hex()}")
|
||||
return True
|
||||
|
||||
@@ -297,9 +298,9 @@ class CommandHandler:
|
||||
self._emit_notice(outgoing, link, None, f"bad identity hash: {e}")
|
||||
return True
|
||||
|
||||
if h in self.hub._banned:
|
||||
self.hub._banned.discard(h)
|
||||
self.hub._persist_banned_identities_to_config(link, None, outgoing)
|
||||
if self.hub.trust_manager.is_banned(h):
|
||||
self.hub.trust_manager.remove_ban(h)
|
||||
self.hub.trust_manager.persist_banned_identities_to_config(link, None, outgoing)
|
||||
self._emit_notice(outgoing, link, None, f"kline removed for {h.hex()}")
|
||||
else:
|
||||
self._emit_notice(outgoing, link, None, f"not klined: {h.hex()}")
|
||||
|
||||
+1
-1
@@ -238,7 +238,7 @@ class RoomManager:
|
||||
"""Check if peer is a room operator."""
|
||||
if peer_hash is None:
|
||||
return False
|
||||
if self.hub._is_server_op(peer_hash):
|
||||
if self.hub.trust_manager.is_server_op(peer_hash):
|
||||
return True
|
||||
st = self._room_state_ensure(room)
|
||||
founder = st.get("founder")
|
||||
|
||||
+12
-97
@@ -31,6 +31,7 @@ from .rooms import RoomManager
|
||||
from .router import MessageRouter, OutgoingList
|
||||
from .session import SessionManager
|
||||
from .stats import StatsManager
|
||||
from .trust import TrustManager
|
||||
from .util import expand_path
|
||||
|
||||
|
||||
@@ -63,14 +64,13 @@ class HubService:
|
||||
|
||||
# Stats manager for metrics and reporting
|
||||
self.stats_manager = StatsManager(self)
|
||||
|
||||
# Trust manager for trusted/banned identities
|
||||
self.trust_manager = TrustManager(self)
|
||||
|
||||
self.identity: RNS.Identity | None = None
|
||||
self.destination: RNS.Destination | None = None
|
||||
|
||||
|
||||
self._trusted: set[bytes] = set()
|
||||
self._banned: set[bytes] = set()
|
||||
|
||||
self._prune_thread: threading.Thread | None = None
|
||||
|
||||
self._ping_thread: threading.Thread | None = None
|
||||
@@ -331,16 +331,10 @@ class HubService:
|
||||
raise RuntimeError("identity_path is not set")
|
||||
self.identity = self._load_identity(self.config.identity_path)
|
||||
|
||||
self._trusted = {
|
||||
self._parse_identity_hash(h)
|
||||
for h in (self.config.trusted_identities or ())
|
||||
if str(h).strip()
|
||||
}
|
||||
self._banned = {
|
||||
self._parse_identity_hash(h)
|
||||
for h in (self.config.banned_identities or ())
|
||||
if str(h).strip()
|
||||
}
|
||||
self.trust_manager.load_from_config(
|
||||
self.config.trusted_identities,
|
||||
self.config.banned_identities,
|
||||
)
|
||||
|
||||
self._load_registered_rooms_from_registry()
|
||||
|
||||
@@ -612,8 +606,8 @@ class HubService:
|
||||
|
||||
with self._state_lock:
|
||||
old_cfg = self.config
|
||||
old_trusted = set(self._trusted)
|
||||
old_banned = set(self._banned)
|
||||
old_trusted = set(self.trust_manager._trusted)
|
||||
old_banned = set(self.trust_manager._banned)
|
||||
old_registry = dict(self.room_manager._room_registry)
|
||||
|
||||
# Stage config parse
|
||||
@@ -661,8 +655,8 @@ class HubService:
|
||||
with self._state_lock:
|
||||
# Apply (all-or-nothing)
|
||||
self.config = new_cfg
|
||||
self._trusted = new_trusted
|
||||
self._banned = new_banned
|
||||
self.trust_manager._trusted = new_trusted
|
||||
self.trust_manager._banned = new_banned
|
||||
self.room_manager._room_registry = new_registry
|
||||
|
||||
# Merge registry into live per-room state (for active rooms).
|
||||
@@ -713,9 +707,6 @@ class HubService:
|
||||
return
|
||||
self.room_manager._room_registry = registry
|
||||
|
||||
def _is_server_op(self, peer_hash: bytes | None) -> bool:
|
||||
return self._is_trusted(peer_hash)
|
||||
|
||||
def _resolve_identity_hash(
|
||||
self, token: str, *, room: str | None = None
|
||||
) -> bytes | None:
|
||||
@@ -806,82 +797,6 @@ class HubService:
|
||||
return None
|
||||
return expand_path(str(p))
|
||||
|
||||
def _persist_banned_identities_to_config(
|
||||
self,
|
||||
link: RNS.Link,
|
||||
room: str | None,
|
||||
outgoing: list[tuple[RNS.Link, bytes]] | None = None,
|
||||
) -> None:
|
||||
cfg_path = self._config_path_for_writes()
|
||||
if not cfg_path:
|
||||
self._emit_notice(
|
||||
outgoing, link, room, "ban updated (not persisted; no config_path)"
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
from tomlkit import dumps, parse, table # type: ignore
|
||||
except Exception:
|
||||
self._emit_notice(
|
||||
outgoing,
|
||||
link,
|
||||
room,
|
||||
"ban updated (not persisted; missing dependency tomlkit)",
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
with self._config_write_lock:
|
||||
st = None
|
||||
try:
|
||||
st = os.stat(cfg_path)
|
||||
except Exception:
|
||||
st = None
|
||||
|
||||
with open(cfg_path, encoding="utf-8") as f:
|
||||
doc = parse(f.read())
|
||||
|
||||
hub = doc.get("hub")
|
||||
if hub is None:
|
||||
hub = table()
|
||||
doc["hub"] = hub
|
||||
|
||||
existing = hub.get("banned_identities")
|
||||
existing_list: list[str] = []
|
||||
if isinstance(existing, list):
|
||||
for x in existing:
|
||||
if x is None:
|
||||
continue
|
||||
sx = str(x).strip().lower()
|
||||
if sx.startswith("0x"):
|
||||
sx = sx[2:]
|
||||
if sx:
|
||||
existing_list.append(sx)
|
||||
|
||||
merged = set(existing_list)
|
||||
merged.update(h.hex() for h in sorted(self._banned))
|
||||
hub["banned_identities"] = sorted(merged)
|
||||
|
||||
new_text = dumps(doc)
|
||||
with open(cfg_path, "w", encoding="utf-8") as f:
|
||||
f.write(new_text)
|
||||
|
||||
if st is not None:
|
||||
try:
|
||||
os.chmod(cfg_path, st.st_mode)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
self._emit_notice(
|
||||
outgoing, link, room, f"ban updated (persist failed: {e})"
|
||||
)
|
||||
|
||||
def _is_trusted(self, peer_hash: bytes | None) -> bool:
|
||||
if not peer_hash:
|
||||
return False
|
||||
with self._state_lock:
|
||||
return peer_hash in self._trusted
|
||||
|
||||
def _notice_to(self, link: RNS.Link, room: str | None, text: str) -> None:
|
||||
if self.identity is None:
|
||||
return
|
||||
|
||||
+3
-2
@@ -90,8 +90,9 @@ class StatsManager:
|
||||
memberships = room_stats["memberships"]
|
||||
top_rooms = room_stats["top_rooms"]
|
||||
|
||||
trusted_count = len(self.hub._trusted)
|
||||
banned_count = len(self.hub._banned)
|
||||
trust_stats = self.hub.trust_manager.get_stats()
|
||||
trusted_count = trust_stats["trusted_count"]
|
||||
banned_count = trust_stats["banned_count"]
|
||||
c = dict(self._counters)
|
||||
|
||||
lines: list[str] = []
|
||||
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
"""Trust and ban management for the RRC hub."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import RNS
|
||||
from .service import HubService
|
||||
|
||||
|
||||
class TrustManager:
|
||||
"""
|
||||
Manages trusted and banned identities for the hub.
|
||||
|
||||
Handles:
|
||||
- Trusted identity lists (server operators)
|
||||
- Banned identity lists
|
||||
- Persistence of ban list to config
|
||||
- Trust/ban checks
|
||||
"""
|
||||
|
||||
def __init__(self, hub: HubService) -> None:
|
||||
self.hub = hub
|
||||
self.log = hub.log
|
||||
|
||||
self._trusted: set[bytes] = set()
|
||||
self._banned: set[bytes] = set()
|
||||
|
||||
def load_from_config(self, trusted_list: list[str] | None, banned_list: list[str] | None) -> None:
|
||||
"""Load trusted and banned identities from config lists."""
|
||||
self._trusted = {
|
||||
self.hub._parse_identity_hash(h)
|
||||
for h in (trusted_list or ())
|
||||
if str(h).strip()
|
||||
}
|
||||
self._banned = {
|
||||
self.hub._parse_identity_hash(h)
|
||||
for h in (banned_list or ())
|
||||
if str(h).strip()
|
||||
}
|
||||
|
||||
def is_trusted(self, peer_hash: bytes | None) -> bool:
|
||||
"""Check if a peer identity is in the trusted list."""
|
||||
if not peer_hash:
|
||||
return False
|
||||
with self.hub._state_lock:
|
||||
return peer_hash in self._trusted
|
||||
|
||||
def is_server_op(self, peer_hash: bytes | None) -> bool:
|
||||
"""Check if a peer is a server operator (currently same as trusted)."""
|
||||
return self.is_trusted(peer_hash)
|
||||
|
||||
def is_banned(self, peer_hash: bytes | None) -> bool:
|
||||
"""Check if a peer identity is in the banned list."""
|
||||
if not peer_hash:
|
||||
return False
|
||||
with self.hub._state_lock:
|
||||
return peer_hash in self._banned
|
||||
|
||||
def add_ban(self, peer_hash: bytes) -> None:
|
||||
"""Add a peer identity to the banned list."""
|
||||
with self.hub._state_lock:
|
||||
self._banned.add(peer_hash)
|
||||
|
||||
def remove_ban(self, peer_hash: bytes) -> None:
|
||||
"""Remove a peer identity from the banned list."""
|
||||
with self.hub._state_lock:
|
||||
self._banned.discard(peer_hash)
|
||||
|
||||
def get_stats(self) -> dict[str, int]:
|
||||
"""Get statistics about trusted and banned identities."""
|
||||
with self.hub._state_lock:
|
||||
return {
|
||||
"trusted_count": len(self._trusted),
|
||||
"banned_count": len(self._banned),
|
||||
}
|
||||
|
||||
def update_from_config(self, trusted_list: list[str] | None, banned_list: list[str] | None) -> tuple[set[bytes], set[bytes]]:
|
||||
"""
|
||||
Update trusted and banned lists from config.
|
||||
Returns the old (trusted, banned) sets for comparison.
|
||||
"""
|
||||
with self.hub._state_lock:
|
||||
old_trusted = set(self._trusted)
|
||||
old_banned = set(self._banned)
|
||||
|
||||
new_trusted = {
|
||||
self.hub._parse_identity_hash(h)
|
||||
for h in (trusted_list or ())
|
||||
if str(h).strip()
|
||||
}
|
||||
new_banned = {
|
||||
self.hub._parse_identity_hash(h)
|
||||
for h in (banned_list or ())
|
||||
if str(h).strip()
|
||||
}
|
||||
|
||||
with self.hub._state_lock:
|
||||
self._trusted = new_trusted
|
||||
self._banned = new_banned
|
||||
|
||||
return old_trusted, old_banned
|
||||
|
||||
def persist_banned_identities_to_config(
|
||||
self,
|
||||
link: RNS.Link,
|
||||
room: str | None,
|
||||
outgoing: list[tuple[RNS.Link, bytes]] | None = None,
|
||||
) -> None:
|
||||
"""Persist the current banned identities list to the config file."""
|
||||
cfg_path = self.hub._config_path_for_writes()
|
||||
if not cfg_path:
|
||||
self.hub._emit_notice(
|
||||
outgoing, link, room, "ban updated (not persisted; no config_path)"
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
from tomlkit import dumps, parse, table # type: ignore
|
||||
except Exception:
|
||||
self.hub._emit_notice(
|
||||
outgoing,
|
||||
link,
|
||||
room,
|
||||
"ban updated (not persisted; missing dependency tomlkit)",
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
with self.hub._config_write_lock:
|
||||
st = None
|
||||
try:
|
||||
st = os.stat(cfg_path)
|
||||
except Exception:
|
||||
st = None
|
||||
|
||||
with open(cfg_path, encoding="utf-8") as f:
|
||||
doc = parse(f.read())
|
||||
|
||||
hub = doc.get("hub")
|
||||
if hub is None:
|
||||
hub = table()
|
||||
doc["hub"] = hub
|
||||
|
||||
existing = hub.get("banned_identities")
|
||||
existing_list: list[str] = []
|
||||
if isinstance(existing, list):
|
||||
for x in existing:
|
||||
if x is None:
|
||||
continue
|
||||
sx = str(x).strip().lower()
|
||||
if sx.startswith("0x"):
|
||||
sx = sx[2:]
|
||||
if sx:
|
||||
existing_list.append(sx)
|
||||
|
||||
with self.hub._state_lock:
|
||||
merged = set(existing_list)
|
||||
merged.update(h.hex() for h in sorted(self._banned))
|
||||
hub["banned_identities"] = sorted(merged)
|
||||
|
||||
new_text = dumps(doc)
|
||||
with open(cfg_path, "w", encoding="utf-8") as f:
|
||||
f.write(new_text)
|
||||
|
||||
if st is not None:
|
||||
try:
|
||||
os.chmod(cfg_path, st.st_mode)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
self.hub._emit_notice(
|
||||
outgoing, link, room, f"ban updated (persist failed: {e})"
|
||||
)
|
||||
Reference in New Issue
Block a user