Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c900cf38c9 | |||
| 014ebc25c6 | |||
|
|
d5e9308fb5 | ||
|
|
7d5e891261 | ||
|
|
c382ed790f | ||
| cb72e57da9 | |||
|
|
aaf5ad23e2 | ||
|
|
ce1b1dad7d | ||
|
|
67ebc7e556 | ||
|
|
b31fb748b8 | ||
|
|
eb27326763 | ||
|
|
f40d5a51ae | ||
|
|
4aa83a2dfb |
4
.github/workflows/docker-test.yml
vendored
4
.github/workflows/docker-test.yml
vendored
@@ -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
|
||||
|
||||
16
.github/workflows/docker.yml
vendored
16
.github/workflows/docker.yml
vendored
@@ -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
|
||||
|
||||
14
.github/workflows/publish.yml
vendored
14
.github/workflows/publish.yml
vendored
@@ -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
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# RNS Page Node
|
||||
|
||||
[](https://github.com/Sudo-Ivan/rns-page-node/actions/workflows/docker.yml)
|
||||
[](https://github.com/Sudo-Ivan/rns-page-node/actions/workflows/docker-test.yml)
|
||||
[](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
949
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
# rns_page_node package
|
||||
__all__ = ['main']
|
||||
__all__ = ["main"]
|
||||
|
||||
@@ -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()
|
||||
|
||||
32
setup.py
32
setup.py
@@ -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",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user