Reticulum-Go/bindings/python/examples/pageserver/main.py

186 lines
6.5 KiB
Python

#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2024-2026 Quad4.io
#
# NomadNet-style pageserver over the Python librns bindings.
# Usage: python3 main.py -c config [-i identity] [-a announce_sec] [-p page_file] [-P request_path]
from __future__ import annotations
import argparse
import sys
import time
from pathlib import Path
import rns
from rns.errors import Error
DEFAULT_ANNOUNCE_SEC = 900
DEFAULT_PAGE_PATH = "/page/index.mu"
DEFAULT_FILE_PATH = "/file/test.txt"
DEFAULT_PAGE_FILE = "pages/index.mu"
DEFAULT_FILE_FILE = "files/test.txt"
DEFAULT_IDENTITY_PATH = "identity"
REQ_DATA_CAP = 64 * 1024
FALLBACK_PAGE = (
"> Python pageserver\n\n"
"librns via Reticulum-Go\n\n"
"Fallback page (file not found).\n\n"
"`[Home`:/page/index.mu]\n"
"`[Download Test File`:/file/test.txt]`_`f\n\n"
"---\n"
)
FALLBACK_FILE = "Test file from Reticulum-Go node!\n"
def print_last_error(what: str) -> None:
msg = rns.last_error()
if msg:
print(f"{what}: {msg}", file=sys.stderr)
else:
print(what, file=sys.stderr)
def load_bytes(path: str, fallback: str) -> bytes:
try:
return Path(path).read_bytes()
except OSError:
print(f"warning: could not read {path}, using built-in content", file=sys.stderr)
return fallback.encode("utf-8")
def load_or_create_identity(path: str) -> rns.Identity:
try:
identity = rns.Identity.load(path)
print(f"loaded identity from {path}", file=sys.stderr)
return identity
except Error:
identity = rns.Identity.generate()
identity.save(path)
print(f"created and saved identity to {path}", file=sys.stderr)
return identity
def run(
config_path: str,
identity_path: str,
page_file: str,
file_file: str,
request_path: str,
file_path: str,
announce_sec: int,
) -> int:
ver = rns.version()
if ver != rns.API_VERSION:
print(f"librns version mismatch: got {ver} want {rns.API_VERSION}", file=sys.stderr)
return 1
page_body = load_bytes(page_file, FALLBACK_PAGE)
file_body = load_bytes(file_file, FALLBACK_FILE)
node = rns.Node.create(config_path)
identity = load_or_create_identity(identity_path)
try:
node.set_identity(identity)
node.start()
with rns.Destination.create(node, None, "nomadnetwork", ("node",), True) as dest:
dest.register_request_handler(request_path)
dest.register_request_handler(file_path)
dest_hash = dest.hash()
dest_hex = rns.hash_to_hex(dest_hash)
print(f"DEST_HASH={dest_hex}")
print(f"REQUEST_PATH={request_path}")
print(f"FILE_PATH={file_path}")
print(f"librns {ver} pageserver listening as nomadnetwork.node", file=sys.stderr)
print("announce name=librns-python-pageserver interval={}s".format(announce_sec), file=sys.stderr)
print(f"serving {len(page_body)} bytes from {page_file}", file=sys.stderr)
print(f"serving {len(file_body)} bytes from {file_file} as {file_path}", file=sys.stderr)
app_data = b"librns-python-pageserver"
try:
dest.announce(app_data)
print("announce sent", file=sys.stderr)
except Error:
print_last_error("destination.announce failed")
req_buf = bytearray(REQ_DATA_CAP)
last_announce = time.monotonic()
while True:
if announce_sec > 0 and time.monotonic() - last_announce >= announce_sec:
try:
dest.announce(app_data)
print("announce refreshed", file=sys.stderr)
except Error:
pass
last_announce = time.monotonic()
try:
ev = rns.Event.poll(node, 200, req_buf)
except Error as exc:
if exc.code == Error.TIMEOUT:
continue
print_last_error("Event.poll failed")
return 1
if ev.kind == rns.EventKind.LINK_ESTABLISHED:
print("inbound link established", file=sys.stderr)
elif ev.kind == rns.EventKind.LINK_CLOSED:
print("link closed", file=sys.stderr)
elif ev.kind == rns.EventKind.REQUEST_INCOMING:
path = ev.path()
print(f"request incoming path={path}", file=sys.stderr)
req_id = ev.request_id()
if path == request_path:
try:
rns.request_respond(node, req_id, page_body)
print(f"served {request_path} ({len(page_body)} bytes)", file=sys.stderr)
except Error:
print_last_error("request_respond failed")
elif path == file_path:
try:
rns.request_respond_file(node, req_id, "test.txt", file_body)
print(f"served {file_path} ({len(file_body)} bytes)", file=sys.stderr)
except Error:
print_last_error("request_respond_file failed")
else:
try:
rns.request_respond(node, req_id, b"page not found\n")
except Error:
print_last_error("request_respond failed")
finally:
identity.close()
node.close()
return 0
def main() -> int:
parser = argparse.ArgumentParser(description="Serve a NomadNet page over librns.")
parser.add_argument("-c", required=True, help="Reticulum config file")
parser.add_argument("-i", default=DEFAULT_IDENTITY_PATH, help="Identity file path")
parser.add_argument("-a", type=int, default=DEFAULT_ANNOUNCE_SEC, help="Announce interval seconds")
parser.add_argument("-p", default=DEFAULT_PAGE_FILE, help="Micron page file")
parser.add_argument("-f", default=DEFAULT_FILE_FILE, help="Download file")
parser.add_argument("-P", default=DEFAULT_PAGE_PATH, help="Request path to register")
args = parser.parse_args()
if args.a < 0:
print("announce interval must be >= 0", file=sys.stderr)
return 1
return run(
args.c,
args.i,
args.p,
args.f,
args.P,
DEFAULT_FILE_PATH,
args.a,
)
if __name__ == "__main__":
sys.exit(main())