mirror of
https://github.com/markqvist/rnsh.git
synced 2025-06-18 02:59:33 -04:00
194 lines
No EOL
7.1 KiB
Python
194 lines
No EOL
7.1 KiB
Python
#!/usr/bin/env python3
|
|
|
|
# MIT License
|
|
#
|
|
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
# of this software and associated documentation files (the "Software"), to deal
|
|
# in the Software without restriction, including without limitation the rights
|
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
# 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 IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
# SOFTWARE.
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import base64
|
|
import enum
|
|
import functools
|
|
import logging as __logging
|
|
import os
|
|
import queue
|
|
import shlex
|
|
import signal
|
|
import sys
|
|
import termios
|
|
import threading
|
|
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.rnslogging as rnslogging
|
|
import 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
|
|
|
|
module_logger = __logging.getLogger(__name__)
|
|
|
|
|
|
def _get_logger(name: str):
|
|
global module_logger
|
|
return module_logger.getChild(name)
|
|
|
|
|
|
_identity = None
|
|
_reticulum = None
|
|
_allow_all = False
|
|
_allowed_identity_hashes = []
|
|
_cmd: [str] | None = None
|
|
DATA_AVAIL_MSG = "data available"
|
|
_finished: asyncio.Event = None
|
|
_retry_timer: retry.RetryThread | None = None
|
|
_destination: RNS.Destination | None = None
|
|
_loop: asyncio.AbstractEventLoop | None = None
|
|
_no_remote_command = True
|
|
_remote_cmd_as_args = False
|
|
|
|
|
|
async def _check_finished(timeout: float = 0):
|
|
return await process.event_wait(_finished, timeout=timeout)
|
|
|
|
|
|
def _sigint_handler(sig, loop):
|
|
global _finished
|
|
log = _get_logger("_sigint_handler")
|
|
log.debug(signal.Signals(sig).name)
|
|
if _finished is not None:
|
|
_finished.set()
|
|
else:
|
|
raise KeyboardInterrupt()
|
|
|
|
|
|
async def listen(configdir, command, identitypath=None, service_name=None, verbosity=0, quietness=0, allowed=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, _reticulum, _cmd, _destination, _no_remote_command
|
|
global _remote_cmd_as_args, _finished
|
|
log = _get_logger("_listen")
|
|
if not loop:
|
|
loop = asyncio.get_running_loop()
|
|
if service_name is None or len(service_name) == 0:
|
|
service_name = "default"
|
|
|
|
log.info(f"Using service name {service_name}")
|
|
|
|
|
|
targetloglevel = RNS.LOG_INFO + verbosity - quietness
|
|
_reticulum = RNS.Reticulum(configdir=configdir, loglevel=targetloglevel)
|
|
rnslogging.RnsHandler.set_log_level_with_rns_level(targetloglevel)
|
|
_identity = rnsh.rnsh.prepare_identity(identitypath, service_name)
|
|
_destination = RNS.Destination(_identity, RNS.Destination.IN, RNS.Destination.SINGLE, rnsh.rnsh.APP_NAME)
|
|
|
|
_cmd = command
|
|
if _cmd is None or len(_cmd) == 0:
|
|
shell = None
|
|
try:
|
|
shell = pwd.getpwuid(os.getuid()).pw_shell
|
|
except Exception as e:
|
|
log.error(f"Error looking up shell: {e}")
|
|
log.info(f"Using {shell} for default command.")
|
|
_cmd = [shell] if shell else None
|
|
else:
|
|
log.info(f"Using command {shlex.join(_cmd)}")
|
|
|
|
_no_remote_command = no_remote_command
|
|
session.ListenerSession.allow_remote_command = not no_remote_command
|
|
_remote_cmd_as_args = remote_cmd_as_args
|
|
if (_cmd is None or len(_cmd) == 0 or _cmd[0] is None or len(_cmd[0]) == 0) \
|
|
and (_no_remote_command or _remote_cmd_as_args):
|
|
raise Exception(f"Unable to look up shell for {os.getlogin}, cannot proceed with -A or -C and no <program>.")
|
|
|
|
session.ListenerSession.default_command = _cmd
|
|
session.ListenerSession.remote_cmd_as_args = _remote_cmd_as_args
|
|
|
|
if disable_auth:
|
|
_allow_all = True
|
|
session.ListenerSession.allow_all = True
|
|
else:
|
|
if allowed is not None:
|
|
for a in allowed:
|
|
try:
|
|
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH // 8) * 2
|
|
if len(a) != dest_len:
|
|
raise ValueError(
|
|
"Allowed destination length is invalid, must be {hex} hexadecimal " +
|
|
"characters ({byte} bytes).".format(
|
|
hex=dest_len, byte=dest_len // 2))
|
|
try:
|
|
destination_hash = bytes.fromhex(a)
|
|
_allowed_identity_hashes.append(destination_hash)
|
|
session.ListenerSession.allowed_identity_hashes.append(destination_hash)
|
|
except Exception:
|
|
raise ValueError("Invalid destination entered. Check your input.")
|
|
except Exception as e:
|
|
log.error(str(e))
|
|
exit(1)
|
|
|
|
if len(_allowed_identity_hashes) < 1 and not disable_auth:
|
|
log.warning("Warning: No allowed identities configured, rnsh will not accept any connections!")
|
|
|
|
def link_established(lnk: RNS.Link):
|
|
session.ListenerSession(session.RNSOutlet.get_outlet(lnk), lnk.get_channel(), loop)
|
|
_destination.set_link_established_callback(link_established)
|
|
|
|
_finished = asyncio.Event()
|
|
signal.signal(signal.SIGINT, _sigint_handler)
|
|
|
|
log.info("rnsh listening for commands on " + RNS.prettyhexrep(_destination.hash))
|
|
|
|
if announce_period is not None:
|
|
_destination.announce()
|
|
|
|
last_announce = time.time()
|
|
sleeper = helpers.SleepRate(0.01)
|
|
|
|
try:
|
|
while not await _check_finished():
|
|
if announce_period and 0 < announce_period < time.time() - last_announce:
|
|
last_announce = time.time()
|
|
_destination.announce()
|
|
if len(session.ListenerSession.sessions) > 0:
|
|
# no sleep if there's work to do
|
|
if not await session.ListenerSession.pump_all():
|
|
await sleeper.sleep_async()
|
|
else:
|
|
await asyncio.sleep(0.25)
|
|
finally:
|
|
log.warning("Shutting down")
|
|
await session.ListenerSession.terminate_all("Shutting down")
|
|
await asyncio.sleep(1)
|
|
links_still_active = list(filter(lambda l: l.status != RNS.Link.CLOSED, _destination.links))
|
|
for link in links_still_active:
|
|
if link.status not in [RNS.Link.CLOSED]:
|
|
link.teardown()
|
|
await asyncio.sleep(0.01) |