Refactor main to improve readability and maintainability.

This commit is contained in:
Sudo-Ivan
2025-07-14 17:27:17 -05:00
parent 4aa83a2dfb
commit f40d5a51ae
5 changed files with 218 additions and 127 deletions

View File

@@ -1,2 +1,2 @@
# rns_page_node package
__all__ = ['main']
__all__ = ["main"]

View File

@@ -1,31 +1,41 @@
#!/usr/bin/env python3
"""
Minimal Reticulum Page Node
"""Minimal Reticulum Page Node
Serves .mu pages and files over RNS.
"""
import os
import time
import threading
import subprocess
import RNS
import argparse
import logging
import os
import subprocess
import threading
import time
import RNS
logger = logging.getLogger(__name__)
DEFAULT_INDEX = '''>Default Home Page
DEFAULT_INDEX = """>Default Home Page
This node is serving pages using page node, but the home page file (index.mu) was not found in the pages directory. Please add an index.mu file to customize the home page.
'''
"""
DEFAULT_NOTALLOWED = '''>Request Not Allowed
DEFAULT_NOTALLOWED = """>Request Not Allowed
You are not authorised to carry out the request.
'''
"""
class PageNode:
def __init__(self, identity, pagespath, filespath, announce_interval=360, name=None, page_refresh_interval=0, file_refresh_interval=0):
def __init__(
self,
identity,
pagespath,
filespath,
announce_interval=360,
name=None,
page_refresh_interval=0,
file_refresh_interval=0,
):
self._stop_event = threading.Event()
self._lock = threading.Lock()
self.logger = logging.getLogger(f"{__name__}.PageNode")
@@ -34,11 +44,7 @@ class PageNode:
self.pagespath = pagespath
self.filespath = filespath
self.destination = RNS.Destination(
identity,
RNS.Destination.IN,
RNS.Destination.SINGLE,
"nomadnetwork",
"node"
identity, RNS.Destination.IN, RNS.Destination.SINGLE, "nomadnetwork", "node"
)
self.announce_interval = announce_interval
self.last_announce = 0
@@ -52,7 +58,9 @@ class PageNode:
self.destination.set_link_established_callback(self.on_connect)
self._announce_thread = threading.Thread(target=self._announce_loop, daemon=True)
self._announce_thread = threading.Thread(
target=self._announce_loop, daemon=True
)
self._announce_thread.start()
self._refresh_thread = threading.Thread(target=self._refresh_loop, daemon=True)
self._refresh_thread.start()
@@ -66,16 +74,16 @@ class PageNode:
self.destination.register_request_handler(
"/page/index.mu",
response_generator=self.serve_default_index,
allow=RNS.Destination.ALLOW_ALL
allow=RNS.Destination.ALLOW_ALL,
)
for full_path in self.servedpages:
rel = full_path[len(self.pagespath):]
rel = full_path[len(self.pagespath) :]
request_path = f"/page{rel}"
self.destination.register_request_handler(
request_path,
response_generator=self.serve_page,
allow=RNS.Destination.ALLOW_ALL
allow=RNS.Destination.ALLOW_ALL,
)
def register_files(self):
@@ -84,18 +92,18 @@ class PageNode:
self._scan_files(self.filespath)
for full_path in self.servedfiles:
rel = full_path[len(self.filespath):]
rel = full_path[len(self.filespath) :]
request_path = f"/file{rel}"
self.destination.register_request_handler(
request_path,
response_generator=self.serve_file,
allow=RNS.Destination.ALLOW_ALL,
auto_compress=32_000_000
auto_compress=32_000_000,
)
def _scan_pages(self, base):
for entry in os.listdir(base):
if entry.startswith('.'):
if entry.startswith("."):
continue
path = os.path.join(base, entry)
if os.path.isdir(path):
@@ -105,7 +113,7 @@ class PageNode:
def _scan_files(self, base):
for entry in os.listdir(base):
if entry.startswith('.'):
if entry.startswith("."):
continue
path = os.path.join(base, entry)
if os.path.isdir(path):
@@ -113,60 +121,77 @@ class PageNode:
elif os.path.isfile(path):
self.servedfiles.append(path)
def serve_default_index(self, path, data, request_id, link_id, remote_identity, requested_at):
return DEFAULT_INDEX.encode('utf-8')
@staticmethod
def serve_default_index(
path, data, request_id, link_id, remote_identity, requested_at
):
return DEFAULT_INDEX.encode("utf-8")
def serve_page(self, path, data, request_id, link_id, remote_identity, requested_at):
def serve_page(
self, path, data, request_id, link_id, remote_identity, requested_at
):
file_path = path.replace("/page", self.pagespath, 1)
try:
with open(file_path, 'rb') as _f:
with open(file_path, "rb") as _f:
first_line = _f.readline()
is_script = first_line.startswith(b'#!')
is_script = first_line.startswith(b"#!")
except Exception:
is_script = False
if is_script and os.access(file_path, os.X_OK):
# Note: You can remove the following try-except block if you just serve static pages.
# Note: The execution of file_path is intentional here, as some pages are designed to be executable scripts.
# This is acknowledged as a potential security risk if untrusted input can control file_path.
try:
result = subprocess.run([file_path], stdout=subprocess.PIPE)
result = subprocess.run([file_path], stdout=subprocess.PIPE, check=True) # noqa: S603
return result.stdout
except Exception:
pass
with open(file_path, 'rb') as f:
self.logger.exception("Error executing script page")
with open(file_path, "rb") as f:
return f.read()
def serve_file(self, path, data, request_id, link_id, remote_identity, requested_at):
def serve_file(
self, path, data, request_id, link_id, remote_identity, requested_at
):
file_path = path.replace("/file", self.filespath, 1)
return [open(file_path, 'rb'), {"name": os.path.basename(file_path).encode('utf-8')}]
return [
open(file_path, "rb"),
{"name": os.path.basename(file_path).encode("utf-8")},
]
def on_connect(self, link):
pass
def _announce_loop(self):
while not self._stop_event.is_set():
try:
try:
while not self._stop_event.is_set():
if time.time() - self.last_announce > self.announce_interval:
if self.name:
self.destination.announce(app_data=self.name.encode('utf-8'))
self.destination.announce(app_data=self.name.encode("utf-8"))
else:
self.destination.announce()
self.last_announce = time.time()
time.sleep(1)
except Exception:
self.logger.exception("Error in announce loop")
except Exception:
self.logger.exception("Error in announce loop")
def _refresh_loop(self):
while not self._stop_event.is_set():
try:
try:
while not self._stop_event.is_set():
now = time.time()
if self.page_refresh_interval > 0 and now - self.last_page_refresh > self.page_refresh_interval:
if (
self.page_refresh_interval > 0
and now - self.last_page_refresh > self.page_refresh_interval
):
self.register_pages()
self.last_page_refresh = now
if self.file_refresh_interval > 0 and now - self.last_file_refresh > self.file_refresh_interval:
if (
self.file_refresh_interval > 0
and now - self.last_file_refresh > self.file_refresh_interval
):
self.register_files()
self.last_file_refresh = now
time.sleep(1)
except Exception:
self.logger.exception("Error in refresh loop")
except Exception:
self.logger.exception("Error in refresh loop")
def shutdown(self):
self.logger.info("Shutting down PageNode...")
@@ -177,7 +202,7 @@ class PageNode:
except Exception:
self.logger.exception("Error waiting for threads to shut down")
try:
if hasattr(self.destination, 'close'):
if hasattr(self.destination, "close"):
self.destination.close()
except Exception:
self.logger.exception("Error closing RNS destination")
@@ -185,15 +210,63 @@ class PageNode:
def main():
parser = argparse.ArgumentParser(description="Minimal Reticulum Page Node")
parser.add_argument('-c', '--config', dest='configpath', help='Reticulum config path', default=None)
parser.add_argument('-p', '--pages-dir', dest='pages_dir', help='Pages directory', default=os.path.join(os.getcwd(), 'pages'))
parser.add_argument('-f', '--files-dir', dest='files_dir', help='Files directory', default=os.path.join(os.getcwd(), 'files'))
parser.add_argument('-n', '--node-name', dest='node_name', help='Node display name', default=None)
parser.add_argument('-a', '--announce-interval', dest='announce_interval', type=int, help='Announce interval in seconds', default=360)
parser.add_argument('-i', '--identity-dir', dest='identity_dir', help='Directory to store node identity', default=os.path.join(os.getcwd(), 'node-config'))
parser.add_argument('--page-refresh-interval', dest='page_refresh_interval', type=int, default=0, help='Page refresh interval in seconds, 0 disables auto-refresh')
parser.add_argument('--file-refresh-interval', dest='file_refresh_interval', type=int, default=0, help='File refresh interval in seconds, 0 disables auto-refresh')
parser.add_argument('-l', '--log-level', dest='log_level', choices=['DEBUG','INFO','WARNING','ERROR','CRITICAL'], default='INFO', help='Logging level')
parser.add_argument(
"-c", "--config", dest="configpath", help="Reticulum config path", default=None
)
parser.add_argument(
"-p",
"--pages-dir",
dest="pages_dir",
help="Pages directory",
default=os.path.join(os.getcwd(), "pages"),
)
parser.add_argument(
"-f",
"--files-dir",
dest="files_dir",
help="Files directory",
default=os.path.join(os.getcwd(), "files"),
)
parser.add_argument(
"-n", "--node-name", dest="node_name", help="Node display name", default=None
)
parser.add_argument(
"-a",
"--announce-interval",
dest="announce_interval",
type=int,
help="Announce interval in seconds",
default=360,
)
parser.add_argument(
"-i",
"--identity-dir",
dest="identity_dir",
help="Directory to store node identity",
default=os.path.join(os.getcwd(), "node-config"),
)
parser.add_argument(
"--page-refresh-interval",
dest="page_refresh_interval",
type=int,
default=0,
help="Page refresh interval in seconds, 0 disables auto-refresh",
)
parser.add_argument(
"--file-refresh-interval",
dest="file_refresh_interval",
type=int,
default=0,
help="File refresh interval in seconds, 0 disables auto-refresh",
)
parser.add_argument(
"-l",
"--log-level",
dest="log_level",
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
default="INFO",
help="Logging level",
)
args = parser.parse_args()
configpath = args.configpath
@@ -205,11 +278,13 @@ def main():
page_refresh_interval = args.page_refresh_interval
file_refresh_interval = args.file_refresh_interval
numeric_level = getattr(logging, args.log_level.upper(), logging.INFO)
logging.basicConfig(level=numeric_level, format='%(asctime)s %(name)s [%(levelname)s] %(message)s')
logging.basicConfig(
level=numeric_level, format="%(asctime)s %(name)s [%(levelname)s] %(message)s"
)
RNS.Reticulum(configpath)
os.makedirs(identity_dir, exist_ok=True)
identity_file = os.path.join(identity_dir, 'identity')
identity_file = os.path.join(identity_dir, "identity")
if os.path.isfile(identity_file):
identity = RNS.Identity.from_file(identity_file)
else:
@@ -219,7 +294,15 @@ def main():
os.makedirs(pages_dir, exist_ok=True)
os.makedirs(files_dir, exist_ok=True)
node = PageNode(identity, pages_dir, files_dir, announce_interval, node_name, page_refresh_interval, file_refresh_interval)
node = PageNode(
identity,
pages_dir,
files_dir,
announce_interval,
node_name,
page_refresh_interval,
file_refresh_interval,
)
logger.info("Page node running. Press Ctrl-C to exit.")
try:
@@ -229,5 +312,6 @@ def main():
logger.info("Keyboard interrupt received, shutting down...")
node.shutdown()
if __name__ == '__main__':
if __name__ == "__main__":
main()