From f924086198dece6b3bb5d5f772295b3674d14241 Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Mon, 27 Apr 2026 00:06:33 +0200 Subject: [PATCH] Refactored rnsh to use argparse --- RNS/Utilities/rnsh/args.py | 59 +++++++++++++++++++++++++++++++++ RNS/Utilities/rnsh/initiator.py | 20 +++++------ RNS/Utilities/rnsh/listener.py | 20 +++++------ RNS/Utilities/rnsh/process.py | 2 +- RNS/Utilities/rnsh/protocol.py | 4 +-- RNS/Utilities/rnsh/retry.py | 22 +++++++++--- RNS/Utilities/rnsh/rnsh.py | 31 ++++++++--------- RNS/Utilities/rnsh/session.py | 8 ++--- 8 files changed, 119 insertions(+), 47 deletions(-) create mode 100644 RNS/Utilities/rnsh/args.py diff --git a/RNS/Utilities/rnsh/args.py b/RNS/Utilities/rnsh/args.py new file mode 100644 index 0000000..a9ace4c --- /dev/null +++ b/RNS/Utilities/rnsh/args.py @@ -0,0 +1,59 @@ +import argparse +import sys + +from RNS.Utilities.rnsh._version import __version__ as __rnsh_version__ +from RNS._version import __version__ + +DEFAULT_SERVICE_NAME = "default" + +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("--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") + parser.add_argument("-p", "--print-identity", action="store_true", default=False, help="print identity and destination info and exit") + parser.add_argument("--version", action="version", version="rnsh {rv} (protocol {pv})".format(rv=__version__, pv=__rnsh_version__)) + + # Listener options + parser.add_argument("-l", "--listen", action="store_true", default=False, help="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") + parser.add_argument("-s", "--service", action="store", default=None, help="service name for identity file if not the default", type=str) + parser.add_argument("-b", "--announce",action="store", default=None,help="announce on startup and every PERIOD seconds; specify 0 to announce on startup only",metavar="PERIOD", type=int) + parser.add_argument("-a", "--allowed", action="append", default=None, metavar="HASH", type=str, help="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") + parser.add_argument("-n", "--no-auth", action="store_true", default=False, help="disable authentication (allow any identity to connect)") + parser.add_argument("-A", "--remote-command-as-args", action="store_true", default=False, help="concatenate remote command to the argument list of the default program or shell") + parser.add_argument("-C", "--no-remote-command", action="store_true", default=False, help="disable executing command lines received from the remote initiator") + + # Initiator options + parser.add_argument("-N", "--no-id", action="store_true", default=False, help="disable identity announcement on connect") + parser.add_argument("-m", "--mirror", action="store_true", default=False, help="return with the exit code of the remote process") + parser.add_argument("-w", "--timeout", action="store", default=None, help="connect and request timeout in seconds", metavar="SECONDS", type=float) + + parser.add_argument("destination", nargs="?", default=None, help="hexadecimal hash of the destination to connect to", type=str) + + return parser + + +def parse_arguments(argv=None): + if argv is None: argv = sys.argv[1:] + + # Split at -- to separate rnsh options from the command to execute. + # Everything before -- (or the entire argv if no --) goes to argparse. + # Everything after -- becomes the command list. + try: + split_idx = argv.index("--") + rnsh_argv = argv[:split_idx] + command = argv[split_idx + 1:] + except ValueError: + rnsh_argv = argv + command = [] + + parser = setup_argument_parser() + args = parser.parse_args(rnsh_argv) + args.command = command + + if args.listen and not args.service: args.service = DEFAULT_SERVICE_NAME + + return args, parser diff --git a/RNS/Utilities/rnsh/initiator.py b/RNS/Utilities/rnsh/initiator.py index 96df290..c83a442 100644 --- a/RNS/Utilities/rnsh/initiator.py +++ b/RNS/Utilities/rnsh/initiator.py @@ -39,18 +39,18 @@ import time import tty from typing import Callable, TypeVar import RNS -import rnsh.exception as exception -import rnsh.process as process -import rnsh.retry as retry -import rnsh.session as session +import RNS.Utilities.rnsh.exception as exception +import RNS.Utilities.rnsh.process as process +import RNS.Utilities.rnsh.retry as retry +import RNS.Utilities.rnsh.session as session import re import contextlib -import rnsh.args + import pwd import bz2 -import rnsh.protocol as protocol -import rnsh.helpers as helpers -import rnsh.rnsh +import RNS.Utilities.rnsh.protocol as protocol +import RNS.Utilities.rnsh.helpers as helpers +import RNS.Utilities.rnsh.rnsh as rnsh _identity = None _reticulum = None @@ -154,7 +154,7 @@ async def _initiate_link(configdir, rnsconfigdir, identitypath=None, verbosity=0 _reticulum = RNS.Reticulum(configdir=rnsconfigdir, loglevel=targetloglevel, logdest=RNS.LOG_FILE) if _identity is None: - _identity = rnsh.rnsh.prepare_identity(identitypath) + _identity = rnsh.prepare_identity(identitypath) if not RNS.Transport.has_path(destination_hash): RNS.Transport.request_path(destination_hash) @@ -169,7 +169,7 @@ async def _initiate_link(configdir, rnsconfigdir, identitypath=None, verbosity=0 listener_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, - rnsh.rnsh.APP_NAME + rnsh.APP_NAME ) if _link is None or _link.status == RNS.Link.PENDING: diff --git a/RNS/Utilities/rnsh/listener.py b/RNS/Utilities/rnsh/listener.py index fa89741..5fb4991 100644 --- a/RNS/Utilities/rnsh/listener.py +++ b/RNS/Utilities/rnsh/listener.py @@ -36,17 +36,17 @@ import time import tty from typing import Callable, TypeVar import RNS -import rnsh.exception as exception -import rnsh.process as process -import rnsh.retry as retry -import rnsh.session as session +import RNS.Utilities.rnsh.exception as exception +import RNS.Utilities.rnsh.process as process +import RNS.Utilities.rnsh.retry as retry +import RNS.Utilities.rnsh.session as session import re import contextlib -import rnsh.args + import pwd -import rnsh.protocol as protocol -import rnsh.helpers as helpers -import rnsh.rnsh +import RNS.Utilities.rnsh.protocol as protocol +import RNS.Utilities.rnsh.helpers as helpers +import RNS.Utilities.rnsh.rnsh as rnsh _identity = None @@ -123,8 +123,8 @@ async def listen(configdir, rnsconfigdir, command, identitypath=None, service_na # 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.rnsh.prepare_identity(identitypath, service_name) - _destination = RNS.Destination(_identity, RNS.Destination.IN, RNS.Destination.SINGLE, rnsh.rnsh.APP_NAME) + _identity = rnsh.prepare_identity(identitypath, service_name) + _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) diff --git a/RNS/Utilities/rnsh/process.py b/RNS/Utilities/rnsh/process.py index 66e9976..e0ebce4 100644 --- a/RNS/Utilities/rnsh/process.py +++ b/RNS/Utilities/rnsh/process.py @@ -40,7 +40,7 @@ import types import typing import RNS -import rnsh.exception as exception +import RNS.Utilities.rnsh.exception as exception CTRL_C = "\x03".encode("utf-8") CTRL_D = "\x04".encode("utf-8") diff --git a/RNS/Utilities/rnsh/protocol.py b/RNS/Utilities/rnsh/protocol.py index 664a833..3ed20cc 100644 --- a/RNS/Utilities/rnsh/protocol.py +++ b/RNS/Utilities/rnsh/protocol.py @@ -3,7 +3,7 @@ from __future__ import annotations import RNS from RNS.vendor import umsgpack from RNS.Buffer import StreamDataMessage as RNSStreamDataMessage -import rnsh.retry +import RNS.Utilities.rnsh.retry import abc import contextlib import struct @@ -77,7 +77,7 @@ class VersionInfoMessage(RNS.MessageBase): def __init__(self, sw_version: str = None): super().__init__() - self.sw_version = sw_version or rnsh.__version__ + self.sw_version = sw_version or RNS.Utilities.rnsh.__version__ self.protocol_version = PROTOCOL_VERSION def pack(self) -> bytes: return umsgpack.packb((self.sw_version, self.protocol_version)) diff --git a/RNS/Utilities/rnsh/retry.py b/RNS/Utilities/rnsh/retry.py index b761a10..7bb6b4e 100644 --- a/RNS/Utilities/rnsh/retry.py +++ b/RNS/Utilities/rnsh/retry.py @@ -1,6 +1,10 @@ -# MIT License +# Based on the original rnsh program by Aaron Heise (@acehoss) +# https://github.com/acehoss/rnsh - MIT License - Copyright (c) 2023 Aaron Heise +# This version of rnsh is included in RNS under the Reticulum License # -# Copyright (c) 2023 Aaron Heise +# Reticulum License +# +# Copyright (c) 2016-2025 Mark Qvist # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -9,8 +13,16 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. +# - The Software shall not be used in any kind of system which includes amongst +# its functions the ability to purposefully do harm to human beings. +# +# - The Software shall not be used, directly or indirectly, in the creation of +# an artificial intelligence, machine learning or language model training +# dataset, including but not limited to any use that contributes to the +# training or development of such a model or algorithm. +# +# - The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, @@ -23,7 +35,7 @@ import asyncio import threading import time -import rnsh.exception as exception +import RNS.Utilities.rnsh.exception as exception from typing import Callable from contextlib import AbstractContextManager import types diff --git a/RNS/Utilities/rnsh/rnsh.py b/RNS/Utilities/rnsh/rnsh.py index f3a3f79..b1ccbde 100644 --- a/RNS/Utilities/rnsh/rnsh.py +++ b/RNS/Utilities/rnsh/rnsh.py @@ -32,12 +32,13 @@ import os import sys import RNS -import rnsh.process as process -import rnsh.session as session -import rnsh.args -import rnsh.loop -import rnsh.listener as listener -import rnsh.initiator as initiator +import RNS.Utilities.rnsh.process as process +import RNS.Utilities.rnsh.session as session +import RNS.Utilities.rnsh.args +import RNS.Utilities.rnsh.loop +import RNS.Utilities.rnsh.listener as listener +import RNS.Utilities.rnsh.initiator as initiator +from RNS.Utilities.rnsh.args import parse_arguments APP_NAME = "rnsh" loop: asyncio.AbstractEventLoop | None = None @@ -91,13 +92,13 @@ def ensure_config_directory(): async def _rnsh_cli_main(): global verbose_set - args = rnsh.args.Args(sys.argv) + args, parser = parse_arguments() verbose_set = args.verbose > 0 configdir = ensure_config_directory() if args.print_identity: - print_identity(args.config, args.identity, args.service_name, args.listen) + print_identity(args.config, args.identity, args.service, args.listen) return 0 if args.listen: @@ -110,17 +111,17 @@ async def _rnsh_cli_main(): await listener.listen(configdir=configdir, rnsconfigdir=args.config, - command=args.command_line, + command=args.command, identitypath=args.identity, - service_name=args.service_name, + service_name=args.service, verbosity=args.verbose, quietness=args.quiet, - allowed=args.allowed, + allowed=args.allowed or [], allowed_file=allowed_file, disable_auth=args.no_auth, announce_period=args.announce, - no_remote_command=args.no_remote_cmd, - remote_cmd_as_args=args.remote_cmd_as_args) + no_remote_command=args.no_remote_command, + remote_cmd_as_args=args.remote_command_as_args) return 0 if args.destination is not None: @@ -132,12 +133,12 @@ async def _rnsh_cli_main(): noid=args.no_id, destination=args.destination, timeout=args.timeout, - command=args.command_line + command=args.command ) return return_code if args.mirror else 0 else: print("") - print(rnsh.args.usage) + parser.print_help() print("") return 1 diff --git a/RNS/Utilities/rnsh/session.py b/RNS/Utilities/rnsh/session.py index 0c494a6..82b8dfa 100644 --- a/RNS/Utilities/rnsh/session.py +++ b/RNS/Utilities/rnsh/session.py @@ -1,11 +1,11 @@ from __future__ import annotations import contextlib import functools -import rnsh.exception as exception import asyncio -import rnsh.process as process -import rnsh.helpers as helpers -import rnsh.protocol as protocol +import RNS.Utilities.rnsh.exception as exception +import RNS.Utilities.rnsh.process as process +import RNS.Utilities.rnsh.helpers as helpers +import RNS.Utilities.rnsh.protocol as protocol import enum from typing import TypeVar, Generic, Callable, List from abc import abstractmethod, ABC