Compare commits
119 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
185db82bf0
|
|||
|
86ddce80db
|
|||
|
d1676ce5ec
|
|||
|
ec47ecd872
|
|||
|
ed01ccccbb
|
|||
|
9ba9d277c2
|
|||
|
39dac5c2db
|
|||
|
a40ba430b8
|
|||
|
70c8826af0
|
|||
|
97d978611e
|
|||
|
a295a52904
|
|||
|
64c016250a
|
|||
|
457013b94a
|
|||
|
02af0e1ddf
|
|||
|
438d12ab71
|
|||
|
4d1b49daa4
|
|||
|
beab7b2565
|
|||
|
2998b8d833
|
|||
|
f09622ae76
|
|||
|
d7efe9de7f
|
|||
|
f49c6293f9
|
|||
|
1b0aaad689
|
|||
|
6e28f908be
|
|||
|
a32215f434
|
|||
|
59e016815b
|
|||
|
6b8ce85ea2
|
|||
|
62c280daf2
|
|||
|
e817238fb9
|
|||
|
37bc4948d1
|
|||
| 8080f2855f | |||
|
|
95fa215162 | ||
|
|
cd08064678 | ||
|
|
01b3a54abf | ||
|
|
cb41f89cc9 | ||
|
|
d06a93995e | ||
|
|
dbfe2fd35c | ||
|
|
07754bc9fa | ||
|
|
898919e160 | ||
|
761c1b356c
|
|||
|
|
54b47c8eaf | ||
|
|
55343b7be2 | ||
|
9c6da64cbe
|
|||
|
d0a484f692
|
|||
|
|
31dd0828a2 | ||
|
954f6ecd36
|
|||
|
85c8785502
|
|||
|
112348d862
|
|||
|
4f8f2786ab
|
|||
|
3e6e078367
|
|||
|
73c9d12f26
|
|||
|
fc50bc6fb5
|
|||
|
8f1d5ee02a
|
|||
|
86f0a687d2
|
|||
|
694ab011ec
|
|||
|
53d74a3732
|
|||
|
30f050c8d4
|
|||
|
070157737b
|
|||
|
8538d9feb3
|
|||
|
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
|
|||
|
a2efdb136a
|
|||
|
001613b4fa
|
|||
|
74564d0ef2
|
|||
|
81142ad194
|
|||
|
fee1d2e2d6
|
|||
|
7c93fdb71d
|
|||
| 9e435eeebc | |||
| 5dfcc1f2ce | |||
| 2def60b457 | |||
| f708ad4ee1 | |||
| f7568d81aa | |||
| 251f9bacef | |||
| 07892dbfee | |||
| 54e6849968 | |||
| ea27c380cb | |||
|
|
a338be85e1 | ||
|
|
e31cb3418b | ||
|
|
798725dca6 | ||
|
|
6f393497f0 | ||
|
|
14b5aabf2b | ||
| fb36907447 | |||
| 62fde2617b | |||
| 9f5ea23eb7 | |||
| 19fad61706 | |||
| c900cf38c9 | |||
| 014ebc25c6 | |||
|
|
d5e9308fb5 | ||
|
|
7d5e891261 | ||
|
|
c382ed790f | ||
| cb72e57da9 | |||
|
|
aaf5ad23e2 | ||
|
|
ce1b1dad7d | ||
|
|
67ebc7e556 | ||
|
|
b31fb748b8 |
@@ -0,0 +1,150 @@
|
||||
name: Build and Publish Docker Image
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
tags:
|
||||
- "v*"
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
REGISTRY: git.quad4.io
|
||||
IMAGE_NAME: RNS-Things/rns-page-node
|
||||
DEV_IMAGE_NAME: RNS-Things/rns-page-node-dev
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
outputs:
|
||||
image_digest: ${{ steps.build.outputs.digest }}
|
||||
image_tags: ${{ steps.meta.outputs.tags }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: https://git.quad4.io/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: https://git.quad4.io/actions/setup-qemu-action@3a1695b1353f9f8868722ffaafc1f164ef35fa5e # v3
|
||||
with:
|
||||
platforms: amd64,arm64
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: https://git.quad4.io/actions/setup-buildx-action@7fbd262f0ca05b45700d8eaaf71f40837d036cc7 # v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: https://git.quad4.io/actions/login-action@bb91d7e20cfedb030a44164cb558bba899e1010a # v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: https://git.quad4.io/actions/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=raw,value=latest
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=sha,format=short
|
||||
|
||||
- name: Build and push Docker image
|
||||
id: build
|
||||
uses: https://git.quad4.io/actions/build-push-action@dc0c2d97df39a6939d9db7d572445529e2365ec6 # v6
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
no-cache: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
BUILD_DATE=${{ github.event.head_commit.timestamp }}
|
||||
VCS_REF=${{ github.sha }}
|
||||
VERSION=${{ steps.meta.outputs.version }}
|
||||
|
||||
- name: Download Trivy
|
||||
run: |
|
||||
curl -L -o /tmp/trivy.deb https://git.quad4.io/Quad4-Software/Trivy-Assets/raw/commit/917e0e52b2f663cbbe13e63b7176262e248265ae/trivy_0.68.2_Linux-64bit.deb
|
||||
sudo dpkg -i /tmp/trivy.deb || sudo apt-get install -f -y
|
||||
|
||||
- name: Scan Docker image
|
||||
run: |
|
||||
# Extract the first tag from the multi-line tags output
|
||||
IMAGE_TAG=$(echo "${{ steps.meta.outputs.tags }}" | head -n 1)
|
||||
trivy image --exit-code 1 "$IMAGE_TAG"
|
||||
|
||||
build-dev:
|
||||
if: github.event_name == 'pull_request' || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: https://git.quad4.io/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: https://git.quad4.io/actions/setup-qemu-action@3a1695b1353f9f8868722ffaafc1f164ef35fa5e # v3
|
||||
with:
|
||||
platforms: amd64,arm64
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: https://git.quad4.io/actions/setup-buildx-action@7fbd262f0ca05b45700d8eaaf71f40837d036cc7 # v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: https://git.quad4.io/actions/login-action@bb91d7e20cfedb030a44164cb558bba899e1010a # v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
|
||||
- name: Extract DEV metadata (tags, labels) for Docker
|
||||
id: meta-dev
|
||||
uses: https://git.quad4.io/actions/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.DEV_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=raw,value=dev
|
||||
type=sha,format=short
|
||||
|
||||
- name: Build and push dev Docker image
|
||||
id: build-dev
|
||||
uses: https://git.quad4.io/actions/build-push-action@dc0c2d97df39a6939d9db7d572445529e2365ec6 # v6
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
no-cache: true
|
||||
tags: ${{ steps.meta-dev.outputs.tags }}
|
||||
labels: ${{ steps.meta-dev.outputs.labels }}
|
||||
build-args: |
|
||||
BUILD_DATE=${{ github.event.head_commit.timestamp }}
|
||||
VCS_REF=${{ github.sha }}
|
||||
VERSION=${{ steps.meta-dev.outputs.version }}
|
||||
|
||||
- name: Download Trivy
|
||||
run: |
|
||||
curl -L -o /tmp/trivy.deb https://git.quad4.io/Quad4-Software/Trivy-Assets/raw/commit/917e0e52b2f663cbbe13e63b7176262e248265ae/trivy_0.68.2_Linux-64bit.deb
|
||||
sudo dpkg -i /tmp/trivy.deb || sudo apt-get install -f -y
|
||||
|
||||
- name: Scan Docker image (dev)
|
||||
run: |
|
||||
# Extract the first tag from the multi-line tags output
|
||||
IMAGE_TAG=$(echo "${{ steps.meta-dev.outputs.tags }}" | head -n 1)
|
||||
trivy image --exit-code 1 "$IMAGE_TAG"
|
||||
@@ -0,0 +1,67 @@
|
||||
name: Create Release
|
||||
|
||||
# This workflow creates releases:
|
||||
# 1. Build packages
|
||||
# 2. Create Gitea release with all artifacts atomically
|
||||
# This ensures releases cannot be modified once published.
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version to release (e.g., 0.6.8)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Build and Release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: https://git.quad4.io/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: https://git.quad4.io/actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v5
|
||||
with:
|
||||
python-version: "3.13"
|
||||
|
||||
- name: Install build and twine
|
||||
run: python3 -m pip install build twine --user
|
||||
|
||||
- name: Build a binary wheel and a source tarball
|
||||
run: python3 -m build
|
||||
|
||||
- name: Generate SHA256 checksums
|
||||
run: |
|
||||
sha256sum dist/*.tar.gz dist/*.whl | sed 's|dist/||g' > dist/SHA256SUMS
|
||||
echo "### SHA256 Checksums" > release_notes.md
|
||||
echo '```' >> release_notes.md
|
||||
cat dist/SHA256SUMS >> release_notes.md
|
||||
echo '```' >> release_notes.md
|
||||
|
||||
- name: Publish to Gitea PyPI registry
|
||||
run: python3 -m twine upload --repository-url ${{ github.server_url }}/api/packages/${{ github.repository_owner }}/pypi -u ${{ secrets.REGISTRY_USERNAME }} -p ${{ secrets.REGISTRY_PASSWORD }} dist/*.tar.gz dist/*.whl
|
||||
continue-on-error: true
|
||||
|
||||
- name: Create Gitea Release with artifacts
|
||||
uses: https://git.quad4.io/actions/gitea-release-action@4875285c0950474efb7ca2df55233c51333eeb74
|
||||
with:
|
||||
tag_name: ${{ inputs.version || github.ref_name }}
|
||||
name: Release ${{ inputs.version || github.ref_name }}
|
||||
body_path: release_notes.md
|
||||
files: |
|
||||
dist/*.tar.gz
|
||||
dist/*.whl
|
||||
dist/SHA256SUMS
|
||||
@@ -0,0 +1,42 @@
|
||||
name: Safety
|
||||
on:
|
||||
push:
|
||||
branches: [main, master]
|
||||
schedule:
|
||||
- cron: "0 0 * * 0" # weekly
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
security:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: https://git.quad4.io/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: https://git.quad4.io/actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v5
|
||||
with:
|
||||
python-version: "3.13"
|
||||
|
||||
- name: Install Poetry
|
||||
run: |
|
||||
curl -sSL https://install.python-poetry.org | python3 -
|
||||
echo "$HOME/.local/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
poetry config virtualenvs.create false
|
||||
poetry install --no-interaction --no-ansi
|
||||
pip install --upgrade filelock virtualenv
|
||||
|
||||
- name: Run pip-audit
|
||||
uses: https://git.quad4.io/actions/gh-action-pip-audit@66a6ee35b1b25f89c6bdc9f7c11284f08061823a # v1.1.0
|
||||
|
||||
- name: Download Trivy
|
||||
run: |
|
||||
curl -L -o /tmp/trivy.deb https://git.quad4.io/Quad4-Software/Trivy-Assets/raw/commit/917e0e52b2f663cbbe13e63b7176262e248265ae/trivy_0.68.2_Linux-64bit.deb
|
||||
sudo dpkg -i /tmp/trivy.deb || sudo apt-get install -f -y
|
||||
|
||||
- name: Scan Repository
|
||||
run: |
|
||||
trivy fs --exit-code 1 .
|
||||
Vendored
-27
@@ -1,27 +0,0 @@
|
||||
name: Docker Build Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Build Docker Image
|
||||
run: docker build . --file Dockerfile --build-arg PYTHON_VERSION=${{ matrix.python-version }} --tag lxmfy-test:${{ matrix.python-version }}
|
||||
Vendored
-86
@@ -1,86 +0,0 @@
|
||||
name: Build and Publish Docker Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
tags: [ 'v*' ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
platforms: amd64,arm64
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
type=ref,event=branch,prefix=,suffix=,enable={{is_default_branch}}
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=sha,format=short
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
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@v5
|
||||
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
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile.rootless
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta_rootless.outputs.tags }}
|
||||
labels: ${{ steps.meta_rootless.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
Vendored
-100
@@ -1,100 +0,0 @@
|
||||
name: Publish Python 🐍 distribution 📦 to PyPI
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version to release (e.g., 0.6.8)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build distribution 📦
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5.3.0
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- name: Install pypa/build
|
||||
run: python3 -m pip install build --user
|
||||
- name: Build a binary wheel and a source tarball
|
||||
run: python3 -m build
|
||||
- name: Store the distribution packages
|
||||
uses: actions/upload-artifact@v4.5.0
|
||||
with:
|
||||
name: python-package-distributions
|
||||
path: dist/
|
||||
|
||||
publish-to-pypi:
|
||||
name: Publish Python 🐍 distribution 📦 to PyPI
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
needs:
|
||||
- build
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: pypi
|
||||
url: https://pypi.org/p/rns-page-node
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Download all the dists
|
||||
uses: actions/download-artifact@v4.1.8
|
||||
with:
|
||||
name: python-package-distributions
|
||||
path: dist/
|
||||
- name: Publish distribution 📦 to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@v1.12.3
|
||||
|
||||
github-release:
|
||||
name: Sign the Python 🐍 distribution 📦 and create GitHub Release
|
||||
needs:
|
||||
- publish-to-pypi
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Download all the dists
|
||||
uses: actions/download-artifact@v4.1.8
|
||||
with:
|
||||
name: python-package-distributions
|
||||
path: dist/
|
||||
- name: Sign the dists with Sigstore
|
||||
uses: sigstore/gh-action-sigstore-python@v3.0.0
|
||||
with:
|
||||
inputs: >-
|
||||
./dist/*.tar.gz
|
||||
./dist/*.whl
|
||||
- name: Create GitHub Release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: >-
|
||||
gh release create
|
||||
"$GITHUB_REF_NAME"
|
||||
--repo "$GITHUB_REPOSITORY"
|
||||
--notes ""
|
||||
- name: Upload artifact signatures to GitHub Release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: >-
|
||||
gh release upload
|
||||
"$GITHUB_REF_NAME" dist/**
|
||||
--repo "$GITHUB_REPOSITORY"
|
||||
Vendored
+9
@@ -3,3 +3,12 @@ node-config/
|
||||
files/
|
||||
.ruff_cache/
|
||||
__pycache__/
|
||||
dist/
|
||||
*.egg-info/
|
||||
.ruff_cache/
|
||||
.venv/
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
ARG PYTHON_VERSION=3.13
|
||||
FROM python:${PYTHON_VERSION}-alpine
|
||||
|
||||
LABEL org.opencontainers.image.source="https://github.com/Sudo-Ivan/rns-page-node"
|
||||
LABEL org.opencontainers.image.description="A simple way to serve pages and files over the Reticulum network."
|
||||
LABEL org.opencontainers.image.licenses="GPL-3.0"
|
||||
LABEL org.opencontainers.image.authors="Sudo-Ivan"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add --no-cache gcc python3-dev musl-dev linux-headers
|
||||
|
||||
RUN pip install poetry
|
||||
ENV POETRY_VIRTUALENVS_IN_PROJECT=true
|
||||
|
||||
COPY pyproject.toml poetry.lock* ./
|
||||
COPY README.md ./
|
||||
COPY rns_page_node ./rns_page_node
|
||||
|
||||
RUN poetry install --no-interaction --no-ansi
|
||||
|
||||
ENV PATH="/app/.venv/bin:$PATH"
|
||||
|
||||
ENTRYPOINT ["poetry", "run", "rns-page-node"]
|
||||
@@ -1,18 +0,0 @@
|
||||
FROM python:3.13-alpine AS builder
|
||||
|
||||
RUN apk update
|
||||
RUN apk add --no-cache build-base libffi-dev cargo pkgconfig gcc python3-dev musl-dev linux-headers
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
RUN pip install poetry
|
||||
|
||||
COPY pyproject.toml ./
|
||||
COPY README.md ./
|
||||
COPY rns_page_node ./rns_page_node
|
||||
|
||||
RUN poetry build --format wheel
|
||||
|
||||
FROM scratch AS dist
|
||||
|
||||
COPY --from=builder /src/dist .
|
||||
@@ -1,28 +0,0 @@
|
||||
ARG PYTHON_VERSION=3.13
|
||||
FROM python:${PYTHON_VERSION}-alpine
|
||||
|
||||
LABEL org.opencontainers.image.source="https://github.com/Sudo-Ivan/rns-page-node"
|
||||
LABEL org.opencontainers.image.description="A simple way to serve pages and files over the Reticulum network."
|
||||
LABEL org.opencontainers.image.licenses="GPL-3.0"
|
||||
LABEL org.opencontainers.image.authors="Sudo-Ivan"
|
||||
|
||||
RUN addgroup -g 1000 app && adduser -D -u 1000 -G app app
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add --no-cache gcc python3-dev musl-dev linux-headers
|
||||
|
||||
RUN pip install poetry
|
||||
ENV POETRY_VIRTUALENVS_IN_PROJECT=true
|
||||
|
||||
COPY pyproject.toml poetry.lock* ./
|
||||
COPY README.md ./
|
||||
COPY rns_page_node ./rns_page_node
|
||||
|
||||
RUN poetry install --no-interaction --no-ansi
|
||||
|
||||
ENV PATH="/app/.venv/bin:$PATH"
|
||||
|
||||
USER app
|
||||
|
||||
ENTRYPOINT ["poetry", "run", "rns-page-node"]
|
||||
@@ -1,20 +1,31 @@
|
||||
# Makefile for rns-page-node
|
||||
|
||||
# Extract version from pyproject.toml
|
||||
VERSION := $(shell grep "^version =" pyproject.toml | cut -d '"' -f 2)
|
||||
VCS_REF := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
||||
BUILD_DATE := $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
# Detect if docker buildx is available
|
||||
DOCKER_BUILD := $(shell docker buildx version >/dev/null 2>&1 && echo "docker buildx build" || echo "docker build")
|
||||
DOCKER_BUILD_LOAD := $(shell docker buildx version >/dev/null 2>&1 && echo "docker buildx build --load" || echo "docker build")
|
||||
|
||||
.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
|
||||
# Build arguments for Docker
|
||||
DOCKER_BUILD_ARGS := --build-arg VERSION=$(VERSION) \
|
||||
--build-arg VCS_REF=$(VCS_REF) \
|
||||
--build-arg BUILD_DATE=$(BUILD_DATE)
|
||||
|
||||
.PHONY: all build sdist wheel clean install lint format docker-wheels docker-build docker-run help test docker-test test-advanced
|
||||
|
||||
all: build
|
||||
|
||||
build: clean
|
||||
python3 setup.py sdist bdist_wheel
|
||||
poetry run python3 -m build
|
||||
|
||||
sdist:
|
||||
python3 setup.py sdist
|
||||
poetry run python3 -m build --sdist
|
||||
|
||||
wheel:
|
||||
python3 setup.py bdist_wheel
|
||||
poetry run python3 -m build --wheel
|
||||
|
||||
clean:
|
||||
rm -rf build dist *.egg-info
|
||||
@@ -29,35 +40,21 @@ format:
|
||||
ruff check --fix .
|
||||
|
||||
docker-wheels:
|
||||
$(DOCKER_BUILD) --target builder -f Dockerfile.build -t rns-page-node-builder .
|
||||
$(DOCKER_BUILD) --target builder -f docker/Dockerfile -t rns-page-node-builder .
|
||||
docker create --name builder-container rns-page-node-builder true
|
||||
docker cp builder-container:/src/dist ./dist
|
||||
docker cp builder-container:/app/dist ./dist
|
||||
docker rm builder-container
|
||||
|
||||
docker-build:
|
||||
$(DOCKER_BUILD) $(BUILD_ARGS) -f Dockerfile -t rns-page-node:latest .
|
||||
$(DOCKER_BUILD_LOAD) $(DOCKER_BUILD_ARGS) $(BUILD_ARGS) -f docker/Dockerfile -t git.quad4.io/rns-things/rns-page-node:latest -t git.quad4.io/rns-things/rns-page-node:$(VERSION) .
|
||||
|
||||
docker-run:
|
||||
docker-run: setup-dirs
|
||||
docker run --rm -it \
|
||||
-v ./pages:/app/pages \
|
||||
-v ./files:/app/files \
|
||||
-v ./node-config:/app/node-config \
|
||||
rns-page-node:latest \
|
||||
--node-name "Page Node" \
|
||||
--pages-dir /app/pages \
|
||||
--files-dir /app/files \
|
||||
--identity-dir /app/node-config \
|
||||
--announce-interval 360
|
||||
|
||||
docker-build-rootless:
|
||||
$(DOCKER_BUILD) $(BUILD_ARGS) -f Dockerfile.rootless -t rns-page-node-rootless:latest .
|
||||
|
||||
docker-run-rootless:
|
||||
docker run --rm -it \
|
||||
-v ./pages:/app/pages \
|
||||
-v ./files:/app/files \
|
||||
-v ./node-config:/app/node-config \
|
||||
rns-page-node-rootless:latest \
|
||||
-v ./reticulum-config:/home/app/.reticulum \
|
||||
git.quad4.io/rns-things/rns-page-node:latest \
|
||||
--node-name "Page Node" \
|
||||
--pages-dir /app/pages \
|
||||
--files-dir /app/files \
|
||||
@@ -67,10 +64,16 @@ docker-run-rootless:
|
||||
test:
|
||||
bash tests/run_tests.sh
|
||||
|
||||
test-advanced:
|
||||
poetry run python3 tests/test_advanced.py
|
||||
|
||||
docker-test:
|
||||
$(DOCKER_BUILD) -f tests/Dockerfile.tests -t rns-page-node-tests .
|
||||
$(DOCKER_BUILD_LOAD) -f docker/Dockerfile.tests -t rns-page-node-tests .
|
||||
docker run --rm rns-page-node-tests
|
||||
|
||||
setup-dirs:
|
||||
mkdir -p pages files node-config reticulum-config
|
||||
|
||||
help:
|
||||
@echo "Makefile commands:"
|
||||
@echo " all - alias for build"
|
||||
@@ -82,9 +85,8 @@ help:
|
||||
@echo " lint - run ruff linter"
|
||||
@echo " format - run ruff --fix"
|
||||
@echo " docker-wheels - build Python wheels in Docker"
|
||||
@echo " docker-build - build runtime Docker image"
|
||||
@echo " docker-build - build runtime Docker image (version: $(VERSION))"
|
||||
@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 " test - run local integration tests"
|
||||
@echo " docker-test - build and run integration tests in Docker"
|
||||
@echo " test-advanced - run advanced tests (smoke, performance, leak, etc)"
|
||||
@@ -1,39 +1,76 @@
|
||||
# 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/)
|
||||
[Русский](docs/languages/README.ru.md) | [中文](docs/languages/README.zh.md) | [日本語](docs/languages/README.ja.md) | [Italiano](docs/languages/README.it.md) | [Deutsch](docs/languages/README.de.md)
|
||||
|
||||
A simple way to serve pages and files over the [Reticulum network](https://reticulum.network/). Drop-in replacement for [NomadNet](https://github.com/markqvist/NomadNet) nodes that primarily serve pages and files.
|
||||
|
||||
## Features
|
||||
|
||||
- Serves pages and files over RNS
|
||||
- Dynamic page support with environment variables
|
||||
- Form data and request parameter parsing
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Pip
|
||||
pipx install git+https://git.quad4.io/RNS-Things/rns-page-node.git
|
||||
# Pipx via Git
|
||||
pipx install git+https://git.quad4.io/RNS-Things/rns-page-node.git
|
||||
# UV
|
||||
uv venv
|
||||
source .venv/bin/activate
|
||||
uv pip install git+https://git.quad4.io/RNS-Things/rns-page-node.git
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
pip install rns-page-node
|
||||
```
|
||||
|
||||
```bash
|
||||
# will use current directory for pages and files
|
||||
rns-page-node
|
||||
```
|
||||
|
||||
## Usage
|
||||
or with command-line options:
|
||||
|
||||
```bash
|
||||
rns-page-node --node-name "Page Node" --pages-dir ./pages --files-dir ./files --identity-dir ./node-config --announce-interval 360
|
||||
```
|
||||
|
||||
or with a config file:
|
||||
|
||||
```bash
|
||||
rns-page-node /path/to/config.conf
|
||||
```
|
||||
|
||||
### Configuration File
|
||||
|
||||
You can use a configuration file to persist settings. See `config.example` for an example.
|
||||
|
||||
Config file format is simple `key=value` pairs:
|
||||
|
||||
```
|
||||
# Comment lines start with #
|
||||
node-name=My Page Node
|
||||
pages-dir=./pages
|
||||
files-dir=./files
|
||||
identity-dir=./node-config
|
||||
announce-interval=360
|
||||
```
|
||||
|
||||
Priority order: Command-line arguments > Config file > Defaults
|
||||
|
||||
### Docker/Podman
|
||||
|
||||
```bash
|
||||
docker run -it --rm -v ./pages:/app/pages -v ./files:/app/files -v ./node-config:/app/node-config -v ./config:/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 ./reticulum-config:/home/app/.reticulum git.quad4.io/rns-things/rns-page-node:latest
|
||||
```
|
||||
|
||||
### Docker/Podman Rootless
|
||||
|
||||
```bash
|
||||
mkdir -p ./pages ./files ./node-config ./config
|
||||
chown -R 1000:1000 ./pages ./files ./node-config ./config
|
||||
podman run -it --rm -v ./pages:/app/pages -v ./files:/app/files -v ./node-config:/app/node-config -v ./config:/app/config ghcr.io/sudo-ivan/rns-page-node:latest-rootless
|
||||
mkdir -p ./pages ./files ./node-config ./reticulum-config
|
||||
chown -R 1000:1000 ./pages ./files ./node-config ./reticulum-config
|
||||
podman run -it --rm -v ./pages:/app/pages -v ./files:/app/files -v ./node-config:/app/node-config -v ./reticulum-config:/home/app/.reticulum git.quad4.io/rns-things/rns-page-node:latest
|
||||
```
|
||||
|
||||
Mounting volumes are optional, you can also copy pages and files to the container `podman cp` or `docker cp`.
|
||||
@@ -58,20 +95,30 @@ make docker-wheels
|
||||
|
||||
## Pages
|
||||
|
||||
Supports Micron `.mu` and dynamic pages with `#!` in the micron files.
|
||||
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
|
||||
|
||||
```
|
||||
-c, --config: The path to the Reticulum config file.
|
||||
-n, --node-name: The name of the node.
|
||||
-p, --pages-dir: The directory to serve pages from.
|
||||
-f, --files-dir: The directory to serve files from.
|
||||
-i, --identity-dir: The directory to persist the node's identity.
|
||||
-a, --announce-interval: The interval to announce the node's presence.
|
||||
-r, --page-refresh-interval: The interval to refresh pages.
|
||||
-f, --file-refresh-interval: The interval to refresh files.
|
||||
-l, --log-level: The logging level.
|
||||
Positional arguments:
|
||||
node_config Path to rns-page-node config file
|
||||
|
||||
Optional arguments:
|
||||
-c, --config Path to the Reticulum config file
|
||||
-n, --node-name Name of the node
|
||||
-p, --pages-dir Directory to serve pages from
|
||||
-f, --files-dir Directory to serve files from
|
||||
-i, --identity-dir Directory to persist the node's identity
|
||||
-a, --announce-interval Interval to announce the node's presence (in minutes, default: 360 = 6 hours)
|
||||
--page-refresh-interval Interval to refresh pages (in seconds, 0 = disabled)
|
||||
--file-refresh-interval Interval to refresh files (in seconds, 0 = disabled)
|
||||
-l, --log-level Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
version: '3'
|
||||
|
||||
vars:
|
||||
VERSION:
|
||||
sh: grep "^version =" pyproject.toml | cut -d '"' -f 2
|
||||
VCS_REF:
|
||||
sh: git rev-parse --short HEAD 2>/dev/null || echo "unknown"
|
||||
BUILD_DATE:
|
||||
sh: date -u +"%Y-%m-%dT%H:%M:%SZ"
|
||||
DOCKER_BUILD:
|
||||
sh: docker buildx version >/dev/null 2>&1 && echo "docker buildx build" || echo "docker build"
|
||||
DOCKER_BUILD_LOAD:
|
||||
sh: docker buildx version >/dev/null 2>&1 && echo "docker buildx build --load" || echo "docker build"
|
||||
IMAGE_NAME: git.quad4.io/rns-things/rns-page-node
|
||||
|
||||
tasks:
|
||||
default:
|
||||
desc: Show available tasks
|
||||
cmds:
|
||||
- task --list
|
||||
|
||||
build:
|
||||
desc: Clean and build sdist and wheel
|
||||
deps: [clean]
|
||||
cmds:
|
||||
- poetry run python3 -m build
|
||||
|
||||
sdist:
|
||||
desc: Build source distribution
|
||||
cmds:
|
||||
- poetry run python3 -m build --sdist
|
||||
|
||||
wheel:
|
||||
desc: Build wheel
|
||||
cmds:
|
||||
- poetry run python3 -m build --wheel
|
||||
|
||||
clean:
|
||||
desc: Remove build artifacts
|
||||
cmds:
|
||||
- rm -rf build dist *.egg-info
|
||||
|
||||
install:
|
||||
desc: Install built wheel
|
||||
deps: [build]
|
||||
cmds:
|
||||
- pip install dist/*.whl
|
||||
|
||||
install-dev:
|
||||
desc: Install package in development mode
|
||||
cmds:
|
||||
- pip install -e .
|
||||
|
||||
lint:
|
||||
desc: Run ruff linter
|
||||
cmds:
|
||||
- ruff check .
|
||||
|
||||
format:
|
||||
desc: Run ruff formatter with auto-fix
|
||||
cmds:
|
||||
- ruff check --fix .
|
||||
|
||||
check:
|
||||
desc: Run all code quality checks
|
||||
deps: [lint]
|
||||
|
||||
test:
|
||||
desc: Run local integration tests
|
||||
cmds:
|
||||
- bash tests/run_tests.sh
|
||||
|
||||
test-advanced:
|
||||
desc: Run advanced tests (smoke, performance, leak, fuzzing, property-based)
|
||||
cmds:
|
||||
- poetry run python3 tests/test_advanced.py
|
||||
|
||||
docker-wheels:
|
||||
desc: Build Python wheels in Docker
|
||||
cmds:
|
||||
- '{{.DOCKER_BUILD}} --target builder -f docker/Dockerfile -t rns-page-node-builder .'
|
||||
- docker create --name builder-container rns-page-node-builder true
|
||||
- docker cp builder-container:/app/dist ./dist
|
||||
- docker rm builder-container
|
||||
|
||||
docker-build:
|
||||
desc: Build runtime Docker image
|
||||
cmds:
|
||||
- >
|
||||
{{.DOCKER_BUILD_LOAD}}
|
||||
--build-arg VERSION={{.VERSION}}
|
||||
--build-arg VCS_REF={{.VCS_REF}}
|
||||
--build-arg BUILD_DATE={{.BUILD_DATE}}
|
||||
-f docker/Dockerfile
|
||||
-t {{.IMAGE_NAME}}:latest
|
||||
-t {{.IMAGE_NAME}}:{{.VERSION}}
|
||||
.
|
||||
|
||||
docker-run:
|
||||
desc: Run runtime Docker image
|
||||
deps: [setup-dirs]
|
||||
cmds:
|
||||
- >
|
||||
docker run --rm -it
|
||||
-v ./pages:/app/pages
|
||||
-v ./files:/app/files
|
||||
-v ./node-config:/app/node-config
|
||||
-v ./reticulum-config:/home/app/.reticulum
|
||||
{{.IMAGE_NAME}}:latest
|
||||
--node-name "Page Node"
|
||||
--pages-dir /app/pages
|
||||
--files-dir /app/files
|
||||
--identity-dir /app/node-config
|
||||
--announce-interval 360
|
||||
|
||||
docker-test:
|
||||
desc: Build and run integration tests in Docker
|
||||
cmds:
|
||||
- '{{.DOCKER_BUILD_LOAD}} -f docker/Dockerfile.tests -t rns-page-node-tests .'
|
||||
- docker run --rm rns-page-node-tests
|
||||
|
||||
docker-clean:
|
||||
desc: Remove Docker images and containers
|
||||
cmds:
|
||||
- docker rmi {{.IMAGE_NAME}}:latest {{.IMAGE_NAME}}:{{.VERSION}} 2>/dev/null || true
|
||||
- docker rmi rns-page-node-builder rns-page-node-tests 2>/dev/null || true
|
||||
|
||||
run:
|
||||
desc: Run rns-page-node locally
|
||||
cmds:
|
||||
- poetry run python3 -m rns_page_node.main
|
||||
|
||||
run-dev:
|
||||
desc: Run rns-page-node with development settings
|
||||
cmds:
|
||||
- poetry run python3 -m rns_page_node.main --log-level DEBUG
|
||||
|
||||
venv:
|
||||
desc: Create Python virtual environment
|
||||
cmds:
|
||||
- python3 -m venv .venv
|
||||
- 'echo "Virtual environment created. Activate with: source .venv/bin/activate"'
|
||||
|
||||
deps-install:
|
||||
desc: Install dependencies using pip
|
||||
cmds:
|
||||
- pip install -r requirements.txt || pip install -e .
|
||||
|
||||
deps-update:
|
||||
desc: Update dependencies
|
||||
cmds:
|
||||
- pip install --upgrade -r requirements.txt || pip install --upgrade -e .
|
||||
|
||||
version:
|
||||
desc: Show current version
|
||||
cmds:
|
||||
- 'echo "Version: {{.VERSION}}"'
|
||||
- 'echo "VCS Ref: {{.VCS_REF}}"'
|
||||
- 'echo "Build Date: {{.BUILD_DATE}}"'
|
||||
|
||||
setup-dirs:
|
||||
desc: Create required directories for running the node
|
||||
cmds:
|
||||
- mkdir -p pages files node-config reticulum-config
|
||||
|
||||
nix-shell:
|
||||
desc: Enter Nix development shell
|
||||
cmds:
|
||||
- nix develop
|
||||
|
||||
nix-build:
|
||||
desc: Build with Nix
|
||||
cmds:
|
||||
- nix build
|
||||
|
||||
all:
|
||||
desc: Run build, lint, and test
|
||||
deps: [build, check, test]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
ARG PYTHON_VERSION=3.13
|
||||
FROM python:${PYTHON_VERSION}-alpine AS builder
|
||||
|
||||
RUN apk add --no-cache \
|
||||
gcc \
|
||||
musl-dev \
|
||||
libffi-dev \
|
||||
cargo \
|
||||
pkgconfig \
|
||||
python3-dev \
|
||||
linux-headers
|
||||
|
||||
RUN pip install --no-cache-dir poetry
|
||||
|
||||
WORKDIR /app
|
||||
COPY pyproject.toml poetry.lock* README.md ./
|
||||
COPY rns_page_node ./rns_page_node
|
||||
|
||||
RUN poetry config virtualenvs.in-project true && \
|
||||
poetry install --no-interaction --no-ansi --only main && \
|
||||
poetry build --format wheel && \
|
||||
.venv/bin/pip install dist/*.whl
|
||||
|
||||
FROM python:${PYTHON_VERSION}-alpine
|
||||
|
||||
ARG VERSION
|
||||
ARG VCS_REF
|
||||
ARG BUILD_DATE
|
||||
|
||||
LABEL org.opencontainers.image.created=$BUILD_DATE \
|
||||
org.opencontainers.image.title="RNS Page Node" \
|
||||
org.opencontainers.image.description="A simple way to serve pages and files over the Reticulum network." \
|
||||
org.opencontainers.image.url="https://git.quad4.io/RNS-Things/rns-page-node" \
|
||||
org.opencontainers.image.documentation="https://git.quad4.io/RNS-Things/rns-page-node/src/branch/main/README.md" \
|
||||
org.opencontainers.image.source="https://git.quad4.io/RNS-Things/rns-page-node" \
|
||||
org.opencontainers.image.version=$VERSION \
|
||||
org.opencontainers.image.revision=$VCS_REF \
|
||||
org.opencontainers.image.vendor="RNS-Things" \
|
||||
org.opencontainers.image.licenses="GPL-3.0" \
|
||||
org.opencontainers.image.authors="Sudo-Ivan" \
|
||||
org.opencontainers.image.base.name="python:${PYTHON_VERSION}-alpine"
|
||||
|
||||
RUN addgroup -g 1000 app && adduser -D -u 1000 -G app app && \
|
||||
apk add --no-cache su-exec
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /app/.venv /app/.venv
|
||||
ENV PATH="/app/.venv/bin:$PATH"
|
||||
|
||||
RUN mkdir -p pages files node-config && \
|
||||
chown -R app:app /app && \
|
||||
mkdir -p /home/app/.reticulum && \
|
||||
chown -R app:app /home/app/.reticulum
|
||||
|
||||
COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||
|
||||
VOLUME ["/app/pages", "/app/files", "/app/node-config", "/home/app/.reticulum"]
|
||||
|
||||
ENTRYPOINT ["entrypoint.sh"]
|
||||
CMD ["rns-page-node"]
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM python:3.10-slim
|
||||
FROM python:3.14-slim
|
||||
|
||||
RUN apt-get update && apt-get install -y build-essential libssl-dev && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Fix permissions if they are wrong (e.g. volume mounts)
|
||||
# We only do this if we are root
|
||||
if [ "$(id -u)" = '0' ]; then
|
||||
chown -R app:app /app /home/app/.reticulum
|
||||
fi
|
||||
|
||||
# If the first argument is an option (starts with a dash), prepend the app command
|
||||
if [ "${1#-}" != "$1" ]; then
|
||||
set -- rns-page-node "$@"
|
||||
fi
|
||||
|
||||
# If we are root, drop privileges and run the command
|
||||
if [ "$(id -u)" = '0' ]; then
|
||||
exec su-exec app "$@"
|
||||
else
|
||||
exec "$@"
|
||||
fi
|
||||
@@ -0,0 +1,126 @@
|
||||
# RNS Page Node
|
||||
|
||||
[English](../../README.md) | [Русский](README.ru.md) | [中文](README.zh.md) | [日本語](README.ja.md) | [Italiano](README.it.md)
|
||||
|
||||
Ein einfacher Weg, um Seiten und Dateien über das [Reticulum-Netzwerk](https://reticulum.network/) bereitzustellen. Drop-in-Ersatz für [NomadNet](https://github.com/markqvist/NomadNet)-Knoten, die hauptsächlich Seiten und Dateien bereitstellen.
|
||||
|
||||
## Funktionen
|
||||
|
||||
- Bereitstellung von Seiten und Dateien über RNS
|
||||
- Unterstützung dynamischer Seiten mit Umgebungsvariablen
|
||||
- Parsing von Formulardaten und Anfrageparametern
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Pip
|
||||
pipx install git+https://git.quad4.io/RNS-Things/rns-page-node.git
|
||||
# Pipx via Git
|
||||
pipx install git+https://git.quad4.io/RNS-Things/rns-page-node.git
|
||||
# UV
|
||||
uv venv
|
||||
source .venv/bin/activate
|
||||
uv pip install git+https://git.quad4.io/RNS-Things/rns-page-node.git
|
||||
```
|
||||
|
||||
## Verwendung
|
||||
|
||||
```bash
|
||||
# verwendet das aktuelle Verzeichnis für Seiten und Dateien
|
||||
rns-page-node
|
||||
```
|
||||
|
||||
oder mit Befehlszeilenoptionen:
|
||||
|
||||
```bash
|
||||
rns-page-node --node-name "Page Node" --pages-dir ./pages --files-dir ./files --identity-dir ./node-config --announce-interval 360
|
||||
```
|
||||
|
||||
oder mit einer Konfigurationsdatei:
|
||||
|
||||
```bash
|
||||
rns-page-node /pfad/zur/config.conf
|
||||
```
|
||||
|
||||
### Konfigurationsdatei
|
||||
|
||||
Sie können eine Konfigurationsdatei verwenden, um Einstellungen dauerhaft zu speichern. Siehe `config.example` für ein Beispiel.
|
||||
|
||||
Das Format der Konfigurationsdatei besteht aus einfachen `Schlüssel=Wert`-Paaren:
|
||||
|
||||
```
|
||||
# Kommentarzeilen beginnen mit #
|
||||
node-name=Mein Seitenknoten
|
||||
pages-dir=./pages
|
||||
files-dir=./files
|
||||
identity-dir=./node-config
|
||||
announce-interval=360
|
||||
```
|
||||
|
||||
Prioritätsreihenfolge: Befehlszeilenargumente > Konfigurationsdatei > Standardwerte
|
||||
|
||||
### Docker/Podman
|
||||
|
||||
```bash
|
||||
docker run -it --rm -v ./pages:/app/pages -v ./files:/app/files -v ./node-config:/app/node-config -v ./reticulum-config:/home/app/.reticulum git.quad4.io/rns-things/rns-page-node:latest
|
||||
```
|
||||
|
||||
### Docker/Podman Rootless (ohne Root)
|
||||
|
||||
```bash
|
||||
mkdir -p ./pages ./files ./node-config ./reticulum-config
|
||||
chown -R 1000:1000 ./pages ./files ./node-config ./reticulum-config
|
||||
podman run -it --rm -v ./pages:/app/pages -v ./files:/app/files -v ./node-config:/app/node-config -v ./reticulum-config:/home/app/.reticulum git.quad4.io/rns-things/rns-page-node:latest
|
||||
```
|
||||
|
||||
Das Einbinden von Volumes ist optional, Sie können Seiten und Dateien auch mit `podman cp` oder `docker cp` in den Container kopieren.
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
make build
|
||||
```
|
||||
|
||||
Wheels bauen:
|
||||
|
||||
```bash
|
||||
make wheel
|
||||
```
|
||||
|
||||
### Build Wheels in Docker
|
||||
|
||||
```bash
|
||||
make docker-wheels
|
||||
```
|
||||
|
||||
## Seiten
|
||||
|
||||
Unterstützt dynamische ausführbare Seiten mit vollständigem Parsing der Anfragedaten. Seiten können Folgendes empfangen:
|
||||
- Formularfelder über `field_*` Umgebungsvariablen
|
||||
- Verknüpfungsvariablen über `var_*` Umgebungsvariablen
|
||||
- Remote-Identität über die Umgebungsvariable `remote_identity`
|
||||
- Link-ID über die Umgebungsvariable `link_id`
|
||||
|
||||
Dies ermöglicht die Erstellung von Foren, Chats und anderen interaktiven Anwendungen, die mit NomadNet-Clients kompatibel sind.
|
||||
|
||||
## Optionen
|
||||
|
||||
```
|
||||
Positionsargumente:
|
||||
node_config Pfad zur rns-page-node-Konfigurationsdatei
|
||||
|
||||
Optionale Argumente:
|
||||
-c, --config Pfad zur Reticulum-Konfigurationsdatei
|
||||
-n, --node-name Name des Knotens
|
||||
-p, --pages-dir Verzeichnis, aus dem Seiten bereitgestellt werden
|
||||
-f, --files-dir Verzeichnis, aus dem Dateien bereitgestellt werden
|
||||
-i, --identity-dir Verzeichnis zum Speichern der Identität des Knotens
|
||||
-a, --announce-interval Intervall zur Bekanntgabe der Anwesenheit des Knotens (in Minuten, Standard: 360 = 6 Stunden)
|
||||
--page-refresh-interval Intervall zum Aktualisieren von Seiten (in Sekunden, 0 = deaktiviert)
|
||||
--file-refresh-interval Intervall zum Aktualisieren von Dateien (in Sekunden, 0 = deaktiviert)
|
||||
-l, --log-level Protokollierungsebene (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
||||
```
|
||||
|
||||
## Lizenz
|
||||
|
||||
Dieses Projekt enthält Teile der Codebasis von [NomadNet](https://github.com/markqvist/NomadNet), die unter der GNU General Public License v3.0 (GPL-3.0) lizenziert ist. Als abgeleitetes Werk wird dieses Projekt ebenfalls unter den Bedingungen der GPL-3.0 verbreitet. Die vollständige Lizenz finden Sie in der Datei [LICENSE](LICENSE).
|
||||
@@ -0,0 +1,126 @@
|
||||
# RNS Page Node
|
||||
|
||||
[English](../../README.md) | [Русский](README.ru.md) | [中文](README.zh.md) | [日本語](README.ja.md) | [Deutsch](README.de.md)
|
||||
|
||||
Un modo semplice per servire pagine e file sulla [rete Reticulum](https://reticulum.network/). Sostituto drop-in per i nodi [NomadNet](https://github.com/markqvist/NomadNet) che servono principalmente pagine e file.
|
||||
|
||||
## Caratteristiche
|
||||
|
||||
- Serve pagine e file su RNS
|
||||
- Supporto per pagine dinamiche con variabili d'ambiente
|
||||
- Parsing dei dati dei moduli e dei parametri di richiesta
|
||||
|
||||
## Installazione
|
||||
|
||||
```bash
|
||||
# Pip
|
||||
pipx install git+https://git.quad4.io/RNS-Things/rns-page-node.git
|
||||
# Pipx via Git
|
||||
pipx install git+https://git.quad4.io/RNS-Things/rns-page-node.git
|
||||
# UV
|
||||
uv venv
|
||||
source .venv/bin/activate
|
||||
uv pip install git+https://git.quad4.io/RNS-Things/rns-page-node.git
|
||||
```
|
||||
|
||||
## Utilizzo
|
||||
|
||||
```bash
|
||||
# userà la directory corrente per pagine e file
|
||||
rns-page-node
|
||||
```
|
||||
|
||||
o con le opzioni della riga di comando:
|
||||
|
||||
```bash
|
||||
rns-page-node --node-name "Page Node" --pages-dir ./pages --files-dir ./files --identity-dir ./node-config --announce-interval 360
|
||||
```
|
||||
|
||||
o con un file di configurazione:
|
||||
|
||||
```bash
|
||||
rns-page-node /percorso/del/config.conf
|
||||
```
|
||||
|
||||
### File di configurazione
|
||||
|
||||
È possibile utilizzare un file di configurazione per rendere persistenti le impostazioni. Vedere `config.example` per un esempio.
|
||||
|
||||
Il formato del file di configurazione consiste in semplici coppie `chiave=valore`:
|
||||
|
||||
```
|
||||
# Le righe di commento iniziano con #
|
||||
node-name=Mio Nodo Pagina
|
||||
pages-dir=./pages
|
||||
files-dir=./files
|
||||
identity-dir=./node-config
|
||||
announce-interval=360
|
||||
```
|
||||
|
||||
Ordine di priorità: Argomenti della riga di comando > File di configurazione > Predefiniti
|
||||
|
||||
### Docker/Podman
|
||||
|
||||
```bash
|
||||
docker run -it --rm -v ./pages:/app/pages -v ./files:/app/files -v ./node-config:/app/node-config -v ./reticulum-config:/home/app/.reticulum git.quad4.io/rns-things/rns-page-node:latest
|
||||
```
|
||||
|
||||
### Docker/Podman Rootless
|
||||
|
||||
```bash
|
||||
mkdir -p ./pages ./files ./node-config ./reticulum-config
|
||||
chown -R 1000:1000 ./pages ./files ./node-config ./reticulum-config
|
||||
podman run -it --rm -v ./pages:/app/pages -v ./files:/app/files -v ./node-config:/app/node-config -v ./reticulum-config:/home/app/.reticulum git.quad4.io/rns-things/rns-page-node:latest
|
||||
```
|
||||
|
||||
Il montaggio dei volumi è opzionale, è anche possibile copiare pagine e file nel container con `podman cp` o `docker cp`.
|
||||
|
||||
## Compilazione
|
||||
|
||||
```bash
|
||||
make build
|
||||
```
|
||||
|
||||
Costruire le Wheels:
|
||||
|
||||
```bash
|
||||
make wheel
|
||||
```
|
||||
|
||||
### Costruire le Wheels in Docker
|
||||
|
||||
```bash
|
||||
make docker-wheels
|
||||
```
|
||||
|
||||
## Pagine
|
||||
|
||||
Supporta pagine dinamiche eseguibili con parsing completo dei dati di richiesta. Le pagine possono ricevere:
|
||||
- Campi del modulo tramite variabili d'ambiente `field_*`
|
||||
- Variabili di collegamento tramite variabili d'ambiente `var_*`
|
||||
- Identità remota tramite la variabile d'ambiente `remote_identity`
|
||||
- ID collegamento tramite la variabile d'ambiente `link_id`
|
||||
|
||||
Ciò consente la creazione di forum, chat e altre applicazioni interattive compatibili con i client NomadNet.
|
||||
|
||||
## Opzioni
|
||||
|
||||
```
|
||||
Argomenti posizionali:
|
||||
node_config Percorso del file di configurazione di rns-page-node
|
||||
|
||||
Argomenti opzionali:
|
||||
-c, --config Percorso del file di configurazione di Reticulum
|
||||
-n, --node-name Nome del nodo
|
||||
-p, --pages-dir Directory da cui servire le pagine
|
||||
-f, --files-dir Directory da cui servire i file
|
||||
-i, --identity-dir Directory per rendere persistente l'identità del nodo
|
||||
-a, --announce-interval Intervallo per annunciare la presenza del nodo (in minuti, predefinito: 360 = 6 ore)
|
||||
--page-refresh-interval Intervallo per aggiornare le pagine (in secondi, 0 = disabilitato)
|
||||
--file-refresh-interval Intervallo per aggiornare i file (in secondi, 0 = disabilitato)
|
||||
-l, --log-level Livello di logging (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
||||
```
|
||||
|
||||
## Licenza
|
||||
|
||||
Questo progetto incorpora parti della base di codice di [NomadNet](https://github.com/markqvist/NomadNet), che è concesso in licenza con la GNU General Public License v3.0 (GPL-3.0). Come opera derivata, questo progetto è distribuito anche secondo i termini della licenza GPL-3.0. Vedere il file [LICENSE](LICENSE) per la licenza completa.
|
||||
@@ -0,0 +1,126 @@
|
||||
# RNS Page Node
|
||||
|
||||
[English](../../README.md) | [Русский](README.ru.md) | [中文](README.zh.md) | [Italiano](README.it.md) | [Deutsch](README.de.md)
|
||||
|
||||
[Reticulum ネットワーク](https://reticulum.network/)を介してページやファイルを提供するためのシンプルな方法です。主にページやファイルを提供する [NomadNet](https://github.com/markqvist/NomadNet) ノードのドロップイン代替品です。
|
||||
|
||||
## 特徴
|
||||
|
||||
- RNS を介したページおよびファイルの提供
|
||||
- 環境変数による動的ページのサポート
|
||||
- フォームデータとリクエストパラメータの解析
|
||||
|
||||
## インストール
|
||||
|
||||
```bash
|
||||
# Pip
|
||||
pipx install git+https://git.quad4.io/RNS-Things/rns-page-node.git
|
||||
# Pipx via Git
|
||||
pipx install git+https://git.quad4.io/RNS-Things/rns-page-node.git
|
||||
# UV
|
||||
uv venv
|
||||
source .venv/bin/activate
|
||||
uv pip install git+https://git.quad4.io/RNS-Things/rns-page-node.git
|
||||
```
|
||||
|
||||
## 使用法
|
||||
|
||||
```bash
|
||||
# カレントディレクトリをページとファイルのソースとして使用します
|
||||
rns-page-node
|
||||
```
|
||||
|
||||
または、コマンドラインオプションを使用します:
|
||||
|
||||
```bash
|
||||
rns-page-node --node-name "Page Node" --pages-dir ./pages --files-dir ./files --identity-dir ./node-config --announce-interval 360
|
||||
```
|
||||
|
||||
または、設定ファイルを使用します:
|
||||
|
||||
```bash
|
||||
rns-page-node /path/to/config.conf
|
||||
```
|
||||
|
||||
### 設定ファイル
|
||||
|
||||
設定を永続化するために設定ファイルを使用できます。例については `config.example` を参照してください。
|
||||
|
||||
設定ファイルの形式は単純な `key=value` のペアです:
|
||||
|
||||
```
|
||||
# # で始まる行はコメントです
|
||||
node-name=My Page Node
|
||||
pages-dir=./pages
|
||||
files-dir=./files
|
||||
identity-dir=./node-config
|
||||
announce-interval=360
|
||||
```
|
||||
|
||||
優先順位:コマンドライン引数 > 設定ファイル > デフォルト
|
||||
|
||||
### Docker/Podman
|
||||
|
||||
```bash
|
||||
docker run -it --rm -v ./pages:/app/pages -v ./files:/app/files -v ./node-config:/app/node-config -v ./reticulum-config:/home/app/.reticulum git.quad4.io/rns-things/rns-page-node:latest
|
||||
```
|
||||
|
||||
### Docker/Podman ルートレス (Rootless)
|
||||
|
||||
```bash
|
||||
mkdir -p ./pages ./files ./node-config ./reticulum-config
|
||||
chown -R 1000:1000 ./pages ./files ./node-config ./reticulum-config
|
||||
podman run -it --rm -v ./pages:/app/pages -v ./files:/app/files -v ./node-config:/app/node-config -v ./reticulum-config:/home/app/.reticulum git.quad4.io/rns-things/rns-page-node:latest
|
||||
```
|
||||
|
||||
ボリュームのマウントはオプションです。`podman cp` または `docker cp` を使用してページやファイルをコンテナにコピーすることもできます。
|
||||
|
||||
## ビルド
|
||||
|
||||
```bash
|
||||
make build
|
||||
```
|
||||
|
||||
Wheels のビルド:
|
||||
|
||||
```bash
|
||||
make wheel
|
||||
```
|
||||
|
||||
### Docker での Wheels のビルド
|
||||
|
||||
```bash
|
||||
make docker-wheels
|
||||
```
|
||||
|
||||
## ページ
|
||||
|
||||
完全なリクエストデータ解析を備えた動的実行可能ページをサポートします。ページは以下を受け取ることができます:
|
||||
- `field_*` 環境変数を介したフォームフィールド
|
||||
- `var_*` 環境変数を介したリンク変数
|
||||
- `remote_identity` 環境変数を介したリモート ID
|
||||
- `link_id` 環境変数を介したリンク ID
|
||||
|
||||
これにより、NomadNet クライアントと互換性のあるフォーラム、チャット、その他のインタラクティブなアプリケーションの作成が可能になります。
|
||||
|
||||
## オプション
|
||||
|
||||
```
|
||||
位置引数:
|
||||
node_config rns-page-node 設定ファイルのパス
|
||||
|
||||
オプション引数:
|
||||
-c, --config Reticulum 設定ファイルのパス
|
||||
-n, --node-name ノードの名前
|
||||
-p, --pages-dir ページを提供するディレクトリ
|
||||
-f, --files-dir ファイルを提供するディレクトリ
|
||||
-i, --identity-dir ノードの ID を永続化するディレクトリ
|
||||
-a, --announce-interval ノードの存在をアナウンスする間隔(分単位、デフォルト:360 = 6 時間)
|
||||
--page-refresh-interval ページを更新する間隔(秒単位、0 = 無効)
|
||||
--file-refresh-interval ファイルを更新する間隔(秒単位、0 = 無効)
|
||||
-l, --log-level ログレベル (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
||||
```
|
||||
|
||||
## ライセンス
|
||||
|
||||
このプロジェクトには、GNU General Public License v3.0 (GPL-3.0) の下でライセンスされている [NomadNet](https://github.com/markqvist/NomadNet) コードベースの一部が組み込まれています。派生作品として、このプロジェクトも GPL-3.0 の条項に基づいて配布されます。完全なライセンスについては、[LICENSE](LICENSE) ファイルを参照してください。
|
||||
@@ -0,0 +1,118 @@
|
||||
# RNS Page Node
|
||||
|
||||
[English](../../README.md) | [中文](README.zh.md) | [日本語](README.ja.md) | [Italiano](README.it.md) | [Deutsch](README.de.md)
|
||||
|
||||
Простой способ для раздачи страниц и файлов через сеть [Reticulum](https://reticulum.network/). Прямая замена для узлов [NomadNet](https://github.com/markqvist/NomadNet), которые в основном служат для раздачи страниц и файлов.
|
||||
|
||||
## Особенности
|
||||
|
||||
- Раздача страниц и файлов через RNS
|
||||
- Поддержка динамических страниц с переменными окружения
|
||||
- Разбор данных форм и параметров запросов
|
||||
|
||||
## Установка
|
||||
|
||||
```bash
|
||||
# Pip
|
||||
pipx install git+https://git.quad4.io/RNS-Things/rns-page-node.git
|
||||
# Pipx через Git
|
||||
pipx install git+https://git.quad4.io/RNS-Things/rns-page-node.git
|
||||
# uv
|
||||
uv venv
|
||||
source .venv/bin/activate
|
||||
uv pip install git+https://git.quad4.io/RNS-Things/rns-page-node.git
|
||||
```
|
||||
|
||||
## Использование
|
||||
```bash
|
||||
# будет использовать текущий каталог для страниц и файлов
|
||||
rns-page-node
|
||||
```
|
||||
|
||||
или с параметрами командной строки:
|
||||
```bash
|
||||
rns-page-node --node-name "Page Node" --pages-dir ./pages --files-dir ./files --identity-dir ./node-config --announce-interval 360
|
||||
```
|
||||
|
||||
или с файлом конфигурации:
|
||||
```bash
|
||||
rns-page-node /путь/к/config.conf
|
||||
```
|
||||
|
||||
### Файл Конфигурации
|
||||
|
||||
Вы можете использовать файл конфигурации для сохранения настроек. См. `config.example` для примера.
|
||||
|
||||
Формат файла конфигурации - простые пары `ключ=значение`:
|
||||
|
||||
```
|
||||
# Строки комментариев начинаются с #
|
||||
node-name=Мой Page Node
|
||||
pages-dir=./pages
|
||||
files-dir=./files
|
||||
identity-dir=./node-config
|
||||
announce-interval=360
|
||||
```
|
||||
|
||||
Порядок приоритета: Аргументы командной строки > Файл конфигурации > Значения по умолчанию
|
||||
|
||||
### Docker/Podman
|
||||
```bash
|
||||
docker run -it --rm -v ./pages:/app/pages -v ./files:/app/files -v ./node-config:/app/node-config -v ./reticulum-config:/home/app/.reticulum git.quad4.io/rns-things/rns-page-node:latest
|
||||
```
|
||||
|
||||
### Docker/Podman без root-доступа
|
||||
```bash
|
||||
mkdir -p ./pages ./files ./node-config ./reticulum-config
|
||||
chown -R 1000:1000 ./pages ./files ./node-config ./reticulum-config
|
||||
podman run -it --rm -v ./pages:/app/pages -v ./files:/app/files -v ./node-config:/app/node-config -v ./reticulum-config:/home/app/.reticulum git.quad4.io/rns-things/rns-page-node:latest
|
||||
```
|
||||
|
||||
Монтирование томов необязательно, вы также можете скопировать страницы и файлы в контейнер с помощью `podman cp` или `docker cp`.
|
||||
|
||||
## Сборка
|
||||
```bash
|
||||
make build
|
||||
```
|
||||
|
||||
Сборка wheels:
|
||||
```bash
|
||||
make wheel
|
||||
```
|
||||
|
||||
### Сборка Wheels в Docker
|
||||
```bash
|
||||
make docker-wheels
|
||||
```
|
||||
|
||||
## Страницы
|
||||
|
||||
Поддержка динамических исполняемых страниц с полным разбором данных запросов. Страницы могут получать:
|
||||
- Поля форм через переменные окружения `field_*`
|
||||
- Переменные ссылок через переменные окружения `var_*`
|
||||
- Удаленную идентификацию через переменную окружения `remote_identity`
|
||||
- ID соединения через переменную окружения `link_id`
|
||||
|
||||
Это позволяет создавать форумы, чаты и другие интерактивные приложения, совместимые с клиентами NomadNet.
|
||||
|
||||
## Параметры
|
||||
|
||||
```
|
||||
Позиционные аргументы:
|
||||
node_config Путь к файлу конфигурации rns-page-node
|
||||
|
||||
Необязательные аргументы:
|
||||
-c, --config Путь к файлу конфигурации Reticulum
|
||||
-n, --node-name Имя узла
|
||||
-p, --pages-dir Каталог для раздачи страниц
|
||||
-f, --files-dir Каталог для раздачи файлов
|
||||
-i, --identity-dir Каталог для сохранения идентификационных данных узла
|
||||
-a, --announce-interval Интервал анонсирования присутствия узла (в минутах, по умолчанию: 360 = 6 часов)
|
||||
--page-refresh-interval Интервал обновления страниц (в секундах, 0 = отключено)
|
||||
--file-refresh-interval Интервал обновления файлов (в секундах, 0 = отключено)
|
||||
-l, --log-level Уровень логирования (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
||||
```
|
||||
|
||||
## Лицензия
|
||||
|
||||
Этот проект включает части кодовой базы [NomadNet](https://github.com/markqvist/NomadNet), которая лицензирована под GNU General Public License v3.0 (GPL-3.0). Как производная работа, этот проект также распространяется на условиях GPL-3.0. Полный текст лицензии смотрите в файле [LICENSE](LICENSE).
|
||||
@@ -0,0 +1,126 @@
|
||||
# RNS Page Node
|
||||
|
||||
[English](../../README.md) | [Русский](README.ru.md) | [日本語](README.ja.md) | [Italiano](README.it.md) | [Deutsch](README.de.md)
|
||||
|
||||
一种通过 [Reticulum 网络](https://reticulum.network/) 提供页面和文件的简单方法。主要用于提供页面和文件的 [NomadNet](https://github.com/markqvist/NomadNet) 节点的掉入式替代方案。
|
||||
|
||||
## 特性
|
||||
|
||||
- 通过 RNS 提供页面和文件
|
||||
- 支持带有环境变量的动态页面
|
||||
- 表单数据和请求参数解析
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
# Pip
|
||||
pipx install git+https://git.quad4.io/RNS-Things/rns-page-node.git
|
||||
# Pipx via Git
|
||||
pipx install git+https://git.quad4.io/RNS-Things/rns-page-node.git
|
||||
# UV
|
||||
uv venv
|
||||
source .venv/bin/activate
|
||||
uv pip install git+https://git.quad4.io/RNS-Things/rns-page-node.git
|
||||
```
|
||||
|
||||
## 使用
|
||||
|
||||
```bash
|
||||
# 将使用当前目录作为页面和文件的来源
|
||||
rns-page-node
|
||||
```
|
||||
|
||||
或者使用命令行选项:
|
||||
|
||||
```bash
|
||||
rns-page-node --node-name "Page Node" --pages-dir ./pages --files-dir ./files --identity-dir ./node-config --announce-interval 360
|
||||
```
|
||||
|
||||
或者使用配置文件:
|
||||
|
||||
```bash
|
||||
rns-page-node /path/to/config.conf
|
||||
```
|
||||
|
||||
### 配置文件
|
||||
|
||||
您可以使用配置文件来持久化设置。请参阅 `config.example` 获取示例。
|
||||
|
||||
配置文件格式为简单的 `key=value` 键值对:
|
||||
|
||||
```
|
||||
# 以 # 开头的行为注释
|
||||
node-name=我的页面节点
|
||||
pages-dir=./pages
|
||||
files-dir=./files
|
||||
identity-dir=./node-config
|
||||
announce-interval=360
|
||||
```
|
||||
|
||||
优先级:命令行参数 > 配置文件 > 默认值
|
||||
|
||||
### Docker/Podman
|
||||
|
||||
```bash
|
||||
docker run -it --rm -v ./pages:/app/pages -v ./files:/app/files -v ./node-config:/app/node-config -v ./reticulum-config:/home/app/.reticulum git.quad4.io/rns-things/rns-page-node:latest
|
||||
```
|
||||
|
||||
### Docker/Podman 无根模式 (Rootless)
|
||||
|
||||
```bash
|
||||
mkdir -p ./pages ./files ./node-config ./reticulum-config
|
||||
chown -R 1000:1000 ./pages ./files ./node-config ./reticulum-config
|
||||
podman run -it --rm -v ./pages:/app/pages -v ./files:/app/files -v ./node-config:/app/node-config -v ./reticulum-config:/home/app/.reticulum git.quad4.io/rns-things/rns-page-node:latest
|
||||
```
|
||||
|
||||
挂载卷是可选的,您也可以使用 `podman cp` 或 `docker cp` 将页面和文件复制到容器中。
|
||||
|
||||
## 编译
|
||||
|
||||
```bash
|
||||
make build
|
||||
```
|
||||
|
||||
编译 Wheels:
|
||||
|
||||
```bash
|
||||
make wheel
|
||||
```
|
||||
|
||||
### 在 Docker 中编译 Wheels
|
||||
|
||||
```bash
|
||||
make docker-wheels
|
||||
```
|
||||
|
||||
## 页面
|
||||
|
||||
支持具有完整请求数据解析的动态可执行页面。页面可以接收:
|
||||
- 通过 `field_*` 环境变量接收表单字段
|
||||
- 通过 `var_*` 环境变量接收链接变量
|
||||
- 通过 `remote_identity` 环境变量接收远程身份
|
||||
- 通过 `link_id` 环境变量接收链接 ID
|
||||
|
||||
这使得创建与 NomadNet 客户端兼容的论坛、聊天和其他交互式应用程序成为可能。
|
||||
|
||||
## 选项
|
||||
|
||||
```
|
||||
位置参数:
|
||||
node_config rns-page-node 配置文件的路径
|
||||
|
||||
可选参数:
|
||||
-c, --config Reticulum 配置文件的路径
|
||||
-n, --node-name 节点名称
|
||||
-p, --pages-dir 提供页面的目录
|
||||
-f, --files-dir 提供文件的目录
|
||||
-i, --identity-dir 持久化节点身份的目录
|
||||
-a, --announce-interval 宣告节点存在的间隔(以分钟为单位,默认值:360 = 6 小时)
|
||||
--page-refresh-interval 刷新页面的间隔(以秒为单位,0 = 禁用)
|
||||
--file-refresh-interval 刷新文件的间隔(以秒为单位,0 = 禁用)
|
||||
-l, --log-level 日志级别 (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
||||
```
|
||||
|
||||
## 许可证
|
||||
|
||||
本工程集成了 [NomadNet](https://github.com/markqvist/NomadNet) 代码库的部分内容,该代码库采用 GNU General Public License v3.0 (GPL-3.0) 许可。作为衍生作品,本工程也根据 GPL-3.0 的条款进行分发。有关完整许可证,请参阅 [LICENSE](LICENSE) 文件。
|
||||
Generated
+61
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1766902085,
|
||||
"narHash": "sha256-coBu0ONtFzlwwVBzmjacUQwj3G+lybcZ1oeNSQkgC0M=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "c0b0e0fddf73fd517c3471e546c0df87a42d53f4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
description = "A simple way to serve pages and files over the Reticulum network";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
};
|
||||
|
||||
python = pkgs.python3;
|
||||
|
||||
pythonPackages = python.pkgs;
|
||||
in
|
||||
{
|
||||
devShells.default = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
python
|
||||
poetry
|
||||
go-task
|
||||
pythonPackages.build
|
||||
pythonPackages.pip
|
||||
pythonPackages.setuptools
|
||||
pythonPackages.wheel
|
||||
pythonPackages.ruff
|
||||
];
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
Generated
+622
-1149
File diff suppressed because it is too large
Load Diff
+14
-6
@@ -1,16 +1,25 @@
|
||||
[project]
|
||||
name = "rns-page-node"
|
||||
version = "1.0.0"
|
||||
version = "1.3.1"
|
||||
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.10"
|
||||
requires-python = ">=3.9.2"
|
||||
dependencies = [
|
||||
"rns (>=1.0.0,<1.5.0)"
|
||||
"rns (>=1.1.2,<1.5.0)",
|
||||
"cryptography>=46.0.3"
|
||||
]
|
||||
classifiers = [
|
||||
"Programming Language :: Python :: 3",
|
||||
"Operating System :: OS Independent",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://git.quad4.io/RNS-Things/rns-page-node"
|
||||
Repository = "https://git.quad4.io/RNS-Things/rns-page-node"
|
||||
|
||||
[project.scripts]
|
||||
rns-page-node = "rns_page_node.main:main"
|
||||
@@ -20,6 +29,5 @@ requires = ["poetry-core>=2.0.0,<3.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
ruff = "^0.12.3"
|
||||
safety = "^3.6.0"
|
||||
|
||||
ruff = "^0.14.10"
|
||||
twine = "^6.2.0"
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
|
||||
}
|
||||
+6
-1
@@ -1 +1,6 @@
|
||||
rns=1.0.0
|
||||
cffi==2.0.0 ; python_full_version >= "3.9.2" and platform_python_implementation != "PyPy"
|
||||
cryptography==46.0.3 ; python_full_version >= "3.9.2"
|
||||
pycparser==2.23 ; platform_python_implementation != "PyPy" and implementation_name != "PyPy" and python_full_version >= "3.9.2"
|
||||
pyserial==3.5 ; python_full_version >= "3.9.2"
|
||||
rns==1.1.2 ; python_full_version >= "3.9.2"
|
||||
typing-extensions==4.15.0 ; python_full_version >= "3.9.2" and python_full_version < "3.11.0"
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
# rns_page_node package
|
||||
"""RNS Page Node package.
|
||||
|
||||
A minimal Reticulum page node that serves .mu pages and files over RNS.
|
||||
"""
|
||||
|
||||
__all__ = ["main"]
|
||||
|
||||
+371
-106
@@ -1,22 +1,21 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Minimal Reticulum Page Node
|
||||
"""Minimal Reticulum Page Node.
|
||||
|
||||
Serves .mu pages and files over RNS.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import RNS
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_INDEX = """>Default Home Page
|
||||
|
||||
This node is serving pages using page node, but the home page file (index.mu) was not found in the pages directory. Please add an index.mu file to customize the home page.
|
||||
This node is serving pages using rns-page-node, but index.mu was not found.
|
||||
Please add an index.mu file to customize the home page.
|
||||
"""
|
||||
|
||||
DEFAULT_NOTALLOWED = """>Request Not Allowed
|
||||
@@ -25,7 +24,49 @@ 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:
|
||||
"""A Reticulum page node that serves .mu pages and files over RNS."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
identity,
|
||||
@@ -36,15 +77,30 @@ class PageNode:
|
||||
page_refresh_interval=0,
|
||||
file_refresh_interval=0,
|
||||
):
|
||||
"""Initialize the PageNode.
|
||||
|
||||
Args:
|
||||
identity: RNS Identity for the node
|
||||
pagespath: Path to directory containing .mu pages
|
||||
filespath: Path to directory containing files to serve
|
||||
announce_interval: Minutes between announcements (default: 360) == 6 hours
|
||||
name: Display name for the node (optional)
|
||||
page_refresh_interval: Seconds between page rescans (0 = disabled)
|
||||
file_refresh_interval: Seconds between file rescans (0 = disabled)
|
||||
|
||||
"""
|
||||
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
|
||||
@@ -59,27 +115,36 @@ class PageNode:
|
||||
self.destination.set_link_established_callback(self.on_connect)
|
||||
|
||||
self._announce_thread = threading.Thread(
|
||||
target=self._announce_loop, daemon=True
|
||||
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):
|
||||
with self._lock:
|
||||
self.servedpages = []
|
||||
self._scan_pages(self.pagespath)
|
||||
"""Scan pages directory and register request handlers for all .mu files."""
|
||||
pages = self._scan_pages(self.pagespath)
|
||||
|
||||
if not os.path.isfile(os.path.join(self.pagespath, "index.mu")):
|
||||
with self._lock:
|
||||
self.servedpages = pages
|
||||
|
||||
pagespath = Path(self.pagespath).resolve()
|
||||
|
||||
if not (pagespath / "index.mu").is_file():
|
||||
self.destination.register_request_handler(
|
||||
"/page/index.mu",
|
||||
response_generator=self.serve_default_index,
|
||||
allow=RNS.Destination.ALLOW_ALL,
|
||||
)
|
||||
|
||||
for full_path in self.servedpages:
|
||||
rel = full_path[len(self.pagespath) :]
|
||||
request_path = f"/page{rel}"
|
||||
for full_path in pages:
|
||||
page_path = Path(full_path).resolve()
|
||||
try:
|
||||
rel = page_path.relative_to(pagespath).as_posix()
|
||||
except ValueError:
|
||||
continue
|
||||
request_path = f"/page/{rel}"
|
||||
self.destination.register_request_handler(
|
||||
request_path,
|
||||
response_generator=self.serve_page,
|
||||
@@ -87,13 +152,21 @@ class PageNode:
|
||||
)
|
||||
|
||||
def register_files(self):
|
||||
with self._lock:
|
||||
self.servedfiles = []
|
||||
self._scan_files(self.filespath)
|
||||
"""Scan files directory and register request handlers for all files."""
|
||||
files = self._scan_files(self.filespath)
|
||||
|
||||
for full_path in self.servedfiles:
|
||||
rel = full_path[len(self.filespath) :]
|
||||
request_path = f"/file{rel}"
|
||||
with self._lock:
|
||||
self.servedfiles = files
|
||||
|
||||
filespath = Path(self.filespath).resolve()
|
||||
|
||||
for full_path in files:
|
||||
file_path = Path(full_path).resolve()
|
||||
try:
|
||||
rel = file_path.relative_to(filespath).as_posix()
|
||||
except ValueError:
|
||||
continue
|
||||
request_path = f"/file/{rel}"
|
||||
self.destination.register_request_handler(
|
||||
request_path,
|
||||
response_generator=self.serve_file,
|
||||
@@ -102,140 +175,282 @@ class PageNode:
|
||||
)
|
||||
|
||||
def _scan_pages(self, base):
|
||||
for entry in os.listdir(base):
|
||||
if entry.startswith("."):
|
||||
"""Return a list of page paths under the given directory, excluding .allowed files."""
|
||||
base_path = Path(base)
|
||||
if not base_path.exists():
|
||||
return []
|
||||
served = []
|
||||
for entry in base_path.iterdir():
|
||||
if entry.name.startswith("."):
|
||||
continue
|
||||
path = os.path.join(base, entry)
|
||||
if os.path.isdir(path):
|
||||
self._scan_pages(path)
|
||||
elif os.path.isfile(path) and not entry.endswith(".allowed"):
|
||||
self.servedpages.append(path)
|
||||
if entry.is_dir():
|
||||
served.extend(self._scan_pages(entry))
|
||||
elif entry.is_file() and entry.name.endswith(".mu"):
|
||||
served.append(str(entry))
|
||||
return served
|
||||
|
||||
def _scan_files(self, base):
|
||||
for entry in os.listdir(base):
|
||||
if entry.startswith("."):
|
||||
"""Return all file paths under the given directory."""
|
||||
base_path = Path(base)
|
||||
if not base_path.exists():
|
||||
return []
|
||||
served = []
|
||||
for entry in base_path.iterdir():
|
||||
if entry.name.startswith("."):
|
||||
continue
|
||||
path = os.path.join(base, entry)
|
||||
if os.path.isdir(path):
|
||||
self._scan_files(path)
|
||||
elif os.path.isfile(path):
|
||||
self.servedfiles.append(path)
|
||||
if entry.is_dir():
|
||||
served.extend(self._scan_files(entry))
|
||||
elif entry.is_file():
|
||||
served.append(str(entry))
|
||||
return served
|
||||
|
||||
@staticmethod
|
||||
def serve_default_index(
|
||||
path, data, request_id, link_id, remote_identity, requested_at
|
||||
_path,
|
||||
_data,
|
||||
_request_id,
|
||||
_link_id,
|
||||
_remote_identity,
|
||||
_requested_at,
|
||||
):
|
||||
"""Serve the default index page when no index.mu file exists."""
|
||||
return DEFAULT_INDEX.encode("utf-8")
|
||||
|
||||
def serve_page(
|
||||
self, path, data, request_id, link_id, remote_identity, requested_at
|
||||
self,
|
||||
path,
|
||||
data,
|
||||
_request_id,
|
||||
_link_id,
|
||||
remote_identity,
|
||||
_requested_at,
|
||||
):
|
||||
file_path = path.replace("/page", self.pagespath, 1)
|
||||
"""Serve a .mu page file, executing it as a script if it has a shebang."""
|
||||
pagespath = Path(self.pagespath).resolve()
|
||||
relative_path = path[6:] if path.startswith("/page/") else path[5:]
|
||||
file_path = (pagespath / relative_path).resolve()
|
||||
|
||||
if not str(file_path).startswith(str(pagespath)):
|
||||
return DEFAULT_NOTALLOWED.encode("utf-8")
|
||||
is_script = False
|
||||
file_content = None
|
||||
try:
|
||||
with open(file_path, "rb") as _f:
|
||||
first_line = _f.readline()
|
||||
is_script = first_line.startswith(b"#!")
|
||||
except Exception:
|
||||
is_script = False
|
||||
if is_script and os.access(file_path, os.X_OK):
|
||||
# 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.
|
||||
with file_path.open("rb") as file_handle:
|
||||
first_line = file_handle.readline()
|
||||
is_script = first_line.startswith(b"#!")
|
||||
file_handle.seek(0)
|
||||
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):
|
||||
try:
|
||||
result = subprocess.run([file_path], stdout=subprocess.PIPE, check=True) # noqa: S603
|
||||
env_map = os.environ.copy()
|
||||
if _link_id is not None:
|
||||
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,
|
||||
delimit=False,
|
||||
)
|
||||
if data is not None and isinstance(data, dict):
|
||||
for e in data:
|
||||
if isinstance(e, str) and (
|
||||
e.startswith("field_") or e.startswith("var_")
|
||||
):
|
||||
env_map[e] = data[e]
|
||||
result = subprocess.run( # noqa: S603
|
||||
[str(file_path)],
|
||||
stdout=subprocess.PIPE,
|
||||
check=True,
|
||||
env=env_map,
|
||||
)
|
||||
return result.stdout
|
||||
except Exception:
|
||||
self.logger.exception("Error executing script page")
|
||||
with open(file_path, "rb") as f:
|
||||
return f.read()
|
||||
except Exception as e:
|
||||
RNS.log(f"Error executing script page: {e}", RNS.LOG_ERROR)
|
||||
if file_content is not None:
|
||||
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(
|
||||
self, path, data, request_id, link_id, remote_identity, requested_at
|
||||
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")},
|
||||
]
|
||||
"""Serve a file from the files directory."""
|
||||
filespath = Path(self.filespath).resolve()
|
||||
relative_path = path[6:] if path.startswith("/file/") else path[5:]
|
||||
file_path = (filespath / relative_path).resolve()
|
||||
|
||||
if not file_path.is_file() or not str(file_path).startswith(str(filespath)):
|
||||
return DEFAULT_NOTALLOWED.encode("utf-8")
|
||||
|
||||
try:
|
||||
return [
|
||||
file_path.open("rb"),
|
||||
{"name": file_path.name.encode("utf-8")},
|
||||
]
|
||||
except OSError as err:
|
||||
RNS.log(f"Error opening file {file_path}: {err}", RNS.LOG_ERROR)
|
||||
return DEFAULT_NOTALLOWED.encode("utf-8")
|
||||
|
||||
def on_connect(self, link):
|
||||
pass
|
||||
"""Handle new link connections."""
|
||||
|
||||
def _announce_loop(self):
|
||||
"""Periodically announce the node until shutdown is requested."""
|
||||
interval_seconds = max(self.announce_interval, 0) * 60
|
||||
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")
|
||||
now = time.time()
|
||||
if (
|
||||
self.last_announce == 0
|
||||
or now - self.last_announce >= interval_seconds
|
||||
):
|
||||
try:
|
||||
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:
|
||||
RNS.log(f"Error in announce loop: {e}", RNS.LOG_ERROR)
|
||||
|
||||
def _refresh_loop(self):
|
||||
"""Refresh page and file registrations at configured intervals."""
|
||||
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
|
||||
and now - self.last_page_refresh >= self.page_refresh_interval
|
||||
):
|
||||
self.register_pages()
|
||||
self.last_page_refresh = now
|
||||
self.last_page_refresh = time.time()
|
||||
if (
|
||||
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.last_file_refresh = now
|
||||
time.sleep(1)
|
||||
except Exception:
|
||||
self.logger.exception("Error in refresh loop")
|
||||
self.last_file_refresh = time.time()
|
||||
|
||||
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:
|
||||
RNS.log(f"Error in refresh loop: {e}", RNS.LOG_ERROR)
|
||||
|
||||
def shutdown(self):
|
||||
self.logger.info("Shutting down PageNode...")
|
||||
"""Gracefully shutdown the PageNode and cleanup resources."""
|
||||
RNS.log("Shutting down PageNode...", RNS.LOG_INFO)
|
||||
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")
|
||||
except Exception as e:
|
||||
RNS.log(f"Error waiting for threads to shut down: {e}", RNS.LOG_ERROR)
|
||||
try:
|
||||
if hasattr(self.destination, "close"):
|
||||
self.destination.close()
|
||||
except Exception:
|
||||
self.logger.exception("Error closing RNS destination")
|
||||
except Exception as e:
|
||||
RNS.log(f"Error closing RNS destination: {e}", RNS.LOG_ERROR)
|
||||
|
||||
|
||||
def main():
|
||||
"""Run the RNS page node application."""
|
||||
parser = argparse.ArgumentParser(description="Minimal Reticulum Page Node")
|
||||
parser.add_argument(
|
||||
"-c", "--config", dest="configpath", help="Reticulum config path", default=None
|
||||
"node_config",
|
||||
nargs="?",
|
||||
help="Path to rns-page-node config file",
|
||||
default=None,
|
||||
)
|
||||
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"),
|
||||
default=str(Path.cwd() / "pages"),
|
||||
)
|
||||
parser.add_argument(
|
||||
"-f",
|
||||
"--files-dir",
|
||||
dest="files_dir",
|
||||
help="Files directory",
|
||||
default=os.path.join(os.getcwd(), "files"),
|
||||
default=str(Path.cwd() / "files"),
|
||||
)
|
||||
parser.add_argument(
|
||||
"-n", "--node-name", dest="node_name", help="Node display name", default=None
|
||||
"-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",
|
||||
help="Announce interval in minutes",
|
||||
default=360,
|
||||
)
|
||||
parser.add_argument(
|
||||
@@ -243,7 +458,7 @@ def main():
|
||||
"--identity-dir",
|
||||
dest="identity_dir",
|
||||
help="Directory to store node identity",
|
||||
default=os.path.join(os.getcwd(), "node-config"),
|
||||
default=str(Path.cwd() / "node-config"),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--page-refresh-interval",
|
||||
@@ -269,30 +484,79 @@ def main():
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
configpath = args.configpath
|
||||
pages_dir = args.pages_dir
|
||||
files_dir = args.files_dir
|
||||
node_name = args.node_name
|
||||
announce_interval = args.announce_interval
|
||||
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"
|
||||
config = {}
|
||||
if args.node_config:
|
||||
config = load_config(args.node_config)
|
||||
|
||||
def get_config_value(arg_value, arg_default, config_key, value_type=str):
|
||||
"""Get value from CLI args, config file, or default.
|
||||
|
||||
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)
|
||||
os.makedirs(identity_dir, exist_ok=True)
|
||||
identity_file = os.path.join(identity_dir, "identity")
|
||||
if os.path.isfile(identity_file):
|
||||
identity = RNS.Identity.from_file(identity_file)
|
||||
Path(identity_dir).mkdir(parents=True, exist_ok=True)
|
||||
identity_file = Path(identity_dir) / "identity"
|
||||
if identity_file.is_file():
|
||||
identity = RNS.Identity.from_file(str(identity_file))
|
||||
else:
|
||||
identity = RNS.Identity()
|
||||
identity.to_file(identity_file)
|
||||
identity.to_file(str(identity_file))
|
||||
|
||||
os.makedirs(pages_dir, exist_ok=True)
|
||||
os.makedirs(files_dir, exist_ok=True)
|
||||
Path(pages_dir).mkdir(parents=True, exist_ok=True)
|
||||
Path(files_dir).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
node = PageNode(
|
||||
identity,
|
||||
@@ -303,13 +567,14 @@ def main():
|
||||
page_refresh_interval,
|
||||
file_refresh_interval,
|
||||
)
|
||||
logger.info("Page node running. Press Ctrl-C to exit.")
|
||||
RNS.log("Page node running. Press Ctrl-C to exit.", RNS.LOG_INFO)
|
||||
RNS.log(f"Node address: {RNS.prettyhexrep(node.destination.hash)}", RNS.LOG_INFO)
|
||||
|
||||
try:
|
||||
while True:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
logger.info("Keyboard interrupt received, shutting down...")
|
||||
RNS.log("Keyboard interrupt received, shutting down...", RNS.LOG_INFO)
|
||||
node.shutdown()
|
||||
|
||||
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
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.0.0",
|
||||
author="Sudo-Ivan",
|
||||
author_email="",
|
||||
version="1.3.1",
|
||||
description="A simple way to serve pages and files over the Reticulum network.",
|
||||
long_description=long_description,
|
||||
long_description=open("README.md").read(),
|
||||
long_description_content_type="text/markdown",
|
||||
url="https://github.com/Sudo-Ivan/rns-page-node",
|
||||
author="Sudo-Ivan",
|
||||
url="https://git.quad4.io/RNS-Things/rns-page-node",
|
||||
packages=find_packages(),
|
||||
license="GPL-3.0",
|
||||
python_requires=">=3.10",
|
||||
install_requires=[
|
||||
"rns>=1.0.0,<1.5.0",
|
||||
"rns>=1.1.2,<1.5.0",
|
||||
"cryptography>=46.0.3",
|
||||
],
|
||||
entry_points={
|
||||
"console_scripts": [
|
||||
@@ -25,7 +20,8 @@ setup(
|
||||
},
|
||||
classifiers=[
|
||||
"Programming Language :: Python :: 3",
|
||||
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||
"Operating System :: OS Independent",
|
||||
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||
],
|
||||
python_requires=">=3.9.2",
|
||||
)
|
||||
|
||||
Regular → Executable
+31
-5
@@ -9,17 +9,39 @@ rm -rf config node-config pages files node.log
|
||||
mkdir -p config node-config pages files
|
||||
|
||||
# Create a sample page and a test file
|
||||
cat > pages/index.mu << EOF
|
||||
>Test Page
|
||||
This is a test page.
|
||||
cat > pages/index.mu << 'EOF'
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
|
||||
print("`F0f0`_`Test Page`_")
|
||||
print("This is a test page with environment variable support.")
|
||||
print()
|
||||
|
||||
print("`F0f0`_`Environment Variables`_")
|
||||
params = []
|
||||
for key, value in os.environ.items():
|
||||
if key.startswith(('field_', 'var_')):
|
||||
params.append(f"- `Faaa`{key}`f: `F0f0`{value}`f")
|
||||
|
||||
if params:
|
||||
print("\n".join(params))
|
||||
else:
|
||||
print("- No parameters received")
|
||||
|
||||
print()
|
||||
print("`F0f0`_`Remote Identity`_")
|
||||
remote_id = os.environ.get('remote_identity', '33aff86b736acd47dca07e84630fd192') # Mock for testing
|
||||
print(f"`Faaa`{remote_id}`f")
|
||||
EOF
|
||||
|
||||
chmod +x pages/index.mu
|
||||
|
||||
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 &
|
||||
poetry run 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
|
||||
@@ -38,7 +60,11 @@ if [ ! -f node-config/identity ]; then
|
||||
fi
|
||||
|
||||
# Run the client test
|
||||
python3 test_client.py
|
||||
poetry run python3 test_client.py
|
||||
|
||||
# Run advanced tests
|
||||
echo "Running advanced tests (smoke, performance, leak, fuzzing, property-based)..."
|
||||
poetry run python3 test_advanced.py
|
||||
|
||||
# Clean up
|
||||
kill $NODE_PID
|
||||
Executable
+248
@@ -0,0 +1,248 @@
|
||||
#!/usr/bin/env python3
|
||||
import random
|
||||
import shutil
|
||||
import string
|
||||
import threading
|
||||
import time
|
||||
import tracemalloc
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
import RNS
|
||||
|
||||
from rns_page_node.main import PageNode
|
||||
|
||||
|
||||
class AdvancedTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.test_dir = Path("./test_advanced_tmp")
|
||||
cls.test_dir.mkdir(exist_ok=True)
|
||||
cls.pages_dir = cls.test_dir / "pages"
|
||||
cls.files_dir = cls.test_dir / "files"
|
||||
cls.identity_dir = cls.test_dir / "node-config"
|
||||
|
||||
cls.pages_dir.mkdir(exist_ok=True)
|
||||
cls.files_dir.mkdir(exist_ok=True)
|
||||
cls.identity_dir.mkdir(exist_ok=True)
|
||||
|
||||
# Create test files
|
||||
(cls.pages_dir / "index.mu").write_text("Hello World")
|
||||
(cls.files_dir / "test.txt").write_bytes(b"File content")
|
||||
|
||||
# Initialize RNS
|
||||
RNS.Reticulum(str(cls.test_dir / "config"))
|
||||
cls.identity = RNS.Identity()
|
||||
|
||||
cls.node = PageNode(
|
||||
cls.identity,
|
||||
str(cls.pages_dir),
|
||||
str(cls.files_dir),
|
||||
announce_interval=0,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.node.shutdown()
|
||||
# Small sleep to allow threads to exit
|
||||
time.sleep(0.5)
|
||||
shutil.rmtree(cls.test_dir, ignore_errors=True)
|
||||
|
||||
def test_smoke(self):
|
||||
"""Basic smoke test to ensure node is initialized and has handlers."""
|
||||
self.assertIsNotNone(self.node.destination)
|
||||
self.assertTrue(len(self.node.servedpages) >= 1)
|
||||
|
||||
def test_performance(self):
|
||||
"""Measure performance of request handlers."""
|
||||
start_time = time.time()
|
||||
iterations = 100
|
||||
for _ in range(iterations):
|
||||
# Simulate a request to serve_page
|
||||
self.node.serve_page("/page/index.mu", None, None, None, None, None)
|
||||
|
||||
duration = time.time() - start_time
|
||||
avg_time = duration / iterations
|
||||
print(
|
||||
f"\n[Performance] Avg serve_page time: {avg_time:.6f}s over {iterations} iterations",
|
||||
)
|
||||
self.assertLess(avg_time, 0.01, "Performance too slow")
|
||||
|
||||
def test_leaks(self):
|
||||
"""Test for memory and thread leaks."""
|
||||
tracemalloc.start()
|
||||
initial_threads = threading.active_count()
|
||||
|
||||
# Perform some operations
|
||||
for _ in range(50):
|
||||
self.node.register_pages()
|
||||
self.node.register_files()
|
||||
self.node.serve_page("/page/index.mu", None, None, None, None, None)
|
||||
|
||||
current_threads = threading.active_count()
|
||||
_, peak = tracemalloc.get_traced_memory()
|
||||
tracemalloc.stop()
|
||||
|
||||
print(f"\n[Leak Test] Peak memory: {peak / 1024 / 1024:.2f} MB")
|
||||
print(f"[Leak Test] Thread count change: {current_threads - initial_threads}")
|
||||
|
||||
# Allow some thread variation but not excessive growth
|
||||
self.assertLessEqual(
|
||||
current_threads,
|
||||
initial_threads + 5,
|
||||
"Potential thread leak detected",
|
||||
)
|
||||
|
||||
def test_fuzzing(self):
|
||||
"""Fuzz request handlers with random inputs."""
|
||||
print("\n[Fuzzing] Starting fuzzing of request handlers...")
|
||||
for _ in range(100):
|
||||
# Random path fuzzing
|
||||
random_path = "/" + "".join(
|
||||
random.choices(string.ascii_letters + string.digits + "/.", k=20),
|
||||
)
|
||||
# Should not crash
|
||||
res_p = self.node.serve_page(random_path, None, None, None, None, None)
|
||||
res_f = self.node.serve_file(random_path, None, None, None, None, None)
|
||||
|
||||
# Close file handles if returned to avoid ResourceWarnings
|
||||
if isinstance(res_f, list) and len(res_f) > 0 and hasattr(res_f[0], "close"):
|
||||
res_f[0].close()
|
||||
|
||||
# Random data fuzzing
|
||||
random_data = {
|
||||
"field_" + "".join(random.choices(string.ascii_letters, k=5)): "".join(
|
||||
random.choices(string.ascii_letters + string.digits, k=20),
|
||||
)
|
||||
for _ in range(3)
|
||||
}
|
||||
self.node.serve_page("/page/index.mu", random_data, None, None, None, None)
|
||||
|
||||
def test_property_based(self):
|
||||
"""Property-based testing for path traversal and response types."""
|
||||
# Property: serve_page should never return contents from outside pages_dir
|
||||
traversal_paths = [
|
||||
"/page/../../etc/passwd",
|
||||
"/page/../main.py",
|
||||
"/page/./index.mu/../../../",
|
||||
]
|
||||
for path in traversal_paths:
|
||||
response = self.node.serve_page(path, None, None, None, None, None)
|
||||
self.assertIn(
|
||||
b"Not Allowed",
|
||||
response,
|
||||
f"Path traversal succeeded for {path}",
|
||||
)
|
||||
|
||||
# Property: serve_file should always return a list with [fileobj, headers] or bytes
|
||||
response = self.node.serve_file("/file/test.txt", None, None, None, None, None)
|
||||
try:
|
||||
self.assertTrue(isinstance(response, list) or isinstance(response, bytes))
|
||||
if isinstance(response, list):
|
||||
self.assertEqual(len(response), 2)
|
||||
self.assertTrue(hasattr(response[0], "read"))
|
||||
finally:
|
||||
if isinstance(response, list) and len(response) > 0 and hasattr(response[0], "close"):
|
||||
response[0].close()
|
||||
|
||||
def test_property_config_loading(self):
|
||||
"""Property-based testing for configuration loading."""
|
||||
from rns_page_node.main import load_config
|
||||
|
||||
config_file = self.test_dir / "prop_config"
|
||||
|
||||
for _ in range(50):
|
||||
# Generate random valid and invalid config lines
|
||||
expected = {}
|
||||
lines = []
|
||||
for i in range(10):
|
||||
if random.random() > 0.3:
|
||||
# Valid line
|
||||
key = f"key_{i}_{''.join(random.choices(string.ascii_letters, k=5))}"
|
||||
val = f"val_{i}_{''.join(random.choices(string.ascii_letters, k=5))}"
|
||||
lines.append(f"{key} = {val}")
|
||||
expected[key] = val
|
||||
else:
|
||||
# Invalid line (comment or no =)
|
||||
if random.random() > 0.5:
|
||||
lines.append(f"# comment {''.join(random.choices(string.ascii_letters, k=10))}")
|
||||
else:
|
||||
lines.append("".join(random.choices(string.ascii_letters, k=15)))
|
||||
|
||||
config_file.write_text("\n".join(lines))
|
||||
loaded = load_config(str(config_file))
|
||||
self.assertEqual(loaded, expected)
|
||||
|
||||
def test_property_scanning(self):
|
||||
"""Property-based testing for directory scanning."""
|
||||
scan_test_dir = self.test_dir / "scan_test"
|
||||
if scan_test_dir.exists():
|
||||
shutil.rmtree(scan_test_dir)
|
||||
scan_test_dir.mkdir()
|
||||
|
||||
expected_pages = []
|
||||
expected_files = []
|
||||
|
||||
for i in range(20):
|
||||
name = "".join(random.choices(string.ascii_letters, k=8))
|
||||
if random.random() > 0.5:
|
||||
# Page scenario
|
||||
if random.random() > 0.2:
|
||||
# Normal page
|
||||
f = scan_test_dir / f"{name}.mu"
|
||||
f.touch()
|
||||
expected_pages.append(str(f))
|
||||
else:
|
||||
# .allowed file (should be ignored by pages)
|
||||
f = scan_test_dir / f"{name}.allowed"
|
||||
f.touch()
|
||||
else:
|
||||
# File scenario
|
||||
if random.random() > 0.2:
|
||||
# Normal file
|
||||
f = scan_test_dir / name
|
||||
f.touch()
|
||||
expected_files.append(str(f))
|
||||
else:
|
||||
# Hidden file (should be ignored by both)
|
||||
f = scan_test_dir / f".{name}"
|
||||
f.touch()
|
||||
|
||||
# We need to test the methods on a PageNode instance
|
||||
# Pages scan
|
||||
found_pages = self.node._scan_pages(str(scan_test_dir))
|
||||
self.assertCountEqual(found_pages, expected_pages)
|
||||
|
||||
# Files scan (files scan includes .mu files too as they are just files)
|
||||
# but excludes hidden files.
|
||||
found_files = self.node._scan_files(str(scan_test_dir))
|
||||
# Our expected_files only tracked "normal" files, but _scan_files
|
||||
# includes everything that isn't hidden and isn't a directory.
|
||||
actual_expected_files = [str(f) for f in scan_test_dir.iterdir()
|
||||
if not f.name.startswith(".") and f.is_file()]
|
||||
self.assertCountEqual(found_files, actual_expected_files)
|
||||
|
||||
def test_property_script_execution(self):
|
||||
"""Property-based testing for script execution vs reading."""
|
||||
script_path = self.pages_dir / "prop_script.mu"
|
||||
|
||||
# Property: File with shebang AND executable bit -> Executed
|
||||
script_path.write_text("#!/bin/sh\necho 'script output'")
|
||||
script_path.chmod(0o755)
|
||||
response = self.node.serve_page("/page/prop_script.mu", None, None, None, None, None)
|
||||
self.assertEqual(response.strip(), b"script output")
|
||||
|
||||
# Property: File with shebang but NO executable bit -> Read as text
|
||||
script_path.chmod(0o644)
|
||||
response = self.node.serve_page("/page/prop_script.mu", None, None, None, None, None)
|
||||
self.assertIn(b"#!/bin/sh", response)
|
||||
|
||||
# Property: File without shebang -> Read as text even if executable
|
||||
script_path.write_text("plain text content")
|
||||
script_path.chmod(0o755)
|
||||
response = self.node.serve_page("/page/prop_script.mu", None, None, None, None, None)
|
||||
self.assertEqual(response, b"plain text content")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+133
-9
@@ -20,7 +20,11 @@ server_identity = RNS.Identity.from_file(identity_file)
|
||||
|
||||
# Create a destination to the server node
|
||||
destination = RNS.Destination(
|
||||
server_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "nomadnetwork", "node"
|
||||
server_identity,
|
||||
RNS.Destination.OUT,
|
||||
RNS.Destination.SINGLE,
|
||||
"nomadnetwork",
|
||||
"node",
|
||||
)
|
||||
|
||||
# Ensure we know a path to the destination
|
||||
@@ -36,6 +40,18 @@ global_link = RNS.Link(destination)
|
||||
responses = {}
|
||||
done_event = threading.Event()
|
||||
|
||||
# Test data for environment variables
|
||||
test_data_dict = {
|
||||
"var_field_test": "dictionary_value",
|
||||
"var_field_message": "hello_world",
|
||||
"var_action": "test_action",
|
||||
}
|
||||
test_data_dict2 = {
|
||||
"field_username": "testuser",
|
||||
"field_message": "hello_from_form",
|
||||
"var_action": "submit",
|
||||
}
|
||||
|
||||
|
||||
# Callback for page response
|
||||
def on_page(response):
|
||||
@@ -44,10 +60,45 @@ def on_page(response):
|
||||
text = data.decode("utf-8")
|
||||
else:
|
||||
text = str(data)
|
||||
print("Received page:")
|
||||
print("Received page (no data):")
|
||||
print(text)
|
||||
responses["page"] = text
|
||||
if "file" in responses:
|
||||
check_responses()
|
||||
|
||||
|
||||
# Callback for page response with dictionary data
|
||||
def on_page_dict(response):
|
||||
data = response.response
|
||||
if isinstance(data, bytes):
|
||||
text = data.decode("utf-8")
|
||||
else:
|
||||
text = str(data)
|
||||
print("Received page (dict data):")
|
||||
print(text)
|
||||
responses["page_dict"] = text
|
||||
check_responses()
|
||||
|
||||
|
||||
# Callback for page response with second dict data
|
||||
def on_page_dict2(response):
|
||||
data = response.response
|
||||
if isinstance(data, bytes):
|
||||
text = data.decode("utf-8")
|
||||
else:
|
||||
text = str(data)
|
||||
print("Received page (dict2 data):")
|
||||
print(text)
|
||||
responses["page_dict2"] = text
|
||||
check_responses()
|
||||
|
||||
|
||||
def check_responses():
|
||||
if (
|
||||
"page" in responses
|
||||
and "page_dict" in responses
|
||||
and "page_dict2" in responses
|
||||
and "file" in responses
|
||||
):
|
||||
done_event.set()
|
||||
|
||||
|
||||
@@ -78,27 +129,100 @@ def on_file(response):
|
||||
else:
|
||||
print("Received file (unhandled format):", data)
|
||||
responses["file"] = str(data)
|
||||
if "page" in responses:
|
||||
done_event.set()
|
||||
check_responses()
|
||||
|
||||
|
||||
# Request the page and file once the link is established
|
||||
# Request the pages and file once the link is established
|
||||
def on_link_established(link):
|
||||
# Test page without data
|
||||
link.request("/page/index.mu", None, response_callback=on_page)
|
||||
# Test page with dictionary data (simulates var_ prefixed data)
|
||||
link.request("/page/index.mu", test_data_dict, response_callback=on_page_dict)
|
||||
# Test page with form field data (simulates field_ prefixed data)
|
||||
link.request("/page/index.mu", test_data_dict2, response_callback=on_page_dict2)
|
||||
# Test file serving
|
||||
link.request("/file/text.txt", None, response_callback=on_file)
|
||||
|
||||
|
||||
# Register callbacks
|
||||
global_link.set_link_established_callback(on_link_established)
|
||||
global_link.set_link_closed_callback(lambda l: done_event.set())
|
||||
global_link.set_link_closed_callback(lambda link: done_event.set())
|
||||
|
||||
# Wait for responses or timeout
|
||||
if not done_event.wait(timeout=30):
|
||||
print("Test timed out.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if responses.get("page") and responses.get("file"):
|
||||
print("Tests passed!")
|
||||
|
||||
# Validate test results
|
||||
def validate_test_results():
|
||||
"""Validate that all responses contain expected content"""
|
||||
# Check basic page response (no data)
|
||||
if "page" not in responses:
|
||||
print("ERROR: No basic page response received", file=sys.stderr)
|
||||
return False
|
||||
|
||||
page_content = responses["page"]
|
||||
if "No parameters received" not in page_content:
|
||||
print("ERROR: Basic page should show 'No parameters received'", file=sys.stderr)
|
||||
return False
|
||||
if "33aff86b736acd47dca07e84630fd192" not in page_content:
|
||||
print("ERROR: Basic page should show mock remote identity", file=sys.stderr)
|
||||
return False
|
||||
|
||||
# Check page with dictionary data
|
||||
if "page_dict" not in responses:
|
||||
print("ERROR: No dictionary data page response received", file=sys.stderr)
|
||||
return False
|
||||
|
||||
dict_content = responses["page_dict"]
|
||||
if "var_field_test" not in dict_content or "dictionary_value" not in dict_content:
|
||||
print(
|
||||
"ERROR: Dictionary data page should contain processed environment variables",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return False
|
||||
if "33aff86b736acd47dca07e84630fd192" not in dict_content:
|
||||
print(
|
||||
"ERROR: Dictionary data page should show mock remote identity",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return False
|
||||
|
||||
# Check page with second dictionary data (form fields)
|
||||
if "page_dict2" not in responses:
|
||||
print("ERROR: No dict2 data page response received", file=sys.stderr)
|
||||
return False
|
||||
|
||||
dict2_content = responses["page_dict2"]
|
||||
if "field_username" not in dict2_content or "testuser" not in dict2_content:
|
||||
print(
|
||||
"ERROR: Dict2 data page should contain processed environment variables",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return False
|
||||
if "33aff86b736acd47dca07e84630fd192" not in dict2_content:
|
||||
print(
|
||||
"ERROR: Dict2 data page should show mock remote identity",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return False
|
||||
|
||||
# Check file response
|
||||
if "file" not in responses:
|
||||
print("ERROR: No file response received", file=sys.stderr)
|
||||
return False
|
||||
|
||||
file_content = responses["file"]
|
||||
if "This is a test file" not in file_content:
|
||||
print("ERROR: File content doesn't match expected content", file=sys.stderr)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
if validate_test_results():
|
||||
print("All tests passed! Environment variable processing works correctly.")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("Tests failed.", file=sys.stderr)
|
||||
|
||||
@@ -34,7 +34,11 @@ server_identity = RNS.Identity.recall(destination_hash)
|
||||
print(f"Recalled server identity for {DESTINATION_HEX}")
|
||||
|
||||
destination = RNS.Destination(
|
||||
server_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "nomadnetwork", "node"
|
||||
server_identity,
|
||||
RNS.Destination.OUT,
|
||||
RNS.Destination.SINGLE,
|
||||
"nomadnetwork",
|
||||
"node",
|
||||
)
|
||||
link = RNS.Link(destination)
|
||||
|
||||
@@ -53,9 +57,9 @@ def on_page(response):
|
||||
|
||||
|
||||
link.set_link_established_callback(
|
||||
lambda l: l.request("/page/index.mu", None, response_callback=on_page)
|
||||
lambda link: link.request("/page/index.mu", None, response_callback=on_page),
|
||||
)
|
||||
link.set_link_closed_callback(lambda l: done_event.set())
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user