mirror of
https://github.com/kc1awv/rrcd.git
synced 2026-06-08 06:01:54 -07:00
enforce fixed hub destination namespace
This commit is contained in:
@@ -11,6 +11,8 @@ This project follows the versioning policy in VERSIONING.md.
|
||||
the existing-member fanout notifications
|
||||
- Added focused test coverage for JOINED/PARTED fanout nick hints, including
|
||||
disconnect-driven `PARTED` notifications
|
||||
- Hardened hub discovery by fixing the advertised destination namespace to
|
||||
`rrc.hub` and removing config or CLI overrides for that value
|
||||
|
||||
## 0.3.1 - 2026-05-17
|
||||
|
||||
|
||||
@@ -37,8 +37,11 @@ a default identity at `~/.rrcd/hub_identity`, plus a room registry at
|
||||
To override the default state directory (`~/.rrcd/`), set `RRCD_HOME`, e.g.
|
||||
`RRCD_HOME=/tmp/rrcd rrcd`.
|
||||
|
||||
To specify a custom identity and destination name:
|
||||
- `rrcd --identity ~/.rrcd/hub_identity --dest-name rrc.hub`
|
||||
To specify a custom identity path:
|
||||
- `rrcd --identity ~/.rrcd/hub_identity`
|
||||
|
||||
The hub destination namespace is fixed at `rrc.hub` so clients can discover
|
||||
hubs consistently.
|
||||
|
||||
Optional:
|
||||
|
||||
|
||||
+4
-66
@@ -3,12 +3,13 @@ from __future__ import annotations
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
from dataclasses import asdict, replace
|
||||
from dataclasses import replace
|
||||
from pathlib import Path
|
||||
|
||||
import RNS
|
||||
|
||||
from .config import HubRuntimeConfig
|
||||
from .constants import HUB_DEST_NAME
|
||||
from .logging_config import configure_logging
|
||||
from .paths import (
|
||||
default_config_path,
|
||||
@@ -19,62 +20,6 @@ from .paths import (
|
||||
from .service import HubService
|
||||
|
||||
|
||||
def _load_toml(path: str) -> dict:
|
||||
import tomllib
|
||||
|
||||
with open(path, "rb") as f:
|
||||
return tomllib.load(f)
|
||||
|
||||
|
||||
def _apply_config_file(cfg: HubRuntimeConfig, path: str) -> HubRuntimeConfig:
|
||||
data = _load_toml(path)
|
||||
|
||||
hub = data.get("hub") if isinstance(data, dict) else None
|
||||
if isinstance(hub, dict):
|
||||
data = {**data, **hub}
|
||||
|
||||
log_table = data.get("logging") if isinstance(data, dict) else None
|
||||
if isinstance(log_table, dict):
|
||||
mapped: dict[str, object] = {}
|
||||
if "level" in log_table:
|
||||
mapped["log_level"] = log_table.get("level")
|
||||
if "rns_level" in log_table:
|
||||
mapped["log_rns_level"] = log_table.get("rns_level")
|
||||
if "console" in log_table:
|
||||
mapped["log_console"] = log_table.get("console")
|
||||
if "file" in log_table:
|
||||
mapped["log_file"] = log_table.get("file")
|
||||
if "format" in log_table:
|
||||
mapped["log_format"] = log_table.get("format")
|
||||
if "datefmt" in log_table:
|
||||
mapped["log_datefmt"] = log_table.get("datefmt")
|
||||
data = {**data, **mapped}
|
||||
|
||||
allowed = set(asdict(cfg).keys())
|
||||
# This identifies where to reload/persist from; do not let the file override it.
|
||||
allowed.discard("config_path")
|
||||
updates = {k: v for k, v in data.items() if k in allowed}
|
||||
|
||||
for list_key in ("trusted_identities", "banned_identities"):
|
||||
if list_key in updates and isinstance(updates[list_key], list):
|
||||
updates[list_key] = tuple(str(x) for x in updates[list_key])
|
||||
|
||||
if "announce" in data and "announce_on_start" not in updates:
|
||||
try:
|
||||
updates["announce_on_start"] = bool(data["announce"])
|
||||
except Exception:
|
||||
pass
|
||||
if "configdir" in updates and updates["configdir"] == "":
|
||||
updates["configdir"] = None
|
||||
if "greeting" in updates and updates["greeting"] == "":
|
||||
updates["greeting"] = None
|
||||
if "log_file" in updates and updates["log_file"] == "":
|
||||
updates["log_file"] = None
|
||||
if "log_datefmt" in updates and updates["log_datefmt"] == "":
|
||||
updates["log_datefmt"] = None
|
||||
return replace(cfg, **updates) if updates else cfg
|
||||
|
||||
|
||||
def _write_default_config(config_path: str, identity_path: str) -> None:
|
||||
cfg_dir = os.path.dirname(config_path)
|
||||
if cfg_dir:
|
||||
@@ -105,8 +50,8 @@ identity_path = {identity_path!r}
|
||||
# A running hub can reload both rrcd.toml and rooms.toml with the /reload command.
|
||||
room_registry_path = {room_registry_path!r}
|
||||
|
||||
# Destination name to host the hub on.
|
||||
dest_name = "rrc.hub"
|
||||
# The hub destination namespace is fixed for client discovery.
|
||||
# Hubs always announce on {HUB_DEST_NAME!r}.
|
||||
|
||||
# Announcing (Reticulum destination announces)
|
||||
#
|
||||
@@ -297,10 +242,6 @@ def _build_arg_parser() -> argparse.ArgumentParser:
|
||||
default=str(default_room_registry_path()),
|
||||
help="Path to separate room registry TOML (created on first run)",
|
||||
)
|
||||
p.add_argument(
|
||||
"--dest-name", default=None, help="Destination app name (default: rrc.hub)"
|
||||
)
|
||||
|
||||
p.add_argument(
|
||||
"--no-announce",
|
||||
action="store_true",
|
||||
@@ -414,9 +355,6 @@ def main(argv: list[str] | None = None) -> None:
|
||||
data = temp_mgr.load_toml(config_path)
|
||||
cfg = temp_mgr.apply_config_data(cfg, data)
|
||||
|
||||
if args.dest_name is not None:
|
||||
cfg = replace(cfg, dest_name=args.dest_name)
|
||||
|
||||
if args.no_announce:
|
||||
cfg = replace(cfg, announce_on_start=False)
|
||||
if args.announce_period is not None:
|
||||
|
||||
+4
-1
@@ -4,6 +4,8 @@ import threading
|
||||
from dataclasses import asdict, dataclass, replace
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from .constants import HUB_DEST_NAME
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .service import HubService
|
||||
|
||||
@@ -14,7 +16,7 @@ class HubRuntimeConfig:
|
||||
room_registry_path: str | None = None
|
||||
configdir: str | None = None
|
||||
identity_path: str | None = None
|
||||
dest_name: str = "rrc.hub"
|
||||
dest_name: str = HUB_DEST_NAME
|
||||
announce_on_start: bool = True
|
||||
announce_period_s: float = 0.0
|
||||
hub_name: str = "rrc"
|
||||
@@ -94,6 +96,7 @@ class ConfigManager:
|
||||
|
||||
allowed = set(asdict(base).keys())
|
||||
allowed.discard("config_path")
|
||||
allowed.discard("dest_name")
|
||||
|
||||
updates = {k: v for k, v in data.items() if k in allowed}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# RRC protocol constants (numeric keys and message types)
|
||||
|
||||
RRC_VERSION = 1
|
||||
HUB_DEST_NAME = "rrc.hub"
|
||||
|
||||
# Envelope keys
|
||||
K_V = 0
|
||||
|
||||
+10
-5
@@ -5,6 +5,7 @@ import os
|
||||
import signal
|
||||
import threading
|
||||
import time
|
||||
from dataclasses import replace
|
||||
from typing import Any
|
||||
|
||||
import RNS
|
||||
@@ -13,6 +14,7 @@ from .codec import encode
|
||||
from .commands import CommandHandler
|
||||
from .config import ConfigManager, HubRuntimeConfig
|
||||
from .constants import (
|
||||
HUB_DEST_NAME,
|
||||
T_PING,
|
||||
)
|
||||
from .envelope import make_envelope
|
||||
@@ -29,7 +31,11 @@ from .util import expand_path
|
||||
|
||||
class HubService:
|
||||
def __init__(self, config: HubRuntimeConfig) -> None:
|
||||
self.config = config
|
||||
self.config = (
|
||||
replace(config, dest_name=HUB_DEST_NAME)
|
||||
if config.dest_name != HUB_DEST_NAME
|
||||
else config
|
||||
)
|
||||
self.log = logging.getLogger("rrcd.hub")
|
||||
self._state_lock = threading.RLock()
|
||||
self._shutdown = threading.Event()
|
||||
@@ -66,9 +72,9 @@ class HubService:
|
||||
|
||||
self._load_registered_rooms_from_registry()
|
||||
|
||||
parts = [p for p in str(self.config.dest_name).split(".") if p]
|
||||
parts = [p for p in HUB_DEST_NAME.split(".") if p]
|
||||
if not parts:
|
||||
raise ValueError("dest_name must not be empty")
|
||||
raise ValueError("HUB_DEST_NAME must not be empty")
|
||||
app_name, aspects = parts[0], parts[1:]
|
||||
|
||||
self.destination = RNS.Destination(
|
||||
@@ -92,8 +98,7 @@ class HubService:
|
||||
self._announce_thread.start()
|
||||
|
||||
self.log.info(
|
||||
"Hub running dest_name=%s dest_hash=%s",
|
||||
self.config.dest_name,
|
||||
"Hub running at dest_hash=%s",
|
||||
self.destination.hash.hex() if self.destination else "-",
|
||||
)
|
||||
self.log.info(
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from rrcd.cli import _build_arg_parser, _write_default_config
|
||||
from rrcd.constants import HUB_DEST_NAME
|
||||
|
||||
|
||||
def test_arg_parser_rejects_dest_name_override() -> None:
|
||||
parser = _build_arg_parser()
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
parser.parse_args(["--dest-name", "custom.hub"])
|
||||
|
||||
|
||||
def test_default_config_does_not_emit_dest_name_field(tmp_path: Path) -> None:
|
||||
config_path = tmp_path / "rrcd.toml"
|
||||
identity_path = tmp_path / "hub_identity"
|
||||
|
||||
_write_default_config(str(config_path), str(identity_path))
|
||||
|
||||
content = config_path.read_text(encoding="utf-8")
|
||||
assert "dest_name =" not in content
|
||||
assert f"Hubs always announce on {HUB_DEST_NAME!r}." in content
|
||||
@@ -0,0 +1,36 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from rrcd.config import ConfigManager, HubRuntimeConfig
|
||||
from rrcd.constants import HUB_DEST_NAME
|
||||
from rrcd.service import HubService
|
||||
|
||||
|
||||
class _FakeHub:
|
||||
def __init__(self) -> None:
|
||||
self.log = logging.getLogger("test")
|
||||
self.config = HubRuntimeConfig()
|
||||
|
||||
|
||||
def test_apply_config_data_ignores_dest_name_override() -> None:
|
||||
manager = ConfigManager(_FakeHub())
|
||||
base = HubRuntimeConfig()
|
||||
|
||||
updated = manager.apply_config_data(
|
||||
base,
|
||||
{
|
||||
"dest_name": "custom.hub",
|
||||
"hub": {"dest_name": "custom.hub"},
|
||||
"hub_name": "custom-name",
|
||||
},
|
||||
)
|
||||
|
||||
assert updated.dest_name == HUB_DEST_NAME
|
||||
assert updated.hub_name == "custom-name"
|
||||
|
||||
|
||||
def test_service_normalizes_custom_dest_name() -> None:
|
||||
service = HubService(HubRuntimeConfig(dest_name="custom.hub"))
|
||||
|
||||
assert service.config.dest_name == HUB_DEST_NAME
|
||||
Reference in New Issue
Block a user