From 02edfd7e3885c88b62a9a81cebd602b8359753c9 Mon Sep 17 00:00:00 2001 From: kc1awv Date: Thu, 15 Jan 2026 15:43:35 -0500 Subject: [PATCH] update nickname and room name limits to use byte size instead of character count --- rrcd/cli.py | 26 +++++++++++++++++--------- rrcd/config.py | 4 ++-- rrcd/router.py | 16 ++++++++-------- rrcd/service.py | 16 ++++++++++------ rrcd/session.py | 14 +++++++++++--- rrcd/stats.py | 4 ++-- rrcd/util.py | 21 +++++++++++---------- 7 files changed, 61 insertions(+), 40 deletions(-) diff --git a/rrcd/cli.py b/rrcd/cli.py index 6d39a47..f9bdd01 100644 --- a/rrcd/cli.py +++ b/rrcd/cli.py @@ -146,10 +146,6 @@ room_invite_timeout_s = 900.0 # Optional behaviors. include_joined_member_list = false -# Nickname policy. -# Maximum accepted nickname length (Unicode characters). 0 disables length limiting. -nick_max_chars = 32 - # Limits. # These limits help mitigate abuse and resource exhaustion, but can be adjusted # based on your use case. @@ -157,9 +153,10 @@ nick_max_chars = 32 # N.B. max_msg_body_bytes should not allow messages so large that they cannot # fit within the link MTU after UTF-8 encoding and envelope overhead. The # default of 350 bytes is a safe choice for the default Reticulum MTU of 500. -max_rooms_per_session = 32 -max_room_name_len = 64 +max_nick_bytes = 32 +max_room_name_bytes = 64 max_msg_body_bytes = 350 +max_rooms_per_session = 32 rate_limit_msgs_per_minute = 240 # Hub-initiated liveness checks (0 disables). @@ -330,7 +327,16 @@ def _build_arg_parser() -> argparse.ArgumentParser: p.add_argument("--max-rooms", type=int, default=None, help="Max rooms per session") p.add_argument( - "--max-room-name-len", type=int, default=None, help="Max room name length" + "--max-nick-bytes", + type=int, + default=None, + help="Max nickname size in UTF-8 bytes", + ) + p.add_argument( + "--max-room-name-bytes", + type=int, + default=None, + help="Max room name size in UTF-8 bytes", ) p.add_argument( @@ -426,8 +432,10 @@ def main(argv: list[str] | None = None) -> None: if args.max_rooms is not None: cfg = replace(cfg, max_rooms_per_session=int(args.max_rooms)) - if args.max_room_name_len is not None: - cfg = replace(cfg, max_room_name_len=int(args.max_room_name_len)) + if args.max_nick_bytes is not None: + cfg = replace(cfg, max_nick_bytes=int(args.max_nick_bytes)) + if args.max_room_name_bytes is not None: + cfg = replace(cfg, max_room_name_bytes=int(args.max_room_name_bytes)) if args.rate_limit_msgs_per_minute is not None: cfg = replace( diff --git a/rrcd/config.py b/rrcd/config.py index 8c9429c..00ba6ec 100644 --- a/rrcd/config.py +++ b/rrcd/config.py @@ -25,9 +25,9 @@ class HubRuntimeConfig: room_registry_prune_interval_s: float = 3600.0 room_invite_timeout_s: float = 900.0 include_joined_member_list: bool = False - nick_max_chars: int = 32 + max_nick_bytes: int = 32 max_rooms_per_session: int = 32 - max_room_name_len: int = 64 + max_room_name_bytes: int = 64 max_msg_body_bytes: int = 350 rate_limit_msgs_per_minute: int = 240 ping_interval_s: float = 0.0 diff --git a/rrcd/router.py b/rrcd/router.py index 8b6d135..3704bdd 100644 --- a/rrcd/router.py +++ b/rrcd/router.py @@ -300,7 +300,7 @@ class MessageRouter: new_nick = None if isinstance(nick, str): - n = normalize_nick(nick, max_chars=self.hub.config.nick_max_chars) + n = normalize_nick(nick, max_bytes=self.hub.config.max_nick_bytes) if n is not None: new_nick = n sess["nick"] = n @@ -311,7 +311,7 @@ class MessageRouter: if new_nick is None: legacy_nick = body.get(B_HELLO_NICK_LEGACY) n2 = normalize_nick( - legacy_nick, max_chars=self.hub.config.nick_max_chars + legacy_nick, max_bytes=self.hub.config.max_nick_bytes ) if n2 is not None: new_nick = n2 @@ -360,7 +360,7 @@ class MessageRouter: new_nick = None if isinstance(nick, str): - n = normalize_nick(nick, max_chars=self.hub.config.nick_max_chars) + n = normalize_nick(nick, max_bytes=self.hub.config.max_nick_bytes) if n is not None: new_nick = n sess["nick"] = n @@ -370,7 +370,7 @@ class MessageRouter: if new_nick is None: legacy_nick = body.get(B_HELLO_NICK_LEGACY) n2 = normalize_nick( - legacy_nick, max_chars=self.hub.config.nick_max_chars + legacy_nick, max_bytes=self.hub.config.max_nick_bytes ) if n2 is not None: new_nick = n2 @@ -686,10 +686,10 @@ class MessageRouter: text="message requires room name", ) return - + # Validate message body size (UTF-8 bytes) if isinstance(body, str): - body_bytes = len(body.encode('utf-8', errors='replace')) + body_bytes = len(body.encode("utf-8", errors="replace")) if body_bytes > self.hub.config.max_msg_body_bytes: if self.hub.identity is not None: self.hub.message_helper.emit_error( @@ -781,7 +781,7 @@ class MessageRouter: incoming_nick = env.get(K_NICK) if incoming_nick is not None: - n = normalize_nick(incoming_nick, max_chars=self.hub.config.nick_max_chars) + n = normalize_nick(incoming_nick, max_bytes=self.hub.config.max_nick_bytes) if n is not None: old_session_nick = sess.get("nick") if old_session_nick != n: @@ -794,7 +794,7 @@ class MessageRouter: env.pop(K_NICK, None) else: nick = sess.get("nick") - n = normalize_nick(nick, max_chars=self.hub.config.nick_max_chars) + n = normalize_nick(nick, max_bytes=self.hub.config.max_nick_bytes) if n is not None: env[K_NICK] = n diff --git a/rrcd/service.py b/rrcd/service.py index 316e072..7f43436 100644 --- a/rrcd/service.py +++ b/rrcd/service.py @@ -97,10 +97,10 @@ class HubService: self.destination.hash.hex() if self.destination else "-", ) self.log.info( - "Policy nick_max_chars=%s max_rooms=%s max_room_name_len=%s rate_limit_msgs_per_minute=%s", - self.config.nick_max_chars, + "Policy max_nick_bytes=%s max_rooms=%s max_room_name_bytes=%s rate_limit_msgs_per_minute=%s", + self.config.max_nick_bytes, self.config.max_rooms_per_session, - self.config.max_room_name_len, + self.config.max_room_name_bytes, self.config.rate_limit_msgs_per_minute, ) @@ -330,7 +330,7 @@ class HubService: f"banned={len(old_banned)}->{len(new_banned)} " f"registered_rooms={len(old_registry)}->{len(new_registry)}" ) - lines.append(f"policy: nick_max_chars={new_cfg.nick_max_chars}") + lines.append(f"policy: max_nick_bytes={new_cfg.max_nick_bytes}") if cfg_changes: lines.append("config_changes:") @@ -503,8 +503,12 @@ class HubService: r = room.strip().lower() if not r: raise ValueError("room name must not be empty") - if len(r) > int(self.config.max_room_name_len): - raise ValueError("room name too long") + # Check UTF-8 byte length + room_bytes = len(r.encode("utf-8", errors="replace")) + if room_bytes > int(self.config.max_room_name_bytes): + raise ValueError( + f"room name too long: {room_bytes} bytes > {self.config.max_room_name_bytes} bytes" + ) return r def _on_packet(self, link: RNS.Link, data: bytes) -> None: diff --git a/rrcd/session.py b/rrcd/session.py index 4939062..3a9c2c7 100644 --- a/rrcd/session.py +++ b/rrcd/session.py @@ -135,16 +135,24 @@ class SessionManager: self.hub.room_manager.remove_member(room, link) if remaining_members and peer_hash and self.hub.identity: - notification_body = [peer_hash] if self.hub.config.include_joined_member_list else None + notification_body = ( + [peer_hash] if self.hub.config.include_joined_member_list else None + ) member_notification = make_envelope( - T_PARTED, src=self.hub.identity.hash, room=room, body=notification_body + T_PARTED, + src=self.hub.identity.hash, + room=room, + body=notification_body, ) member_notification_payload = encode(member_notification) for member_link in remaining_members: try: import RNS + RNS.Packet(member_link, member_notification_payload).send() - self.hub.stats_manager.inc("bytes_out", len(member_notification_payload)) + self.hub.stats_manager.inc( + "bytes_out", len(member_notification_payload) + ) except Exception: pass diff --git a/rrcd/stats.py b/rrcd/stats.py index 56fcb0e..bcc506e 100644 --- a/rrcd/stats.py +++ b/rrcd/stats.py @@ -109,8 +109,8 @@ class StatsManager: lines.append( f"limits: rate_limit_msgs_per_minute={self.hub.config.rate_limit_msgs_per_minute} " f"max_rooms_per_session={self.hub.config.max_rooms_per_session} " - f"max_room_name_len={self.hub.config.max_room_name_len} " - f"nick_max_chars={self.hub.config.nick_max_chars}" + f"max_room_name_bytes={self.hub.config.max_room_name_bytes} " + f"max_nick_bytes={self.hub.config.max_nick_bytes}" ) lines.append( f"features: ping_interval_s={self.hub.config.ping_interval_s} " diff --git a/rrcd/util.py b/rrcd/util.py index 6afbe29..e2702f2 100644 --- a/rrcd/util.py +++ b/rrcd/util.py @@ -2,14 +2,14 @@ from __future__ import annotations import os -_DEFAULT_NICK_MAX_CHARS = 32 +_DEFAULT_NICK_MAX_BYTES = 32 def expand_path(p: str) -> str: return os.path.expanduser(os.path.expandvars(p)) -def normalize_nick(value, *, max_chars: int = _DEFAULT_NICK_MAX_CHARS) -> str | None: +def normalize_nick(value, *, max_bytes: int = _DEFAULT_NICK_MAX_BYTES) -> str | None: if not isinstance(value, str): return None @@ -18,19 +18,20 @@ def normalize_nick(value, *, max_chars: int = _DEFAULT_NICK_MAX_CHARS) -> str | return None try: - limit = int(max_chars) + limit = int(max_bytes) except Exception: - limit = int(_DEFAULT_NICK_MAX_CHARS) + limit = int(_DEFAULT_NICK_MAX_BYTES) - if limit > 0 and len(s) > limit: + # Check UTF-8 byte length + try: + encoded = s.encode("utf-8", "strict") + except UnicodeError: + return None + + if limit > 0 and len(encoded) > limit: return None if "\n" in s or "\r" in s or "\x00" in s: return None - try: - s.encode("utf-8", "strict") - except UnicodeError: - return None - return s