From ec8d43a548017de41cdf73e7e3eb9844a5d349cd Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Sun, 19 Jul 2026 00:57:27 +0200 Subject: [PATCH] Updated rnsh config args to work similarly to other RNS utils --- RNS/Utilities/rnsh/args.py | 3 +- RNS/Utilities/rnsh/initiator.py | 9 ++--- RNS/Utilities/rnsh/listener.py | 17 ++++++--- RNS/Utilities/rnsh/rnsh.py | 63 +++++++++++++++++++++------------ docs/source/using.rst | 60 +++++++++++++++---------------- 5 files changed, 87 insertions(+), 65 deletions(-) diff --git a/RNS/Utilities/rnsh/args.py b/RNS/Utilities/rnsh/args.py index 71a411d8..75244601 100644 --- a/RNS/Utilities/rnsh/args.py +++ b/RNS/Utilities/rnsh/args.py @@ -44,7 +44,8 @@ def setup_argument_parser(): parser = argparse.ArgumentParser(description="Reticulum Remote Shell Utility", epilog="When specifying a command to execute, separate rnsh\noptions from the command and its arguments with --\n\nFor example:\n rnsh -l -- /bin/bash --login\n rnsh -- ls -la /tmp", formatter_class=argparse.RawDescriptionHelpFormatter) # Common options - parser.add_argument("--config", "-c", action="store", default=None, help="path to alternative Reticulum config directory", type=str) + parser.add_argument("--config", "-c", action="store", default=None, help="path to config directory", type=str) + parser.add_argument("--rnsconfig", action="store", default=None, help="path to alternative Reticulum config directory", type=str) parser.add_argument("--identity", "-i", action="store", default=None, help="path to identity file to use", type=str) parser.add_argument("-v", "--verbose", action="count", default=0, help="increase verbosity") parser.add_argument("-q", "--quiet", action="count", default=0, help="decrease verbosity") diff --git a/RNS/Utilities/rnsh/initiator.py b/RNS/Utilities/rnsh/initiator.py index 67efa68b..fa008a7c 100644 --- a/RNS/Utilities/rnsh/initiator.py +++ b/RNS/Utilities/rnsh/initiator.py @@ -144,7 +144,7 @@ class RemoteExecutionError(Exception): def __init__(self, msg): self.msg = msg -async def _initiate_link(configdir, rnsconfigdir, identitypath=None, verbosity=0, quietness=0, noid=False, destination=None, +async def _initiate_link(configdir, rnsconfigdir, identitypath=None, logfile=None, verbosity=0, quietness=0, noid=False, destination=None, timeout=RNS.Transport.PATH_REQUEST_TIMEOUT): global _identity, _reticulum, _link, _destination, _remote_exec_grace @@ -160,11 +160,11 @@ async def _initiate_link(configdir, rnsconfigdir, identitypath=None, verbosity=0 if _reticulum is None: targetloglevel = compute_target_rns_loglevel(verbosity, quietness, RNS.LOG_ERROR) - RNS.logfile = os.path.join(configdir, "logfile") + RNS.logfile = logfile _reticulum = RNS.Reticulum(configdir=rnsconfigdir, loglevel=targetloglevel, logdest=RNS.LOG_FILE) if _identity is None: - _identity = rnsh.prepare_identity(identitypath) + _identity = rnsh.prepare_identity(identity_path=identitypath, service_name=None, configdir=configdir) if not RNS.Transport.has_path(destination_hash): RNS.Transport.request_path(destination_hash) @@ -211,8 +211,9 @@ async def _handle_error(errmsg: RNS.MessageBase): raise RemoteExecutionError(f"Remote error: {errmsg.msg}") -async def initiate(configdir: str, rnsconfigdir:str, identitypath: str, verbosity: int, quietness: int, noid: bool, destination: str, +async def initiate(configdir: str, rnsconfigdir:str, identitypath: str, logfile:str, verbosity: int, quietness: int, noid: bool, destination: str, timeout: float, command: [str] | None = None): + global _finished, _link if timeout is None: timeout = RNS.Transport.PATH_REQUEST_TIMEOUT diff --git a/RNS/Utilities/rnsh/listener.py b/RNS/Utilities/rnsh/listener.py index f9a1ec93..97f585fa 100644 --- a/RNS/Utilities/rnsh/listener.py +++ b/RNS/Utilities/rnsh/listener.py @@ -118,25 +118,32 @@ def compute_target_rns_loglevel(verbosity: int, quietness: int, base_level: int except Exception: return base_level -async def listen(configdir, rnsconfigdir, command, identitypath=None, service_name=None, verbosity=0, quietness=0, allowed=None, - allowed_file=None, disable_auth=None, announce_period=900, no_remote_command=True, remote_cmd_as_args=False, +async def listen(configdir, rnsconfigdir, command, identitypath=None, logfile=None, service_name=None, verbosity=0, quietness=0, + allowed=None, allowed_file=None, disable_auth=None, announce_period=900, no_remote_command=True, remote_cmd_as_args=False, loop: asyncio.AbstractEventLoop = None): global _identity, _allow_all, _allowed_identity_hashes, _allowed_file, _allowed_file_identity_hashes global _reticulum, _cmd, _destination, _no_remote_command, _remote_cmd_as_args, _finished if not loop: loop = asyncio.get_running_loop() - if service_name is None or len(service_name) == 0: - service_name = "default" + if service_name is None or len(service_name) == 0: service_name = "default" RNS.log(f"Using service name {service_name}", RNS.LOG_INFO) + if logfile: + RNS.log(f"Logging to {logfile}", RNS.LOG_NOTICE) + logdest = RNS.LOG_FILE + RNS.logfile = logfile + else: + RNS.log(f"Logging to console", RNS.LOG_NOTICE) + logdest = RNS.LOG_STDOUT # More -v should increase verbosity (higher RNS.loglevel); -q should decrease it targetloglevel = compute_target_rns_loglevel(verbosity, quietness, RNS.LOG_INFO) _reticulum = RNS.Reticulum(configdir=rnsconfigdir, loglevel=targetloglevel) - _identity = rnsh.prepare_identity(identitypath, service_name) + _identity = rnsh.prepare_identity(identity_path=identitypath, service_name=service_name, configdir=configdir) _destination = RNS.Destination(_identity, RNS.Destination.IN, RNS.Destination.SINGLE, rnsh.APP_NAME) RNS.log(f"rnsh listening for commands on {RNS.prettyhexrep(_destination.hash)}", RNS.LOG_NOTICE) + RNS.logdest = logdest _cmd = command if _cmd is None or len(_cmd) == 0: diff --git a/RNS/Utilities/rnsh/rnsh.py b/RNS/Utilities/rnsh/rnsh.py index 2d804217..99ad24e1 100644 --- a/RNS/Utilities/rnsh/rnsh.py +++ b/RNS/Utilities/rnsh/rnsh.py @@ -57,43 +57,54 @@ loop: asyncio.AbstractEventLoop | None = None def _sanitize_service_name(service_name:str) -> str: return re.sub(r'\W+', '', service_name) -def prepare_identity(identity_path, service_name: str = None) -> tuple[RNS.Identity]: +def prepare_identity(identity_path=None, service_name=None, configdir=None): service_name = _sanitize_service_name(service_name or "") - if identity_path is None: - identity_path = RNS.Reticulum.identitypath + "/" + APP_NAME + \ - (f".{service_name}" if service_name and len(service_name) > 0 else "") + if not identity_path: + identity_path = f"{configdir}/identity" + (f".{service_name}" if service_name and len(service_name) > 0 else "") identity = None if os.path.isfile(identity_path): + RNS.log(f"Loading identity from {identity_path}", RNS.LOG_VERBOSE) identity = RNS.Identity.from_file(identity_path) if identity is None: - RNS.log("No valid saved identity found, creating new...", RNS.LOG_INFO) + RNS.log(f"Creating identity {identity_path}", RNS.LOG_NOTICE) identity = RNS.Identity() identity.to_file(identity_path) return identity -def print_identity(configdir, identitypath, service_name, include_destination: bool): - reticulum = RNS.Reticulum(configdir=configdir, loglevel=RNS.LOG_INFO) - if service_name and len(service_name) > 0: - print(f"Using service name \"{service_name}\"") - identity = prepare_identity(identitypath, service_name) +def print_identity(configdir, rnsconfigdir, identitypath, service_name, include_destination: bool): + reticulum = RNS.Reticulum(configdir=rnsconfigdir, loglevel=RNS.LOG_INFO) + if service_name and len(service_name) > 0: print(f"Using service name \"{service_name}\"") + identity = prepare_identity(identity_path=identitypath, service_name=service_name, configdir=configdir) destination = RNS.Destination(identity, RNS.Destination.IN, RNS.Destination.SINGLE, APP_NAME) print("Identity : " + str(identity)) - if include_destination: - print("Listening on : " + RNS.prettyhexrep(destination.hash)) + if include_destination: print("Listening on : " + RNS.prettyhexrep(destination.hash)) exit(0) verbose_set = False -def ensure_config_directory(): - if os.path.isdir(os.path.expanduser("~/.config/rnsh")): return os.path.expanduser("~/.config/rnsh") - elif os.path.isdir(os.path.expanduser("~/.rnsh")): return os.path.expanduser("~/.rnsh") +def ensure_config_directory(configdir=None): + if configdir: + if os.path.isdir(os.path.expanduser(configdir)): return os.path.expanduser(configdir) + else: + try: + RNS.log(f"Creating configuration directory: {os.path.expanduser(configdir)}") + os.makedirs(os.path.expanduser(configdir)) + return os.path.expanduser(configdir) + + except Exception as e: + RNS.log(f"Could not get or create rnsh configuration directory, aborting", RNS.LOG_CRITICAL) + os._exit(1) + + elif os.path.isdir(os.path.expanduser("~/.config/rnsh")): return os.path.expanduser("~/.config/rnsh") + elif os.path.isdir(os.path.expanduser("~/.rnsh")): return os.path.expanduser("~/.rnsh") else: try: + RNS.log(f"Creating configuration directory: {os.path.expanduser('~/.rnsh')}") os.makedirs(os.path.expanduser("~/.rnsh")) return os.path.expanduser("~/.rnsh") @@ -106,24 +117,29 @@ async def _rnsh_cli_main(): global verbose_set args, parser = parse_arguments() verbose_set = args.verbose > 0 - configdir = ensure_config_directory() + configdir = ensure_config_directory(args.config) + + if not configdir: + RNS.log(f"Could not resolve rnsh configuration directory", RNS.LOG_CRITICAL) + os._exit(1) if args.print_identity: - print_identity(args.config, args.identity, args.service, args.listen) + print_identity(configdir, args.rnsconfig, args.identity, args.service, args.listen) return 0 + logfile = f"{configdir}/logfile" + if args.listen: allowed_file = None dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2 - if os.path.isfile(os.path.expanduser("~/.config/rnsh/allowed_identities")): - allowed_file = os.path.expanduser("~/.config/rnsh/allowed_identities") - elif os.path.isfile(os.path.expanduser("~/.rnsh/allowed_identities")): - allowed_file = os.path.expanduser("~/.rnsh/allowed_identities") + if os.path.isfile(os.path.expanduser(f"{configdir}/allowed_identities")): + allowed_file = os.path.expanduser(f"{configdir}/allowed_identities") await listener.listen(configdir=configdir, - rnsconfigdir=args.config, + rnsconfigdir=args.rnsconfig, command=args.command, identitypath=args.identity, + logfile=logfile, service_name=args.service, verbosity=args.verbose, quietness=args.quiet, @@ -137,8 +153,9 @@ async def _rnsh_cli_main(): if args.destination is not None: return_code = await initiator.initiate(configdir=configdir, - rnsconfigdir=args.config, + rnsconfigdir=args.rnsconfig, identitypath=args.identity, + logfile=logfile, verbosity=args.verbose, quietness=args.quiet, noid=args.no_id, diff --git a/docs/source/using.rst b/docs/source/using.rst index 2ca8d607..683751bb 100644 --- a/docs/source/using.rst +++ b/docs/source/using.rst @@ -1002,39 +1002,35 @@ available. These are only recognised immediately after a newline character: destination hexadecimal hash of the destination to connect to options: - -h, --help show this help message and exit - --config, -c CONFIG path to alternative Reticulum config directory - --identity, -i IDENTITY - path to identity file to use - -v, --verbose increase verbosity - -q, --quiet decrease verbosity - -p, --print-identity print identity and destination info and exit - --version show program's version number and exit - -l, --listen listen (server) mode; any command specified after -- - will be used as the default command when the initiator - does not provide one or when remote command execution - is disabled; if no command is specified, the default - shell of the user running rnsh will be used - -s, --service SERVICE - service name for identity file if not the default - -b, --announce PERIOD - announce on startup and every PERIOD seconds; specify - 0 to announce on startup only - -a, --allowed HASH allow this identity to connect (may be specified - multiple times); allowed identities can also be - specified in ~/.rnsh/allowed_identities or - ~/.config/rnsh/allowed_identities, one hash per line - -n, --no-auth disable authentication (allow any identity to connect) + -h, --help show this help message and exit + --config, -c PATH path to config directory + --rnsconfig, -c PATH path to alternative Reticulum config directory + --identity, -i IDENTITY path to identity file to use + -v, --verbose increase verbosity + -q, --quiet decrease verbosity + -p, --print-identity print identity and destination info and exit + --version show program's version number and exit + -l, --listen listen (server) mode; any command specified after -- + will be used as the default command when the initiator + does not provide one or when remote command execution + is disabled; if no command is specified, the default + shell of the user running rnsh will be used + -s, --service SERVICE service name for identity file if not the default + -b, --announce PERIOD announce on startup and every PERIOD seconds; specify + 0 to announce on startup only + -a, --allowed HASH allow this identity to connect (may be specified + multiple times); allowed identities can also be + specified in ~/.rnsh/allowed_identities or + ~/.config/rnsh/allowed_identities, one hash per line + -n, --no-auth disable authentication (allow any identity to connect) -A, --remote-command-as-args - concatenate remote command to the argument list of the - default program or shell - -C, --no-remote-command - disable executing command lines received from the - remote initiator - -N, --no-id disable identity announcement on connect - -m, --mirror return with the exit code of the remote process - -w, --timeout SECONDS - connect and request timeout in seconds + concatenate remote command to the argument list of the + default program or shell + -C, --no-remote-command disable executing command lines received from the + remote initiator + -N, --no-id disable identity announcement on connect + -m, --mirror return with the exit code of the remote process + -w, --timeout SECONDS connect and request timeout in seconds When specifying a command to execute, separate rnsh options from the command and its arguments with --. For example: