13 Commits

Author SHA1 Message Date
c900cf38c9 Bump to 1.1.0 2025-09-23 01:59:01 -05:00
014ebc25c6 Update default home page message and print node address in terminal output. 2025-09-23 01:57:19 -05:00
Sudo-Ivan
d5e9308fb5 Update GitHub Actions workflows to use updated action versions
- Updated actions/checkout to v5.0.0
- Updated actions/setup-python to v6.0.0
- Updated docker/build-push-action to v6.18.0
- Updated actions/upload-artifact to v4.6.2
- Updated actions/download-artifact to v5.0.0
- Updated sigstore/gh-action-sigstore-python to v3.0.1
2025-09-22 18:44:45 -05:00
Sudo-Ivan
7d5e891261 Update dependencies in poetry.lock and pyproject.toml
- Bump anyio from 4.9.0 to 4.10.0
- Bump authlib from 1.6.0 to 1.6.4
- Bump certifi from 2025.7.14 to 2025.8.3
- Bump cffi from 1.17.1 to 2.0.0
- Bump ruamel.yaml.clib from 0.2.12 to 0.2.13
- Bump ruff from 0.12.3 to 0.12.12
- Bump safety from 3.6.0 to 3.6.1
- Bump typer from 0.16.0 to 0.19.1
- Bump typing-extensions from 4.14.1 to 4.15.0
2025-09-22 14:24:57 -05:00
Sudo-Ivan
c382ed790f Update GitHub Actions workflows to use full-length commit hashes for actions 2025-09-22 14:24:40 -05:00
cb72e57da9 Merge pull request #2 from Sudo-Ivan/dependabot/github_actions/dot-github/workflows/pypa/gh-action-pypi-publish-1.13.0
Bump pypa/gh-action-pypi-publish from 1.12.3 to 1.13.0 in /.github/workflows
2025-09-15 02:24:07 -05:00
dependabot[bot]
aaf5ad23e2 Bump pypa/gh-action-pypi-publish in /.github/workflows
Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.12.3 to 1.13.0.
- [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases)
- [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.12.3...v1.13.0)

---
updated-dependencies:
- dependency-name: pypa/gh-action-pypi-publish
  dependency-version: 1.13.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-04 15:36:31 +00:00
Sudo-Ivan
ce1b1dad7d Update Docker volume path 2025-08-18 03:17:23 -05:00
Sudo-Ivan
67ebc7e556 Fix formatting issues in test_client.py and test_client2.py for consistency in lambda function parameters. 2025-08-04 17:24:21 -05:00
Sudo-Ivan
b31fb748b8 Add node address to output and Fix formatting issues 2025-08-04 17:24:15 -05:00
Sudo-Ivan
eb27326763 Bump package version to 1.0.0. 2025-07-14 17:31:45 -05:00
Sudo-Ivan
f40d5a51ae Refactor main to improve readability and maintainability. 2025-07-14 17:27:17 -05:00
Sudo-Ivan
4aa83a2dfb Add badges to README.md. 2025-07-14 17:22:26 -05:00
11 changed files with 738 additions and 611 deletions

View File

@@ -18,9 +18,9 @@ jobs:
python-version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: ${{ matrix.python-version }}
- name: Build Docker Image

View File

@@ -20,18 +20,18 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392
with:
platforms: amd64,arm64
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435
- name: Log in to the Container registry
uses: docker/login-action@v3
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@@ -39,7 +39,7 @@ jobs:
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
@@ -51,7 +51,7 @@ jobs:
type=sha,format=short
- name: Build and push Docker image
uses: docker/build-push-action@v5
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: .
platforms: linux/amd64,linux/arm64
@@ -63,7 +63,7 @@ jobs:
- name: Extract metadata (tags, labels) for Docker (rootless)
id: meta_rootless
uses: docker/metadata-action@v5
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-rootless
tags: |
@@ -74,7 +74,7 @@ jobs:
type=sha,format=short,suffix=-rootless
- name: Build and push rootless Docker image
uses: docker/build-push-action@v5
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: .
file: ./Dockerfile.rootless

