mirror of
https://github.com/pyMC-dev/pyMC_Repeater.git
synced 2026-08-08 09:52:55 +02:00
Add PathHelper for processing PATH packets and update routing logic
This commit is contained in:
@@ -5,5 +5,6 @@ from .discovery import DiscoveryHelper
|
||||
from .advert import AdvertHelper
|
||||
from .login import LoginHelper
|
||||
from .text import TextHelper
|
||||
from .path import PathHelper
|
||||
|
||||
__all__ = ["TraceHelper", "DiscoveryHelper", "AdvertHelper", "LoginHelper", "TextHelper"]
|
||||
__all__ = ["TraceHelper", "DiscoveryHelper", "AdvertHelper", "LoginHelper", "TextHelper", "PathHelper"]
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import logging
|
||||
import time
|
||||
|
||||
logger = logging.getLogger("PathHelper")
|
||||
|
||||
|
||||
class PathHelper:
|
||||
def __init__(self, acl_dict=None, log_fn=None):
|
||||
|
||||
self.acl_dict = acl_dict or {}
|
||||
self.log_fn = log_fn or logger.info
|
||||
|
||||
async def process_path_packet(self, packet):
|
||||
|
||||
from pymc_core.protocol.crypto import CryptoUtils
|
||||
|
||||
try:
|
||||
if len(packet.payload) < 2:
|
||||
return False
|
||||
|
||||
dest_hash = packet.payload[0]
|
||||
src_hash = packet.payload[1]
|
||||
|
||||
# Get the ACL for this destination identity
|
||||
identity_acl = self.acl_dict.get(dest_hash)
|
||||
if not identity_acl:
|
||||
logger.debug(f"No ACL for dest 0x{dest_hash:02X}, allowing forward")
|
||||
return False
|
||||
|
||||
# Find the client by source hash
|
||||
client = None
|
||||
for client_info in identity_acl.get_all_clients():
|
||||
pubkey = client_info.id.get_public_key()
|
||||
if pubkey[0] == src_hash:
|
||||
client = client_info
|
||||
break
|
||||
|
||||
if not client:
|
||||
logger.debug(f"PATH packet from unknown client 0x{src_hash:02X}, allowing forward")
|
||||
return False
|
||||
|
||||
# Get shared secret for decryption
|
||||
shared_secret = client.shared_secret
|
||||
if not shared_secret or len(shared_secret) == 0:
|
||||
logger.debug(f"No shared secret for client 0x{src_hash:02X}, cannot decrypt PATH")
|
||||
return False
|
||||
|
||||
# Decrypt the PATH packet payload
|
||||
# Payload format: dest_hash(1) + src_hash(1) + mac(2) + encrypted_data
|
||||
if len(packet.payload) < 4:
|
||||
logger.debug(f"PATH packet too short: {len(packet.payload)} bytes")
|
||||
return False
|
||||
|
||||
mac_and_data = packet.payload[2:] # Skip dest_hash and src_hash
|
||||
decrypted = CryptoUtils.mac_then_decrypt(shared_secret, mac_and_data)
|
||||
|
||||
if not decrypted:
|
||||
logger.debug(f"Failed to decrypt PATH packet from 0x{src_hash:02X}")
|
||||
return False
|
||||
|
||||
# Parse decrypted PATH data
|
||||
# Format: path_len(1) + path[path_len] + extra_type(1) + extra[...]
|
||||
if len(decrypted) < 1:
|
||||
logger.debug(f"Decrypted PATH data too short")
|
||||
return False
|
||||
|
||||
path_len = decrypted[0]
|
||||
if len(decrypted) < 1 + path_len:
|
||||
logger.debug(f"PATH data truncated: need {1 + path_len} bytes, got {len(decrypted)}")
|
||||
return False
|
||||
|
||||
path_data = decrypted[1:1 + path_len]
|
||||
|
||||
# Update client's out_path (same as C++ memcpy)
|
||||
client.out_path = bytearray(path_data)
|
||||
client.out_path_len = path_len
|
||||
client.last_activity = int(time.time())
|
||||
|
||||
logger.info(
|
||||
f"Updated out_path for client 0x{src_hash:02X} -> 0x{dest_hash:02X}: "
|
||||
f"path_len={path_len}, path={[hex(b) for b in path_data]}"
|
||||
)
|
||||
|
||||
# Don't mark as do_not_retransmit - let it forward normally
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing PATH packet: {e}", exc_info=True)
|
||||
return False
|
||||
@@ -52,14 +52,18 @@ class RepeaterCLI:
|
||||
if not is_admin:
|
||||
return "Error: Admin permission required"
|
||||
|
||||
logger.debug(f"handle_command received: '{command}' (len={len(command)})")
|
||||
|
||||
# Extract optional sequence prefix (XX|)
|
||||
prefix = ""
|
||||
if len(command) > 4 and command[2] == '|':
|
||||
prefix = command[:3]
|
||||
command = command[3:]
|
||||
logger.debug(f"Extracted prefix: '{prefix}', remaining command: '{command}'")
|
||||
|
||||
# Strip leading/trailing whitespace
|
||||
command = command.strip()
|
||||
logger.debug(f"After strip: '{command}'")
|
||||
|
||||
# Route to appropriate handler
|
||||
reply = self._route_command(command)
|
||||
@@ -203,6 +207,7 @@ class RepeaterCLI:
|
||||
def _cmd_get(self, param: str) -> str:
|
||||
"""Handle get commands."""
|
||||
param = param.strip()
|
||||
logger.debug(f"_cmd_get called with param: '{param}' (len={len(param)})")
|
||||
|
||||
if param == "af":
|
||||
af = self.repeater_config.get('airtime_factor', 1.0)
|
||||
|
||||
@@ -120,41 +120,7 @@ class TextHelper:
|
||||
f"dest=0x{dest_hash:02X}, src=0x{src_hash:02X}"
|
||||
)
|
||||
|
||||
# Check if this is a CLI command to the repeater
|
||||
if dest_hash == self.repeater_hash and self.cli:
|
||||
# Try to extract message text
|
||||
try:
|
||||
# Assume payload format: [dest_hash, src_hash, ...message...]
|
||||
message_bytes = packet.payload[2:]
|
||||
message_text = message_bytes.decode('utf-8', errors='ignore').strip()
|
||||
|
||||
# Check if it's a CLI command
|
||||
if self._is_cli_command(message_text):
|
||||
# Check admin permission
|
||||
is_admin = self._check_admin_permission(src_hash)
|
||||
|
||||
# Get sender's public key for CLI
|
||||
sender_pubkey = bytes([src_hash]) + b'\x00' * 31 # Placeholder
|
||||
|
||||
# Handle CLI command
|
||||
reply = self.cli.handle_command(
|
||||
sender_pubkey=sender_pubkey,
|
||||
command=message_text,
|
||||
is_admin=is_admin
|
||||
)
|
||||
|
||||
logger.info(f"CLI command from 0x{src_hash:02X}: {message_text[:50]} -> {reply[:100]}")
|
||||
|
||||
# Send reply back to sender using TXT_MSG
|
||||
await self._send_cli_reply(packet, reply, handler_info)
|
||||
|
||||
packet.mark_do_not_retransmit()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing CLI command: {e}")
|
||||
# Fall through to normal text handling
|
||||
|
||||
# Normal text message handling
|
||||
# Let handler decrypt the message first
|
||||
await handler_info["handler"](packet)
|
||||
|
||||
# Call placeholder for custom processing
|
||||
@@ -194,12 +160,53 @@ class TextHelper:
|
||||
f"from 0x{src_hash:02X}"
|
||||
)
|
||||
|
||||
# Example: Extract decrypted message if available
|
||||
# Extract decrypted message if available
|
||||
if hasattr(packet, "decrypted") and packet.decrypted:
|
||||
message_text = packet.decrypted.get("text", "<unknown>")
|
||||
|
||||
# Clean message text - remove null bytes and trailing whitespace
|
||||
message_text = message_text.rstrip('\x00').rstrip()
|
||||
|
||||
logger.info(
|
||||
f"[{identity_type}:{identity_name}] Message: {message_text}"
|
||||
)
|
||||
|
||||
# Check if this is a CLI command to the repeater (AFTER decryption)
|
||||
if dest_hash == self.repeater_hash and self.cli and self._is_cli_command(message_text):
|
||||
try:
|
||||
# Check admin permission
|
||||
is_admin = self._check_admin_permission(src_hash)
|
||||
|
||||
# If not admin, log and return without sending reply
|
||||
if not is_admin:
|
||||
logger.warning(f"CLI command denied from 0x{src_hash:02X} (not admin): {message_text[:50]}")
|
||||
return
|
||||
|
||||
# Get client for full public key
|
||||
repeater_acl = self.acl_dict.get(self.repeater_hash)
|
||||
sender_pubkey = bytes([src_hash]) + b'\x00' * 31 # Default
|
||||
if repeater_acl:
|
||||
for client_info in repeater_acl.get_all_clients():
|
||||
if client_info.id.get_public_key()[0] == src_hash:
|
||||
sender_pubkey = client_info.id.get_public_key()
|
||||
break
|
||||
|
||||
# Handle CLI command
|
||||
reply = self.cli.handle_command(
|
||||
sender_pubkey=sender_pubkey,
|
||||
command=message_text,
|
||||
is_admin=is_admin
|
||||
)
|
||||
|
||||
logger.info(f"CLI command from 0x{src_hash:02X}: {message_text[:50]} -> {reply[:100]}")
|
||||
|
||||
# Send reply back to sender
|
||||
handler_info = self.handlers.get(dest_hash)
|
||||
if handler_info:
|
||||
await self._send_cli_reply(packet, reply, handler_info)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing CLI command: {e}", exc_info=True)
|
||||
|
||||
async def _send_packet(self, packet, wait_for_ack: bool = False):
|
||||
|
||||
@@ -245,7 +252,7 @@ class TextHelper:
|
||||
return any(message.startswith(prefix) for prefix in command_prefixes)
|
||||
|
||||
def _check_admin_permission(self, src_hash: int) -> bool:
|
||||
"""Check if sender has admin permissions (bit 0x01)."""
|
||||
"""Check if sender has admin permissions (bit 0x02)."""
|
||||
# Get the repeater's ACL
|
||||
repeater_acl = self.acl_dict.get(self.repeater_hash)
|
||||
if not repeater_acl:
|
||||
@@ -256,9 +263,10 @@ class TextHelper:
|
||||
for client_info in clients:
|
||||
pubkey = client_info.id.get_public_key()
|
||||
if pubkey[0] == src_hash:
|
||||
# Check admin bit (0x01)
|
||||
# Check admin bit (0x02 = PERM_ACL_ADMIN)
|
||||
permissions = getattr(client_info, 'permissions', 0)
|
||||
return (permissions & 0x01) != 0
|
||||
PERM_ACL_ADMIN = 0x02
|
||||
return (permissions & 0x02) == PERM_ACL_ADMIN
|
||||
|
||||
return False
|
||||
|
||||
@@ -266,10 +274,12 @@ class TextHelper:
|
||||
"""
|
||||
Send CLI reply back to sender using TXT_MSG datagram.
|
||||
|
||||
Follows the C++ pattern:
|
||||
Follows the C++ pattern (lines 603-609 in MyMesh.cpp):
|
||||
- Creates TXT_MSG datagram with TXT_TYPE_CLI_DATA flag
|
||||
- Encrypts with shared secret from ACL client
|
||||
- Sends via flood or direct depending on client's out_path
|
||||
- Uses client->out_path_len to decide routing:
|
||||
* if out_path_len < 0: sendFlood()
|
||||
* else: sendDirect() with stored out_path
|
||||
"""
|
||||
from pymc_core.protocol import PacketBuilder, Identity
|
||||
from pymc_core.protocol.constants import PAYLOAD_TYPE_TXT_MSG
|
||||
@@ -277,6 +287,10 @@ class TextHelper:
|
||||
|
||||
try:
|
||||
src_hash = original_packet.payload[1]
|
||||
dest_hash = original_packet.payload[0]
|
||||
|
||||
incoming_route = original_packet.get_route_type()
|
||||
logger.debug(f"CLI reply: original packet dest=0x{dest_hash:02X}, src=0x{src_hash:02X}, incoming_route={incoming_route}")
|
||||
|
||||
# Find the client in repeater's ACL to get shared secret
|
||||
repeater_acl = self.acl_dict.get(self.repeater_hash)
|
||||
@@ -310,27 +324,38 @@ class TextHelper:
|
||||
reply_bytes = reply_text.encode('utf-8')
|
||||
plaintext = timestamp.to_bytes(4, 'little') + bytes([flags]) + reply_bytes
|
||||
|
||||
# Create datagram using PacketBuilder
|
||||
# Decide routing based on client->out_path_len (C++ pattern)
|
||||
# out_path is populated by PATH packets, NOT from incoming text message route
|
||||
route_type = "flood" if client.out_path_len < 0 else "direct"
|
||||
logger.debug(f"CLI reply: client.out_path_len={client.out_path_len}, using route_type={route_type}")
|
||||
|
||||
reply_packet = PacketBuilder.create_datagram(
|
||||
ptype=PAYLOAD_TYPE_TXT_MSG,
|
||||
dest=client.id,
|
||||
local_identity=handler_info["identity"],
|
||||
secret=shared_secret,
|
||||
plaintext=plaintext,
|
||||
route_type="flood" if client.out_path_len < 0 else "direct"
|
||||
route_type=route_type
|
||||
)
|
||||
|
||||
# Add path for direct routing if available
|
||||
# Debug reply packet structure
|
||||
if len(reply_packet.payload) >= 2:
|
||||
reply_dest_hash = reply_packet.payload[0]
|
||||
reply_src_hash = reply_packet.payload[1]
|
||||
logger.debug(f"CLI reply: Packet created - dest=0x{reply_dest_hash:02X}, src=0x{reply_src_hash:02X}, route={reply_packet.get_route_type()}")
|
||||
|
||||
# Add path for direct routing if available from PATH packets
|
||||
if client.out_path_len >= 0 and len(client.out_path) > 0:
|
||||
reply_packet.path = bytearray(client.out_path[:client.out_path_len])
|
||||
reply_packet.path_len = client.out_path_len
|
||||
logger.debug(f"CLI reply: Added stored out_path - path_len={reply_packet.path_len}, path={[hex(b) for b in reply_packet.path]}")
|
||||
|
||||
# Send with delay (CLI_REPLY_DELAY_MILLIS = 1500ms in C++)
|
||||
CLI_REPLY_DELAY_MS = 1500
|
||||
# Send with delay (CLI_REPLY_DELAY_MILLIS = 600ms in C++)
|
||||
CLI_REPLY_DELAY_MS = 600
|
||||
await asyncio.sleep(CLI_REPLY_DELAY_MS / 1000.0)
|
||||
|
||||
await self._send_packet(reply_packet, wait_for_ack=False)
|
||||
logger.info(f"CLI reply sent to 0x{src_hash:02X}: {reply_text[:50]}")
|
||||
logger.info(f"CLI reply sent to 0x{src_hash:02X} via {route_type.upper()}: {reply_text[:50]}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending CLI reply: {e}", exc_info=True)
|
||||
|
||||
+9
-1
@@ -6,7 +6,7 @@ import sys
|
||||
from repeater.config import get_radio_for_board, load_config
|
||||
from repeater.engine import RepeaterHandler
|
||||
from repeater.web.http_server import HTTPStatsServer, _log_buffer
|
||||
from repeater.handler_helpers import TraceHelper, DiscoveryHelper, AdvertHelper, LoginHelper, TextHelper
|
||||
from repeater.handler_helpers import TraceHelper, DiscoveryHelper, AdvertHelper, LoginHelper, TextHelper, PathHelper
|
||||
from repeater.packet_router import PacketRouter
|
||||
from repeater.identity_manager import IdentityManager
|
||||
|
||||
@@ -30,6 +30,7 @@ class RepeaterDaemon:
|
||||
self.discovery_helper = None
|
||||
self.login_helper = None
|
||||
self.text_helper = None
|
||||
self.path_helper = None
|
||||
self.acl = None
|
||||
self.router = None
|
||||
|
||||
@@ -214,6 +215,13 @@ class RepeaterDaemon:
|
||||
)
|
||||
|
||||
logger.info("Text message processing helper initialized")
|
||||
|
||||
# Initialize PATH packet helper for updating client out_path
|
||||
self.path_helper = PathHelper(
|
||||
acl_dict=self.login_helper.get_acl_dict(), # Per-identity ACLs
|
||||
log_fn=logger.info,
|
||||
)
|
||||
logger.info("PATH packet processing helper initialized")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize dispatcher: {e}")
|
||||
|
||||
@@ -6,6 +6,7 @@ from pymc_core.node.handlers.control import ControlHandler
|
||||
from pymc_core.node.handlers.advert import AdvertHandler
|
||||
from pymc_core.node.handlers.login_server import LoginServerHandler
|
||||
from pymc_core.node.handlers.text import TextMessageHandler
|
||||
from pymc_core.node.handlers.path import PathHandler
|
||||
|
||||
logger = logging.getLogger("PacketRouter")
|
||||
|
||||
@@ -108,6 +109,12 @@ class PacketRouter:
|
||||
if handled:
|
||||
processed_by_injection = True
|
||||
|
||||
elif payload_type == PathHandler.payload_type():
|
||||
# Process PATH packet to update client out_path for direct routing
|
||||
if self.daemon.path_helper:
|
||||
await self.daemon.path_helper.process_path_packet(packet)
|
||||
# Note: process_path_packet returns False to allow forwarding
|
||||
|
||||
# Only pass to repeater engine if not already processed by injection
|
||||
if self.daemon.repeater_handler and not processed_by_injection:
|
||||
metadata = {
|
||||
|
||||
Reference in New Issue
Block a user