Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c900cf38c9 | |||
| 014ebc25c6 | |||
|
|
d5e9308fb5 | ||
|
|
7d5e891261 | ||
|
|
c382ed790f | ||
| cb72e57da9 | |||
|
|
aaf5ad23e2 | ||
|
|
ce1b1dad7d | ||
|
|
67ebc7e556 | ||
|
|
b31fb748b8 | ||
|
|
eb27326763 | ||
|
|
f40d5a51ae | ||
|
|
4aa83a2dfb | ||
|
|
a8b09611a1 | ||
|
|
d0dd9e88c4 | ||
|
|
09d84c2533 | ||
|
|
06ab592cb9 | ||
|
|
843c3a1a56 | ||
|
|
37ac95753c | ||
|
|
a493c57ad2 | ||
|
|
698bfb2e81 | ||
|
|
eaf2e544c4 | ||
|
|
89f88e24ea | ||
|
|
a47b78c13d | ||
|
|
4831f5261d | ||
|
|
46f90e461f | ||
|
|
a1bbe8bc8a | ||
|
|
7092883834 | ||
|
|
74f5174254 | ||
|
|
f9699c060a | ||
|
|
a3ccd49439 | ||
|
|
ece0473beb | ||
|
|
89065f6e0a | ||
|
|
e873d8e754 | ||
|
|
5561205b3e | ||
|
|
d6601adb38 | ||
|
|
98c71a888e | ||
|
|
65bd70c05a | ||
|
|
b77b73576f |
7
.deepsource.toml
Normal file
7
.deepsource.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
version = 1
|
||||
|
||||
[[analyzers]]
|
||||
name = "python"
|
||||
|
||||
[analyzers.meta]
|
||||
runtime_version = "3.x.x"
|
||||
27
.github/workflows/docker-test.yml
vendored
Normal file
27
.github/workflows/docker-test.yml
vendored
Normal 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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
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 }}
|
||||
40
.github/workflows/docker.yml
vendored
40
.github/workflows/docker.yml
vendored
@@ -20,13 +20,18 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392
|
||||
with:
|
||||
platforms: amd64,arm64
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
@@ -34,7 +39,7 @@ jobs:
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
@@ -46,23 +51,36 @@ jobs:
|
||||
type=sha,format=short
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker (rootless)
|
||||
id: meta_rootless
|
||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-rootless
|
||||
tags: |
|
||||
type=raw,value=latest-rootless,enable={{is_default_branch}}
|
||||
type=ref,event=branch,prefix=,suffix=-rootless,enable={{is_default_branch}}
|
||||
type=semver,pattern={{version}},suffix=-rootless
|
||||
type=semver,pattern={{major}}.{{minor}},suffix=-rootless
|
||||
type=sha,format=short,suffix=-rootless
|
||||
|
||||
- name: Build and push rootless Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile.rootless
|
||||
file: ./Dockerfile.rootless
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}-rootless
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
tags: ${{ steps.meta_rootless.outputs.tags }}
|
||||
labels: ${{ steps.meta_rootless.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
100
.github/workflows/publish.yml
vendored
Normal file
100
.github/workflows/publish.yml
vendored
Normal 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@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
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@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
with:
|
||||
name: python-package-distributions
|
||||
path: dist/
|
||||
- name: Publish distribution 📦 to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e
|
||||
|
||||
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@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
with:
|
||||
name: python-package-distributions
|
||||
path: dist/
|
||||
- name: Sign the dists with Sigstore
|
||||
uses: sigstore/gh-action-sigstore-python@f7ad0af51a5648d09a20d00370f0a91c3bdf8f84 # v3.0.1
|
||||
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"
|
||||
17
Dockerfile
17
Dockerfile
@@ -1,4 +1,5 @@
|
||||
FROM python:3.13-alpine
|
||||
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."
|
||||
@@ -7,11 +8,17 @@ LABEL org.opencontainers.image.authors="Sudo-Ivan"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt ./
|
||||
COPY setup.py ./
|
||||
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 pip install --upgrade pip setuptools wheel && pip install -r requirements.txt .
|
||||
RUN poetry install --no-interaction --no-ansi
|
||||
|
||||
ENTRYPOINT ["rns-page-node"]
|
||||
ENV PATH="/app/.venv/bin:$PATH"
|
||||
|
||||
ENTRYPOINT ["poetry", "run", "rns-page-node"]
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
FROM python:3.13-alpine AS builder
|
||||
|
||||
RUN apk update
|
||||
RUN apk add build-base libffi-dev cargo pkgconfig
|
||||
RUN apk add --no-cache build-base libffi-dev cargo pkgconfig gcc python3-dev musl-dev linux-headers
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
COPY setup.py ./
|
||||
RUN pip install poetry
|
||||
|
||||
COPY pyproject.toml ./
|
||||
COPY README.md ./
|
||||
COPY rns_page_node ./rns_page_node
|
||||
|
||||
RUN pip install --upgrade pip setuptools wheel
|
||||
|
||||
RUN pip wheel . --no-deps --wheel-dir /src/dist
|
||||
RUN poetry build --format wheel
|
||||
|
||||
FROM scratch AS dist
|
||||
|
||||
COPY --from=builder /src/dist .
|
||||
COPY --from=builder /src/dist .
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
FROM python:3.13-alpine
|
||||
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."
|
||||
@@ -9,17 +10,19 @@ RUN addgroup -g 1000 app && adduser -D -u 1000 -G app app
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt setup.py README.md ./
|
||||
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 pip install --upgrade pip setuptools wheel && pip install -r requirements.txt .
|
||||
RUN poetry install --no-interaction --no-ansi
|
||||
|
||||
ENV PATH="/app/.venv/bin:$PATH"
|
||||
|
||||
USER app
|
||||
|
||||
ENTRYPOINT ["rns-page-node"]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
ENTRYPOINT ["poetry", "run", "rns-page-node"]
|
||||
|
||||
22
Makefile
22
Makefile
@@ -1,6 +1,9 @@
|
||||
# Makefile for rns-page-node
|
||||
|
||||
.PHONY: all build sdist wheel clean install lint format docker-wheels docker-build docker-run docker-build-rootless docker-run-rootless help
|
||||
# Detect if docker buildx is available
|
||||
DOCKER_BUILD := $(shell docker buildx version >/dev/null 2>&1 && echo "docker buildx build" || echo "docker build")
|
||||
|
||||
.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
|
||||
|
||||
@@ -26,13 +29,13 @@ format:
|
||||
ruff check --fix .
|
||||
|
||||
docker-wheels:
|
||||
docker build --target builder -f 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 -f Dockerfile -t rns-page-node:latest .
|
||||
$(DOCKER_BUILD) $(BUILD_ARGS) -f Dockerfile -t rns-page-node:latest .
|
||||
|
||||
docker-run:
|
||||
docker run --rm -it \
|
||||
@@ -47,7 +50,7 @@ docker-run:
|
||||
--announce-interval 360
|
||||
|
||||
docker-build-rootless:
|
||||
docker build -f Dockerfile.rootless -t rns-page-node-rootless:latest .
|
||||
$(DOCKER_BUILD) $(BUILD_ARGS) -f Dockerfile.rootless -t rns-page-node-rootless:latest .
|
||||
|
||||
docker-run-rootless:
|
||||
docker run --rm -it \
|
||||
@@ -61,6 +64,13 @@ docker-run-rootless:
|
||||
--identity-dir /app/node-config \
|
||||
--announce-interval 360
|
||||
|
||||
test:
|
||||
bash tests/run_tests.sh
|
||||
|
||||
docker-test:
|
||||
$(DOCKER_BUILD) -f tests/Dockerfile.tests -t rns-page-node-tests .
|
||||
docker run --rm rns-page-node-tests
|
||||
|
||||
help:
|
||||
@echo "Makefile commands:"
|
||||
@echo " all - alias for build"
|
||||
@@ -75,4 +85,6 @@ help:
|
||||
@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"
|
||||
@echo " docker-run-rootless - run rootless runtime Docker image"
|
||||
@echo " test - run local integration tests"
|
||||
@echo " docker-test - build and run integration tests in Docker"
|
||||
|
||||
41
README.md
41
README.md
@@ -1,15 +1,15 @@
|
||||
# RNS Page Node
|
||||
|
||||
[](https://github.com/Sudo-Ivan/rns-page-node/actions/workflows/docker.yml)
|
||||
[](https://github.com/Sudo-Ivan/rns-page-node/actions/workflows/docker-test.yml)
|
||||
[](https://app.deepsource.com/gh/Sudo-Ivan/rns-page-node/)
|
||||
|
||||
A simple way to serve pages and files over the [Reticulum network](https://reticulum.network/). Drop-in replacement for [NomadNet](https://github.com/markqvist/NomadNet) nodes that primarily serve pages and files.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
pip install git+https://github.com/Sudo-Ivan/rns-page-node.git
|
||||
|
||||
# or
|
||||
|
||||
pipx install git+https://github.com/Sudo-Ivan/rns-page-node.git
|
||||
pip install rns-page-node
|
||||
```
|
||||
|
||||
```bash
|
||||
@@ -25,7 +25,7 @@ rns-page-node --node-name "Page Node" --pages-dir ./pages --files-dir ./files --
|
||||
### Docker/Podman
|
||||
|
||||
```bash
|
||||
docker run -it --rm -v ./pages:/app/pages -v ./files:/app/files -v ./node-config:/app/node-config -v ./config:/app/config ghcr.io/sudo-ivan/rns-page-node:latest
|
||||
docker run -it --rm -v ./pages:/app/pages -v ./files:/app/files -v ./node-config:/app/node-config -v ./config:/root/.reticulum ghcr.io/sudo-ivan/rns-page-node:latest
|
||||
```
|
||||
|
||||
### Docker/Podman Rootless
|
||||
@@ -38,9 +38,27 @@ podman run -it --rm -v ./pages:/app/pages -v ./files:/app/files -v ./node-config
|
||||
|
||||
Mounting volumes are optional, you can also copy pages and files to the container `podman cp` or `docker cp`.
|
||||
|
||||
## Page formats
|
||||
## Build
|
||||
|
||||
- Micron `.mu`
|
||||
```bash
|
||||
make build
|
||||
```
|
||||
|
||||
Build wheels:
|
||||
|
||||
```bash
|
||||
make wheel
|
||||
```
|
||||
|
||||
### Build Wheels in Docker
|
||||
|
||||
```bash
|
||||
make docker-wheels
|
||||
```
|
||||
|
||||
## Pages
|
||||
|
||||
Supports Micron `.mu` and dynamic pages with `#!` in the micron files.
|
||||
|
||||
## Options
|
||||
|
||||
@@ -51,12 +69,11 @@ Mounting volumes are optional, you can also copy pages and files to the containe
|
||||
-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.
|
||||
-r, --page-refresh-interval: The interval to refresh pages.
|
||||
-f, --file-refresh-interval: The interval to refresh files.
|
||||
-l, --log-level: The logging level.
|
||||
```
|
||||
|
||||
## To-Do
|
||||
|
||||
- [ ] Pypi
|
||||
|
||||
## License
|
||||
|
||||
This project incorporates portions of the [NomadNet](https://github.com/markqvist/NomadNet) codebase, which is licensed under the GNU General Public License v3.0 (GPL-3.0). As a derivative work, this project is also distributed under the terms of the GPL-3.0. See the [LICENSE](LICENSE) file for full license.
|
||||
|
||||
1535
poetry.lock
generated
1535
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,15 @@
|
||||
[project]
|
||||
name = "rns-page-node"
|
||||
version = "0.1.0"
|
||||
license = {file = "LICENSE"}
|
||||
version = "1.1.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"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"rns (>=0.9.6,<0.10.0)"
|
||||
"rns (>=1.0.0,<1.5.0)"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
@@ -20,5 +20,6 @@ requires = ["poetry-core>=2.0.0,<3.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
ruff = "^0.11.11"
|
||||
ruff = "^0.12.12"
|
||||
safety = "^3.6.1"
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
rns==0.9.6
|
||||
rns=1.0.0
|
||||
@@ -1,2 +1,2 @@
|
||||
# rns_page_node package
|
||||
__all__ = ['main']
|
||||
__all__ = ["main"]
|
||||
|
||||
@@ -1,38 +1,49 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Minimal Reticulum Page Node
|
||||
"""Minimal Reticulum Page Node
|
||||
Serves .mu pages and files over RNS.
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
import threading
|
||||
import subprocess
|
||||
import RNS
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
|
||||
DEFAULT_INDEX = '''>Default Home Page
|
||||
import RNS
|
||||
|
||||
This node is serving pages using page node, but the home page file (index.mu) was not found in the pages directory. Please add an index.mu file to customize the home page.
|
||||
'''
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NOTALLOWED = '''>Request Not Allowed
|
||||
DEFAULT_INDEX = """>Default Home Page
|
||||
|
||||
This node is serving pages using rns-page-node, but the home page file (index.mu) was not found in the pages directory. Please add an index.mu file to customize the home page.
|
||||
"""
|
||||
|
||||
DEFAULT_NOTALLOWED = """>Request Not Allowed
|
||||
|
||||
You are not authorised to carry out the request.
|
||||
'''
|
||||
"""
|
||||
|
||||
|
||||
class PageNode:
|
||||
def __init__(self, identity, pagespath, filespath, announce_interval=360, name=None, page_refresh_interval=0, file_refresh_interval=0):
|
||||
def __init__(
|
||||
self,
|
||||
identity,
|
||||
pagespath,
|
||||
filespath,
|
||||
announce_interval=360,
|
||||
name=None,
|
||||
page_refresh_interval=0,
|
||||
file_refresh_interval=0,
|
||||
):
|
||||
self._stop_event = threading.Event()
|
||||
self._lock = threading.Lock()
|
||||
self.logger = logging.getLogger(f"{__name__}.PageNode")
|
||||
self.identity = identity
|
||||
self.name = name
|
||||
self.pagespath = pagespath
|
||||
self.filespath = filespath
|
||||
self.destination = RNS.Destination(
|
||||
identity,
|
||||
RNS.Destination.IN,
|
||||
RNS.Destination.SINGLE,
|
||||
"nomadnetwork",
|
||||
"node"
|
||||
identity, RNS.Destination.IN, RNS.Destination.SINGLE, "nomadnetwork", "node",
|
||||
)
|
||||
self.announce_interval = announce_interval
|
||||
self.last_announce = 0
|
||||
@@ -46,46 +57,52 @@ class PageNode:
|
||||
|
||||
self.destination.set_link_established_callback(self.on_connect)
|
||||
|
||||
threading.Thread(target=self._announce_loop, daemon=True).start()
|
||||
threading.Thread(target=self._refresh_loop, daemon=True).start()
|
||||
self._announce_thread = threading.Thread(
|
||||
target=self._announce_loop, daemon=True,
|
||||
)
|
||||
self._announce_thread.start()
|
||||
self._refresh_thread = threading.Thread(target=self._refresh_loop, daemon=True)
|
||||
self._refresh_thread.start()
|
||||
|
||||
def register_pages(self):
|
||||
self.servedpages = []
|
||||
self._scan_pages(self.pagespath)
|
||||
with self._lock:
|
||||
self.servedpages = []
|
||||
self._scan_pages(self.pagespath)
|
||||
|
||||
if not os.path.isfile(os.path.join(self.pagespath, "index.mu")):
|
||||
self.destination.register_request_handler(
|
||||
"/page/index.mu",
|
||||
response_generator=self.serve_default_index,
|
||||
allow=RNS.Destination.ALLOW_ALL
|
||||
allow=RNS.Destination.ALLOW_ALL,
|
||||
)
|
||||
|
||||
for full_path in self.servedpages:
|
||||
rel = full_path[len(self.pagespath):]
|
||||
rel = full_path[len(self.pagespath) :]
|
||||
request_path = f"/page{rel}"
|
||||
self.destination.register_request_handler(
|
||||
request_path,
|
||||
response_generator=self.serve_page,
|
||||
allow=RNS.Destination.ALLOW_ALL
|
||||
allow=RNS.Destination.ALLOW_ALL,
|
||||
)
|
||||
|
||||
def register_files(self):
|
||||
self.servedfiles = []
|
||||
self._scan_files(self.filespath)
|
||||
with self._lock:
|
||||
self.servedfiles = []
|
||||
self._scan_files(self.filespath)
|
||||
|
||||
for full_path in self.servedfiles:
|
||||
rel = full_path[len(self.filespath):]
|
||||
rel = full_path[len(self.filespath) :]
|
||||
request_path = f"/file{rel}"
|
||||
self.destination.register_request_handler(
|
||||
request_path,
|
||||
response_generator=self.serve_file,
|
||||
allow=RNS.Destination.ALLOW_ALL,
|
||||
auto_compress=32_000_000
|
||||
auto_compress=32_000_000,
|
||||
)
|
||||
|
||||
def _scan_pages(self, base):
|
||||
for entry in os.listdir(base):
|
||||
if entry.startswith('.'):
|
||||
if entry.startswith("."):
|
||||
continue
|
||||
path = os.path.join(base, entry)
|
||||
if os.path.isdir(path):
|
||||
@@ -95,7 +112,7 @@ class PageNode:
|
||||
|
||||
def _scan_files(self, base):
|
||||
for entry in os.listdir(base):
|
||||
if entry.startswith('.'):
|
||||
if entry.startswith("."):
|
||||
continue
|
||||
path = os.path.join(base, entry)
|
||||
if os.path.isdir(path):
|
||||
@@ -103,66 +120,152 @@ class PageNode:
|
||||
elif os.path.isfile(path):
|
||||
self.servedfiles.append(path)
|
||||
|
||||
def serve_default_index(self, path, data, request_id, link_id, remote_identity, requested_at):
|
||||
return DEFAULT_INDEX.encode('utf-8')
|
||||
@staticmethod
|
||||
def serve_default_index(
|
||||
path, data, request_id, link_id, remote_identity, requested_at,
|
||||
):
|
||||
return DEFAULT_INDEX.encode("utf-8")
|
||||
|
||||
def serve_page(self, path, data, request_id, link_id, remote_identity, requested_at):
|
||||
def serve_page(
|
||||
self, path, data, request_id, link_id, remote_identity, requested_at,
|
||||
):
|
||||
file_path = path.replace("/page", self.pagespath, 1)
|
||||
try:
|
||||
with open(file_path, 'rb') as _f:
|
||||
with open(file_path, "rb") as _f:
|
||||
first_line = _f.readline()
|
||||
is_script = first_line.startswith(b'#!')
|
||||
is_script = first_line.startswith(b"#!")
|
||||
except Exception:
|
||||
is_script = False
|
||||
if is_script and os.access(file_path, os.X_OK):
|
||||
# Note: You can remove the following try-except block and just serve the page content statically
|
||||
# Note: The execution of file_path is intentional here, as some pages are designed to be executable scripts.
|
||||
# This is acknowledged as a potential security risk if untrusted input can control file_path.
|
||||
try:
|
||||
result = subprocess.run([file_path], stdout=subprocess.PIPE)
|
||||
result = subprocess.run([file_path], stdout=subprocess.PIPE, check=True) # noqa: S603
|
||||
return result.stdout
|
||||
except Exception:
|
||||
pass
|
||||
with open(file_path, 'rb') as f:
|
||||
self.logger.exception("Error executing script page")
|
||||
with open(file_path, "rb") as f:
|
||||
return f.read()
|
||||
|
||||
def serve_file(self, path, data, request_id, link_id, remote_identity, requested_at):
|
||||
def serve_file(
|
||||
self, path, data, request_id, link_id, remote_identity, requested_at,
|
||||
):
|
||||
file_path = path.replace("/file", self.filespath, 1)
|
||||
return [open(file_path, 'rb'), {"name": os.path.basename(file_path).encode('utf-8')}]
|
||||
return [
|
||||
open(file_path, "rb"),
|
||||
{"name": os.path.basename(file_path).encode("utf-8")},
|
||||
]
|
||||
|
||||
def on_connect(self, link):
|
||||
pass
|
||||
|
||||
def _announce_loop(self):
|
||||
while True:
|
||||
if time.time() - self.last_announce > self.announce_interval:
|
||||
if self.name:
|
||||
self.destination.announce(app_data=self.name.encode('utf-8'))
|
||||
else:
|
||||
self.destination.announce()
|
||||
self.last_announce = time.time()
|
||||
time.sleep(1)
|
||||
try:
|
||||
while not self._stop_event.is_set():
|
||||
if time.time() - self.last_announce > self.announce_interval:
|
||||
if self.name:
|
||||
self.destination.announce(app_data=self.name.encode("utf-8"))
|
||||
else:
|
||||
self.destination.announce()
|
||||
self.last_announce = time.time()
|
||||
time.sleep(1)
|
||||
except Exception:
|
||||
self.logger.exception("Error in announce loop")
|
||||
|
||||
def _refresh_loop(self):
|
||||
while True:
|
||||
now = time.time()
|
||||
if self.page_refresh_interval > 0 and now - self.last_page_refresh > self.page_refresh_interval:
|
||||
self.register_pages()
|
||||
self.last_page_refresh = now
|
||||
if self.file_refresh_interval > 0 and now - self.last_file_refresh > self.file_refresh_interval:
|
||||
self.register_files()
|
||||
self.last_file_refresh = now
|
||||
time.sleep(1)
|
||||
try:
|
||||
while not self._stop_event.is_set():
|
||||
now = time.time()
|
||||
if (
|
||||
self.page_refresh_interval > 0
|
||||
and now - self.last_page_refresh > self.page_refresh_interval
|
||||
):
|
||||
self.register_pages()
|
||||
self.last_page_refresh = now
|
||||
if (
|
||||
self.file_refresh_interval > 0
|
||||
and now - self.last_file_refresh > self.file_refresh_interval
|
||||
):
|
||||
self.register_files()
|
||||
self.last_file_refresh = now
|
||||
time.sleep(1)
|
||||
except Exception:
|
||||
self.logger.exception("Error in refresh loop")
|
||||
|
||||
def shutdown(self):
|
||||
self.logger.info("Shutting down PageNode...")
|
||||
self._stop_event.set()
|
||||
try:
|
||||
self._announce_thread.join(timeout=5)
|
||||
self._refresh_thread.join(timeout=5)
|
||||
except Exception:
|
||||
self.logger.exception("Error waiting for threads to shut down")
|
||||
try:
|
||||
if hasattr(self.destination, "close"):
|
||||
self.destination.close()
|
||||
except Exception:
|
||||
self.logger.exception("Error closing RNS destination")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Minimal Reticulum Page Node")
|
||||
parser.add_argument('-c', '--config', dest='configpath', help='Reticulum config path', default=None)
|
||||
parser.add_argument('-p', '--pages-dir', dest='pages_dir', help='Pages directory', default=os.path.join(os.getcwd(), 'pages'))
|
||||
parser.add_argument('-f', '--files-dir', dest='files_dir', help='Files directory', default=os.path.join(os.getcwd(), 'files'))
|
||||
parser.add_argument('-n', '--node-name', dest='node_name', help='Node display name', default=None)
|
||||
parser.add_argument('-a', '--announce-interval', dest='announce_interval', type=int, help='Announce interval in seconds', default=360)
|
||||
parser.add_argument('-i', '--identity-dir', dest='identity_dir', help='Directory to store node identity', default=os.path.join(os.getcwd(), 'node-config'))
|
||||
parser.add_argument('--page-refresh-interval', dest='page_refresh_interval', type=int, default=0, help='Page refresh interval in seconds, 0 disables auto-refresh')
|
||||
parser.add_argument('--file-refresh-interval', dest='file_refresh_interval', type=int, default=0, help='File refresh interval in seconds, 0 disables auto-refresh')
|
||||
parser.add_argument(
|
||||
"-c", "--config", dest="configpath", help="Reticulum config path", default=None,
|
||||
)
|
||||
parser.add_argument(
|
||||
"-p",
|
||||
"--pages-dir",
|
||||
dest="pages_dir",
|
||||
help="Pages directory",
|
||||
default=os.path.join(os.getcwd(), "pages"),
|
||||
)
|
||||
parser.add_argument(
|
||||
"-f",
|
||||
"--files-dir",
|
||||
dest="files_dir",
|
||||
help="Files directory",
|
||||
default=os.path.join(os.getcwd(), "files"),
|
||||
)
|
||||
parser.add_argument(
|
||||
"-n", "--node-name", dest="node_name", help="Node display name", default=None,
|
||||
)
|
||||
parser.add_argument(
|
||||
"-a",
|
||||
"--announce-interval",
|
||||
dest="announce_interval",
|
||||
type=int,
|
||||
help="Announce interval in seconds",
|
||||
default=360,
|
||||
)
|
||||
parser.add_argument(
|
||||
"-i",
|
||||
"--identity-dir",
|
||||
dest="identity_dir",
|
||||
help="Directory to store node identity",
|
||||
default=os.path.join(os.getcwd(), "node-config"),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--page-refresh-interval",
|
||||
dest="page_refresh_interval",
|
||||
type=int,
|
||||
default=0,
|
||||
help="Page refresh interval in seconds, 0 disables auto-refresh",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--file-refresh-interval",
|
||||
dest="file_refresh_interval",
|
||||
type=int,
|
||||
default=0,
|
||||
help="File refresh interval in seconds, 0 disables auto-refresh",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-l",
|
||||
"--log-level",
|
||||
dest="log_level",
|
||||
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
|
||||
default="INFO",
|
||||
help="Logging level",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
configpath = args.configpath
|
||||
@@ -173,10 +276,14 @@ def main():
|
||||
identity_dir = args.identity_dir
|
||||
page_refresh_interval = args.page_refresh_interval
|
||||
file_refresh_interval = args.file_refresh_interval
|
||||
numeric_level = getattr(logging, args.log_level.upper(), logging.INFO)
|
||||
logging.basicConfig(
|
||||
level=numeric_level, format="%(asctime)s %(name)s [%(levelname)s] %(message)s",
|
||||
)
|
||||
|
||||
RNS.Reticulum(configpath)
|
||||
os.makedirs(identity_dir, exist_ok=True)
|
||||
identity_file = os.path.join(identity_dir, 'identity')
|
||||
identity_file = os.path.join(identity_dir, "identity")
|
||||
if os.path.isfile(identity_file):
|
||||
identity = RNS.Identity.from_file(identity_file)
|
||||
else:
|
||||
@@ -186,14 +293,26 @@ def main():
|
||||
os.makedirs(pages_dir, exist_ok=True)
|
||||
os.makedirs(files_dir, exist_ok=True)
|
||||
|
||||
node = PageNode(identity, pages_dir, files_dir, announce_interval, node_name, page_refresh_interval, file_refresh_interval)
|
||||
print("Page node running. Press Ctrl-C to exit.")
|
||||
node = PageNode(
|
||||
identity,
|
||||
pages_dir,
|
||||
files_dir,
|
||||
announce_interval,
|
||||
node_name,
|
||||
page_refresh_interval,
|
||||
file_refresh_interval,
|
||||
)
|
||||
logger.info("Page node running. Press Ctrl-C to exit.")
|
||||
logger.info("Node address: %s", RNS.prettyhexrep(node.destination.hash))
|
||||
|
||||
|
||||
try:
|
||||
while True:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
print("Shutting down.")
|
||||
logger.info("Keyboard interrupt received, shutting down...")
|
||||
node.shutdown()
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
34
setup.py
34
setup.py
@@ -1,31 +1,31 @@
|
||||
from setuptools import setup, find_packages
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
with open('README.md', 'r', encoding='utf-8') as fh:
|
||||
with open("README.md", encoding="utf-8") as fh:
|
||||
long_description = fh.read()
|
||||
|
||||
setup(
|
||||
name='rns-page-node',
|
||||
version='0.1.0',
|
||||
author='Sudo-Ivan',
|
||||
author_email='',
|
||||
description='A simple way to serve pages and files over the Reticulum network.',
|
||||
name="rns-page-node",
|
||||
version="1.0.0",
|
||||
author="Sudo-Ivan",
|
||||
author_email="",
|
||||
description="A simple way to serve pages and files over the Reticulum network.",
|
||||
long_description=long_description,
|
||||
long_description_content_type='text/markdown',
|
||||
url='https://github.com/Sudo-Ivan/rns-page-node',
|
||||
long_description_content_type="text/markdown",
|
||||
url="https://github.com/Sudo-Ivan/rns-page-node",
|
||||
packages=find_packages(),
|
||||
python_requires='>=3.9',
|
||||
license="GPL-3.0",
|
||||
python_requires=">=3.10",
|
||||
install_requires=[
|
||||
'rns>=0.9.6,<0.10.0',
|
||||
"rns>=1.0.0,<1.5.0",
|
||||
],
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'rns-page-node=rns_page_node.main:main',
|
||||
"console_scripts": [
|
||||
"rns-page-node=rns_page_node.main:main",
|
||||
],
|
||||
},
|
||||
license='GPL-3.0',
|
||||
classifiers=[
|
||||
'Programming Language :: Python :: 3',
|
||||
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
|
||||
'Operating System :: OS Independent',
|
||||
"Programming Language :: Python :: 3",
|
||||
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||
"Operating System :: OS Independent",
|
||||
],
|
||||
)
|
||||
|
||||
4
tests/.gitignore
vendored
Normal file
4
tests/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
pages/
|
||||
node-config/
|
||||
node.log
|
||||
config/
|
||||
14
tests/Dockerfile.tests
Normal file
14
tests/Dockerfile.tests
Normal file
@@ -0,0 +1,14 @@
|
||||
FROM python:3.10-slim
|
||||
|
||||
RUN apt-get update && apt-get install -y build-essential libssl-dev && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . /app
|
||||
RUN pip install --no-cache-dir .
|
||||
|
||||
WORKDIR /app/tests
|
||||
|
||||
RUN chmod +x run_tests.sh
|
||||
|
||||
CMD ["bash", "run_tests.sh"]
|
||||
44
tests/run_tests.sh
Normal file
44
tests/run_tests.sh
Normal file
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||
|
||||
# Remove previous test artifacts
|
||||
rm -rf config node-config pages files node.log
|
||||
|
||||
# Create directories for config, node identity, pages, and files
|
||||
mkdir -p config node-config pages files
|
||||
|
||||
# Create a sample page and a test file
|
||||
cat > pages/index.mu << EOF
|
||||
>Test Page
|
||||
This is a test page.
|
||||
EOF
|
||||
|
||||
cat > files/text.txt << EOF
|
||||
This is a test file.
|
||||
EOF
|
||||
|
||||
# Start the page node in the background
|
||||
python3 ../rns_page_node/main.py -c config -i node-config -p pages -f files > node.log 2>&1 &
|
||||
NODE_PID=$!
|
||||
|
||||
# Wait for node to generate its identity file
|
||||
echo "Waiting for node identity..."
|
||||
for i in {1..40}; do
|
||||
if [ -f node-config/identity ]; then
|
||||
echo "Identity file found"
|
||||
break
|
||||
fi
|
||||
sleep 0.25
|
||||
done
|
||||
if [ ! -f node-config/identity ]; then
|
||||
echo "Error: node identity file not found" >&2
|
||||
kill $NODE_PID
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run the client test
|
||||
python3 test_client.py
|
||||
|
||||
# Clean up
|
||||
kill $NODE_PID
|
||||
105
tests/test_client.py
Normal file
105
tests/test_client.py
Normal file
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
import RNS
|
||||
|
||||
# Determine base directory for tests
|
||||
dir_path = os.path.abspath(os.path.dirname(__file__))
|
||||
config_dir = os.path.join(dir_path, "config")
|
||||
identity_dir = os.path.join(dir_path, "node-config")
|
||||
|
||||
# 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")
|
||||
server_identity = RNS.Identity.from_file(identity_file)
|
||||
|
||||
# Create a destination to the server node
|
||||
destination = RNS.Destination(
|
||||
server_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "nomadnetwork", "node",
|
||||
)
|
||||
|
||||
# Ensure we know a path to the destination
|
||||
if not RNS.Transport.has_path(destination.hash):
|
||||
RNS.Transport.request_path(destination.hash)
|
||||
while not RNS.Transport.has_path(destination.hash):
|
||||
time.sleep(0.1)
|
||||
|
||||
# Establish a link to the server
|
||||
global_link = RNS.Link(destination)
|
||||
|
||||
# Containers for responses
|
||||
responses = {}
|
||||
done_event = threading.Event()
|
||||
|
||||
|
||||
# Callback for page response
|
||||
def on_page(response):
|
||||
data = response.response
|
||||
if isinstance(data, bytes):
|
||||
text = data.decode("utf-8")
|
||||
else:
|
||||
text = str(data)
|
||||
print("Received page:")
|
||||
print(text)
|
||||
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"):
|
||||
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")
|
||||
# Handle response as a raw file object
|
||||
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")
|
||||
# Handle response as raw bytes
|
||||
elif isinstance(data, bytes):
|
||||
text = data.decode("utf-8")
|
||||
print("Received file:")
|
||||
print(text)
|
||||
responses["file"] = text
|
||||
else:
|
||||
print("Received file (unhandled format):", data)
|
||||
responses["file"] = str(data)
|
||||
if "page" in responses:
|
||||
done_event.set()
|
||||
|
||||
|
||||
# Request the page and file once the link is established
|
||||
def on_link_established(link):
|
||||
link.request("/page/index.mu", None, response_callback=on_page)
|
||||
link.request("/file/text.txt", None, response_callback=on_file)
|
||||
|
||||
|
||||
# Register callbacks
|
||||
global_link.set_link_established_callback(on_link_established)
|
||||
global_link.set_link_closed_callback(lambda link: done_event.set())
|
||||
|
||||
# Wait for responses or timeout
|
||||
if not done_event.wait(timeout=30):
|
||||
print("Test timed out.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if responses.get("page") and responses.get("file"):
|
||||
print("Tests passed!")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("Tests failed.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
65
tests/test_client2.py
Normal file
65
tests/test_client2.py
Normal file
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
import RNS
|
||||
|
||||
dir_path = os.path.abspath(os.path.dirname(__file__))
|
||||
config_dir = os.path.join(dir_path, "config")
|
||||
|
||||
RNS.Reticulum(config_dir)
|
||||
|
||||
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,
|
||||
)
|
||||
sys.exit(1)
|
||||
destination_hash = bytes.fromhex(DESTINATION_HEX)
|
||||
|
||||
if not RNS.Transport.has_path(destination_hash):
|
||||
print("Requesting path to server...")
|
||||
RNS.Transport.request_path(destination_hash)
|
||||
while not RNS.Transport.has_path(destination_hash):
|
||||
time.sleep(0.1)
|
||||
|
||||
server_identity = RNS.Identity.recall(destination_hash)
|
||||
print(f"Recalled server identity for {DESTINATION_HEX}")
|
||||
|
||||
destination = RNS.Destination(
|
||||
server_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "nomadnetwork", "node",
|
||||
)
|
||||
link = RNS.Link(destination)
|
||||
|
||||
done_event = threading.Event()
|
||||
|
||||
|
||||
def on_page(response):
|
||||
data = response.response
|
||||
if isinstance(data, bytes):
|
||||
text = data.decode("utf-8")
|
||||
else:
|
||||
text = str(data)
|
||||
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())
|
||||
|
||||
if not done_event.wait(timeout=30):
|
||||
print("Timed out waiting for page", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print("Done fetching page.")
|
||||
sys.exit(0)
|
||||
Reference in New Issue
Block a user