Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
3438b271a5
|
|||
| d6228d6d63 | |||
|
ccf954681b
|
|||
|
4ec44900cf
|
|||
|
d4099fb9a2
|
|||
|
1571b315b2
|
|||
|
71bd49bd7d
|
|||
|
382413dc08
|
|||
|
0621facc7d
|
|||
|
50cbfed5fa
|
|||
|
36d9a3350b
|
|||
|
515a9d9dbf
|
|||
|
3c27b4f9b8
|
|||
|
851c8c05d4
|
|||
|
8002a75e26
|
|||
|
06e6b55ecc
|
|||
|
48e47bd0bd
|
|||
|
9c074a0333
|
|||
|
f2314f862c
|
|||
|
6e57536650
|
|||
|
5fd7551874
|
|||
|
62d592c4d0
|
|||
|
8af2a9abbb
|
|||
|
64ca8bd4d2
|
|||
|
f1d025bd0e
|
|||
|
087ff563a2
|
|||
|
882dacf2bb
|
36
.github/workflows/publish.yml
vendored
36
.github/workflows/publish.yml
vendored
@@ -1,5 +1,14 @@
|
|||||||
name: Publish Python 🐍 distribution 📦 to PyPI
|
name: Publish Python 🐍 distribution 📦 to PyPI
|
||||||
|
|
||||||
|
# This workflow creates immutable releases:
|
||||||
|
# 1. Build packages
|
||||||
|
# 2. Publish to PyPI (only on tag push)
|
||||||
|
# 3. After successful PyPI publish:
|
||||||
|
# - Sign artifacts
|
||||||
|
# - Check if GitHub release exists (idempotent)
|
||||||
|
# - Create release with all artifacts atomically
|
||||||
|
# This ensures releases cannot be modified once published.
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
@@ -83,18 +92,27 @@ jobs:
|
|||||||
inputs: >-
|
inputs: >-
|
||||||
./dist/*.tar.gz
|
./dist/*.tar.gz
|
||||||
./dist/*.whl
|
./dist/*.whl
|
||||||
- name: Create GitHub Release
|
- name: Check if release exists
|
||||||
|
id: check_release
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
if gh release view "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
|
||||||
|
echo "exists=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "Release $GITHUB_REF_NAME already exists, skipping creation"
|
||||||
|
else
|
||||||
|
echo "exists=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "Release $GITHUB_REF_NAME does not exist, will create"
|
||||||
|
fi
|
||||||
|
continue-on-error: true
|
||||||
|
- name: Create GitHub Release with artifacts
|
||||||
|
if: steps.check_release.outputs.exists != 'true'
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ github.token }}
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
run: >-
|
run: >-
|
||||||
gh release create
|
gh release create
|
||||||
"$GITHUB_REF_NAME"
|
"$GITHUB_REF_NAME"
|
||||||
--repo "$GITHUB_REPOSITORY"
|
--repo "$GITHUB_REPOSITORY"
|
||||||
--notes ""
|
--title "Release $GITHUB_REF_NAME"
|
||||||
- name: Upload artifact signatures to GitHub Release
|
--notes "PyPI: https://pypi.org/project/rns-page-node/$GITHUB_REF_NAME/"
|
||||||
env:
|
dist/*
|
||||||
GITHUB_TOKEN: ${{ github.token }}
|
|
||||||
run: >-
|
|
||||||
gh release upload
|
|
||||||
"$GITHUB_REF_NAME" dist/**
|
|
||||||
--repo "$GITHUB_REPOSITORY"
|
|
||||||
17
.github/workflows/safety.yml
vendored
Normal file
17
.github/workflows/safety.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
name: Safety
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * 0' # weekly
|
||||||
|
jobs:
|
||||||
|
security:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@main
|
||||||
|
- name: Run Safety CLI to check for vulnerabilities
|
||||||
|
uses: pyupio/safety-action@7baf6605473beffc874c1313ddf2db085c0cacf2 # v1
|
||||||
|
with:
|
||||||
|
api-key: ${{ secrets.SAFETY_API_KEY }}
|
||||||
9
.github/workflows/tests.yml
vendored
9
.github/workflows/tests.yml
vendored
@@ -8,13 +8,18 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ matrix.os }}
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
|
os: ["ubuntu-latest", "windows-latest"]
|
||||||
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -40,5 +45,5 @@ jobs:
|
|||||||
if: failure()
|
if: failure()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: test-logs-python-${{ matrix.python-version }}
|
name: test-logs-${{ matrix.os }}-${{ matrix.python-version }}
|
||||||
path: tests/node.log
|
path: tests/node.log
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -11,6 +11,4 @@ dist/
|
|||||||
.env.local
|
.env.local
|
||||||
.env.development.local
|
.env.development.local
|
||||||
.env.test.local
|
.env.test.local
|
||||||
.env.production.local
|
.env.production.local
|
||||||
|
|
||||||
microvm/
|
|
||||||
6
Makefile
6
Makefile
@@ -9,13 +9,13 @@ DOCKER_BUILD_LOAD := $(shell docker buildx version >/dev/null 2>&1 && echo "dock
|
|||||||
all: build
|
all: build
|
||||||
|
|
||||||
build: clean
|
build: clean
|
||||||
python3 setup.py sdist bdist_wheel
|
python3 -m build
|
||||||
|
|
||||||
sdist:
|
sdist:
|
||||||
python3 setup.py sdist
|
python3 -m build --sdist
|
||||||
|
|
||||||
wheel:
|
wheel:
|
||||||
python3 setup.py bdist_wheel
|
python3 -m build --wheel
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf build dist *.egg-info
|
rm -rf build dist *.egg-info
|
||||||
|
|||||||
69
README.md
69
README.md
@@ -10,13 +10,9 @@ A simple way to serve pages and files over the [Reticulum network](https://retic
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Static and Dynamic pages.
|
- Serves pages and files over RNS
|
||||||
- Serve files
|
- Dynamic page support with environment variables
|
||||||
- Simple
|
- Form data and request parameter parsing
|
||||||
|
|
||||||
## To-Do
|
|
||||||
|
|
||||||
- Parameter parsing for forums, chat etc...
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@@ -36,22 +32,47 @@ uv venv
|
|||||||
source .venv/bin/activate
|
source .venv/bin/activate
|
||||||
uv pip install rns-page-node
|
uv pip install rns-page-node
|
||||||
|
|
||||||
# Git
|
# Pipx via Git
|
||||||
|
|
||||||
pipx install git+https://github.com/Sudo-Ivan/rns-page-node.git
|
pipx install git+https://github.com/Sudo-Ivan/rns-page-node.git
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# will use current directory for pages and files
|
# will use current directory for pages and files
|
||||||
rns-page-node
|
rns-page-node
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
or with command-line options:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
rns-page-node --node-name "Page Node" --pages-dir ./pages --files-dir ./files --identity-dir ./node-config --announce-interval 360
|
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
|
### Docker/Podman
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -88,20 +109,30 @@ make docker-wheels
|
|||||||
|
|
||||||
## Pages
|
## Pages
|
||||||
|
|
||||||
Supports dynamic pages but not request data parsing yet.
|
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
|
||||||
|
|
||||||
|
This enables forums, chats, and other interactive applications compatible with NomadNet clients.
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
```
|
```
|
||||||
-c, --config: The path to the Reticulum config file.
|
Positional arguments:
|
||||||
-n, --node-name: The name of the node.
|
node_config Path to rns-page-node config file
|
||||||
-p, --pages-dir: The directory to serve pages from.
|
|
||||||
-f, --files-dir: The directory to serve files from.
|
Optional arguments:
|
||||||
-i, --identity-dir: The directory to persist the node's identity.
|
-c, --config Path to the Reticulum config file
|
||||||
-a, --announce-interval: The interval to announce the node's presence.
|
-n, --node-name Name of the node
|
||||||
-r, --page-refresh-interval: The interval to refresh pages.
|
-p, --pages-dir Directory to serve pages from
|
||||||
-f, --file-refresh-interval: The interval to refresh files.
|
-f, --files-dir Directory to serve files from
|
||||||
-l, --log-level: The logging level.
|
-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)
|
||||||
```
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|||||||
86
README.ru.md
86
README.ru.md
@@ -1,49 +1,73 @@
|
|||||||
# RNS Page Node
|
# RNS Page Node
|
||||||
|
|
||||||
|
[English](README.md)
|
||||||
|
|
||||||
Простой способ для раздачи страниц и файлов через сеть [Reticulum](https://reticulum.network/). Прямая замена для узлов [NomadNet](https://github.com/markqvist/NomadNet), которые в основном служат для раздачи страниц и файлов.
|
Простой способ для раздачи страниц и файлов через сеть [Reticulum](https://reticulum.network/). Прямая замена для узлов [NomadNet](https://github.com/markqvist/NomadNet), которые в основном служат для раздачи страниц и файлов.
|
||||||
|
|
||||||
## Использование
|
## Особенности
|
||||||
|
|
||||||
|
- Раздача страниц и файлов через RNS
|
||||||
|
- Поддержка динамических страниц с переменными окружения
|
||||||
|
- Разбор данных форм и параметров запросов
|
||||||
|
|
||||||
|
## Установка
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Pip
|
# Pip
|
||||||
# Может потребоваться --break-system-packages
|
# Может потребоваться --break-system-packages
|
||||||
|
|
||||||
pip install rns-page-node
|
pip install rns-page-node
|
||||||
|
|
||||||
# Pipx
|
# Pipx
|
||||||
|
|
||||||
pipx install rns-page-node
|
pipx install rns-page-node
|
||||||
|
|
||||||
# uv
|
# uv
|
||||||
|
|
||||||
uv venv
|
uv venv
|
||||||
source .venv/bin/activate
|
source .venv/bin/activate
|
||||||
uv pip install rns-page-node
|
uv pip install rns-page-node
|
||||||
|
|
||||||
# Git
|
# Pipx через Git
|
||||||
|
|
||||||
pipx install git+https://github.com/Sudo-Ivan/rns-page-node.git
|
pipx install git+https://github.com/Sudo-Ivan/rns-page-node.git
|
||||||
```
|
|
||||||
|
|
||||||
|
```
|
||||||
|
## Использование
|
||||||
```bash
|
```bash
|
||||||
# будет использовать текущий каталог для страниц и файлов
|
# будет использовать текущий каталог для страниц и файлов
|
||||||
rns-page-node
|
rns-page-node
|
||||||
```
|
```
|
||||||
|
|
||||||
## Использование
|
или с параметрами командной строки:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
rns-page-node --node-name "Page Node" --pages-dir ./pages --files-dir ./files --identity-dir ./node-config --announce-interval 360
|
rns-page-node --node-name "Page Node" --pages-dir ./pages --files-dir ./files --identity-dir ./node-config --announce-interval 360
|
||||||
```
|
```
|
||||||
|
|
||||||
### Docker/Podman
|
или с файлом конфигурации:
|
||||||
|
```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
|
```bash
|
||||||
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 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 без root
|
### Docker/Podman без root-доступа
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir -p ./pages ./files ./node-config ./config
|
mkdir -p ./pages ./files ./node-config ./config
|
||||||
chown -R 1000:1000 ./pages ./files ./node-config ./config
|
chown -R 1000:1000 ./pages ./files ./node-config ./config
|
||||||
@@ -53,42 +77,48 @@ podman run -it --rm -v ./pages:/app/pages -v ./files:/app/files -v ./node-config
|
|||||||
Монтирование томов необязательно, вы также можете скопировать страницы и файлы в контейнер с помощью `podman cp` или `docker cp`.
|
Монтирование томов необязательно, вы также можете скопировать страницы и файлы в контейнер с помощью `podman cp` или `docker cp`.
|
||||||
|
|
||||||
## Сборка
|
## Сборка
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make build
|
make build
|
||||||
```
|
```
|
||||||
|
|
||||||
Сборка wheels:
|
Сборка wheels:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make wheel
|
make wheel
|
||||||
```
|
```
|
||||||
|
|
||||||
### Сборка Wheels в Docker
|
### Сборка Wheels в Docker
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make docker-wheels
|
make docker-wheels
|
||||||
```
|
```
|
||||||
|
|
||||||
## Страницы
|
## Страницы
|
||||||
|
|
||||||
Поддерживаются динамические страницы, но парсинг данных запроса пока не реализован.
|
Поддержка динамических исполняемых страниц с полным разбором данных запросов. Страницы могут получать:
|
||||||
|
- Поля форм через переменные окружения `field_*`
|
||||||
|
- Переменные ссылок через переменные окружения `var_*`
|
||||||
|
- Удаленную идентификацию через переменную окружения `remote_identity`
|
||||||
|
- ID соединения через переменную окружения `link_id`
|
||||||
|
|
||||||
## Опции
|
Это позволяет создавать форумы, чаты и другие интерактивные приложения, совместимые с клиентами NomadNet.
|
||||||
|
|
||||||
|
## Параметры
|
||||||
|
|
||||||
```
|
```
|
||||||
-c, --config: Путь к файлу конфигурации Reticulum.
|
Позиционные аргументы:
|
||||||
-n, --node-name: Имя узла.
|
node_config Путь к файлу конфигурации rns-page-node
|
||||||
-p, --pages-dir: Каталог для раздачи страниц.
|
|
||||||
-f, --files-dir: Каталог для раздачи файлов.
|
Необязательные аргументы:
|
||||||
-i, --identity-dir: Каталог для сохранения идентификационных данных узла.
|
-c, --config Путь к файлу конфигурации Reticulum
|
||||||
-a, --announce-interval: Интервал анонсирования присутствия узла.
|
-n, --node-name Имя узла
|
||||||
-r, --page-refresh-interval: Интервал обновления страниц.
|
-p, --pages-dir Каталог для раздачи страниц
|
||||||
-f, --file-refresh-interval: Интервал обновления файлов.
|
-f, --files-dir Каталог для раздачи файлов
|
||||||
-l, --log-level: Уровень логирования.
|
-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).
|
Этот проект включает части кодовой базы [NomadNet](https://github.com/markqvist/NomadNet), которая лицензирована под GNU General Public License v3.0 (GPL-3.0). Как производная работа, этот проект также распространяется на условиях GPL-3.0. Полный текст лицензии смотрите в файле [LICENSE](LICENSE).
|
||||||
|
|
||||||
31
config.example
Normal file
31
config.example
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
124
poetry.lock
generated
124
poetry.lock
generated
@@ -100,52 +100,80 @@ pycparser = {version = "*", markers = "implementation_name != \"PyPy\""}
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cryptography"
|
name = "cryptography"
|
||||||
version = "43.0.3"
|
version = "46.0.3"
|
||||||
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
|
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = "!=3.9.0,!=3.9.1,>=3.8"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"},
|
{file = "cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a"},
|
||||||
{file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"},
|
{file = "cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc"},
|
||||||
{file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f"},
|
{file = "cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d"},
|
||||||
{file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6"},
|
{file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb"},
|
||||||
{file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18"},
|
{file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849"},
|
||||||
{file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd"},
|
{file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8"},
|
||||||
{file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73"},
|
{file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec"},
|
||||||
{file = "cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2"},
|
{file = "cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91"},
|
||||||
{file = "cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd"},
|
{file = "cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e"},
|
||||||
{file = "cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984"},
|
{file = "cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926"},
|
||||||
{file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5"},
|
{file = "cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71"},
|
||||||
{file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4"},
|
{file = "cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac"},
|
||||||
{file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7"},
|
{file = "cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018"},
|
||||||
{file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405"},
|
{file = "cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb"},
|
||||||
{file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16"},
|
{file = "cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c"},
|
||||||
{file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73"},
|
{file = "cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217"},
|
||||||
{file = "cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995"},
|
{file = "cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5"},
|
||||||
{file = "cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362"},
|
{file = "cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715"},
|
||||||
{file = "cryptography-43.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c"},
|
{file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54"},
|
||||||
{file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3"},
|
{file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459"},
|
||||||
{file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83"},
|
{file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422"},
|
||||||
{file = "cryptography-43.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7"},
|
{file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7"},
|
||||||
{file = "cryptography-43.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664"},
|
{file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044"},
|
||||||
{file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08"},
|
{file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665"},
|
||||||
{file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa"},
|
{file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3"},
|
||||||
{file = "cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff"},
|
{file = "cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20"},
|
||||||
{file = "cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805"},
|
{file = "cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de"},
|
||||||
|
{file = "cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914"},
|
||||||
|
{file = "cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db"},
|
||||||
|
{file = "cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21"},
|
||||||
|
{file = "cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936"},
|
||||||
|
{file = "cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683"},
|
||||||
|
{file = "cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d"},
|
||||||
|
{file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0"},
|
||||||
|
{file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc"},
|
||||||
|
{file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3"},
|
||||||
|
{file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971"},
|
||||||
|
{file = "cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac"},
|
||||||
|
{file = "cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04"},
|
||||||
|
{file = "cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506"},
|
||||||
|
{file = "cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963"},
|
||||||
|
{file = "cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4"},
|
||||||
|
{file = "cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df"},
|
||||||
|
{file = "cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f"},
|
||||||
|
{file = "cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372"},
|
||||||
|
{file = "cryptography-46.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a23582810fedb8c0bc47524558fb6c56aac3fc252cb306072fd2815da2a47c32"},
|
||||||
|
{file = "cryptography-46.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7aec276d68421f9574040c26e2a7c3771060bc0cff408bae1dcb19d3ab1e63c"},
|
||||||
|
{file = "cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea"},
|
||||||
|
{file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b"},
|
||||||
|
{file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb"},
|
||||||
|
{file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717"},
|
||||||
|
{file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9"},
|
||||||
|
{file = "cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c"},
|
||||||
|
{file = "cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""}
|
cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\""}
|
||||||
|
typing-extensions = {version = ">=4.13.2", markers = "python_full_version < \"3.11.0\""}
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"]
|
docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"]
|
||||||
docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"]
|
docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"]
|
||||||
nox = ["nox"]
|
nox = ["nox[uv] (>=2024.4.15)"]
|
||||||
pep8test = ["check-sdist", "click", "mypy", "ruff"]
|
pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.14)", "ruff (>=0.11.11)"]
|
||||||
sdist = ["build"]
|
sdist = ["build (>=1.0.0)"]
|
||||||
ssh = ["bcrypt (>=3.1.5)"]
|
ssh = ["bcrypt (>=3.1.5)"]
|
||||||
test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
|
test = ["certifi (>=2024)", "cryptography-vectors (==46.0.3)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
|
||||||
test-randomorder = ["pytest-randomly"]
|
test-randomorder = ["pytest-randomly"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -178,14 +206,15 @@ cp2110 = ["hidapi"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rns"
|
name = "rns"
|
||||||
version = "1.0.1"
|
version = "1.0.4"
|
||||||
description = "Self-configuring, encrypted and resilient mesh networking stack for LoRa, packet radio, WiFi and everything in between"
|
description = "Self-configuring, encrypted and resilient mesh networking stack for LoRa, packet radio, WiFi and everything in between"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "rns-1.0.1-py3-none-any.whl", hash = "sha256:aa77b4c8e1b6899117666e1e55b05b3250416ab5fea2826254358ae320e8b3ed"},
|
{file = "rns-1.0.4-1-py3-none-any.whl", hash = "sha256:f1804f8b07a8b8e1c1b61889f929fdb5cfbd57f4c354108c417135f0d67c5ef6"},
|
||||||
{file = "rns-1.0.1.tar.gz", hash = "sha256:f45ea52b065be09b8e2257425b6fcde1a49899ea41aee349936d182aa1844b26"},
|
{file = "rns-1.0.4-py3-none-any.whl", hash = "sha256:7a2b7893410833b42c0fa7f9a9e3369cebb085cdd26bd83f3031fa6c1051653c"},
|
||||||
|
{file = "rns-1.0.4.tar.gz", hash = "sha256:e70667a767fe523bab8e7ea0627447258c4e6763b7756fbba50c6556dbb84399"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -221,7 +250,20 @@ files = [
|
|||||||
{file = "ruff-0.13.3.tar.gz", hash = "sha256:5b0ba0db740eefdfbcce4299f49e9eaefc643d4d007749d77d047c2bab19908e"},
|
{file = "ruff-0.13.3.tar.gz", hash = "sha256:5b0ba0db740eefdfbcce4299f49e9eaefc643d4d007749d77d047c2bab19908e"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.15.0"
|
||||||
|
description = "Backported and Experimental Type Hints for Python 3.9+"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
groups = ["main"]
|
||||||
|
markers = "python_full_version < \"3.11.0\""
|
||||||
|
files = [
|
||||||
|
{file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"},
|
||||||
|
{file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"},
|
||||||
|
]
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.1"
|
lock-version = "2.1"
|
||||||
python-versions = ">=3.9"
|
python-versions = ">3.9.0,<3.9.1 || >3.9.1"
|
||||||
content-hash = "c30da1264c924dddbf8fb26e3f1bc6705e265db33c9633769692afa72241f478"
|
content-hash = "1be824dfb2b426a79853223eb90b868160d299c549d6eca850d8a982b7336aef"
|
||||||
|
|||||||
@@ -1,16 +1,25 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "rns-page-node"
|
name = "rns-page-node"
|
||||||
version = "1.2.0"
|
version = "1.3.0"
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
description = "A simple way to serve pages and files over the Reticulum network."
|
description = "A simple way to serve pages and files over the Reticulum network."
|
||||||
authors = [
|
authors = [
|
||||||
{name = "Sudo-Ivan"}
|
{name = "Sudo-Ivan"}
|
||||||
]
|
]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">3.9.0,<3.9.1 || >3.9.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rns (>=1.0.1,<1.5.0)"
|
"rns (>=1.0.4,<1.5.0)",
|
||||||
|
"cryptography>=46.0.3"
|
||||||
]
|
]
|
||||||
|
classifiers = [
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Operating System :: OS Independent",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Homepage = "https://github.com/Sudo-Ivan/rns-page-node"
|
||||||
|
Repository = "https://github.com/Sudo-Ivan/rns-page-node"
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
rns-page-node = "rns_page_node.main:main"
|
rns-page-node = "rns_page_node.main:main"
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
rns=1.0.1
|
rns=1.0.4
|
||||||
@@ -24,6 +24,46 @@ You are not authorised to carry out the request.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def load_config(config_file):
|
||||||
|
"""Load configuration from a plain text config file.
|
||||||
|
|
||||||
|
Config format is simple key=value pairs, one per line.
|
||||||
|
Lines starting with # are comments and are ignored.
|
||||||
|
Empty lines are ignored.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config_file: Path to the config file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary of configuration values
|
||||||
|
|
||||||
|
"""
|
||||||
|
config = {}
|
||||||
|
try:
|
||||||
|
with open(config_file, encoding="utf-8") as f:
|
||||||
|
for line_num, line in enumerate(f, 1):
|
||||||
|
line = line.strip()
|
||||||
|
if not line or line.startswith("#"):
|
||||||
|
continue
|
||||||
|
if "=" not in line:
|
||||||
|
RNS.log(
|
||||||
|
f"Invalid config line {line_num} in {config_file}: {line}",
|
||||||
|
RNS.LOG_WARNING,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
key, value = line.split("=", 1)
|
||||||
|
key = key.strip()
|
||||||
|
value = value.strip()
|
||||||
|
if key and value:
|
||||||
|
config[key] = value
|
||||||
|
RNS.log(f"Loaded configuration from {config_file}", RNS.LOG_INFO)
|
||||||
|
except FileNotFoundError:
|
||||||
|
RNS.log(f"Config file not found: {config_file}", RNS.LOG_ERROR)
|
||||||
|
except Exception as e:
|
||||||
|
RNS.log(f"Error reading config file {config_file}: {e}", RNS.LOG_ERROR)
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
class PageNode:
|
class PageNode:
|
||||||
"""A Reticulum page node that serves .mu pages and files over RNS."""
|
"""A Reticulum page node that serves .mu pages and files over RNS."""
|
||||||
|
|
||||||
@@ -43,7 +83,7 @@ class PageNode:
|
|||||||
identity: RNS Identity for the node
|
identity: RNS Identity for the node
|
||||||
pagespath: Path to directory containing .mu pages
|
pagespath: Path to directory containing .mu pages
|
||||||
filespath: Path to directory containing files to serve
|
filespath: Path to directory containing files to serve
|
||||||
announce_interval: Seconds between announcements (default: 360)
|
announce_interval: Minutes between announcements (default: 360) == 6 hours
|
||||||
name: Display name for the node (optional)
|
name: Display name for the node (optional)
|
||||||
page_refresh_interval: Seconds between page rescans (0 = disabled)
|
page_refresh_interval: Seconds between page rescans (0 = disabled)
|
||||||
file_refresh_interval: Seconds between file rescans (0 = disabled)
|
file_refresh_interval: Seconds between file rescans (0 = disabled)
|
||||||
@@ -84,11 +124,12 @@ class PageNode:
|
|||||||
|
|
||||||
def register_pages(self):
|
def register_pages(self):
|
||||||
"""Scan pages directory and register request handlers for all .mu files."""
|
"""Scan pages directory and register request handlers for all .mu files."""
|
||||||
with self._lock:
|
pages = self._scan_pages(self.pagespath)
|
||||||
self.servedpages = []
|
|
||||||
self._scan_pages(self.pagespath)
|
|
||||||
|
|
||||||
pagespath = Path(self.pagespath)
|
with self._lock:
|
||||||
|
self.servedpages = pages
|
||||||
|
|
||||||
|
pagespath = Path(self.pagespath).resolve()
|
||||||
|
|
||||||
if not (pagespath / "index.mu").is_file():
|
if not (pagespath / "index.mu").is_file():
|
||||||
self.destination.register_request_handler(
|
self.destination.register_request_handler(
|
||||||
@@ -97,11 +138,13 @@ class PageNode:
|
|||||||
allow=RNS.Destination.ALLOW_ALL,
|
allow=RNS.Destination.ALLOW_ALL,
|
||||||
)
|
)
|
||||||
|
|
||||||
for full_path in self.servedpages:
|
for full_path in pages:
|
||||||
rel = full_path[len(str(pagespath)) :]
|
page_path = Path(full_path).resolve()
|
||||||
if not rel.startswith("/"):
|
try:
|
||||||
rel = "/" + rel
|
rel = page_path.relative_to(pagespath).as_posix()
|
||||||
request_path = f"/page{rel}"
|
except ValueError:
|
||||||
|
continue
|
||||||
|
request_path = f"/page/{rel}"
|
||||||
self.destination.register_request_handler(
|
self.destination.register_request_handler(
|
||||||
request_path,
|
request_path,
|
||||||
response_generator=self.serve_page,
|
response_generator=self.serve_page,
|
||||||
@@ -110,17 +153,20 @@ class PageNode:
|
|||||||
|
|
||||||
def register_files(self):
|
def register_files(self):
|
||||||
"""Scan files directory and register request handlers for all files."""
|
"""Scan files directory and register request handlers for all files."""
|
||||||
|
files = self._scan_files(self.filespath)
|
||||||
|
|
||||||
with self._lock:
|
with self._lock:
|
||||||
self.servedfiles = []
|
self.servedfiles = files
|
||||||
self._scan_files(self.filespath)
|
|
||||||
|
|
||||||
filespath = Path(self.filespath)
|
filespath = Path(self.filespath).resolve()
|
||||||
|
|
||||||
for full_path in self.servedfiles:
|
for full_path in files:
|
||||||
rel = full_path[len(str(filespath)) :]
|
file_path = Path(full_path).resolve()
|
||||||
if not rel.startswith("/"):
|
try:
|
||||||
rel = "/" + rel
|
rel = file_path.relative_to(filespath).as_posix()
|
||||||
request_path = f"/file{rel}"
|
except ValueError:
|
||||||
|
continue
|
||||||
|
request_path = f"/file/{rel}"
|
||||||
self.destination.register_request_handler(
|
self.destination.register_request_handler(
|
||||||
request_path,
|
request_path,
|
||||||
response_generator=self.serve_file,
|
response_generator=self.serve_file,
|
||||||
@@ -129,24 +175,34 @@ class PageNode:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _scan_pages(self, base):
|
def _scan_pages(self, base):
|
||||||
|
"""Return a list of page paths under the given directory, excluding .allowed files."""
|
||||||
base_path = Path(base)
|
base_path = Path(base)
|
||||||
|
if not base_path.exists():
|
||||||
|
return []
|
||||||
|
served = []
|
||||||
for entry in base_path.iterdir():
|
for entry in base_path.iterdir():
|
||||||
if entry.name.startswith("."):
|
if entry.name.startswith("."):
|
||||||
continue
|
continue
|
||||||
if entry.is_dir():
|
if entry.is_dir():
|
||||||
self._scan_pages(str(entry))
|
served.extend(self._scan_pages(entry))
|
||||||
elif entry.is_file() and not entry.name.endswith(".allowed"):
|
elif entry.is_file() and not entry.name.endswith(".allowed"):
|
||||||
self.servedpages.append(str(entry))
|
served.append(str(entry))
|
||||||
|
return served
|
||||||
|
|
||||||
def _scan_files(self, base):
|
def _scan_files(self, base):
|
||||||
|
"""Return all file paths under the given directory."""
|
||||||
base_path = Path(base)
|
base_path = Path(base)
|
||||||
|
if not base_path.exists():
|
||||||
|
return []
|
||||||
|
served = []
|
||||||
for entry in base_path.iterdir():
|
for entry in base_path.iterdir():
|
||||||
if entry.name.startswith("."):
|
if entry.name.startswith("."):
|
||||||
continue
|
continue
|
||||||
if entry.is_dir():
|
if entry.is_dir():
|
||||||
self._scan_files(str(entry))
|
served.extend(self._scan_files(entry))
|
||||||
elif entry.is_file():
|
elif entry.is_file():
|
||||||
self.servedfiles.append(str(entry))
|
served.append(str(entry))
|
||||||
|
return served
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def serve_default_index(
|
def serve_default_index(
|
||||||
@@ -176,66 +232,62 @@ class PageNode:
|
|||||||
|
|
||||||
if not str(file_path).startswith(str(pagespath)):
|
if not str(file_path).startswith(str(pagespath)):
|
||||||
return DEFAULT_NOTALLOWED.encode("utf-8")
|
return DEFAULT_NOTALLOWED.encode("utf-8")
|
||||||
|
is_script = False
|
||||||
|
file_content = None
|
||||||
try:
|
try:
|
||||||
with file_path.open("rb") as _f:
|
with file_path.open("rb") as file_handle:
|
||||||
first_line = _f.readline()
|
first_line = file_handle.readline()
|
||||||
is_script = first_line.startswith(b"#!")
|
is_script = first_line.startswith(b"#!")
|
||||||
except Exception:
|
file_handle.seek(0)
|
||||||
is_script = False
|
if not is_script:
|
||||||
|
return file_handle.read()
|
||||||
|
file_content = file_handle.read()
|
||||||
|
except FileNotFoundError:
|
||||||
|
return DEFAULT_NOTALLOWED.encode("utf-8")
|
||||||
|
except OSError as err:
|
||||||
|
RNS.log(f"Error reading page {file_path}: {err}", RNS.LOG_ERROR)
|
||||||
|
return DEFAULT_NOTALLOWED.encode("utf-8")
|
||||||
|
|
||||||
if is_script and os.access(str(file_path), os.X_OK):
|
if is_script and os.access(str(file_path), os.X_OK):
|
||||||
try:
|
try:
|
||||||
env = os.environ.copy()
|
env_map = os.environ.copy()
|
||||||
if remote_identity:
|
if _link_id is not None:
|
||||||
env["remote_identity"] = RNS.hexrep(
|
env_map["link_id"] = RNS.hexrep(_link_id, delimit=False)
|
||||||
|
if remote_identity is not None:
|
||||||
|
env_map["remote_identity"] = RNS.hexrep(
|
||||||
remote_identity.hash,
|
remote_identity.hash,
|
||||||
delimit=False,
|
delimit=False,
|
||||||
)
|
)
|
||||||
if data:
|
if data is not None and isinstance(data, dict):
|
||||||
try:
|
for e in data:
|
||||||
RNS.log(f"Processing request data: {data} (type: {type(data)})", RNS.LOG_DEBUG)
|
if isinstance(e, str) and (
|
||||||
if isinstance(data, dict):
|
e.startswith("field_") or e.startswith("var_")
|
||||||
RNS.log(f"Data is dictionary with {len(data)} items", RNS.LOG_DEBUG)
|
):
|
||||||
for key, value in data.items():
|
env_map[e] = data[e]
|
||||||
if isinstance(value, str):
|
|
||||||
if key.startswith(("field_", "var_")):
|
|
||||||
env[key] = value
|
|
||||||
RNS.log(f"Set env[{key}] = {value}", RNS.LOG_DEBUG)
|
|
||||||
elif key == "action":
|
|
||||||
env["var_action"] = value
|
|
||||||
RNS.log(f"Set env[var_action] = {value}", RNS.LOG_DEBUG)
|
|
||||||
else:
|
|
||||||
env[f"field_{key}"] = value
|
|
||||||
RNS.log(f"Set env[field_{key}] = {value}", RNS.LOG_DEBUG)
|
|
||||||
elif isinstance(data, bytes):
|
|
||||||
data_str = data.decode("utf-8")
|
|
||||||
RNS.log(f"Data is bytes, decoded to: {data_str}", RNS.LOG_DEBUG)
|
|
||||||
if data_str:
|
|
||||||
if "|" in data_str and "&" not in data_str:
|
|
||||||
pairs = data_str.split("|")
|
|
||||||
else:
|
|
||||||
pairs = data_str.split("&")
|
|
||||||
for pair in pairs:
|
|
||||||
if "=" in pair:
|
|
||||||
key, value = pair.split("=", 1)
|
|
||||||
if key.startswith(("field_", "var_")):
|
|
||||||
env[key] = value
|
|
||||||
elif key == "action":
|
|
||||||
env["var_action"] = value
|
|
||||||
else:
|
|
||||||
env[f"field_{key}"] = value
|
|
||||||
except Exception as e:
|
|
||||||
RNS.log(f"Error parsing request data: {e}", RNS.LOG_ERROR)
|
|
||||||
result = subprocess.run( # noqa: S603
|
result = subprocess.run( # noqa: S603
|
||||||
[str(file_path)],
|
[str(file_path)],
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
check=True,
|
check=True,
|
||||||
env=env,
|
env=env_map,
|
||||||
)
|
)
|
||||||
return result.stdout
|
return result.stdout
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
RNS.log(f"Error executing script page: {e}", RNS.LOG_ERROR)
|
RNS.log(f"Error executing script page: {e}", RNS.LOG_ERROR)
|
||||||
with file_path.open("rb") as f:
|
if file_content is not None:
|
||||||
return f.read()
|
return file_content
|
||||||
|
try:
|
||||||
|
return self._read_file_bytes(file_path)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return DEFAULT_NOTALLOWED.encode("utf-8")
|
||||||
|
except OSError as err:
|
||||||
|
RNS.log(f"Error reading page fallback {file_path}: {err}", RNS.LOG_ERROR)
|
||||||
|
return DEFAULT_NOTALLOWED.encode("utf-8")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _read_file_bytes(file_path):
|
||||||
|
"""Read a file's bytes and return the contents."""
|
||||||
|
with file_path.open("rb") as file_handle:
|
||||||
|
return file_handle.read()
|
||||||
|
|
||||||
def serve_file(
|
def serve_file(
|
||||||
self,
|
self,
|
||||||
@@ -263,35 +315,76 @@ class PageNode:
|
|||||||
"""Handle new link connections."""
|
"""Handle new link connections."""
|
||||||
|
|
||||||
def _announce_loop(self):
|
def _announce_loop(self):
|
||||||
|
"""Periodically announce the node until shutdown is requested."""
|
||||||
|
interval_seconds = max(self.announce_interval, 0) * 60
|
||||||
try:
|
try:
|
||||||
while not self._stop_event.is_set():
|
while not self._stop_event.is_set():
|
||||||
if time.time() - self.last_announce > self.announce_interval:
|
now = time.time()
|
||||||
if self.name:
|
if (
|
||||||
self.destination.announce(app_data=self.name.encode("utf-8"))
|
self.last_announce == 0
|
||||||
else:
|
or now - self.last_announce >= interval_seconds
|
||||||
self.destination.announce()
|
):
|
||||||
self.last_announce = time.time()
|
try:
|
||||||
time.sleep(1)
|
if self.name:
|
||||||
|
self.destination.announce(
|
||||||
|
app_data=self.name.encode("utf-8"),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.destination.announce()
|
||||||
|
self.last_announce = time.time()
|
||||||
|
except (TypeError, ValueError) as announce_error:
|
||||||
|
RNS.log(
|
||||||
|
f"Error during announce: {announce_error}",
|
||||||
|
RNS.LOG_ERROR,
|
||||||
|
)
|
||||||
|
wait_time = max(
|
||||||
|
(self.last_announce + interval_seconds) - time.time()
|
||||||
|
if self.last_announce
|
||||||
|
else 0,
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
self._stop_event.wait(min(wait_time, 60))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
RNS.log(f"Error in announce loop: {e}", RNS.LOG_ERROR)
|
RNS.log(f"Error in announce loop: {e}", RNS.LOG_ERROR)
|
||||||
|
|
||||||
def _refresh_loop(self):
|
def _refresh_loop(self):
|
||||||
|
"""Refresh page and file registrations at configured intervals."""
|
||||||
try:
|
try:
|
||||||
while not self._stop_event.is_set():
|
while not self._stop_event.is_set():
|
||||||
now = time.time()
|
now = time.time()
|
||||||
if (
|
if (
|
||||||
self.page_refresh_interval > 0
|
self.page_refresh_interval > 0
|
||||||
and now - self.last_page_refresh > self.page_refresh_interval
|
and now - self.last_page_refresh >= self.page_refresh_interval
|
||||||
):
|
):
|
||||||
self.register_pages()
|
self.register_pages()
|
||||||
self.last_page_refresh = now
|
self.last_page_refresh = time.time()
|
||||||
if (
|
if (
|
||||||
self.file_refresh_interval > 0
|
self.file_refresh_interval > 0
|
||||||
and now - self.last_file_refresh > self.file_refresh_interval
|
and now - self.last_file_refresh >= self.file_refresh_interval
|
||||||
):
|
):
|
||||||
self.register_files()
|
self.register_files()
|
||||||
self.last_file_refresh = now
|
self.last_file_refresh = time.time()
|
||||||
time.sleep(1)
|
|
||||||
|
wait_candidates = []
|
||||||
|
if self.page_refresh_interval > 0:
|
||||||
|
wait_candidates.append(
|
||||||
|
max(
|
||||||
|
(self.last_page_refresh + self.page_refresh_interval)
|
||||||
|
- time.time(),
|
||||||
|
0.5,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if self.file_refresh_interval > 0:
|
||||||
|
wait_candidates.append(
|
||||||
|
max(
|
||||||
|
(self.last_file_refresh + self.file_refresh_interval)
|
||||||
|
- time.time(),
|
||||||
|
0.5,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
wait_time = min(wait_candidates) if wait_candidates else 1.0
|
||||||
|
self._stop_event.wait(min(wait_time, 60))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
RNS.log(f"Error in refresh loop: {e}", RNS.LOG_ERROR)
|
RNS.log(f"Error in refresh loop: {e}", RNS.LOG_ERROR)
|
||||||
|
|
||||||
@@ -314,6 +407,12 @@ class PageNode:
|
|||||||
def main():
|
def main():
|
||||||
"""Run the RNS page node application."""
|
"""Run the RNS page node application."""
|
||||||
parser = argparse.ArgumentParser(description="Minimal Reticulum Page Node")
|
parser = argparse.ArgumentParser(description="Minimal Reticulum Page Node")
|
||||||
|
parser.add_argument(
|
||||||
|
"node_config",
|
||||||
|
nargs="?",
|
||||||
|
help="Path to rns-page-node config file",
|
||||||
|
default=None,
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-c",
|
"-c",
|
||||||
"--config",
|
"--config",
|
||||||
@@ -347,7 +446,7 @@ def main():
|
|||||||
"--announce-interval",
|
"--announce-interval",
|
||||||
dest="announce_interval",
|
dest="announce_interval",
|
||||||
type=int,
|
type=int,
|
||||||
help="Announce interval in seconds",
|
help="Announce interval in minutes",
|
||||||
default=360,
|
default=360,
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
@@ -381,14 +480,67 @@ def main():
|
|||||||
)
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
configpath = args.configpath
|
config = {}
|
||||||
pages_dir = args.pages_dir
|
if args.node_config:
|
||||||
files_dir = args.files_dir
|
config = load_config(args.node_config)
|
||||||
node_name = args.node_name
|
|
||||||
announce_interval = args.announce_interval
|
def get_config_value(arg_value, arg_default, config_key, value_type=str):
|
||||||
identity_dir = args.identity_dir
|
"""Get value from CLI args, config file, or default.
|
||||||
page_refresh_interval = args.page_refresh_interval
|
|
||||||
file_refresh_interval = args.file_refresh_interval
|
Priority: CLI arg > config file > default
|
||||||
|
"""
|
||||||
|
if arg_value != arg_default:
|
||||||
|
return arg_value
|
||||||
|
if config_key in config:
|
||||||
|
try:
|
||||||
|
if value_type is int:
|
||||||
|
return int(config[config_key])
|
||||||
|
return config[config_key]
|
||||||
|
except ValueError:
|
||||||
|
RNS.log(
|
||||||
|
f"Invalid {value_type.__name__} value for {config_key}: {config[config_key]}",
|
||||||
|
RNS.LOG_WARNING,
|
||||||
|
)
|
||||||
|
return arg_default
|
||||||
|
|
||||||
|
configpath = get_config_value(args.configpath, None, "reticulum-config")
|
||||||
|
pages_dir = get_config_value(args.pages_dir, str(Path.cwd() / "pages"), "pages-dir")
|
||||||
|
files_dir = get_config_value(args.files_dir, str(Path.cwd() / "files"), "files-dir")
|
||||||
|
node_name = get_config_value(args.node_name, None, "node-name")
|
||||||
|
announce_interval = get_config_value(
|
||||||
|
args.announce_interval,
|
||||||
|
360,
|
||||||
|
"announce-interval",
|
||||||
|
int,
|
||||||
|
)
|
||||||
|
identity_dir = get_config_value(
|
||||||
|
args.identity_dir,
|
||||||
|
str(Path.cwd() / "node-config"),
|
||||||
|
"identity-dir",
|
||||||
|
)
|
||||||
|
page_refresh_interval = get_config_value(
|
||||||
|
args.page_refresh_interval,
|
||||||
|
0,
|
||||||
|
"page-refresh-interval",
|
||||||
|
int,
|
||||||
|
)
|
||||||
|
file_refresh_interval = get_config_value(
|
||||||
|
args.file_refresh_interval,
|
||||||
|
0,
|
||||||
|
"file-refresh-interval",
|
||||||
|
int,
|
||||||
|
)
|
||||||
|
log_level = get_config_value(args.log_level, "INFO", "log-level")
|
||||||
|
|
||||||
|
# Set RNS log level based on command line argument
|
||||||
|
log_level_map = {
|
||||||
|
"CRITICAL": RNS.LOG_CRITICAL,
|
||||||
|
"ERROR": RNS.LOG_ERROR,
|
||||||
|
"WARNING": RNS.LOG_WARNING,
|
||||||
|
"INFO": RNS.LOG_INFO,
|
||||||
|
"DEBUG": RNS.LOG_DEBUG,
|
||||||
|
}
|
||||||
|
RNS.loglevel = log_level_map.get(log_level.upper(), RNS.LOG_INFO)
|
||||||
|
|
||||||
RNS.Reticulum(configpath)
|
RNS.Reticulum(configpath)
|
||||||
Path(identity_dir).mkdir(parents=True, exist_ok=True)
|
Path(identity_dir).mkdir(parents=True, exist_ok=True)
|
||||||
|
|||||||
31
setup.py
31
setup.py
@@ -1,31 +0,0 @@
|
|||||||
from setuptools import find_packages, setup
|
|
||||||
|
|
||||||
with open("README.md", encoding="utf-8") as fh:
|
|
||||||
long_description = fh.read()
|
|
||||||
|
|
||||||
setup(
|
|
||||||
name="rns-page-node",
|
|
||||||
version="1.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.9",
|
|
||||||
install_requires=[
|
|
||||||
"rns>=1.0.1,<1.5.0",
|
|
||||||
],
|
|
||||||
entry_points={
|
|
||||||
"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",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
@@ -20,7 +20,11 @@ server_identity = RNS.Identity.from_file(identity_file)
|
|||||||
|
|
||||||
# Create a destination to the server node
|
# Create a destination to the server node
|
||||||
destination = RNS.Destination(
|
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
|
# Ensure we know a path to the destination
|
||||||
@@ -38,11 +42,15 @@ done_event = threading.Event()
|
|||||||
|
|
||||||
# Test data for environment variables
|
# Test data for environment variables
|
||||||
test_data_dict = {
|
test_data_dict = {
|
||||||
'var_field_test': 'dictionary_value',
|
"var_field_test": "dictionary_value",
|
||||||
'var_field_message': 'hello_world',
|
"var_field_message": "hello_world",
|
||||||
'var_action': 'test_action'
|
"var_action": "test_action",
|
||||||
|
}
|
||||||
|
test_data_dict2 = {
|
||||||
|
"field_username": "testuser",
|
||||||
|
"field_message": "hello_from_form",
|
||||||
|
"var_action": "submit",
|
||||||
}
|
}
|
||||||
test_data_bytes = b'field_bytes_test=bytes_value|field_bytes_message=test_bytes|action=bytes_action'
|
|
||||||
|
|
||||||
|
|
||||||
# Callback for page response
|
# Callback for page response
|
||||||
@@ -57,6 +65,7 @@ def on_page(response):
|
|||||||
responses["page"] = text
|
responses["page"] = text
|
||||||
check_responses()
|
check_responses()
|
||||||
|
|
||||||
|
|
||||||
# Callback for page response with dictionary data
|
# Callback for page response with dictionary data
|
||||||
def on_page_dict(response):
|
def on_page_dict(response):
|
||||||
data = response.response
|
data = response.response
|
||||||
@@ -69,20 +78,27 @@ def on_page_dict(response):
|
|||||||
responses["page_dict"] = text
|
responses["page_dict"] = text
|
||||||
check_responses()
|
check_responses()
|
||||||
|
|
||||||
# Callback for page response with bytes data
|
|
||||||
def on_page_bytes(response):
|
# Callback for page response with second dict data
|
||||||
|
def on_page_dict2(response):
|
||||||
data = response.response
|
data = response.response
|
||||||
if isinstance(data, bytes):
|
if isinstance(data, bytes):
|
||||||
text = data.decode("utf-8")
|
text = data.decode("utf-8")
|
||||||
else:
|
else:
|
||||||
text = str(data)
|
text = str(data)
|
||||||
print("Received page (bytes data):")
|
print("Received page (dict2 data):")
|
||||||
print(text)
|
print(text)
|
||||||
responses["page_bytes"] = text
|
responses["page_dict2"] = text
|
||||||
check_responses()
|
check_responses()
|
||||||
|
|
||||||
|
|
||||||
def check_responses():
|
def check_responses():
|
||||||
if "page" in responses and "page_dict" in responses and "page_bytes" in responses and "file" in responses:
|
if (
|
||||||
|
"page" in responses
|
||||||
|
and "page_dict" in responses
|
||||||
|
and "page_dict2" in responses
|
||||||
|
and "file" in responses
|
||||||
|
):
|
||||||
done_event.set()
|
done_event.set()
|
||||||
|
|
||||||
|
|
||||||
@@ -120,10 +136,10 @@ def on_file(response):
|
|||||||
def on_link_established(link):
|
def on_link_established(link):
|
||||||
# Test page without data
|
# Test page without data
|
||||||
link.request("/page/index.mu", None, response_callback=on_page)
|
link.request("/page/index.mu", None, response_callback=on_page)
|
||||||
# Test page with dictionary data (simulates MeshChat)
|
# Test page with dictionary data (simulates var_ prefixed data)
|
||||||
link.request("/page/index.mu", test_data_dict, response_callback=on_page_dict)
|
link.request("/page/index.mu", test_data_dict, response_callback=on_page_dict)
|
||||||
# Test page with bytes data (URL-encoded style)
|
# Test page with form field data (simulates field_ prefixed data)
|
||||||
link.request("/page/index.mu", test_data_bytes, response_callback=on_page_bytes)
|
link.request("/page/index.mu", test_data_dict2, response_callback=on_page_dict2)
|
||||||
# Test file serving
|
# Test file serving
|
||||||
link.request("/file/text.txt", None, response_callback=on_file)
|
link.request("/file/text.txt", None, response_callback=on_file)
|
||||||
|
|
||||||
@@ -137,10 +153,10 @@ if not done_event.wait(timeout=30):
|
|||||||
print("Test timed out.", file=sys.stderr)
|
print("Test timed out.", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
# Validate test results
|
# Validate test results
|
||||||
def validate_test_results():
|
def validate_test_results():
|
||||||
"""Validate that all responses contain expected content"""
|
"""Validate that all responses contain expected content"""
|
||||||
|
|
||||||
# Check basic page response (no data)
|
# Check basic page response (no data)
|
||||||
if "page" not in responses:
|
if "page" not in responses:
|
||||||
print("ERROR: No basic page response received", file=sys.stderr)
|
print("ERROR: No basic page response received", file=sys.stderr)
|
||||||
@@ -161,23 +177,35 @@ def validate_test_results():
|
|||||||
|
|
||||||
dict_content = responses["page_dict"]
|
dict_content = responses["page_dict"]
|
||||||
if "var_field_test" not in dict_content or "dictionary_value" not in dict_content:
|
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)
|
print(
|
||||||
|
"ERROR: Dictionary data page should contain processed environment variables",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
if "33aff86b736acd47dca07e84630fd192" not in dict_content:
|
if "33aff86b736acd47dca07e84630fd192" not in dict_content:
|
||||||
print("ERROR: Dictionary data page should show mock remote identity", file=sys.stderr)
|
print(
|
||||||
|
"ERROR: Dictionary data page should show mock remote identity",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Check page with bytes data
|
# Check page with second dictionary data (form fields)
|
||||||
if "page_bytes" not in responses:
|
if "page_dict2" not in responses:
|
||||||
print("ERROR: No bytes data page response received", file=sys.stderr)
|
print("ERROR: No dict2 data page response received", file=sys.stderr)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
bytes_content = responses["page_bytes"]
|
dict2_content = responses["page_dict2"]
|
||||||
if "field_bytes_test" not in bytes_content or "bytes_value" not in bytes_content:
|
if "field_username" not in dict2_content or "testuser" not in dict2_content:
|
||||||
print("ERROR: Bytes data page should contain processed environment variables", file=sys.stderr)
|
print(
|
||||||
|
"ERROR: Dict2 data page should contain processed environment variables",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
if "33aff86b736acd47dca07e84630fd192" not in bytes_content:
|
if "33aff86b736acd47dca07e84630fd192" not in dict2_content:
|
||||||
print("ERROR: Bytes data page should show mock remote identity", file=sys.stderr)
|
print(
|
||||||
|
"ERROR: Dict2 data page should show mock remote identity",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Check file response
|
# Check file response
|
||||||
@@ -192,6 +220,7 @@ def validate_test_results():
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
if validate_test_results():
|
if validate_test_results():
|
||||||
print("All tests passed! Environment variable processing works correctly.")
|
print("All tests passed! Environment variable processing works correctly.")
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|||||||
@@ -34,7 +34,11 @@ server_identity = RNS.Identity.recall(destination_hash)
|
|||||||
print(f"Recalled server identity for {DESTINATION_HEX}")
|
print(f"Recalled server identity for {DESTINATION_HEX}")
|
||||||
|
|
||||||
destination = RNS.Destination(
|
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)
|
link = RNS.Link(destination)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user