View File

@@ -23,11 +23,11 @@ jobs:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v5.3.0
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: "3.13"
- name: Install pypa/build
@@ -35,7 +35,7 @@ jobs:
- name: Build a binary wheel and a source tarball
run: python3 -m build
- name: Store the distribution packages
uses: actions/upload-artifact@v4.5.0
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: python-package-distributions
path: dist/
@@ -55,12 +55,12 @@ jobs:
steps:
- name: Download all the dists
uses: actions/download-artifact@v4.1.8
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
name: python-package-distributions
path: dist/
- name: Publish distribution 📦 to PyPI
uses: pypa/gh-action-pypi-publish@v1.12.3
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e
github-release:
name: Sign the Python 🐍 distribution 📦 and create GitHub Release
@@ -73,12 +73,12 @@ jobs:
steps:
- name: Download all the dists
uses: actions/download-artifact@v4.1.8
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
name: python-package-distributions
path: dist/
- name: Sign the dists with Sigstore
uses: sigstore/gh-action-sigstore-python@v3.0.0
uses: sigstore/gh-action-sigstore-python@f7ad0af51a5648d09a20d00370f0a91c3bdf8f84 # v3.0.1
with:
inputs: >-
./dist/*.tar.gz

View File

@@ -1,5 +1,9 @@
# RNS Page Node
[![Build and Publish Docker Image](https://github.com/Sudo-Ivan/rns-page-node/actions/workflows/docker.yml/badge.svg)](https://github.com/Sudo-Ivan/rns-page-node/actions/workflows/docker.yml)
[![Docker Build Test](https://github.com/Sudo-Ivan/rns-page-node/actions/workflows/docker-test.yml/badge.svg)](https://github.com/Sudo-Ivan/rns-page-node/actions/workflows/docker-test.yml)
[![DeepSource](https://app.deepsource.com/gh/Sudo-Ivan/rns-page-node.svg/?label=active+issues&show_trend=true&token=kajzd0SjJXSzkuN3z3kG9gQw)](https://app.deepsource.com/gh/Sudo-Ivan/rns-page-node/)
A simple way to serve pages and files over the [Reticulum network](https://reticulum.network/). Drop-in replacement for [NomadNet](https://github.com/markqvist/NomadNet) nodes that primarily serve pages and files.
## Usage
@@ -21,7 +25,7 @@ rns-page-node --node-name "Page Node" --pages-dir ./pages --files-dir ./files --
### Docker/Podman
```bash
docker run -it --rm -v ./pages:/app/pages -v ./files:/app/files -v ./node-config:/app/node-config -v ./config:/app/config ghcr.io/sudo-ivan/rns-page-node:latest
docker run -it --rm -v ./pages:/app/pages -v ./files:/app/files -v ./node-config:/app/node-config -v ./config:/root/.reticulum ghcr.io/sudo-ivan/rns-page-node:latest
```
### Docker/Podman Rootless

949
poetry.lock generated
View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[project]
name = "rns-page-node"
version = "0.2.0"
version = "1.1.0"
license = "GPL-3.0-only"
description = "A simple way to serve pages and files over the Reticulum network."
authors = [
@@ -20,6 +20,6 @@ requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.poetry.group.dev.dependencies]
ruff = "^0.12.3"
safety = "^3.6.0"
ruff = "^0.12.12"
safety = "^3.6.1"

View File

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

View File

@@ -1,31 +1,40 @@
#!/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.
'''
This node is serving pages using rns-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 +43,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 +57,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 +73,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 +91,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 +112,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 +120,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 +201,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 +209,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 +277,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,8 +293,18 @@ 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.")
logger.info("Node address: %s", RNS.prettyhexrep(node.destination.hash))
try:
while True:
@@ -229,5 +313,6 @@ def main():
logger.info("Keyboard interrupt received, shutting down...")
node.shutdown()
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -1,31 +1,31 @@
from setuptools import setup, find_packages
from setuptools import find_packages, setup
with open('README.md', 'r', encoding='utf-8') as fh:
with open("README.md", encoding="utf-8") as fh:
long_description = fh.read()
setup(
name='rns-page-node',
version='0.2.0',
author='Sudo-Ivan',
author_email='',
description='A simple way to serve pages and files over the Reticulum network.',
name="rns-page-node",
version="1.0.0",
author="Sudo-Ivan",
author_email="",
description="A simple way to serve pages and files over the Reticulum network.",
long_description=long_description,
long_description_content_type='text/markdown',
url='https://github.com/Sudo-Ivan/rns-page-node',
long_description_content_type="text/markdown",
url="https://github.com/Sudo-Ivan/rns-page-node",
packages=find_packages(),
license="GPL-3.0",
python_requires='>=3.10',
python_requires=">=3.10",
install_requires=[
'rns>=1.0.0,<1.5.0',
"rns>=1.0.0,<1.5.0",
],
entry_points={
'console_scripts': [
'rns-page-node=rns_page_node.main:main',
"console_scripts": [
"rns-page-node=rns_page_node.main:main",
],
},
classifiers=[
'Programming Language :: Python :: 3',
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
'Operating System :: OS Independent',
"Programming Language :: Python :: 3",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Operating System :: OS Independent",
],
)

View File

@@ -1,29 +1,26 @@
#!/usr/bin/env python3
import os
import sys
import time
import threading
import time
import RNS
# Determine base directory for tests
dir_path = os.path.abspath(os.path.dirname(__file__))
config_dir = os.path.join(dir_path, 'config')
identity_dir = os.path.join(dir_path, 'node-config')
config_dir = os.path.join(dir_path, "config")
identity_dir = os.path.join(dir_path, "node-config")
# Initialize Reticulum with shared config
RNS.Reticulum(config_dir)
# Load server identity (created by the page node)
identity_file = os.path.join(identity_dir, 'identity')
identity_file = os.path.join(identity_dir, "identity")
server_identity = RNS.Identity.from_file(identity_file)
# Create a destination to the server node
destination = RNS.Destination(
server_identity,
RNS.Destination.OUT,
RNS.Destination.SINGLE,
'nomadnetwork',
'node'
server_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "nomadnetwork", "node",
)
# Ensure we know a path to the destination
@@ -39,66 +36,70 @@ global_link = RNS.Link(destination)
responses = {}
done_event = threading.Event()
# Callback for page response
def on_page(response):
data = response.response
if isinstance(data, bytes):
text = data.decode('utf-8')
text = data.decode("utf-8")
else:
text = str(data)
print('Received page:')
print("Received page:")
print(text)
responses['page'] = text
if 'file' in responses:
responses["page"] = text
if "file" in responses:
done_event.set()
# Callback for file response
def on_file(response):
data = response.response
# Handle response as [fileobj, headers]
if isinstance(data, list) and len(data) == 2 and hasattr(data[0], 'read'):
if isinstance(data, list) and len(data) == 2 and hasattr(data[0], "read"):
fileobj, headers = data
file_data = fileobj.read()
filename = headers.get(b'name', b'').decode('utf-8')
print(f'Received file ({filename}):')
print(file_data.decode('utf-8'))
responses['file'] = file_data.decode('utf-8')
filename = headers.get(b"name", b"").decode("utf-8")
print(f"Received file ({filename}):")
print(file_data.decode("utf-8"))
responses["file"] = file_data.decode("utf-8")
# Handle response as a raw file object
elif hasattr(data, 'read'):
elif hasattr(data, "read"):
file_data = data.read()
filename = os.path.basename('text.txt')
print(f'Received file ({filename}):')
print(file_data.decode('utf-8'))
responses['file'] = file_data.decode('utf-8')
filename = os.path.basename("text.txt")
print(f"Received file ({filename}):")
print(file_data.decode("utf-8"))
responses["file"] = file_data.decode("utf-8")
# Handle response as raw bytes
elif isinstance(data, bytes):
text = data.decode('utf-8')
print('Received file:')
text = data.decode("utf-8")
print("Received file:")
print(text)
responses['file'] = text
responses["file"] = text
else:
print('Received file (unhandled format):', data)
responses['file'] = str(data)
if 'page' in responses:
print("Received file (unhandled format):", data)
responses["file"] = str(data)
if "page" in responses:
done_event.set()
# Request the page and file once the link is established
def on_link_established(link):
link.request('/page/index.mu', None, response_callback=on_page)
link.request('/file/text.txt', None, response_callback=on_file)
link.request("/page/index.mu", None, response_callback=on_page)
link.request("/file/text.txt", None, response_callback=on_file)
# Register callbacks
global_link.set_link_established_callback(on_link_established)
global_link.set_link_closed_callback(lambda l: done_event.set())
global_link.set_link_closed_callback(lambda link: done_event.set())
# Wait for responses or timeout
if not done_event.wait(timeout=30):
print('Test timed out.', file=sys.stderr)
print("Test timed out.", file=sys.stderr)
sys.exit(1)
if responses.get('page') and responses.get('file'):
print('Tests passed!')
if responses.get("page") and responses.get("file"):
print("Tests passed!")
sys.exit(0)
else:
print('Tests failed.', file=sys.stderr)
print("Tests failed.", file=sys.stderr)
sys.exit(1)

View File

@@ -1,20 +1,26 @@
#!/usr/bin/env python3
import os
import sys
import time
import threading
import time
import RNS
dir_path = os.path.abspath(os.path.dirname(__file__))
config_dir = os.path.join(dir_path, 'config')
config_dir = os.path.join(dir_path, "config")
RNS.Reticulum(config_dir)
DESTINATION_HEX = '49b2d959db8528347d0a38083aec1042' # Ivans Node that runs rns-page-node
DESTINATION_HEX = (
"49b2d959db8528347d0a38083aec1042" # Ivans Node that runs rns-page-node
)
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH // 8) * 2
if len(DESTINATION_HEX) != dest_len:
print(f"Invalid destination length (got {len(DESTINATION_HEX)}, expected {dest_len})", file=sys.stderr)
print(
f"Invalid destination length (got {len(DESTINATION_HEX)}, expected {dest_len})",
file=sys.stderr,
)
sys.exit(1)
destination_hash = bytes.fromhex(DESTINATION_HEX)
@@ -28,32 +34,32 @@ server_identity = RNS.Identity.recall(destination_hash)
print(f"Recalled server identity for {DESTINATION_HEX}")
destination = RNS.Destination(
server_identity,
RNS.Destination.OUT,
RNS.Destination.SINGLE,
'nomadnetwork',
'node'
server_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "nomadnetwork", "node",
)
link = RNS.Link(destination)
done_event = threading.Event()
def on_page(response):
data = response.response
if isinstance(data, bytes):
text = data.decode('utf-8')
text = data.decode("utf-8")
else:
text = str(data)
print('Fetched page content:')
print("Fetched page content:")
print(text)
done_event.set()
link.set_link_established_callback(lambda l: l.request('/page/index.mu', None, response_callback=on_page))
link.set_link_closed_callback(lambda l: done_event.set())
link.set_link_established_callback(
lambda link: link.request("/page/index.mu", None, response_callback=on_page),
)
link.set_link_closed_callback(lambda link: done_event.set())
if not done_event.wait(timeout=30):
print('Timed out waiting for page', file=sys.stderr)
print("Timed out waiting for page", file=sys.stderr)
sys.exit(1)
print('Done fetching page.')
print("Done fetching page.")
sys.exit(0)