Compare commits

..

1 Commits

Author SHA1 Message Date
Sudo-Ivan
7d50dcf5b4 Add stats tracking 2025-07-14 17:19:17 -05:00
29 changed files with 2207 additions and 1657 deletions

View File

@@ -1,68 +0,0 @@
name: Create Release
# This workflow creates releases:
# 1. Build packages
# 2. Create Gitea release with all artifacts atomically
# This ensures releases cannot be modified once published.
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
version:
description: 'Version to release (e.g., 0.6.8)'
required: true
type: string
permissions:
contents: write
jobs:
build:
name: Build distribution 📦
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: https://git.quad4.io/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
- name: Set up Python
uses: https://git.quad4.io/actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v5
with:
python-version: "3.13"
- name: Install pypa/build
run: python3 -m pip install build --user
- name: Build a binary wheel and a source tarball
run: python3 -m build
- name: Store the distribution packages
uses: https://git.quad4.io/actions/upload-artifact@8689daa8608e46baf41e4786cb83fbc0dea972cd # v4
with:
name: python-package-distributions
path: dist/
gitea-release:
name: Create Gitea Release
needs:
- build
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Download all the dists
uses: https://git.quad4.io/actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
name: python-package-distributions
path: dist/
- name: Create Gitea Release with artifacts
uses: https://git.quad4.io/actions/gitea-release-action@4875285c0950474efb7ca2df55233c51333eeb74
with:
tag_name: ${{ github.ref_name }}
name: Release ${{ github.ref_name }}
files: |
dist/*.tar.gz
dist/*.whl

View File

@@ -1,22 +0,0 @@
name: Safety
on:
push:
branches: [ main ]
schedule:
- cron: '0 0 * * 0' # weekly
jobs:
security:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: https://git.quad4.io/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Set up Python
uses: https://git.quad4.io/actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v5
with:
python-version: '3.10'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install .
- name: Run pip-audit
uses: https://git.quad4.io/actions/gh-action-pip-audit@66a6ee35b1b25f89c6bdc9f7c11284f08061823a # v1.1.0

27
.github/workflows/docker-test.yml vendored Normal file
View File

@@ -0,0 +1,27 @@
name: Docker Build Test
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Build Docker Image
run: docker build . --file Dockerfile --build-arg PYTHON_VERSION=${{ matrix.python-version }} --tag lxmfy-test:${{ matrix.python-version }}

View File

@@ -1,15 +1,15 @@
name: Build and Publish Docker Image
on:
workflow_dispatch:
push:
branches: [ main ]
tags: [ 'v*' ]
pull_request:
branches: [ main, master ]
branches: [ main ]
env:
REGISTRY: git.quad4.io
IMAGE_NAME: RNS-Things/rns-page-node
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
@@ -17,32 +17,29 @@ jobs:
permissions:
contents: read
packages: write
outputs:
image_digest: ${{ steps.build.outputs.digest }}
image_tags: ${{ steps.meta.outputs.tags }}
steps:
- name: Checkout repository
uses: https://git.quad4.io/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
uses: actions/checkout@v4
- name: Set up QEMU
uses: https://git.quad4.io/actions/setup-qemu-action@3a1695b1353f9f8868722ffaafc1f164ef35fa5e # v3
uses: docker/setup-qemu-action@v3
with:
platforms: amd64,arm64
- name: Set up Docker Buildx
uses: https://git.quad4.io/actions/setup-buildx-action@7fbd262f0ca05b45700d8eaaf71f40837d036cc7 # v3
uses: docker/setup-buildx-action@v3
- name: Log in to the Container registry
uses: https://git.quad4.io/actions/login-action@bb91d7e20cfedb030a44164cb558bba899e1010a # v3
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: https://git.quad4.io/actions/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
@@ -54,23 +51,19 @@ jobs:
type=sha,format=short
- name: Build and push Docker image
id: build
uses: https://git.quad4.io/actions/build-push-action@dc0c2d97df39a6939d9db7d572445529e2365ec6 # v6
uses: docker/build-push-action@v5
with:
context: .
file: ./docker/Dockerfile
platforms: linux/amd64,linux/arm64
push: ${{ startsWith(github.ref, 'refs/tags/v') }}
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
BUILD_DATE=${{ github.event.head_commit.timestamp }}
VCS_REF=${{ github.sha }}
VERSION=${{ steps.meta.outputs.version }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Extract metadata (tags, labels) for Docker (rootless)
id: meta_rootless
uses: https://git.quad4.io/actions/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-rootless
tags: |
@@ -81,15 +74,13 @@ jobs:
type=sha,format=short,suffix=-rootless
- name: Build and push rootless Docker image
uses: https://git.quad4.io/actions/build-push-action@dc0c2d97df39a6939d9db7d572445529e2365ec6 # v6
uses: docker/build-push-action@v5
with:
context: .
file: ./docker/Dockerfile.rootless
file: ./Dockerfile.rootless
platforms: linux/amd64,linux/arm64
push: ${{ startsWith(github.ref, 'refs/tags/v') }}
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta_rootless.outputs.tags }}
labels: ${{ steps.meta_rootless.outputs.labels }}
build-args: |
BUILD_DATE=${{ github.event.head_commit.timestamp }}
VCS_REF=${{ github.sha }}
VERSION=${{ steps.meta_rootless.outputs.version }}
cache-from: type=gha
cache-to: type=gha,mode=max

100
.github/workflows/publish.yml vendored Normal file
View File

@@ -0,0 +1,100 @@
name: Publish Python 🐍 distribution 📦 to PyPI
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
version:
description: 'Version to release (e.g., 0.6.8)'
required: true
type: string
permissions:
contents: read
id-token: write
jobs:
build:
name: Build distribution 📦
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4.2.2
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v5.3.0
with:
python-version: "3.13"
- name: Install pypa/build
run: python3 -m pip install build --user
- 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
with:
name: python-package-distributions
path: dist/
publish-to-pypi:
name: Publish Python 🐍 distribution 📦 to PyPI
if: startsWith(github.ref, 'refs/tags/')
needs:
- build
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/rns-page-node
permissions:
id-token: write
contents: read
steps:
- name: Download all the dists
uses: actions/download-artifact@v4.1.8
with:
name: python-package-distributions
path: dist/
- name: Publish distribution 📦 to PyPI
uses: pypa/gh-action-pypi-publish@v1.12.3
github-release:
name: Sign the Python 🐍 distribution 📦 and create GitHub Release
needs:
- publish-to-pypi
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
steps:
- name: Download all the dists
uses: actions/download-artifact@v4.1.8
with:
name: python-package-distributions
path: dist/
- name: Sign the dists with Sigstore
uses: sigstore/gh-action-sigstore-python@v3.0.0
with:
inputs: >-
./dist/*.tar.gz
./dist/*.whl
- name: Create GitHub Release
env:
GITHUB_TOKEN: ${{ github.token }}
run: >-
gh release create
"$GITHUB_REF_NAME"
--repo "$GITHUB_REPOSITORY"
--notes ""
- name: Upload artifact signatures to GitHub Release
env:
GITHUB_TOKEN: ${{ github.token }}
run: >-
gh release upload
"$GITHUB_REF_NAME" dist/**
--repo "$GITHUB_REPOSITORY"

9
.gitignore vendored
View File

@@ -3,12 +3,3 @@ node-config/
files/
.ruff_cache/
__pycache__/
dist/
*.egg-info/
.ruff_cache/
.venv/
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

24
Dockerfile Normal file
View File

@@ -0,0 +1,24 @@
ARG PYTHON_VERSION=3.13
FROM python:${PYTHON_VERSION}-alpine
LABEL org.opencontainers.image.source="https://github.com/Sudo-Ivan/rns-page-node"
LABEL org.opencontainers.image.description="A simple way to serve pages and files over the Reticulum network."
LABEL org.opencontainers.image.licenses="GPL-3.0"
LABEL org.opencontainers.image.authors="Sudo-Ivan"
WORKDIR /app
RUN apk add --no-cache gcc python3-dev musl-dev linux-headers
RUN pip install poetry
ENV POETRY_VIRTUALENVS_IN_PROJECT=true
COPY pyproject.toml poetry.lock* ./
COPY README.md ./
COPY rns_page_node ./rns_page_node
RUN poetry install --no-interaction --no-ansi
ENV PATH="/app/.venv/bin:$PATH"
ENTRYPOINT ["poetry", "run", "rns-page-node"]

View File

@@ -1,4 +1,4 @@
FROM python:3.14-alpine AS builder
FROM python:3.13-alpine AS builder
RUN apk update
RUN apk add --no-cache build-base libffi-dev cargo pkgconfig gcc python3-dev musl-dev linux-headers

28
Dockerfile.rootless Normal file
View File

@@ -0,0 +1,28 @@
ARG PYTHON_VERSION=3.13
FROM python:${PYTHON_VERSION}-alpine
LABEL org.opencontainers.image.source="https://github.com/Sudo-Ivan/rns-page-node"
LABEL org.opencontainers.image.description="A simple way to serve pages and files over the Reticulum network."
LABEL org.opencontainers.image.licenses="GPL-3.0"
LABEL org.opencontainers.image.authors="Sudo-Ivan"
RUN addgroup -g 1000 app && adduser -D -u 1000 -G app app
WORKDIR /app
RUN apk add --no-cache gcc python3-dev musl-dev linux-headers
RUN pip install poetry
ENV POETRY_VIRTUALENVS_IN_PROJECT=true
COPY pyproject.toml poetry.lock* ./
COPY README.md ./
COPY rns_page_node ./rns_page_node
RUN poetry install --no-interaction --no-ansi
ENV PATH="/app/.venv/bin:$PATH"
USER app
ENTRYPOINT ["poetry", "run", "rns-page-node"]

View File

@@ -1,31 +1,20 @@
# Makefile for rns-page-node
# Extract version from pyproject.toml
VERSION := $(shell grep "^version =" pyproject.toml | cut -d '"' -f 2)
VCS_REF := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
BUILD_DATE := $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
# Detect if docker buildx is available
DOCKER_BUILD := $(shell docker buildx version >/dev/null 2>&1 && echo "docker buildx build" || echo "docker build")
DOCKER_BUILD_LOAD := $(shell docker buildx version >/dev/null 2>&1 && echo "docker buildx build --load" || echo "docker build")
# Build arguments for Docker
DOCKER_BUILD_ARGS := --build-arg VERSION=$(VERSION) \
--build-arg VCS_REF=$(VCS_REF) \
--build-arg BUILD_DATE=$(BUILD_DATE)
.PHONY: all build sdist wheel clean install lint format docker-wheels docker-build docker-run docker-build-rootless docker-run-rootless help test docker-test
all: build
build: clean
python3 -m build
python3 setup.py sdist bdist_wheel
sdist:
python3 -m build --sdist
python3 setup.py sdist
wheel:
python3 -m build --wheel
python3 setup.py bdist_wheel
clean:
rm -rf build dist *.egg-info
@@ -40,20 +29,20 @@ format:
ruff check --fix .
docker-wheels:
$(DOCKER_BUILD) --target builder -f docker/Dockerfile.build -t rns-page-node-builder .
$(DOCKER_BUILD) --target builder -f Dockerfile.build -t rns-page-node-builder .
docker create --name builder-container rns-page-node-builder true
docker cp builder-container:/src/dist ./dist
docker rm builder-container
docker-build:
$(DOCKER_BUILD_LOAD) $(DOCKER_BUILD_ARGS) $(BUILD_ARGS) -f docker/Dockerfile -t git.quad4.io/rns-things/rns-page-node:latest -t git.quad4.io/rns-things/rns-page-node:$(VERSION) .
$(DOCKER_BUILD) $(BUILD_ARGS) -f Dockerfile -t rns-page-node:latest .
docker-run:
docker run --rm -it \
-v ./pages:/app/pages \
-v ./files:/app/files \
-v ./node-config:/app/node-config \
git.quad4.io/rns-things/rns-page-node:latest \
rns-page-node:latest \
--node-name "Page Node" \
--pages-dir /app/pages \
--files-dir /app/files \
@@ -61,14 +50,14 @@ docker-run:
--announce-interval 360
docker-build-rootless:
$(DOCKER_BUILD_LOAD) $(DOCKER_BUILD_ARGS) $(BUILD_ARGS) -f docker/Dockerfile.rootless -t git.quad4.io/rns-things/rns-page-node:latest-rootless -t git.quad4.io/rns-things/rns-page-node:$(VERSION)-rootless .
$(DOCKER_BUILD) $(BUILD_ARGS) -f Dockerfile.rootless -t rns-page-node-rootless:latest .
docker-run-rootless:
docker run --rm -it \
-v ./pages:/app/pages \
-v ./files:/app/files \
-v ./node-config:/app/node-config \
git.quad4.io/rns-things/rns-page-node:latest-rootless \
rns-page-node-rootless:latest \
--node-name "Page Node" \
--pages-dir /app/pages \
--files-dir /app/files \
@@ -79,7 +68,7 @@ test:
bash tests/run_tests.sh
docker-test:
$(DOCKER_BUILD_LOAD) -f docker/Dockerfile.tests -t rns-page-node-tests .
$(DOCKER_BUILD) -f tests/Dockerfile.tests -t rns-page-node-tests .
docker run --rm rns-page-node-tests
help:
@@ -93,7 +82,7 @@ help:
@echo " lint - run ruff linter"
@echo " format - run ruff --fix"
@echo " docker-wheels - build Python wheels in Docker"
@echo " docker-build - build runtime Docker image (version: $(VERSION))"
@echo " docker-build - build runtime Docker image"
@echo " docker-run - run runtime Docker image"
@echo " docker-build-rootless - build rootless runtime Docker image"
@echo " docker-run-rootless - run rootless runtime Docker image"

131
README.md
View File

@@ -1,84 +1,27 @@
# RNS Page Node
[Русская](README.ru.md)
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.
## Features
- Serves pages and files over RNS
- Dynamic page support with environment variables
- Form data and request parameter parsing
## To Do
- [ ] Move to single small and rootless docker image
- [ ] Codebase cleanup
- [ ] Update PyPI publishing workflow
## Usage
```bash
# Pip
# May require --break-system-packages
pip install rns-page-node
# Pipx
pipx install rns-page-node
# uv
uv venv
source .venv/bin/activate
uv pip install rns-page-node
# Pipx via Git
pipx install git+https://git.quad4.io/RNS-Things/rns-page-node.git
```
## Usage
```bash
# will use current directory for pages and files
rns-page-node
```
or with command-line options:
## Usage
```bash
rns-page-node --node-name "Page Node" --pages-dir ./pages --files-dir ./files --identity-dir ./node-config --announce-interval 360
```
or with a config file:
```bash
rns-page-node /path/to/config.conf
```
### Configuration File
You can use a configuration file to persist settings. See `config.example` for an example.
Config file format is simple `key=value` pairs:
```
# Comment lines start with #
node-name=My Page Node
pages-dir=./pages
files-dir=./files
identity-dir=./node-config
announce-interval=360
```
Priority order: Command-line arguments > Config file > Defaults
### Docker/Podman
```bash
docker run -it --rm -v ./pages:/app/pages -v ./files:/app/files -v ./node-config:/app/node-config -v ./config:/root/.reticulum git.quad4.io/rns-things/rns-page-node:latest
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/Podman Rootless
@@ -86,7 +29,7 @@ docker run -it --rm -v ./pages:/app/pages -v ./files:/app/files -v ./node-config
```bash
mkdir -p ./pages ./files ./node-config ./config
chown -R 1000:1000 ./pages ./files ./node-config ./config
podman run -it --rm -v ./pages:/app/pages -v ./files:/app/files -v ./node-config:/app/node-config -v ./config:/app/config git.quad4.io/rns-things/rns-page-node:latest-rootless
podman 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-rootless
```
Mounting volumes are optional, you can also copy pages and files to the container `podman cp` or `docker cp`.
@@ -111,30 +54,60 @@ make docker-wheels
## Pages
Supports dynamic executable pages with full request data parsing. Pages can receive:
- Form fields via `field_*` environment variables
- Link variables via `var_*` environment variables
- Remote identity via `remote_identity` environment variable
- Link ID via `link_id` environment variable
Supports Micron `.mu` and dynamic pages with `#!` in the micron files.
This enables forums, chats, and other interactive applications compatible with NomadNet clients.
## Statistics Tracking
The node now includes comprehensive statistics tracking for monitoring peer connections and page/file requests:
### Command Line Options for Stats
```bash
# Print stats every 60 seconds
rns-page-node --stats-interval 60
# Save stats to JSON file on shutdown
rns-page-node --save-stats node_stats.json
# Actively write stats to file (live updates)
rns-page-node --stats-file stats.json
# Combined: live stats file + periodic display + final save
rns-page-node --stats-file stats.json --stats-interval 300 --save-stats final_stats.json
```
### Docker Stats Usage
```bash
# With periodic stats display
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 --stats-interval 60
# Save stats to mounted volume
docker run -it --rm -v ./pages:/app/pages -v ./files:/app/files -v ./node-config:/app/node-config -v ./config:/app/config -v ./stats:/app/stats ghcr.io/sudo-ivan/rns-page-node:latest --save-stats /app/stats/node_stats.json
```
### Tracked Metrics
- **Connection Statistics**: Total connections, active connections, peer tracking
- **Request Statistics**: Page requests, file requests, requests by path and peer
- **Performance Metrics**: Requests per hour, uptime, response patterns
- **Historical Data**: Recent request history, hourly/daily aggregations
- **Top Content**: Most requested pages and files, most active peers
## Options
```
Positional arguments:
node_config Path to rns-page-node config file
Optional arguments:
-c, --config Path to the Reticulum config file
-n, --node-name Name of the node
-p, --pages-dir Directory to serve pages from
-f, --files-dir Directory to serve files from
-i, --identity-dir Directory to persist the node's identity
-a, --announce-interval Interval to announce the node's presence (in minutes, default: 360 = 6 hours)
--page-refresh-interval Interval to refresh pages (in seconds, 0 = disabled)
--file-refresh-interval Interval to refresh files (in seconds, 0 = disabled)
-l, --log-level Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
-c, --config: The path to the Reticulum config file.
-n, --node-name: The name of the node.
-p, --pages-dir: The directory to serve pages from.
-f, --files-dir: The directory to serve files from.
-i, --identity-dir: The directory to persist the node's identity.
-a, --announce-interval: The interval to announce the node's presence.
--page-refresh-interval: The interval to refresh pages (seconds, 0 disables).
--file-refresh-interval: The interval to refresh files (seconds, 0 disables).
-l, --log-level: The logging level.
--stats-interval: Print stats every N seconds (0 disables).
--save-stats: Save stats to JSON file on shutdown.
```
## License

View File

@@ -1,124 +0,0 @@
# RNS Page Node
[English](README.md)
Простой способ для раздачи страниц и файлов через сеть [Reticulum](https://reticulum.network/). Прямая замена для узлов [NomadNet](https://github.com/markqvist/NomadNet), которые в основном служат для раздачи страниц и файлов.
## Особенности
- Раздача страниц и файлов через RNS
- Поддержка динамических страниц с переменными окружения
- Разбор данных форм и параметров запросов
## Установка
```bash
# Pip
# Может потребоваться --break-system-packages
pip install rns-page-node
# Pipx
pipx install rns-page-node
# uv
uv venv
source .venv/bin/activate
uv pip install rns-page-node
# Pipx через Git
pipx install git+https://git.quad4.io/RNS-Things/rns-page-node.git
```
## Использование
```bash
# будет использовать текущий каталог для страниц и файлов
rns-page-node
```
или с параметрами командной строки:
```bash
rns-page-node --node-name "Page Node" --pages-dir ./pages --files-dir ./files --identity-dir ./node-config --announce-interval 360
```
или с файлом конфигурации:
```bash
rns-page-node /путь/к/config.conf
```
### Файл Конфигурации
Вы можете использовать файл конфигурации для сохранения настроек. См. `config.example` для примера.
Формат файла конфигурации - простые пары `ключ=значение`:
```
# Строки комментариев начинаются с #
node-name=Мой Page Node
pages-dir=./pages
files-dir=./files
identity-dir=./node-config
announce-interval=360
```
Порядок приоритета: Аргументы командной строки > Файл конфигурации > Значения по умолчанию
### Docker/Podman
```bash
docker run -it --rm -v ./pages:/app/pages -v ./files:/app/files -v ./node-config:/app/node-config -v ./config:/root/.reticulum git.quad4.io/rns-things/rns-page-node:latest
```
### Docker/Podman без root-доступа
```bash
mkdir -p ./pages ./files ./node-config ./config
chown -R 1000:1000 ./pages ./files ./node-config ./config
podman run -it --rm -v ./pages:/app/pages -v ./files:/app/files -v ./node-config:/app/node-config -v ./config:/app/config git.quad4.io/rns-things/rns-page-node:latest-rootless
```
Монтирование томов необязательно, вы также можете скопировать страницы и файлы в контейнер с помощью `podman cp` или `docker cp`.
## Сборка
```bash
make build
```
Сборка wheels:
```bash
make wheel
```
### Сборка Wheels в Docker
```bash
make docker-wheels
```
## Страницы
Поддержка динамических исполняемых страниц с полным разбором данных запросов. Страницы могут получать:
- Поля форм через переменные окружения `field_*`
- Переменные ссылок через переменные окружения `var_*`
- Удаленную идентификацию через переменную окружения `remote_identity`
- ID соединения через переменную окружения `link_id`
Это позволяет создавать форумы, чаты и другие интерактивные приложения, совместимые с клиентами NomadNet.
## Параметры
```
Позиционные аргументы:
node_config Путь к файлу конфигурации rns-page-node
Необязательные аргументы:
-c, --config Путь к файлу конфигурации Reticulum
-n, --node-name Имя узла
-p, --pages-dir Каталог для раздачи страниц
-f, --files-dir Каталог для раздачи файлов
-i, --identity-dir Каталог для сохранения идентификационных данных узла
-a, --announce-interval Интервал анонсирования присутствия узла (в минутах, по умолчанию: 360 = 6 часов)
--page-refresh-interval Интервал обновления страниц (в секундах, 0 = отключено)
--file-refresh-interval Интервал обновления файлов (в секундах, 0 = отключено)
-l, --log-level Уровень логирования (DEBUG, INFO, WARNING, ERROR, CRITICAL)
```
## Лицензия
Этот проект включает части кодовой базы [NomadNet](https://github.com/markqvist/NomadNet), которая лицензирована под GNU General Public License v3.0 (GPL-3.0). Как производная работа, этот проект также распространяется на условиях GPL-3.0. Полный текст лицензии смотрите в файле [LICENSE](LICENSE).

View File

@@ -1,201 +0,0 @@
version: '3'
vars:
VERSION:
sh: grep "^version =" pyproject.toml | cut -d '"' -f 2
VCS_REF:
sh: git rev-parse --short HEAD 2>/dev/null || echo "unknown"
BUILD_DATE:
sh: date -u +"%Y-%m-%dT%H:%M:%SZ"
DOCKER_BUILD:
sh: docker buildx version >/dev/null 2>&1 && echo "docker buildx build" || echo "docker build"
DOCKER_BUILD_LOAD:
sh: docker buildx version >/dev/null 2>&1 && echo "docker buildx build --load" || echo "docker build"
IMAGE_NAME: git.quad4.io/rns-things/rns-page-node
tasks:
default:
desc: Show available tasks
cmds:
- task --list
build:
desc: Clean and build sdist and wheel
deps: [clean]
cmds:
- python3 -m build
sdist:
desc: Build source distribution
cmds:
- python3 -m build --sdist
wheel:
desc: Build wheel
cmds:
- python3 -m build --wheel
clean:
desc: Remove build artifacts
cmds:
- rm -rf build dist *.egg-info
install:
desc: Install built wheel
deps: [build]
cmds:
- pip install dist/*.whl
install-dev:
desc: Install package in development mode
cmds:
- pip install -e .
lint:
desc: Run ruff linter
cmds:
- ruff check .
format:
desc: Run ruff formatter with auto-fix
cmds:
- ruff check --fix .
check:
desc: Run all code quality checks
deps: [lint]
test:
desc: Run local integration tests
cmds:
- bash tests/run_tests.sh
docker-wheels:
desc: Build Python wheels in Docker
cmds:
- '{{.DOCKER_BUILD}} --target builder -f docker/Dockerfile.build -t rns-page-node-builder .'
- docker create --name builder-container rns-page-node-builder true
- docker cp builder-container:/src/dist ./dist
- docker rm builder-container
docker-build:
desc: Build runtime Docker image
cmds:
- >
{{.DOCKER_BUILD_LOAD}}
--build-arg VERSION={{.VERSION}}
--build-arg VCS_REF={{.VCS_REF}}
--build-arg BUILD_DATE={{.BUILD_DATE}}
-f docker/Dockerfile
-t {{.IMAGE_NAME}}:latest
-t {{.IMAGE_NAME}}:{{.VERSION}}
.
docker-run:
desc: Run runtime Docker image
cmds:
- >
docker run --rm -it
-v ./pages:/app/pages
-v ./files:/app/files
-v ./node-config:/app/node-config
{{.IMAGE_NAME}}:latest
--node-name "Page Node"
--pages-dir /app/pages
--files-dir /app/files
--identity-dir /app/node-config
--announce-interval 360
docker-build-rootless:
desc: Build rootless runtime Docker image
cmds:
- >
{{.DOCKER_BUILD_LOAD}}
--build-arg VERSION={{.VERSION}}
--build-arg VCS_REF={{.VCS_REF}}
--build-arg BUILD_DATE={{.BUILD_DATE}}
-f docker/Dockerfile.rootless
-t {{.IMAGE_NAME}}:latest-rootless
-t {{.IMAGE_NAME}}:{{.VERSION}}-rootless
.
docker-run-rootless:
desc: Run rootless runtime Docker image
cmds:
- >
docker run --rm -it
-v ./pages:/app/pages
-v ./files:/app/files
-v ./node-config:/app/node-config
{{.IMAGE_NAME}}:latest-rootless
--node-name "Page Node"
--pages-dir /app/pages
--files-dir /app/files
--identity-dir /app/node-config
--announce-interval 360
docker-test:
desc: Build and run integration tests in Docker
cmds:
- '{{.DOCKER_BUILD_LOAD}} -f docker/Dockerfile.tests -t rns-page-node-tests .'
- docker run --rm rns-page-node-tests
docker-clean:
desc: Remove Docker images and containers
cmds:
- docker rmi {{.IMAGE_NAME}}:latest {{.IMAGE_NAME}}:{{.VERSION}} 2>/dev/null || true
- docker rmi {{.IMAGE_NAME}}:latest-rootless {{.IMAGE_NAME}}:{{.VERSION}}-rootless 2>/dev/null || true
- docker rmi rns-page-node-builder rns-page-node-tests 2>/dev/null || true
run:
desc: Run rns-page-node locally
cmds:
- python3 -m rns_page_node.main
run-dev:
desc: Run rns-page-node with development settings
cmds:
- python3 -m rns_page_node.main --log-level DEBUG
venv:
desc: Create Python virtual environment
cmds:
- python3 -m venv .venv
- 'echo "Virtual environment created. Activate with: source .venv/bin/activate"'
deps-install:
desc: Install dependencies using pip
cmds:
- pip install -r requirements.txt || pip install -e .
deps-update:
desc: Update dependencies
cmds:
- pip install --upgrade -r requirements.txt || pip install --upgrade -e .
version:
desc: Show current version
cmds:
- 'echo "Version: {{.VERSION}}"'
- 'echo "VCS Ref: {{.VCS_REF}}"'
- 'echo "Build Date: {{.BUILD_DATE}}"'
setup-dirs:
desc: Create required directories for running the node
cmds:
- mkdir -p pages files node-config
nix-shell:
desc: Enter Nix development shell
cmds:
- nix develop
nix-build:
desc: Build with Nix
cmds:
- nix build
all:
desc: Run build, lint, and test
deps: [build, check, test]

View File

@@ -1,31 +0,0 @@
# rns-page-node configuration file
# Lines starting with # are comments
# Format: key=value
# Reticulum config directory path
# reticulum-config=/path/to/reticulum/config
# Node display name
node-name=My Page Node
# Pages directory
pages-dir=./pages
# Files directory
files-dir=./files
# Node identity directory
identity-dir=./node-config
# Announce interval in minutes (default: 360 = 6 hours)
announce-interval=360
# Page refresh interval in seconds (0 = disabled)
page-refresh-interval=300
# File refresh interval in seconds (0 = disabled)
file-refresh-interval=300
# Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
log-level=INFO

View File

@@ -1,36 +0,0 @@
ARG PYTHON_VERSION=3.13
FROM python:${PYTHON_VERSION}-alpine
ARG BUILD_DATE
ARG VCS_REF
ARG VERSION
LABEL org.opencontainers.image.created=$BUILD_DATE \
org.opencontainers.image.title="RNS Page Node" \
org.opencontainers.image.description="A simple way to serve pages and files over the Reticulum network." \
org.opencontainers.image.url="https://git.quad4.io/RNS-Things/rns-page-node" \
org.opencontainers.image.documentation="https://git.quad4.io/RNS-Things/rns-page-node/src/branch/main/README.md" \
org.opencontainers.image.source="https://git.quad4.io/RNS-Things/rns-page-node" \
org.opencontainers.image.version=$VERSION \
org.opencontainers.image.revision=$VCS_REF \
org.opencontainers.image.vendor="RNS-Things" \
org.opencontainers.image.licenses="GPL-3.0" \
org.opencontainers.image.authors="Sudo-Ivan" \
org.opencontainers.image.base.name="python:${PYTHON_VERSION}-alpine"
WORKDIR /app
RUN apk add --no-cache gcc python3-dev musl-dev linux-headers
RUN pip install poetry
ENV POETRY_VIRTUALENVS_IN_PROJECT=true
COPY pyproject.toml poetry.lock* ./
COPY README.md ./
COPY rns_page_node ./rns_page_node
RUN poetry install --no-interaction --no-ansi
ENV PATH="/app/.venv/bin:$PATH"
ENTRYPOINT ["poetry", "run", "rns-page-node"]

View File

@@ -1,40 +0,0 @@
ARG PYTHON_VERSION=3.13
FROM python:${PYTHON_VERSION}-alpine
ARG BUILD_DATE
ARG VCS_REF
ARG VERSION
LABEL org.opencontainers.image.created=$BUILD_DATE \
org.opencontainers.image.title="RNS Page Node (Rootless)" \
org.opencontainers.image.description="A simple way to serve pages and files over the Reticulum network." \
org.opencontainers.image.url="https://git.quad4.io/RNS-Things/rns-page-node" \
org.opencontainers.image.documentation="https://git.quad4.io/RNS-Things/rns-page-node/src/branch/main/README.md" \
org.opencontainers.image.source="https://git.quad4.io/RNS-Things/rns-page-node" \
org.opencontainers.image.version=$VERSION \
org.opencontainers.image.revision=$VCS_REF \
org.opencontainers.image.vendor="RNS-Things" \
org.opencontainers.image.licenses="GPL-3.0" \
org.opencontainers.image.authors="Sudo-Ivan" \
org.opencontainers.image.base.name="python:${PYTHON_VERSION}-alpine"
RUN addgroup -g 1000 app && adduser -D -u 1000 -G app app
WORKDIR /app
RUN apk add --no-cache gcc python3-dev musl-dev linux-headers
RUN pip install poetry
ENV POETRY_VIRTUALENVS_IN_PROJECT=true
COPY pyproject.toml poetry.lock* ./
COPY README.md ./
COPY rns_page_node ./rns_page_node
RUN poetry install --no-interaction --no-ansi
ENV PATH="/app/.venv/bin:$PATH"
USER app
ENTRYPOINT ["poetry", "run", "rns-page-node"]

61
flake.lock generated
View File

@@ -1,61 +0,0 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1766902085,
"narHash": "sha256-coBu0ONtFzlwwVBzmjacUQwj3G+lybcZ1oeNSQkgC0M=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "c0b0e0fddf73fd517c3471e546c0df87a42d53f4",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

View File

@@ -1,35 +0,0 @@
{
description = "A simple way to serve pages and files over the Reticulum network";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs {
inherit system;
};
python = pkgs.python3;
pythonPackages = python.pkgs;
in
{
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
python
poetry
go-task
pythonPackages.build
pythonPackages.pip
pythonPackages.setuptools
pythonPackages.wheel
pythonPackages.ruff
];
};
});
}

1543
poetry.lock generated
View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +1,16 @@
[project]
name = "rns-page-node"
version = "1.3.0"
version = "0.2.0"
license = "GPL-3.0-only"
description = "A simple way to serve pages and files over the Reticulum network."
authors = [
{name = "Sudo-Ivan"}
]
readme = "README.md"
requires-python = ">=3.9.2"
requires-python = ">=3.10"
dependencies = [
"rns (>=1.0.4,<1.5.0)",
"cryptography>=46.0.3"
"rns (>=1.0.0,<1.5.0)"
]
classifiers = [
"Programming Language :: Python :: 3",
"Operating System :: OS Independent",
]
[project.urls]
Homepage = "https://git.quad4.io/RNS-Things/rns-page-node"
Repository = "https://git.quad4.io/RNS-Things/rns-page-node"
[project.scripts]
rns-page-node = "rns_page_node.main:main"
@@ -29,4 +20,6 @@ requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.poetry.group.dev.dependencies]
ruff = "^0.14.10"
ruff = "^0.12.3"
safety = "^3.6.0"

View File

@@ -1,3 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
}

View File

@@ -1,2 +1 @@
rns=1.0.4
cryptography==46.0.3
rns=1.0.0

View File

@@ -1,6 +1,2 @@
"""RNS Page Node package.
A minimal Reticulum page node that serves .mu pages and files over RNS.
"""
__all__ = ["main"]
# rns_page_node package
__all__ = ['main']

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,28 +1,31 @@
from setuptools import setup, find_packages
with open('README.md', 'r', encoding='utf-8') as fh:
long_description = fh.read()
setup(
name="rns-page-node",
version="1.3.0",
description="A simple way to serve pages and files over the Reticulum network.",
long_description=open("README.md").read(),
long_description_content_type="text/markdown",
author="Sudo-Ivan",
url="https://git.quad4.io/RNS-Things/rns-page-node",
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.',
long_description=long_description,
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',
install_requires=[
"rns>=1.0.4,<1.5.0",
"cryptography>=46.0.3",
'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",
"Operating System :: OS Independent",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
'Programming Language :: Python :: 3',
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
'Operating System :: OS Independent',
],
python_requires=">=3.9.2",
)

View File

@@ -1,4 +1,4 @@
FROM python:3.14-slim
FROM python:3.10-slim
RUN apt-get update && apt-get install -y build-essential libssl-dev && rm -rf /var/lib/apt/lists/*

28
tests/run_tests.sh Executable file → Normal file
View File

@@ -9,33 +9,11 @@ rm -rf config node-config pages files node.log
mkdir -p config node-config pages files
# Create a sample page and a test file
cat > pages/index.mu << 'EOF'
#!/usr/bin/env python3
import os
print("`F0f0`_`Test Page`_")
print("This is a test page with environment variable support.")
print()
print("`F0f0`_`Environment Variables`_")
params = []
for key, value in os.environ.items():
if key.startswith(('field_', 'var_')):
params.append(f"- `Faaa`{key}`f: `F0f0`{value}`f")
if params:
print("\n".join(params))
else:
print("- No parameters received")
print()
print("`F0f0`_`Remote Identity`_")
remote_id = os.environ.get('remote_identity', '33aff86b736acd47dca07e84630fd192') # Mock for testing
print(f"`Faaa`{remote_id}`f")
cat > pages/index.mu << EOF
>Test Page
This is a test page.
EOF
chmod +x pages/index.mu
cat > files/text.txt << EOF
This is a test file.
EOF

View File

@@ -1,21 +1,20 @@
#!/usr/bin/env python3
import os
import sys
import threading
import time
import threading
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
@@ -23,8 +22,8 @@ destination = RNS.Destination(
server_identity,
RNS.Destination.OUT,
RNS.Destination.SINGLE,
"nomadnetwork",
"node",
'nomadnetwork',
'node'
)
# Ensure we know a path to the destination
@@ -40,190 +39,66 @@ global_link = RNS.Link(destination)
responses = {}
done_event = threading.Event()
# Test data for environment variables
test_data_dict = {
"var_field_test": "dictionary_value",
"var_field_message": "hello_world",
"var_action": "test_action",
}
test_data_dict2 = {
"field_username": "testuser",
"field_message": "hello_from_form",
"var_action": "submit",
}
# 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 (no data):")
print('Received page:')
print(text)
responses["page"] = text
check_responses()
# Callback for page response with dictionary data
def on_page_dict(response):
data = response.response
if isinstance(data, bytes):
text = data.decode("utf-8")
else:
text = str(data)
print("Received page (dict data):")
print(text)
responses["page_dict"] = text
check_responses()
# Callback for page response with second dict data
def on_page_dict2(response):
data = response.response
if isinstance(data, bytes):
text = data.decode("utf-8")
else:
text = str(data)
print("Received page (dict2 data):")
print(text)
responses["page_dict2"] = text
check_responses()
def check_responses():
if (
"page" in responses
and "page_dict" in responses
and "page_dict2" in responses
and "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)
check_responses()
print('Received file (unhandled format):', data)
responses['file'] = str(data)
if 'page' in responses:
done_event.set()
# Request the pages and file once the link is established
# Request the page and file once the link is established
def on_link_established(link):
# Test page without data
link.request("/page/index.mu", None, response_callback=on_page)
# Test page with dictionary data (simulates var_ prefixed data)
link.request("/page/index.mu", test_data_dict, response_callback=on_page_dict)
# Test page with form field data (simulates field_ prefixed data)
link.request("/page/index.mu", test_data_dict2, response_callback=on_page_dict2)
# Test file serving
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 link: done_event.set())
global_link.set_link_closed_callback(lambda l: 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)
# Validate test results
def validate_test_results():
"""Validate that all responses contain expected content"""
# Check basic page response (no data)
if "page" not in responses:
print("ERROR: No basic page response received", file=sys.stderr)
return False
page_content = responses["page"]
if "No parameters received" not in page_content:
print("ERROR: Basic page should show 'No parameters received'", file=sys.stderr)
return False
if "33aff86b736acd47dca07e84630fd192" not in page_content:
print("ERROR: Basic page should show mock remote identity", file=sys.stderr)
return False
# Check page with dictionary data
if "page_dict" not in responses:
print("ERROR: No dictionary data page response received", file=sys.stderr)
return False
dict_content = responses["page_dict"]
if "var_field_test" not in dict_content or "dictionary_value" not in dict_content:
print(
"ERROR: Dictionary data page should contain processed environment variables",
file=sys.stderr,
)
return False
if "33aff86b736acd47dca07e84630fd192" not in dict_content:
print(
"ERROR: Dictionary data page should show mock remote identity",
file=sys.stderr,
)
return False
# Check page with second dictionary data (form fields)
if "page_dict2" not in responses:
print("ERROR: No dict2 data page response received", file=sys.stderr)
return False
dict2_content = responses["page_dict2"]
if "field_username" not in dict2_content or "testuser" not in dict2_content:
print(
"ERROR: Dict2 data page should contain processed environment variables",
file=sys.stderr,
)
return False
if "33aff86b736acd47dca07e84630fd192" not in dict2_content:
print(
"ERROR: Dict2 data page should show mock remote identity",
file=sys.stderr,
)
return False
# Check file response
if "file" not in responses:
print("ERROR: No file response received", file=sys.stderr)
return False
file_content = responses["file"]
if "This is a test file" not in file_content:
print("ERROR: File content doesn't match expected content", file=sys.stderr)
return False
return True
if validate_test_results():
print("All tests passed! Environment variable processing works correctly.")
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,26 +1,20 @@
#!/usr/bin/env python3
import os
import sys
import threading
import time
import threading
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)
@@ -37,33 +31,29 @@ destination = RNS.Destination(
server_identity,
RNS.Destination.OUT,
RNS.Destination.SINGLE,
"nomadnetwork",
"node",
'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 link: link.request("/page/index.mu", None, response_callback=on_page),
)
link.set_link_closed_callback(lambda link: 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())
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)