mirror of
https://github.com/markqvist/Reticulum.git
synced 2026-06-23 12:24:30 -07:00
Compare commits
156 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 07ff87974e | |||
| e8fa92950d | |||
| ab6532742e | |||
| 4e583770e5 | |||
| f9b6dc2ab8 | |||
| 1c2bc0c7b8 | |||
| 05760f914c | |||
| 3f6e8605af | |||
| b6bfd1655c | |||
| 8cbd0e22ff | |||
| 15ec64e974 | |||
| 3de16e085e | |||
| 4cbd4ed60c | |||
| b8fbd616e5 | |||
| f8a79d2f51 | |||
| 0218ff4e26 | |||
| 1f3ce7e78f | |||
| 9009e1d232 | |||
| cc73b2c2b9 | |||
| dbf19ed054 | |||
| a1cff4e8ab | |||
| c9822968c8 | |||
| 8acabd95b5 | |||
| 49f6a6924d | |||
| 8d73265cf4 | |||
| fceb7d18d7 | |||
| 337007cf70 | |||
| 4733d6d75a | |||
| c8235544e8 | |||
| 3d1111ff02 | |||
| 83c9f2b10a | |||
| 734eb53aa7 | |||
| 6d39cb8e7c | |||
| 3c3f38b239 | |||
| 86d52d3884 | |||
| 6782672cb8 | |||
| 7fada7e5ab | |||
| 4380026a4e | |||
| 5143ea3d02 | |||
| 4802bcd829 | |||
| 6038096b95 | |||
| 2acfc31350 | |||
| 2742e5253f | |||
| 46f2e994b9 | |||
| 2c97a20c12 | |||
| 9be10ebd47 | |||
| 93cbfe7f7e | |||
| 4589de2115 | |||
| 662054ae25 | |||
| 3cf186f3cb | |||
| 7a91c82e4b | |||
| 72aace40d3 | |||
| 0c9a65b5f1 | |||
| ea749499c3 | |||
| 828cbe7f20 | |||
| 1d8d547872 | |||
| 16c53221e3 | |||
| 74936010c4 | |||
| f3245e1d65 | |||
| 1f74570ed9 | |||
| 88d1b7d2d1 | |||
| fb5dcf0631 | |||
| a23086d3fc | |||
| a4cbcbca97 | |||
| 9dd008d42b | |||
| 76fa07cb90 | |||
| 35d72f27ed | |||
| 852891c779 | |||
| f4aa7dc389 | |||
| d7c3859f61 | |||
| 85d77c10a1 | |||
| 95222c7793 | |||
| 0a18b47e8c | |||
| 70f5126499 | |||
| b60eab0fcf | |||
| 17310fc294 | |||
| 9c892dc1a4 | |||
| c596dab806 | |||
| fcb590e661 | |||
| 328017cca0 | |||
| 63dba562ae | |||
| cf20f26098 | |||
| e1e6063d17 | |||
| ccbbe6f2f8 | |||
| 55c95bf59a | |||
| 043a5dc4e7 | |||
| 32a1cdf494 | |||
| f924086198 | |||
| 6abb31e469 | |||
| 3eee369704 | |||
| 695d4d8684 | |||
| 015692d51e | |||
| 86004a89e5 | |||
| 86031ef3f8 | |||
| 034239daf3 | |||
| a7b0f9924e | |||
| a1d35b34b9 | |||
| 8d7e337dff | |||
| de7e0996ce | |||
| 7377b69144 | |||
| c933cfdaa3 | |||
| 726185cee2 | |||
| de1000bfda | |||
| 555e8c0376 | |||
| d836de3fe7 | |||
| 6ade1269ea | |||
| a8b519e06e | |||
| 7d502306ea | |||
| e9fa57c660 | |||
| 7d4ab17f0d | |||
| d532902320 | |||
| e592244443 | |||
| c1def5da19 | |||
| 6a7f081f12 | |||
| 11555198eb | |||
| 6c77e27a50 | |||
| 17e8159fd8 | |||
| c71f5d8c5e | |||
| 31cc9fc7d1 | |||
| 1d2421b0af | |||
| a5df765951 | |||
| 622019ee06 | |||
| 45e12cc668 | |||
| a21024a57e | |||
| c175491bb0 | |||
| 09b0469faf | |||
| 3d63bbf4bf | |||
| 56d5d01497 | |||
| a70bd44426 | |||
| 8c082b2fcc | |||
| 1732cac806 | |||
| e1340e87eb | |||
| e9bfef2131 | |||
| b408699e65 | |||
| 3d1c508868 | |||
| 84e0746c9c | |||
| b5658c4865 | |||
| d413a4bc53 | |||
| ce5ab902b6 | |||
| 294408b0bb | |||
| 53372fbe4c | |||
| 7fdac2118b | |||
| 1dbf78ed71 | |||
| c9101a0c21 | |||
| 2e6264c04b | |||
| e0aa46ba22 | |||
| 8093c3cd2c | |||
| c6778e4e29 | |||
| c77548d299 | |||
| 26d435ea64 | |||
| c3f0d98e41 | |||
| 3c50f4aee9 | |||
| 4a930ba82a | |||
| 866e63f0fe | |||
| d461cfa8ce | |||
| 18708636fb |
+162
@@ -1,3 +1,163 @@
|
||||
### 2026-05-05: RNS 1.2.2
|
||||
|
||||
This release adds release management workflows to the `rngit` utility. Downloading files and release artifacts from `rngit` will require the latest version of Nomad Network. Other nomadnet clients *may* have to update their file download link handling, if they don't already support passing query parameters for file download links.
|
||||
|
||||
**Changes**
|
||||
- Added release management to `rngit`.
|
||||
- Added release pages to the page node of `rngit`.
|
||||
- Added file downloads in the tree browser of `rngit`.
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
4bf0a376a9778de8a91b9ec8a5bc4b929be928eede8784b20022c7fe52bbce62 rns-1.2.2-py3-none-any.whl
|
||||
d85f8b765dcf718d284388b249ca0e48e785f250bb41773a83e159e46c5bcf70 rnspure-1.2.2-py3-none-any.whl
|
||||
```
|
||||
|
||||
**Release Signatures**
|
||||
Release artifacts include `rsg` signature files that can be validated against the RNS release signing identity `<bc7291552be7a58f361522990465165c>` using `rnid`:
|
||||
|
||||
```sh
|
||||
rnid -i bc7291552be7a58f361522990465165c -V rns-1.2.2-py3-none-any.whl.rsg
|
||||
```
|
||||
|
||||
### 2026-05-04: RNS 1.2.1
|
||||
|
||||
This release adds a nomadnet Git page node to the `rngit` utility.
|
||||
|
||||
**Changes**
|
||||
- Added nomadnet page node to `rngit`.
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
5ccbfc31b528133c4dd06c132034c2151e4eed74bc2dcf40af52385094492c9e rns-1.2.1-py3-none-any.whl
|
||||
cda45994a58f18bf25244a1f396c9197240bc012dd85c86bffc2e73dcf0607de rnspure-1.2.1-py3-none-any.whl
|
||||
```
|
||||
|
||||
**Release Signatures**
|
||||
Release artifacts include `rsg` signature files that can be validated against the RNS release signing identity `<bc7291552be7a58f361522990465165c>` using `rnid`:
|
||||
|
||||
```sh
|
||||
rnid -i bc7291552be7a58f361522990465165c -V rns-1.2.1-py3-none-any.whl.rsg
|
||||
```
|
||||
|
||||
### 2026-04-28: RNS 1.2.0
|
||||
|
||||
This release brings the ability to use Git natively over Reticulum networks, adds the `rnsh` program as part of the included utilities, and additionally includes several improvements and performance optimizations.
|
||||
|
||||
**Changes**
|
||||
- Added Reticulum Git Repositories Node utility as part of included utility programs.
|
||||
- Added git remote helper to interact with git repositories over Reticulum.
|
||||
- Added the `rnsh` program to the included utilities.
|
||||
- Added LocalInterface client TX hold on client app sleep on Android.
|
||||
- Added AutoInterface filters for `rmnet` interfaces on Android.
|
||||
- Added inbound packet wait during transport core initialization.
|
||||
- Added the ability to set logfile destination before RNS initialization.
|
||||
- Added automatic active link teardown on instance shutdown.
|
||||
- Improved link teardown on SIGINT/SIGTERM.
|
||||
- Improved ratchet cleaning.
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
b58e97332241755ed32e309d46e09615a123490430ae85fcbdec9318c9e26154 rns-1.2.0-py3-none-any.whl
|
||||
9813a6c2236edba18af7d3a072a6226bc65ae384d23b1f41467cb3617d65fdae rnspure-1.2.0-py3-none-any.whl
|
||||
```
|
||||
|
||||
**Release Signatures**
|
||||
Release artifacts include `rsg` signature files that can be validated against the RNS release signing identity `<bc7291552be7a58f361522990465165c>` using `rnid`:
|
||||
|
||||
```sh
|
||||
rnid -i bc7291552be7a58f361522990465165c -V rns-1.2.0-py3-none-any.whl.rsg
|
||||
```
|
||||
|
||||
### 2026-04-22: RNS 1.1.9
|
||||
|
||||
This maintenance release fixes a critical security issue, that would allow an attacker to craft a BZ2 decompression bomb via Resource transfers or Buffer StreamDataMessage, causing an out-of-memory condition and crashing the receiving process via OOM killer.
|
||||
|
||||
Big thanks to @defidude (github.com/ratspeak) for discovering and reporting this vulnerability!
|
||||
|
||||
**Changes**
|
||||
- Fixed bz2 decompression bomb vulnerability in Resource transfer assembly and Buffer StreamDataMessage unpacking.
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
39a131aeb5d76fd73bfc67f68135f49ab0cf8628af154e04096a05c208ce77b6 rns-1.1.9-py3-none-any.whl
|
||||
aab7bfc8c65514c9bdf4c22f00d288faf6c9e1777fc002dbe3eb29c286e67128 rnspure-1.1.9-py3-none-any.whl
|
||||
```
|
||||
|
||||
**Release Signatures**
|
||||
Release artifacts include `rsg` signature files that can be validated against the RNS release signing identity `<bc7291552be7a58f361522990465165c>` using `rnid`:
|
||||
|
||||
```sh
|
||||
rnid -i bc7291552be7a58f361522990465165c -V rns-1.1.9-py3-none-any.whl.rsg
|
||||
```
|
||||
|
||||
### 2026-04-21: RNS 1.1.8
|
||||
|
||||
This maintenance release fixes a critical bug in path state management, that could result in significant path convergence degradation under certain conditions.
|
||||
|
||||
**Changes**
|
||||
- Fixed path state potentially being applied before path table entry exists, causing worse paths to be selected.
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
9cf728e9e9a9fe113e4ac14e6b833f7ee65feedf8468e6ab94a261bf205f2632 rns-1.1.8-py3-none-any.whl
|
||||
407dc3975335e9eabaaddb7ed1dc75fc3a1b8d24a7207e740797440c2ad0b3e5 rnspure-1.1.8-py3-none-any.wh
|
||||
```
|
||||
|
||||
**Release Signatures**
|
||||
Release artifacts include `rsg` signature files that can be validated against the RNS release signing identity `<bc7291552be7a58f361522990465165c>` using `rnid`:
|
||||
|
||||
```sh
|
||||
rnid -i bc7291552be7a58f361522990465165c -V rns-1.1.8-py3-none-any.whl.rsg
|
||||
```
|
||||
|
||||
### 2026-04-21: RNS 1.1.7
|
||||
|
||||
**Changes**
|
||||
- Added periodic known destination data cleaning based on local relevance.
|
||||
- Improved resource transfer sequencing timing calculations and reliability.
|
||||
- Improved BackboneInterface error handling on EPOLL errors.
|
||||
- Ensured non-background data persist runs synchronously.
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
4d9702c5d9bb8a3c8b94766cb51cccad5afd78d615af9a6b146730347044e6f0 rns-1.1.7-py3-none-any.whl
|
||||
172dede7656b41b85e4319354ed04649b518e58c54586da7e443579c620a0a5b rnspure-1.1.7-py3-none-any.whl
|
||||
```
|
||||
|
||||
**Release Signatures**
|
||||
Release artifacts include `rsg` signature files that can be validated against the RNS release signing identity `<bc7291552be7a58f361522990465165c>` using `rnid`:
|
||||
|
||||
```sh
|
||||
rnid -i bc7291552be7a58f361522990465165c -V rns-1.1.7-py3-none-any.whl.rsg
|
||||
```
|
||||
|
||||
### 2026-04-18: RNS 1.1.6
|
||||
|
||||
**Changes**
|
||||
- Improved transport memory consumption.
|
||||
- Improved transport tunnel handling.
|
||||
- Improved gracious transport data persist handling.
|
||||
- Added ingress control bypass for pending path requests.
|
||||
- Added local destinations lookup map for better transport efficiency to local destinations.
|
||||
- Fixed disk I/O bound thread execution time starvation on cache management jobs.
|
||||
- Fixed invalid EPOLL modification error handler.
|
||||
- Fixed incorrect default IFAC size for autoconnected, discovered interfaces. Thanks @taprootmx!
|
||||
- Ensure loop-originating closures have variables captured at iteration-time. Thanks @taprootmx!
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
2ce4451668f8c464295cc269188c232e7805ddd618ec0135550a5e6809df5de0 rns-1.1.6-py3-none-any.whl
|
||||
ba3e541e69a2f4892177383c8ec4e7d172d298546317e08270928c0163865aa3 rnspure-1.1.6-py3-none-any.wh
|
||||
```
|
||||
|
||||
**Release Signatures**
|
||||
Release artifacts include `rsg` signature files that can be validated against the RNS release signing identity `<bc7291552be7a58f361522990465165c>` using `rnid`:
|
||||
|
||||
```sh
|
||||
rnid -i bc7291552be7a58f361522990465165c -V rns-1.1.6-py3-none-any.whl.rsg
|
||||
```
|
||||
|
||||
### 2026-04-13: RNS 1.1.5
|
||||
|
||||
**Changes**
|
||||
@@ -21,6 +181,8 @@ Release artifacts include `rsg` signature files that can be validated against th
|
||||
rnid -i bc7291552be7a58f361522990465165c -V rns-1.1.5-py3-none-any.whl.rsg
|
||||
```
|
||||
|
||||
#
|
||||
|
||||
### 2026-03-12: RNS 1.1.4
|
||||
|
||||
**Changes**
|
||||
|
||||
@@ -222,7 +222,7 @@ def link_established(link):
|
||||
|
||||
# Inform the user that the server is
|
||||
# connected
|
||||
RNS.log("Link established with server, hit enter to sand a resource, or type in \"quit\" to quit")
|
||||
RNS.log("Link established with server, hit enter to send a resource, or type in \"quit\" to quit")
|
||||
|
||||
# When a link is closed, we'll inform the
|
||||
# user, and exit the program
|
||||
|
||||
@@ -61,9 +61,18 @@ release: test remove_symlinks build_sdist build_wheel build_pure_wheel documenta
|
||||
|
||||
debug: remove_symlinks build_wheel build_pure_wheel create_symlinks
|
||||
|
||||
upload:
|
||||
@echo Ready to publish release, hit enter to continue
|
||||
upload: upload-rns upload-rnspure
|
||||
|
||||
upload-rns:
|
||||
@echo Ready to publish rns release, hit enter to continue
|
||||
@read VOID
|
||||
@echo Uploading to PyPi...
|
||||
twine upload dist/*
|
||||
twine upload dist/rns-*.whl dist/rns-*.tar.gz
|
||||
@echo Release published
|
||||
|
||||
upload-rnspure:
|
||||
@echo Ready to publish rnspure release, hit enter to continue
|
||||
@read VOID
|
||||
@echo Uploading to PyPi...
|
||||
twine upload dist/rnspure-*.whl
|
||||
@echo Release published
|
||||
@@ -98,12 +98,12 @@ If you want to quickly get an idea of what Reticulum can do, take a look at the
|
||||
[Programs Using Reticulum](https://reticulum.network/manual/software.html)
|
||||
section of the manual, or the following resources:
|
||||
|
||||
- You can use the [rnsh](https://github.com/acehoss/rnsh) program to establish remote shell sessions over Reticulum.
|
||||
- [LXMF](https://github.com/markqvist/lxmf) is a distributed, delay and disruption tolerant message transfer protocol built on Reticulum
|
||||
- The [LXST](https://github.com/markqvist/lxst) protocol and framework provides real-time audio and signals transport over Reticulum. It includes primitives and utilities for building voice-based applications and hardware devices, such as the `rnphone` program, that can be used to build hardware telephones.
|
||||
- For an off-grid, encrypted and resilient mesh communications platform, see [Nomad Network](https://github.com/markqvist/NomadNet)
|
||||
- The Android, Linux, macOS and Windows app [Sideband](https://github.com/markqvist/Sideband) has a graphical interface and many advanced features, such as file transfers, image and voice messages, real-time voice calls, a distributed telemetry system, mapping capabilities and full plugin extensibility.
|
||||
- [MeshChat](https://github.com/liamcottle/reticulum-meshchat) is a user-friendly LXMF client with a web-based interface, that also supports image and voice messages, as well as file transfers. It also includes a built-in page browser for browsing Nomad Network nodes.
|
||||
- [MeshChatX](https://git.quad4.io/RNS-Things/MeshChatX) is a full-featured LXMF client with many built-in tools and functionalities, that also supports image and voice messages, file transfers and voice calls. It also includes a built-in page browser for browsing Nomad Network nodes.
|
||||
- You can use the included [rnsh](https://reticulum.network/manual/using.html#the-rnsh-utility) program to establish remote shell sessions over Reticulum.
|
||||
|
||||
## Where can Reticulum be used?
|
||||
Over practically any medium that can support at least a half-duplex channel
|
||||
@@ -184,8 +184,10 @@ section of the [Reticulum Manual](https://markqvist.github.io/Reticulum/manual/)
|
||||
- A diagnostics tool called `rnprobe` for checking connectivity to destinations
|
||||
- A simple file transfer program called `rncp` making it easy to transfer files between systems
|
||||
- The identity management and encryption utility `rnid` let's you manage Identities and encrypt/decrypt files
|
||||
- The remote command execution program `rnx` let's you run commands and
|
||||
programs and retrieve output from remote systems
|
||||
- The `rnsh` program allows you to establish fully interactive shell session with remote systems
|
||||
- The remote command execution program `rnx` let's you run simple commands and programs and retrieve output from remote systems
|
||||
- The `rngit` program provides a full multi-repository Git node for serving repositories over Reticulum
|
||||
- The included `git-remote-rns` helper allows you to interact with Git repositories over Reticulum
|
||||
|
||||
All tools, including `rnx` and `rncp`, work reliably and well even over very
|
||||
low-bandwidth links like LoRa or Packet Radio. For full-featured remote shells
|
||||
@@ -275,7 +277,7 @@ to find interface definitions for initial connectivity to the global distributed
|
||||
***Important!** Historically, a developer-targeted testnet was made available by the Reticulum project itself. As the amount of global Reticulum nodes and entrypoints have grown to a substantial quantity, this public testnet, including the Amsterdam Testnet entrypoint, has now been decommissioned. If your still have instances that relied on this entrypoint for connectivity, transition to using the distributed backbone instead. Reticulum now includes a full on-network interface discovery and connectivity bootstrapping system. Read the [Bootstrapping Connectivity](https://reticulum.network/manual/gettingstartedfast.html#bootstrapping-connectivity) section of the manual for pointers.*
|
||||
|
||||
## Support Reticulum
|
||||
You can help support the continued development of open, free and private communications systems by donating via one of the following channels:
|
||||
For this to be possible, I need your help. Please support the continued development of open, free and private communications systems by donating via one of the following channels:
|
||||
|
||||
- Monero:
|
||||
```
|
||||
@@ -378,4 +380,5 @@ projects:
|
||||
- [Configobj](https://github.com/DiffSK/configobj) by Michael Foord, Nicola Larosa, Rob Dennis & Eli Courtwright, *BSD License*
|
||||
- [ifaddr](https://github.com/pydron/ifaddr) by Stefan C. Mueller, *MIT License*
|
||||
- [Umsgpack.py](https://github.com/vsergeev/u-msgpack-python) by [Ivan A. Sergeev](https://github.com/vsergeev)
|
||||
- [rnsh](https://github.com/acehoss/rnsh) by [Aaron Heise](https://github.com/acehoss)
|
||||
- [Python](https://www.python.org)
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
>> Reticulum Network Stack
|
||||
|
||||
To understand the foundational philosophy and goals of this system, read the [Zen of Reticulum](Zen%20of%20Reticulum.md).
|
||||
|
||||
Reticulum is the cryptography-based networking stack for building local and wide-area networks with readily available hardware. It can operate even with very high latency and extremely low bandwidth. Reticulum allows you to build wide-area networks with off-the-shelf tools, and offers end-to-end encryption and connectivity, initiator anonymity, autoconfiguring cryptographically backed multi-hop transport, efficient addressing, unforgeable delivery acknowledgements and more.
|
||||
|
||||
The vision of Reticulum is to allow anyone to be their own network operator, and to make it cheap and easy to cover vast areas with a myriad of independent, inter-connectable and autonomous networks. Reticulum **is not** *one* network. It is **a tool** for building *thousands of networks*. Networks without kill-switches, surveillance, censorship and control. Networks that can freely interoperate, associate and disassociate with each other, and require no central oversight. Networks for human beings. *Networks for the people*.
|
||||
|
||||
Reticulum is a complete networking stack, and does not rely on IP or higher layers, but it is possible to use IP as the underlying carrier for Reticulum. It is therefore trivial to tunnel Reticulum over the Internet or private IP networks.
|
||||
|
||||
Having no dependencies on traditional networking stacks frees up overhead that has been used to implement a networking stack built directly on cryptographic principles, allowing resilience and stable functionality, even in open and trustless networks.
|
||||
|
||||
No kernel modules or drivers are required. Reticulum runs completely in userland, and can run on practically any system that runs Python 3.
|
||||
|
||||
>> Read The Manual
|
||||
|
||||
The full documentation for Reticulum is available at [markqvist.github.io/Reticulum/manual/](https://markqvist.github.io/Reticulum/manual/).
|
||||
|
||||
You can also download the [Reticulum manual as a PDF](https://github.com/markqvist/Reticulum/raw/master/docs/Reticulum%20Manual.pdf) or [as an e-book in EPUB format](https://github.com/markqvist/Reticulum/raw/master/docs/Reticulum%20Manual.epub).
|
||||
|
||||
For more info, see [reticulum.network](https://reticulum.network/) and [the FAQ section of the wiki](https://github.com/markqvist/Reticulum/wiki/Frequently-Asked-Questions).
|
||||
|
||||
>> Notable Features
|
||||
|
||||
• Coordination-less globally unique addressing and identification
|
||||
• Fully self-configuring multi-hop routing over heterogeneous carriers
|
||||
• Flexible scalability over heterogeneous topologies
|
||||
• Reticulum can carry data over any mixture of physical mediums and topologies
|
||||
• Low-bandwidth networks can co-exist and interoperate with large, high-bandwidth networks
|
||||
• Initiator anonymity, communicate without revealing your identity
|
||||
• Reticulum does not include source addresses on any packets
|
||||
• Asymmetric X25519 encryption and Ed25519 signatures as a basis for all communication
|
||||
• The foundational Reticulum Identity Keys are 512-bit Elliptic Curve keysets
|
||||
• Forward Secrecy is available for all communication types, both for single packets and over links
|
||||
• Reticulum uses the following format for encrypted tokens:
|
||||
• Ephemeral per-packet and link keys and derived from an ECDH key exchange on Curve25519
|
||||
• AES-256 in CBC mode with PKCS7 padding
|
||||
• HMAC using SHA256 for authentication
|
||||
• IVs are generated through os.urandom()
|
||||
• Unforgeable packet delivery confirmations
|
||||
• Flexible and extensible interface system
|
||||
• Reticulum includes a large variety of built-in interface types
|
||||
• Ability to load and utilise custom user- or community-supplied interface types
|
||||
• Easily create your own custom interfaces for communicating over anything
|
||||
• Authentication and virtual network segmentation on all supported interface types
|
||||
• An intuitive and easy-to-use API
|
||||
• Simpler and easier to use than sockets APIs, but more powerful
|
||||
• Makes building distributed and decentralised applications much simpler
|
||||
• Reliable and efficient transfer of arbitrary amounts of data
|
||||
• Reticulum can handle a few bytes of data or files of many gigabytes
|
||||
• Sequencing, compression, transfer coordination and checksumming are automatic
|
||||
• The API is very easy to use, and provides transfer progress
|
||||
• Lightweight, flexible and expandable Request/Response mechanism
|
||||
• Efficient link establishment
|
||||
• Total cost of setting up an encrypted and verified link is only 3 packets, totalling 297 bytes
|
||||
• Low cost of keeping links open at only 0.44 bits per second
|
||||
• Reliable sequential delivery with Channel and Buffer mechanisms
|
||||
|
||||
>> Reference Implementation
|
||||
|
||||
The Python code in this repository is the Reference Implementation of Reticulum. The Reticulum Protocol is defined entirely and authoritatively by this reference implementation, and its associated manual. It is maintained by Mark Qvist, identified by the Reticulum Identity `B333<bc7291552be7a58f361522990465165c>`b.
|
||||
|
||||
Compatibility with the Reticulum Protocol is defined as having full interoperability, and sufficient functional parity with this reference implementation. Any specific protocol implementation that achieves this is Reticulum. Any that does not is not Reticulum.
|
||||
|
||||
The reference implementation is licensed under the Reticulum License.
|
||||
|
||||
The Reticulum Protocol was dedicated to the Public Domain in 2016.
|
||||
|
||||
>> Examples of Reticulum Applications
|
||||
|
||||
If you want to quickly get an idea of what Reticulum can do, take a look at the [Programs Using Reticulum](https://reticulum.network/manual/software.html) section of the manual, or the following resources:
|
||||
|
||||
• [LXMF](https://github.com/markqvist/lxmf) is a distributed, delay and disruption tolerant message transfer
|
||||
protocol built on Reticulum
|
||||
|
||||
• The [LXST](https://github.com/markqvist/lxst) protocol and framework provides real-time audio and signals
|
||||
transport over Reticulum. It includes primitives and utilities for building voice-based applications and
|
||||
hardware devices, such as the `B333rnphone`b program, that can be used to build hardware telephones.
|
||||
|
||||
• For an off-grid, encrypted and resilient mesh communications platform, see [Nomad Network](https://github.com/markqvist/NomadNet)
|
||||
|
||||
• The Android, Linux, macOS and Windows app [Sideband](https://github.com/markqvist/Sideband) has a graphical
|
||||
interface and many advanced features, such as file transfers, image and voice messages, real-time voice calls,
|
||||
a distributed telemetry system, mapping capabilities and full plugin extensibility.
|
||||
|
||||
• [MeshChatX](https://git.quad4.io/RNS-Things/MeshChatX) is a full-featured LXMF client with many built-in tools
|
||||
and functionalities, that also supports image and voice messages, file transfers and voice calls. It also
|
||||
includes a built-in page browser for browsing Nomad Network nodes.
|
||||
|
||||
• You can use the included [rnsh](https://reticulum.network/manual/using.html#the-rnsh-utility) program to
|
||||
establish remote shell sessions over Reticulum.
|
||||
|
||||
>> Where can Reticulum be used?
|
||||
|
||||
Over practically any medium that can support at least a half-duplex channel with greater throughput than 5 bits per second, and an MTU of 500 bytes. Data radios, modems, LoRa radios, serial lines, AX.25 TNCs, amateur radio digital modes, WiFi and Ethernet devices, free-space optical links, and similar systems are all examples of the types of physical devices Reticulum can use.
|
||||
|
||||
An open-source LoRa-based interface called [RNode](https://markqvist.github.io/Reticulum/manual/hardware.html#rnode) has been designed specifically for use with Reticulum. It is possible to build yourself, or it can be purchased as a complete transceiver that just needs a USB connection to the host.
|
||||
|
||||
Reticulum can also be encapsulated over existing IP networks, so there's nothing stopping you from using it over wired Ethernet, your local WiFi network or the Internet, where it'll work just as well. In fact, one of the strengths of Reticulum is how easily it allows you to connect different mediums into a self-configuring, resilient and encrypted mesh, using any available mixture of available infrastructure.
|
||||
|
||||
As an example, it's possible to set up a Raspberry Pi connected to both a LoRa radio, a packet radio TNC and a WiFi network. Once the interfaces are configured, Reticulum will take care of the rest, and any device on the WiFi network can communicate with nodes on the LoRa and packet radio sides of the network, and vice versa.
|
||||
|
||||
>> How do I get started?
|
||||
|
||||
The best way to get started with the Reticulum Network Stack depends on what you want to do. For full details and examples, have a look at the [Getting Started Fast](https://markqvist.github.io/Reticulum/manual/gettingstartedfast.html) section of the [Reticulum Manual](https://markqvist.github.io/Reticulum/manual/).
|
||||
|
||||
To simply install Reticulum and related utilities on your system, the easiest way is via `B333pip`b. You can then start any program that uses Reticulum, or start Reticulum as a system service with [the rnsd utility](https://markqvist.github.io/Reticulum/manual/using.html#the-rnsd-utility).
|
||||
|
||||
`B333
|
||||
`=
|
||||
pip install rns
|
||||
`=
|
||||
`b
|
||||
|
||||
If you are using an operating system that blocks normal user package installation via `B333pip`b, you can return `B333pip`b to normal behaviour by editing the `B333~/.config/pip/pip.conf`b file, and adding the following directive in the `B333[global]`b section:
|
||||
|
||||
`B333
|
||||
`=
|
||||
[global]
|
||||
break-system-packages = true
|
||||
`=
|
||||
`b
|
||||
|
||||
Alternatively, you can use the `B333pipx`b tool to install Reticulum in an isolated environment:
|
||||
|
||||
`B333
|
||||
`=
|
||||
pipx install rns
|
||||
`=
|
||||
`b
|
||||
|
||||
When first started, Reticulum will create a default configuration file, providing basic connectivity to other Reticulum peers that might be locally reachable. The default config file contains a few examples, and references for creating a more complex configuration.
|
||||
|
||||
If you have an old version of `B333pip`b on your system, you may need to upgrade it first with `B333pip install pip --upgrade`b. If you no not already have `B333pip`b installed, you can install it using the package manager of your system with `B333sudo apt install python3-pip`b or similar.
|
||||
|
||||
For more detailed examples on how to expand communication over many mediums such as packet radio or LoRa, serial ports, or over fast IP links and the Internet using the UDP and TCP interfaces, take a look at the [Supported Interfaces](https://markqvist.github.io/Reticulum/manual/interfaces.html) section of the [Reticulum Manual](https://markqvist.github.io/Reticulum/manual/).
|
||||
|
||||
>> Included Utilities
|
||||
Reticulum includes a range of useful utilities for managing your networks, viewing status and information, and other tasks. You can read more about these programs in the [Included Utility Programs](https://markqvist.github.io/Reticulum/manual/using.html#included-utility-programs) section of the [Reticulum Manual](https://markqvist.github.io/Reticulum/manual/).
|
||||
|
||||
• The system daemon `B333rnsd`b for running Reticulum as an always-available service
|
||||
• An interface status utility called `B333rnstatus`b, that displays information about interfaces
|
||||
• The path lookup and management tool `B333rnpath`b letting you view and modify path tables
|
||||
• A diagnostics tool called `B333rnprobe`b for checking connectivity to destinations
|
||||
• A simple file transfer program called `B333rncp`b making it easy to transfer files between systems
|
||||
• The identity management and encryption utility `B333rnid`b let's you manage Identities and encrypt/decrypt files
|
||||
• The `B333rnsh`b program allows you to establish fully interactive shell session with remote systems
|
||||
• The remote command execution program `B333rnx`b let's you run simple commands and programs and retrieve output from remote systems
|
||||
• The `B333rngit`b program provides a full multi-repository Git node for serving repositories over Reticulum
|
||||
• The included `B333git-remote-rns`b helper allows you to interact with Git repositories over Reticulum
|
||||
|
||||
All tools, including `B333rnx`b and `B333rncp`b, work reliably and well even over very low-bandwidth links like LoRa or Packet Radio. For full-featured remote shells over Reticulum, also have a look at the [rnsh](https://github.com/acehoss/rnsh) program.
|
||||
|
||||
>> Supported interface types and devices
|
||||
|
||||
Reticulum implements a range of generalised interface types that covers most of the communications hardware that Reticulum can run over. If your hardware is not supported, it's [simple to implement a custom interface module](https://markqvist.github.io/Reticulum/manual/interfaces.html#custom-interfaces).
|
||||
|
||||
Currently, the following built-in interfaces are supported:
|
||||
|
||||
• Any Ethernet device
|
||||
• LoRa using [RNode](https://unsigned.io/rnode/)
|
||||
• Packet Radio TNCs (with or without AX.25)
|
||||
• KISS-compatible hardware and software modems
|
||||
• Any device with a serial port
|
||||
• TCP over IP networks
|
||||
• UDP over IP networks
|
||||
• External programs via stdio or pipes
|
||||
• Custom hardware via stdio or pipes
|
||||
|
||||
>> Performance
|
||||
Reticulum targets a *very* wide usable performance envelope, but prioritises functionality and performance on low-bandwidth mediums. The goal is to provide a dynamic performance envelope from 250 bits per second, to 1 gigabit per second on normal hardware.
|
||||
|
||||
Currently, the usable performance envelope is approximately 150 bits per second to 500 megabits per second, with physical mediums faster than that not being saturated. Performance beyond the current level is intended for future upgrades, but not highly prioritised at this point in time.
|
||||
|
||||
>> Current Status
|
||||
All core protocol features are implemented and functioning, but additions will probably occur as real-world use is explored and understood. The API and wire-format can be considered stable.
|
||||
|
||||
>> Dependencies
|
||||
The installation of the default `B333rns`b package requires only two external dependencies, listed below. Almost all systems and distributions have readily available packages for these dependencies, and when the `B333rns`b package is installed with `B333pip`b, they will be downloaded and installed as well.
|
||||
|
||||
• [PyCA/cryptography](https://github.com/pyca/cryptography)
|
||||
• [pyserial](https://github.com/pyserial/pyserial)
|
||||
|
||||
On more unusual systems, and in some rare cases, it might not be possible to install or even compile one or more of the above modules. In such situations, you can use the `B333rnspure`b package instead, which require no external dependencies for installation. Please note that the contents of the `B333rns`b and `B333rnspure`b packages are *identical*. The only difference is that the `B333rnspure`b package lists no dependencies required for installation.
|
||||
|
||||
No matter how Reticulum is installed and started, it will load external dependencies only if they are *needed* and *available*. If for example you want to use Reticulum on a system that cannot support [pyserial](https://github.com/pyserial/pyserial), it is perfectly possible to do so using the `B333rnspure`b package, but Reticulum will not be able to use serial-based interfaces. All other available modules will still be loaded when needed.
|
||||
|
||||
**Please Note!** If you use the `B333rnspure`b package to run Reticulum on systems that do not support [PyCA/cryptography](https://github.com/pyca/cryptography), it is important that you read and understand the [Cryptographic Primitives](#cryptographic-primitives) section of this document.
|
||||
|
||||
>> Bootstrapping Connectivity
|
||||
|
||||
Reticulum is not a service you subscribe to, nor is it a single global network you "join". Reticulum provides functionality for discovering available public interfaces over the network itself, and the broader community has provided various directories of publicly available entrypoints to bootstrap connectivity.
|
||||
|
||||
To learn how to establish initial connectivity over Reticulum, read the [Bootstrapping Connectivity](https://reticulum.network/manual/gettingstartedfast.html#bootstrapping-connectivity) section of the manual.
|
||||
|
||||
If you already have a general idea of how this works, you can use community-run sites such as [directory.rns.recipes](https://directory.rns.recipes/) and [rmap.world](https://rmap.world) to find interface definitions for initial connectivity to the global distributed Reticulum backbone.
|
||||
|
||||
>> Public Testnet
|
||||
***Important!** Historically, a developer-targeted testnet was made available by the Reticulum project itself. As the amount of global Reticulum nodes and entrypoints have grown to a substantial quantity, this public testnet, including the Amsterdam Testnet entrypoint, has now been decommissioned. If your still have instances that relied on this entrypoint for connectivity, transition to using the distributed backbone instead. Reticulum now includes a full on-network interface discovery and connectivity bootstrapping system. Read the [Bootstrapping Connectivity](https://reticulum.network/manual/gettingstartedfast.html#bootstrapping-connectivity) section of the manual for pointers.*
|
||||
|
||||
>> Support Reticulum
|
||||
For this to be possible, I need your help. Please support the continued development of open, free and private communications systems by donating via one of the following channels:
|
||||
|
||||
• Monero:
|
||||
84FpY1QbxHcgdseePYNmhTHcrgMX4nFfBYtz2GKYToqHVVhJp8Eaw1Z1EedRnKD19b3B8NiLCGVxzKV17UMmmeEsCrPyA5w
|
||||
|
||||
• Bitcoin
|
||||
bc1pgqgu8h8xvj4jtafslq396v7ju7hkgymyrzyqft4llfslz5vp99psqfk3a6
|
||||
|
||||
• Ethereum
|
||||
0x91C421DdfB8a30a49A71d63447ddb54cEBe3465E
|
||||
|
||||
• Liberapay: https://liberapay.com/Reticulum/
|
||||
|
||||
• Ko-Fi: https://ko-fi.com/markqvist
|
||||
|
||||
>> Cryptographic Primitives
|
||||
Reticulum uses a simple suite of efficient, strong and well-tested cryptographic primitives, with widely available implementations that can be used both on general-purpose CPUs and on microcontrollers.
|
||||
|
||||
One of the primary considerations for choosing this particular set of primitives is that they can be implemented *safely* with relatively few pitfalls, on practically all current computing platforms.
|
||||
|
||||
The primitives listed here **are authoritative**. Anything claiming to be Reticulum, but not using these exact primitives **is not** Reticulum, and possibly an intentionally compromised or weakened clone. The utilised primitives are:
|
||||
|
||||
• Reticulum Identity Keys are 512-bit Curve25519 keysets
|
||||
• A 256-bit Ed25519 key for signatures
|
||||
• A 256-bit X22519 key for ECDH key exchanges
|
||||
• HKDF for key derivation
|
||||
• Encrypted tokens are based on the [Fernet spec](https://github.com/fernet/spec/)
|
||||
• Ephemeral keys derived from an ECDH key exchange on Curve25519
|
||||
• HMAC using SHA256 for message authentication
|
||||
• IVs must be generated through `B333os.urandom()`b or better
|
||||
• AES-256 in CBC mode with PKCS7 padding
|
||||
• No Fernet version and timestamp metadata fields
|
||||
• SHA-256
|
||||
• SHA-512
|
||||
|
||||
In the default installation configuration, the `B333X25519`b, `B333Ed25519`b, and `B333AES-256-CBC`b primitives are provided by [OpenSSL](https://www.openssl.org/) (via the [PyCA/cryptography](https://github.com/pyca/cryptography) package). The hashing functions `B333SHA-256`b and `B333SHA-512`b are provided by the standard Python [hashlib](https://docs.python.org/3/library/hashlib.html). The `B333HKDF`b, `B333HMAC`b, `B333Token`b primitives, and the `B333PKCS7`b padding function are always provided by the following internal implementations:
|
||||
|
||||
• [HKDF.py](RNS/Cryptography/HKDF.py)
|
||||
• [HMAC.py](RNS/Cryptography/HMAC.py)
|
||||
• [Token.py](RNS/Cryptography/Token.py)
|
||||
• [PKCS7.py](RNS/Cryptography/PKCS7.py)
|
||||
|
||||
Reticulum also includes a complete implementation of all necessary primitives in pure Python. If OpenSSL and PyCA are not available on the system when Reticulum is started, Reticulum will instead use the internal pure-python primitives. A trivial consequence of this is performance, with the OpenSSL backend being *much* faster. The most important consequence however, is the potential loss of security by using primitives that has not seen the same amount of scrutiny, testing and review as those from OpenSSL.
|
||||
|
||||
Please note that by default, installing Reticulum will **require** OpenSSL and PyCA to also be automatically installed if not already available. It is only possible to use the pure-python primitives if this requirement is specifically overridden by the user, for example by installing the `B333rnspure`b package instead of the normal `B333rns`b package, or by running directly from local source-code.
|
||||
|
||||
If you want to use the internal pure-python primitives, it is **highly advisable** that you have a good understanding of the risks that this pose, and make an informed decision on whether those risks are acceptable to you.
|
||||
|
||||
Reticulum is relatively young software, and should be considered as such. While it has been built with cryptography best-practices very foremost in mind, it _has not_ been externally security audited, and there could very well be privacy or security breaking bugs. If you want to help out, or help sponsor an audit, please do get in touch.
|
||||
|
||||
>> Acknowledgements & Credits
|
||||
Reticulum can only exist because of the mountain of Open Source work it was built on top of, the contributions of everyone involved, and everyone that has supported the project through the years. To everyone who has helped, thank you so much.
|
||||
|
||||
A number of other modules and projects are either part of, or used by Reticulum. Sincere thanks to the authors and contributors of the following projects:
|
||||
|
||||
• [PyCA/cryptography](https://github.com/pyca/cryptography), *BSD License*
|
||||
• [Pure-25519](https://github.com/warner/python-pure25519) by [Brian Warner](https://github.com/warner), *MIT License*
|
||||
• [Pysha2](https://github.com/thomdixon/pysha2) by [Thom Dixon](https://github.com/thomdixon), *MIT License*
|
||||
• [Python AES-128](https://github.com/orgurar/python-aes) by [Or Gur Arie](https://github.com/orgurar), *MIT License*
|
||||
• [Python AES-256](https://github.com/boppreh/aes) by [BoppreH](https://github.com/boppreh), *MIT License*
|
||||
• [Curve25519.py](https://gist.github.com/nickovs/cc3c22d15f239a2640c185035c06f8a3#file-curve25519-py) by [Nicko van Someren](https://gist.github.com/nickovs), *Public Domain*
|
||||
• [I2Plib](https://github.com/l-n-s/i2plib) by [Viktor Villainov](https://github.com/l-n-s)
|
||||
• [PySerial](https://github.com/pyserial/pyserial) by Chris Liechti, *BSD License*
|
||||
• [Configobj](https://github.com/DiffSK/configobj) by Michael Foord, Nicola Larosa, Rob Dennis & Eli Courtwright, *BSD License*
|
||||
• [ifaddr](https://github.com/pydron/ifaddr) by Stefan C. Mueller, *MIT License*
|
||||
• [Umsgpack.py](https://github.com/vsergeev/u-msgpack-python) by [Ivan A. Sergeev](https://github.com/vsergeev)
|
||||
• [rnsh](https://github.com/acehoss/rnsh) by [Aaron Heise](https://github.com/acehoss)
|
||||
• [Python](https://www.python.org)
|
||||
+3
-1
@@ -92,7 +92,9 @@ class StreamDataMessage(MessageBase):
|
||||
self.data = raw[2:]
|
||||
|
||||
if self.compressed:
|
||||
self.data = bz2.decompress(self.data)
|
||||
decompressor = bz2.BZ2Decompressor()
|
||||
self.data = decompressor.decompress(self.data, max_length=RawChannelWriter.MAX_CHUNK_LEN)
|
||||
if not decompressor.eof: raise IOError("Decompressed buffer chunk exceeds maximum legitimate size")
|
||||
|
||||
|
||||
class RawChannelReader(RawIOBase, AbstractContextManager):
|
||||
|
||||
+12
-22
@@ -295,33 +295,26 @@ class Destination:
|
||||
app_data = returned_app_data
|
||||
|
||||
signed_data = self.hash+self.identity.get_public_key()+self.name_hash+random_hash+ratchet
|
||||
if app_data != None:
|
||||
signed_data += app_data
|
||||
if app_data != None: signed_data += app_data
|
||||
|
||||
signature = self.identity.sign(signed_data)
|
||||
announce_data = self.identity.get_public_key()+self.name_hash+random_hash+ratchet+signature
|
||||
|
||||
if app_data != None:
|
||||
announce_data += app_data
|
||||
if app_data != None: announce_data += app_data
|
||||
|
||||
self.path_responses[tag] = [time.time(), announce_data]
|
||||
|
||||
if path_response:
|
||||
announce_context = RNS.Packet.PATH_RESPONSE
|
||||
else:
|
||||
announce_context = RNS.Packet.NONE
|
||||
if path_response: announce_context = RNS.Packet.PATH_RESPONSE
|
||||
else: announce_context = RNS.Packet.NONE
|
||||
|
||||
if ratchet:
|
||||
context_flag = RNS.Packet.FLAG_SET
|
||||
else:
|
||||
context_flag = RNS.Packet.FLAG_UNSET
|
||||
if ratchet: context_flag = RNS.Packet.FLAG_SET
|
||||
else: context_flag = RNS.Packet.FLAG_UNSET
|
||||
|
||||
announce_packet = RNS.Packet(self, announce_data, RNS.Packet.ANNOUNCE, context = announce_context,
|
||||
attached_interface = attached_interface, context_flag=context_flag)
|
||||
if send:
|
||||
announce_packet.send()
|
||||
else:
|
||||
return announce_packet
|
||||
|
||||
if send: announce_packet.send()
|
||||
else: return announce_packet
|
||||
|
||||
def accepts_links(self, accepts = None):
|
||||
"""
|
||||
@@ -330,13 +323,10 @@ class Destination:
|
||||
:param accepts: If ``True`` or ``False``, this method sets whether the destination accepts incoming link requests. If not provided or ``None``, the method returns whether the destination currently accepts link requests.
|
||||
:returns: ``True`` or ``False`` depending on whether the destination accepts incoming link requests, if the *accepts* parameter is not provided or ``None``.
|
||||
"""
|
||||
if accepts == None:
|
||||
return self.accept_link_requests
|
||||
if accepts == None: return self.accept_link_requests
|
||||
|
||||
if accepts:
|
||||
self.accept_link_requests = True
|
||||
else:
|
||||
self.accept_link_requests = False
|
||||
if accepts: self.accept_link_requests = True
|
||||
else: self.accept_link_requests = False
|
||||
|
||||
def set_link_established_callback(self, callback):
|
||||
"""
|
||||
|
||||
+143
-36
@@ -94,17 +94,25 @@ class Identity:
|
||||
known_ratchets = {}
|
||||
|
||||
ratchet_persist_lock = threading.Lock()
|
||||
known_destinations_lock = threading.Lock()
|
||||
|
||||
@staticmethod
|
||||
def remember(packet_hash, destination_hash, public_key, app_data = None):
|
||||
if len(public_key) != Identity.KEYSIZE//8:
|
||||
raise TypeError("Can't remember "+RNS.prettyhexrep(destination_hash)+", the public key size of "+str(len(public_key))+" is not valid.", RNS.LOG_ERROR)
|
||||
else:
|
||||
Identity.known_destinations[destination_hash] = [time.time(), packet_hash, public_key, app_data]
|
||||
|
||||
with Identity.known_destinations_lock:
|
||||
if not destination_hash in Identity.known_destinations:
|
||||
Identity.known_destinations[destination_hash] = [time.time(), packet_hash, public_key, app_data, 0]
|
||||
else:
|
||||
entry = Identity.known_destinations[destination_hash]
|
||||
entry[0] = time.time()
|
||||
entry[1] = packet_hash
|
||||
entry[2] = public_key
|
||||
entry[3] = app_data
|
||||
|
||||
@staticmethod
|
||||
def recall(target_hash, from_identity_hash=False):
|
||||
def recall(target_hash, from_identity_hash=False, _no_use=False):
|
||||
"""
|
||||
Recall identity for a destination or identity hash. By default, this function
|
||||
will return the identity associated with a given *destination* hash. As an
|
||||
@@ -120,6 +128,7 @@ class Identity:
|
||||
if from_identity_hash:
|
||||
for destination_hash in Identity.known_destinations:
|
||||
if target_hash == Identity.truncated_hash(Identity.known_destinations[destination_hash][2]):
|
||||
if not _no_use: RNS.Reticulum.get_instance()._used_destination_data(destination_hash)
|
||||
identity_data = Identity.known_destinations[destination_hash]
|
||||
identity = Identity(create_keys=False)
|
||||
identity.load_public_key(identity_data[2])
|
||||
@@ -130,6 +139,7 @@ class Identity:
|
||||
|
||||
else:
|
||||
if target_hash in Identity.known_destinations:
|
||||
if not _no_use: RNS.Reticulum.get_instance()._used_destination_data(target_hash)
|
||||
identity_data = Identity.known_destinations[target_hash]
|
||||
identity = Identity(create_keys=False)
|
||||
identity.load_public_key(identity_data[2])
|
||||
@@ -146,7 +156,7 @@ class Identity:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def recall_app_data(destination_hash):
|
||||
def recall_app_data(destination_hash, _no_use=False):
|
||||
"""
|
||||
Recall last heard app_data for a destination hash.
|
||||
|
||||
@@ -154,13 +164,14 @@ class Identity:
|
||||
:returns: *Bytes* containing app_data, or *None* if the destination is unknown.
|
||||
"""
|
||||
if destination_hash in Identity.known_destinations:
|
||||
if not _no_use: RNS.Reticulum.get_instance()._used_destination_data(destination_hash)
|
||||
app_data = Identity.known_destinations[destination_hash][3]
|
||||
return app_data
|
||||
else:
|
||||
return None
|
||||
|
||||
else: return None
|
||||
|
||||
@staticmethod
|
||||
def save_known_destinations():
|
||||
def save_known_destinations(background=False, recombine=True):
|
||||
# TODO: Improve the storage method so we don't have to
|
||||
# deserialize and serialize the entire table on every
|
||||
# save, but the only changes. It might be possible to
|
||||
@@ -181,32 +192,33 @@ class Identity:
|
||||
Identity.saving_known_destinations = True
|
||||
save_start = time.time()
|
||||
|
||||
storage_known_destinations = {}
|
||||
if os.path.isfile(RNS.Reticulum.storagepath+"/known_destinations"):
|
||||
if recombine:
|
||||
storage_known_destinations = {}
|
||||
if os.path.isfile(RNS.Reticulum.storagepath+"/known_destinations"):
|
||||
try:
|
||||
with open(RNS.Reticulum.storagepath+"/known_destinations","rb") as file:
|
||||
storage_known_destinations = umsgpack.load(file)
|
||||
|
||||
except: pass
|
||||
|
||||
try:
|
||||
with open(RNS.Reticulum.storagepath+"/known_destinations","rb") as file:
|
||||
storage_known_destinations = umsgpack.load(file)
|
||||
|
||||
except: pass
|
||||
for destination_hash in storage_known_destinations:
|
||||
if not destination_hash in Identity.known_destinations:
|
||||
with Identity.known_destinations_lock:
|
||||
Identity.known_destinations[destination_hash] = storage_known_destinations[destination_hash]
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Skipped recombining known destinations from disk, since an error occurred: "+str(e), RNS.LOG_WARNING)
|
||||
|
||||
try:
|
||||
for destination_hash in storage_known_destinations:
|
||||
if not destination_hash in Identity.known_destinations:
|
||||
Identity.known_destinations[destination_hash] = storage_known_destinations[destination_hash]
|
||||
except Exception as e:
|
||||
RNS.log("Skipped recombining known destinations from disk, since an error occurred: "+str(e), RNS.LOG_WARNING)
|
||||
|
||||
RNS.log("Saving "+str(len(Identity.known_destinations))+" known destinations to storage...", RNS.LOG_DEBUG)
|
||||
RNS.log("Saving "+str(len(Identity.known_destinations))+" known destinations to storage...", RNS.LOG_VERBOSE)
|
||||
with open(RNS.Reticulum.storagepath+"/known_destinations","wb") as file:
|
||||
umsgpack.dump(Identity.known_destinations.copy(), file)
|
||||
|
||||
save_time = time.time() - save_start
|
||||
if save_time < 1:
|
||||
time_str = str(round(save_time*1000,2))+"ms"
|
||||
else:
|
||||
time_str = str(round(save_time,2))+"s"
|
||||
if save_time < 1: time_str = str(round(save_time*1000,2))+"ms"
|
||||
else: time_str = str(round(save_time,2))+"s"
|
||||
|
||||
RNS.log("Saved known destinations to storage in "+time_str, RNS.LOG_DEBUG)
|
||||
RNS.log("Saved known destinations to storage in "+time_str, RNS.LOG_VERBOSE)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Error while saving known destinations to disk, the contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
@@ -217,6 +229,7 @@ class Identity:
|
||||
@staticmethod
|
||||
def load_known_destinations():
|
||||
if os.path.isfile(RNS.Reticulum.storagepath+"/known_destinations"):
|
||||
st = time.time()
|
||||
try:
|
||||
with open(RNS.Reticulum.storagepath+"/known_destinations","rb") as file:
|
||||
loaded_known_destinations = umsgpack.load(file)
|
||||
@@ -224,15 +237,102 @@ class Identity:
|
||||
Identity.known_destinations = {}
|
||||
for known_destination in loaded_known_destinations:
|
||||
if len(known_destination) == RNS.Reticulum.TRUNCATED_HASHLENGTH//8:
|
||||
Identity.known_destinations[known_destination] = loaded_known_destinations[known_destination]
|
||||
if len(loaded_known_destinations[known_destination]) < 5:
|
||||
e = loaded_known_destinations[known_destination]
|
||||
loaded_known_destinations[known_destination] = [e[0], e[1], e[2], e[3], 0]
|
||||
|
||||
RNS.log("Loaded "+str(len(Identity.known_destinations))+" known destination from storage", RNS.LOG_VERBOSE)
|
||||
with Identity.known_destinations_lock:
|
||||
Identity.known_destinations[known_destination] = loaded_known_destinations[known_destination]
|
||||
|
||||
RNS.log(f"Loaded {len(Identity.known_destinations)} known destination from storage in {RNS.prettyshorttime(time.time()-st)}", RNS.LOG_VERBOSE)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Error loading known destinations from disk, file will be recreated on exit", RNS.LOG_ERROR)
|
||||
RNS.trace_exception(e)
|
||||
else:
|
||||
RNS.log("Destinations file does not exist, no known destinations loaded", RNS.LOG_VERBOSE)
|
||||
|
||||
@staticmethod
|
||||
def _used_destination_data(destination_hash):
|
||||
with Identity.known_destinations_lock:
|
||||
if destination_hash in Identity.known_destinations:
|
||||
if not Identity.known_destinations[destination_hash][4] < 0:
|
||||
Identity.known_destinations[destination_hash][4] = time.time()
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _retain_destination_data(destination_hash):
|
||||
with Identity.known_destinations_lock:
|
||||
if destination_hash in Identity.known_destinations:
|
||||
Identity.known_destinations[destination_hash][4] = -1
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _unretain_destination_data(destination_hash):
|
||||
with Identity.known_destinations_lock:
|
||||
if destination_hash in Identity.known_destinations:
|
||||
Identity.known_destinations[destination_hash][4] = time.time()
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def clean_known_destinations():
|
||||
now = time.time()
|
||||
st = now
|
||||
total = len(Identity.known_destinations)
|
||||
stale = []
|
||||
no_path = 0
|
||||
retained = 0
|
||||
never_used = 0
|
||||
for destination_hash in Identity.known_destinations:
|
||||
try:
|
||||
if RNS.Transport.has_path(destination_hash): has_path = True
|
||||
else:
|
||||
has_path = False
|
||||
no_path += 1
|
||||
|
||||
with Identity.known_destinations_lock:
|
||||
if destination_hash in Identity.known_destinations:
|
||||
last_announce = Identity.known_destinations[destination_hash][0]
|
||||
last_use = 0
|
||||
was_used = False
|
||||
is_retained = False
|
||||
|
||||
if Identity.known_destinations[destination_hash][4] > 0:
|
||||
was_used = True
|
||||
last_use = Identity.known_destinations[destination_hash][4]
|
||||
|
||||
elif Identity.known_destinations[destination_hash][4] == 0:
|
||||
was_used = False
|
||||
never_used += 1
|
||||
|
||||
elif Identity.known_destinations[destination_hash][4] == -1:
|
||||
is_retained = True
|
||||
retained += 1
|
||||
|
||||
unused_for = time.time() - Identity.known_destinations[destination_hash][4]
|
||||
|
||||
if not is_retained and not has_path:
|
||||
if not was_used and now - last_announce > RNS.Transport.UNUSED_DESTINATION_LINGER: stale.append(destination_hash)
|
||||
elif unused_for > RNS.Transport.DESTINATION_TIMEOUT*1.25: stale.append(destination_hash)
|
||||
|
||||
except Exception as e: RNS.log(f"Faulty entry for {RNS.prettyhexrep(destination_hash)} while cleaning known destinations: {e}", RNS.LOG_DEBUG)
|
||||
|
||||
removed = 0
|
||||
for destination_hash in stale:
|
||||
with Identity.known_destinations_lock:
|
||||
if destination_hash in Identity.known_destinations:
|
||||
Identity.known_destinations.pop(destination_hash)
|
||||
removed += 1
|
||||
|
||||
# RNS.log(f"Total destinations: {total}, stale: {len(stale)}, removed: {removed}, no path: {no_path}, never used: {never_used}, with path: {total-no_path}, used: {total-never_used}, retained: {retained}. Completed in {RNS.prettyshorttime(time.time()-st)}", RNS.LOG_WARNING) # TODO: Remove
|
||||
if not RNS.Transport.owner.is_connected_to_shared_instance: Identity.save_known_destinations(recombine=False)
|
||||
|
||||
@staticmethod
|
||||
def full_hash(data):
|
||||
"""
|
||||
@@ -331,33 +431,40 @@ class Identity:
|
||||
def _clean_ratchets():
|
||||
RNS.log("Cleaning ratchets...", RNS.LOG_DEBUG)
|
||||
try:
|
||||
count = 0
|
||||
removed = 0
|
||||
not_known = 0
|
||||
now = time.time()
|
||||
ratchetdir = RNS.Reticulum.storagepath+"/ratchets"
|
||||
if os.path.isdir(ratchetdir):
|
||||
for filename in os.listdir(ratchetdir):
|
||||
count += 1
|
||||
try:
|
||||
expired = False
|
||||
corrupted = False
|
||||
with open(f"{ratchetdir}/{filename}", "rb") as rf:
|
||||
# TODO: Remove individual ratchet file if corrupt
|
||||
try:
|
||||
ratchet_data = umsgpack.unpackb(rf.read())
|
||||
if now > ratchet_data["received"]+Identity.RATCHET_EXPIRY:
|
||||
expired = True
|
||||
if now > ratchet_data["received"]+Identity.RATCHET_EXPIRY: expired = True
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Corrupted ratchet data while reading {ratchetdir}/{filename}, removing file", RNS.LOG_ERROR)
|
||||
corrupted = True
|
||||
|
||||
if expired or corrupted:
|
||||
destination_hash = bytes.fromhex(filename)
|
||||
if not destination_hash in RNS.Identity.known_destinations: unknown = True; not_known += 1
|
||||
else: unknown = False
|
||||
|
||||
if expired or corrupted or unknown:
|
||||
os.unlink(f"{ratchetdir}/{filename}")
|
||||
removed += 1
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"An error occurred while cleaning ratchets, in the processing of {ratchetdir}/{filename}.", RNS.LOG_ERROR)
|
||||
RNS.log(f"The contained exception was: {e}", RNS.LOG_ERROR)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"An error occurred while cleaning ratchets. The contained exception was: {e}", RNS.LOG_ERROR)
|
||||
except Exception as e: RNS.log(f"An error occurred while cleaning ratchets. The contained exception was: {e}", RNS.LOG_ERROR)
|
||||
RNS.log(f"Processed {count} ratchets in {RNS.prettytime(time.time()-now)}, not in use {not_known}, removed {removed}", RNS.LOG_DEBUG)
|
||||
|
||||
@staticmethod
|
||||
def get_ratchet(destination_hash):
|
||||
@@ -491,9 +598,9 @@ class Identity:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def persist_data():
|
||||
def persist_data(background=False):
|
||||
if not RNS.Transport.owner.is_connected_to_shared_instance:
|
||||
Identity.save_known_destinations()
|
||||
Identity.save_known_destinations(background=background)
|
||||
|
||||
@staticmethod
|
||||
def exit_handler():
|
||||
|
||||
@@ -65,7 +65,7 @@ class AutoInterface(Interface):
|
||||
|
||||
ALL_IGNORE_IFS = ["lo0"]
|
||||
DARWIN_IGNORE_IFS = ["awdl0", "llw0", "lo0", "en5"]
|
||||
ANDROID_IGNORE_IFS = ["dummy0", "lo", "tun0"]
|
||||
ANDROID_IGNORE_IFS = ["dummy0", "lo", "tun0", "rmnet0", "rmnet1", "rmnet2", "rmnet3", "rmnet4", "rmnet5", "rmnet6", "rmnet7"]
|
||||
|
||||
BITRATE_GUESS = 10*1000*1000
|
||||
|
||||
|
||||
@@ -228,10 +228,10 @@ class BackboneInterface(Interface):
|
||||
if interface.socket:
|
||||
fileno = interface.socket.fileno()
|
||||
if fileno in BackboneInterface.spawned_interface_filenos:
|
||||
try:
|
||||
BackboneInterface.epoll.modify(interface.socket.fileno(), select.EPOLLOUT)
|
||||
try: BackboneInterface.epoll.modify(fileno, select.EPOLLOUT)
|
||||
except Exception as e:
|
||||
RNS.trace_exception(e)
|
||||
RNS.log(f"Error occurred on {interface} while modifying socket EPOLL state: {e}", RNS.LOG_WARNING)
|
||||
raise e
|
||||
|
||||
@staticmethod
|
||||
def __job():
|
||||
@@ -270,8 +270,7 @@ class BackboneInterface(Interface):
|
||||
spawned_interface.receive(received_bytes)
|
||||
|
||||
elif client_socket and fileno == client_socket.fileno() and (event & select.EPOLLOUT):
|
||||
try:
|
||||
written = client_socket.send(spawned_interface.transmit_buffer)
|
||||
try: written = client_socket.send(spawned_interface.transmit_buffer)
|
||||
except Exception as e:
|
||||
written = 0
|
||||
if not spawned_interface.detached: RNS.log(f"Error while writing to {spawned_interface}: {e}", RNS.LOG_DEBUG)
|
||||
@@ -293,7 +292,11 @@ class BackboneInterface(Interface):
|
||||
spawned_interface.receive(b"")
|
||||
|
||||
spawned_interface.transmit_buffer = spawned_interface.transmit_buffer[written:]
|
||||
if len(spawned_interface.transmit_buffer) == 0: BackboneInterface.epoll.modify(fileno, select.EPOLLIN)
|
||||
try:
|
||||
if len(spawned_interface.transmit_buffer) == 0: BackboneInterface.epoll.modify(fileno, select.EPOLLIN)
|
||||
except Exception as e:
|
||||
RNS.log(f"Error while setting EPOLLIN on {spawned_interface}: {e}", RNS.LOG_ERROR)
|
||||
|
||||
spawned_interface.txb += written
|
||||
if spawned_interface.parent_interface: spawned_interface.parent_interface.txb += written
|
||||
|
||||
|
||||
@@ -55,8 +55,8 @@ class Interface:
|
||||
|
||||
# How many samples to use for announce
|
||||
# frequency calculations
|
||||
IA_FREQ_SAMPLES = 12
|
||||
OA_FREQ_SAMPLES = 12
|
||||
IA_FREQ_SAMPLES = 128
|
||||
OA_FREQ_SAMPLES = 128
|
||||
|
||||
# Maximum amount of ingress limited announces
|
||||
# to hold at any given time.
|
||||
@@ -66,12 +66,12 @@ class Interface:
|
||||
# considered to be newly created. Two
|
||||
# hours by default.
|
||||
IC_NEW_TIME = 2*60*60
|
||||
IC_BURST_FREQ_NEW = 3.5
|
||||
IC_BURST_FREQ = 12
|
||||
IC_BURST_FREQ_NEW = 6
|
||||
IC_BURST_FREQ = 35
|
||||
IC_BURST_HOLD = 1*60
|
||||
IC_BURST_PENALTY = 5*60
|
||||
IC_HELD_RELEASE_INTERVAL = 30
|
||||
IC_DEQUE_MIN_SAMPLE = 8
|
||||
IC_BURST_PENALTY = 15
|
||||
IC_HELD_RELEASE_INTERVAL = 2
|
||||
IC_DEQUE_MIN_SAMPLE = 32
|
||||
|
||||
AUTOCONFIGURE_MTU = False
|
||||
FIXED_MTU = False
|
||||
@@ -93,6 +93,7 @@ class Interface:
|
||||
self.spawned_interfaces = None
|
||||
self.tunnel_id = None
|
||||
self.ingress_control = True
|
||||
self.phy_keepalive = False
|
||||
self.ic_max_held_announces = Interface.MAX_HELD_ANNOUNCES
|
||||
self.ic_burst_hold = Interface.IC_BURST_HOLD
|
||||
self.ic_burst_active = False
|
||||
@@ -123,13 +124,14 @@ class Interface:
|
||||
if self.ic_burst_active:
|
||||
if ia_freq < freq_threshold and time.time() > self.ic_burst_activated+self.ic_burst_hold:
|
||||
self.ic_burst_active = False
|
||||
self.ic_held_release = time.time() + self.ic_burst_penalty
|
||||
|
||||
return True
|
||||
|
||||
else:
|
||||
if ia_freq > freq_threshold:
|
||||
self.ic_burst_active = True
|
||||
self.ic_burst_activated = time.time()
|
||||
self.ic_held_release = time.time() + self.ic_burst_penalty
|
||||
return True
|
||||
|
||||
else: return False
|
||||
@@ -174,7 +176,7 @@ class Interface:
|
||||
|
||||
def process_held_announces(self):
|
||||
try:
|
||||
if not self.should_ingress_limit() and len(self.held_announces) > 0 and time.time() > self.ic_held_release:
|
||||
if len(self.held_announces) > 0 and time.time() > self.ic_held_release:
|
||||
freq_threshold = self.ic_burst_freq_new if self.age() < self.ic_new_time else self.ic_burst_freq
|
||||
ia_freq = self.incoming_announce_frequency()
|
||||
if ia_freq < freq_threshold:
|
||||
|
||||
@@ -62,6 +62,7 @@ class ThreadingTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
|
||||
class LocalClientInterface(Interface):
|
||||
RECONNECT_WAIT = 8
|
||||
AUTOCONFIGURE_MTU = True
|
||||
CLIENT_SLEEP_PAUSE_TIMEOUT = 12
|
||||
|
||||
def __init__(self, owner, name, target_port = None, connected_socket=None, socket_path=None):
|
||||
super().__init__()
|
||||
@@ -85,8 +86,9 @@ class LocalClientInterface(Interface):
|
||||
self.frame_buffer = b""
|
||||
self.transmit_buffer = b""
|
||||
|
||||
if RNS.vendor.platformutils.use_epoll():
|
||||
self.epoll_backend = True
|
||||
if RNS.vendor.platformutils.use_epoll(): self.epoll_backend = True
|
||||
|
||||
self.pause_on_client_sleep = False
|
||||
|
||||
if connected_socket != None:
|
||||
self.receives = True
|
||||
@@ -99,6 +101,10 @@ class LocalClientInterface(Interface):
|
||||
|
||||
self.is_connected_to_shared_instance = False
|
||||
|
||||
if RNS.vendor.platformutils.is_android():
|
||||
self.pause_on_client_sleep = True
|
||||
self.pause_timeout = time.time() + self.CLIENT_SLEEP_PAUSE_TIMEOUT
|
||||
|
||||
elif self.socket_path != None:
|
||||
self.receives = True
|
||||
self.target_ip = None
|
||||
@@ -145,6 +151,7 @@ class LocalClientInterface(Interface):
|
||||
self.is_connected_to_shared_instance = True
|
||||
self.never_connected = False
|
||||
|
||||
if RNS.vendor.platformutils.is_android(): self.phy_keepalive = True
|
||||
if self.epoll_backend: BackboneInterface.add_client_socket(self.socket, self)
|
||||
|
||||
return True
|
||||
@@ -185,17 +192,36 @@ class LocalClientInterface(Interface):
|
||||
raise IOError("Attempt to reconnect on a non-initiator local interface")
|
||||
|
||||
|
||||
def send_keepalive(self):
|
||||
if self.online:
|
||||
RNS.log(f"Sending keepalive on {self}", RNS.LOG_DEBUG) # TODO: Remove
|
||||
try:
|
||||
if self.epoll_backend:
|
||||
self.transmit_buffer += bytes([HDLC.FLAG])+bytes([HDLC.FLAG])
|
||||
BackboneInterface.tx_ready(self)
|
||||
|
||||
else:
|
||||
self.writing = True
|
||||
data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG])
|
||||
self.socket.sendall(data)
|
||||
self.writing = False
|
||||
|
||||
except Exception as e: RNS.log(f"Exception occurred while sending keepalive on {self}: {e}", RNS.LOG_ERROR)
|
||||
|
||||
def process_incoming(self, data):
|
||||
self.rxb += len(data)
|
||||
if self.parent_interface != None: self.parent_interface.rxb += len(data)
|
||||
|
||||
try:
|
||||
self.owner.inbound(data, self)
|
||||
try: self.owner.inbound(data, self)
|
||||
except Exception as e:
|
||||
RNS.log(f"An error in the processing of an incoming frame for {self}: {e}", RNS.LOG_ERROR)
|
||||
RNS.log(f"An error occurred in the processing of an incoming frame for {self}: {e}", RNS.LOG_ERROR)
|
||||
RNS.trace_exception(e)
|
||||
|
||||
def process_outgoing(self, data):
|
||||
if self.pause_on_client_sleep and time.time() > self.pause_timeout:
|
||||
RNS.log(f"TX paused for LocalInterface client, dropping outbound packet", RNS.LOG_DEBUG) # TODO: Remove
|
||||
return
|
||||
|
||||
if self.online:
|
||||
try:
|
||||
if self.epoll_backend:
|
||||
@@ -238,13 +264,12 @@ class LocalClientInterface(Interface):
|
||||
frame = self.frame_buffer[frame_start+1:frame_end]
|
||||
frame = frame.replace(bytes([HDLC.ESC, HDLC.FLAG ^ HDLC.ESC_MASK]), bytes([HDLC.FLAG]))
|
||||
frame = frame.replace(bytes([HDLC.ESC, HDLC.ESC ^ HDLC.ESC_MASK]), bytes([HDLC.ESC]))
|
||||
if len(frame) > RNS.Reticulum.HEADER_MINSIZE:
|
||||
self.process_incoming(frame)
|
||||
if len(frame) > RNS.Reticulum.HEADER_MINSIZE: self.process_incoming(frame)
|
||||
self.frame_buffer = self.frame_buffer[frame_end:]
|
||||
else:
|
||||
flags_remaining = False
|
||||
else:
|
||||
flags_remaining = False
|
||||
|
||||
else: flags_remaining = False
|
||||
|
||||
else: flags_remaining = False
|
||||
|
||||
def receive(self, data_in):
|
||||
try:
|
||||
@@ -267,6 +292,8 @@ class LocalClientInterface(Interface):
|
||||
RNS.log("Tearing down "+str(self), RNS.LOG_ERROR)
|
||||
self.teardown()
|
||||
|
||||
if self.pause_on_client_sleep: self.pause_timeout = time.time() + self.CLIENT_SLEEP_PAUSE_TIMEOUT
|
||||
|
||||
def read_loop(self):
|
||||
try:
|
||||
self.frame_buffer = b""
|
||||
@@ -328,7 +355,8 @@ class LocalClientInterface(Interface):
|
||||
if hasattr(self, "parent_interface") and self.parent_interface != None:
|
||||
self.parent_interface.clients -= 1
|
||||
if hasattr(RNS.Transport, "owner") and RNS.Transport.owner != None:
|
||||
RNS.Transport.owner._should_persist_data()
|
||||
background = not self.detached
|
||||
RNS.Transport.owner._should_persist_data(background=background)
|
||||
|
||||
if nowarning == False:
|
||||
RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is being torn down. Restart Reticulum to attempt to open this interface again.", RNS.LOG_ERROR)
|
||||
|
||||
+7
-11
@@ -722,12 +722,9 @@ class Link:
|
||||
pass
|
||||
|
||||
def link_closed(self):
|
||||
for resource in self.incoming_resources:
|
||||
resource.cancel()
|
||||
for resource in self.outgoing_resources:
|
||||
resource.cancel()
|
||||
if self._channel:
|
||||
self._channel._shutdown()
|
||||
for resource in self.incoming_resources: resource.cancel()
|
||||
for resource in self.outgoing_resources: resource.cancel()
|
||||
if self._channel: self._channel._shutdown()
|
||||
|
||||
self.prv = None
|
||||
self.pub = None
|
||||
@@ -741,8 +738,7 @@ class Link:
|
||||
self.destination.links.remove(self)
|
||||
|
||||
if self.callbacks.link_closed != None:
|
||||
try:
|
||||
self.callbacks.link_closed(self)
|
||||
try: self.callbacks.link_closed(self)
|
||||
except Exception as e:
|
||||
RNS.log("Error while executing link closed callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
@@ -1181,7 +1177,7 @@ class Link:
|
||||
resource_hash = packet.data[0:RNS.Identity.HASHLENGTH//8]
|
||||
for resource in self.outgoing_resources:
|
||||
if resource_hash == resource.hash:
|
||||
def job(): resource.validate_proof(packet.data)
|
||||
def job(resource=resource): resource.validate_proof(packet.data)
|
||||
threading.Thread(target=job, daemon=True).start()
|
||||
self.__update_phy_stats(packet, query_shared=True)
|
||||
|
||||
@@ -1323,11 +1319,11 @@ class Link:
|
||||
|
||||
def cancel_outgoing_resource(self, resource):
|
||||
if resource in self.outgoing_resources: self.outgoing_resources.remove(resource)
|
||||
else: RNS.log("Attempt to cancel a non-existing outgoing resource", RNS.LOG_ERROR)
|
||||
else: RNS.log("Attempt to cancel a non-existing outgoing resource", RNS.LOG_WARNING)
|
||||
|
||||
def cancel_incoming_resource(self, resource):
|
||||
if resource in self.incoming_resources: self.incoming_resources.remove(resource)
|
||||
else: RNS.log("Attempt to cancel a non-existing incoming resource", RNS.LOG_ERROR)
|
||||
else: RNS.log("Attempt to cancel a non-existing incoming resource", RNS.LOG_WARNING)
|
||||
|
||||
def ready_for_new_resource(self):
|
||||
if len(self.outgoing_resources) > 0: return False
|
||||
|
||||
+2
-2
@@ -293,7 +293,7 @@ class Packet:
|
||||
|
||||
if RNS.Transport.outbound(self): return self.receipt
|
||||
else:
|
||||
RNS.log("No interfaces could process the outbound packet", RNS.LOG_ERROR)
|
||||
RNS.log("No interfaces could process the outbound packet", RNS.LOG_DEBUG)
|
||||
self.sent = False
|
||||
self.receipt = None
|
||||
return False
|
||||
@@ -315,7 +315,7 @@ class Packet:
|
||||
if RNS.Transport.outbound(self):
|
||||
return self.receipt
|
||||
else:
|
||||
RNS.log("No interfaces could process the outbound packet", RNS.LOG_ERROR)
|
||||
RNS.log("Re-send failed. No interfaces could process the outbound packet", RNS.LOG_WARNING)
|
||||
self.sent = False
|
||||
self.receipt = None
|
||||
return False
|
||||
|
||||
+38
-17
@@ -126,6 +126,7 @@ class Resource:
|
||||
PART_TIMEOUT_FACTOR = 4
|
||||
PART_TIMEOUT_FACTOR_AFTER_RTT = 2
|
||||
PROOF_TIMEOUT_FACTOR = 3
|
||||
HMU_WAIT_FACTOR = 3.5
|
||||
MAX_RETRIES = 16
|
||||
MAX_ADV_RETRIES = 4
|
||||
SENDER_GRACE_TIME = 10.0
|
||||
@@ -193,6 +194,7 @@ class Resource:
|
||||
resource.window_flexibility = Resource.WINDOW_FLEXIBILITY
|
||||
resource.last_activity = time.time()
|
||||
resource.started_transferring = resource.last_activity
|
||||
resource.advertisement_packet = advertisement_packet
|
||||
|
||||
resource.storagepath = RNS.Reticulum.resourcepath+"/"+resource.original_hash.hex()
|
||||
resource.meta_storagepath = resource.storagepath+".meta"
|
||||
@@ -359,6 +361,7 @@ class Resource:
|
||||
self.request_id = request_id
|
||||
self.started_transferring = None
|
||||
self.is_response = is_response
|
||||
self.max_decompressed_size = Resource.AUTO_COMPRESS_MAX_SIZE
|
||||
self.auto_compress_limit = Resource.AUTO_COMPRESS_MAX_SIZE
|
||||
self.auto_compress_option = auto_compress
|
||||
|
||||
@@ -594,15 +597,16 @@ class Resource:
|
||||
extra_wait = retries_used * Resource.PER_RETRY_DELAY
|
||||
|
||||
self.update_eifr()
|
||||
expected_hmu_wait_remaining = (self.sdu*8*self.HMU_WAIT_FACTOR)/self.eifr if self.waiting_for_hmu or self.outstanding_parts == 0 else 0
|
||||
expected_tof_remaining = (self.outstanding_parts*self.sdu*8)/self.eifr
|
||||
|
||||
if self.req_resp_rtt_rate != 0:
|
||||
sleep_time = self.last_activity + self.part_timeout_factor*expected_tof_remaining + Resource.RETRY_GRACE_TIME + extra_wait - time.time()
|
||||
sleep_time = self.last_activity + self.part_timeout_factor*expected_tof_remaining + expected_hmu_wait_remaining + Resource.RETRY_GRACE_TIME + extra_wait - time.time()
|
||||
else:
|
||||
sleep_time = self.last_activity + self.part_timeout_factor*((3*self.sdu)/self.eifr) + Resource.RETRY_GRACE_TIME + extra_wait - time.time()
|
||||
|
||||
# TODO: Remove debug at some point
|
||||
# RNS.log(f"EIFR {RNS.prettyspeed(self.eifr)}, ETOF {RNS.prettyshorttime(expected_tof_remaining)} ", RNS.LOG_DEBUG, pt=True)
|
||||
# RNS.log(f"EIFR {RNS.prettyspeed(self.eifr)}, ETOF {RNS.prettyshorttime(expected_tof_remaining)}, EHWR {RNS.prettyshorttime(expected_hmu_wait_remaining)}", RNS.LOG_DEBUG, pt=True)
|
||||
# RNS.log(f"Resource ST {RNS.prettyshorttime(sleep_time)}, RTT {RNS.prettyshorttime(self.rtt or self.link.rtt)}, {self.outstanding_parts} left", RNS.LOG_DEBUG, pt=True)
|
||||
|
||||
if sleep_time < 0:
|
||||
@@ -677,6 +681,16 @@ class Resource:
|
||||
# Strip off random hash
|
||||
data = data[Resource.RANDOM_HASH_SIZE:]
|
||||
|
||||
if not self.compressed: self.data = data
|
||||
else:
|
||||
decompressor = bz2.BZ2Decompressor()
|
||||
self.data = decompressor.decompress(data, max_length=self.max_decompressed_size)
|
||||
if not decompressor.eof:
|
||||
self.status = Resource.CORRUPT
|
||||
self.cancel()
|
||||
RNS.log(f"Decompressed resource exceeded maximum decompressed size. The resource was rejected.", RNS.LOG_ERROR)
|
||||
return
|
||||
|
||||
if self.compressed: self.data = bz2.decompress(data)
|
||||
else: self.data = data
|
||||
|
||||
@@ -755,18 +769,18 @@ class Resource:
|
||||
# Prepare the next segment for advertisement
|
||||
RNS.log(f"Preparing segment {self.segment_index+1} of {self.total_segments} for resource {self}", RNS.LOG_DEBUG)
|
||||
self.preparing_next_segment = True
|
||||
self.next_segment = Resource(
|
||||
self.input_file, self.link,
|
||||
callback = self.callback,
|
||||
segment_index = self.segment_index+1,
|
||||
original_hash=self.original_hash,
|
||||
progress_callback = self.__progress_callback,
|
||||
request_id = self.request_id,
|
||||
is_response = self.is_response,
|
||||
advertise = False,
|
||||
auto_compress = self.auto_compress_option,
|
||||
sent_metadata_size = self.metadata_size,
|
||||
)
|
||||
self.next_segment = Resource(self.input_file, self.link,
|
||||
callback = self.callback,
|
||||
segment_index = self.segment_index+1,
|
||||
original_hash=self.original_hash,
|
||||
progress_callback = self.__progress_callback,
|
||||
request_id = self.request_id,
|
||||
is_response = self.is_response,
|
||||
advertise = False,
|
||||
auto_compress = self.auto_compress_option,
|
||||
sent_metadata_size = self.metadata_size)
|
||||
if self.__progress_callback:
|
||||
self.next_segment.progress_callback(self.__progress_callback)
|
||||
|
||||
def validate_proof(self, proof_data):
|
||||
if not self.status == Resource.FAILED:
|
||||
@@ -959,6 +973,7 @@ class Resource:
|
||||
self.last_activity = time.time()
|
||||
self.req_sent = self.last_activity
|
||||
self.req_sent_bytes = len(request_packet.raw)
|
||||
self.rtt_rxd_bytes_at_part_req = self.rtt_rxd_bytes
|
||||
self.req_resp = None
|
||||
|
||||
except Exception as e:
|
||||
@@ -1056,8 +1071,7 @@ class Resource:
|
||||
self.retries_left = 3
|
||||
|
||||
if self.__progress_callback != None:
|
||||
try:
|
||||
self.__progress_callback(self)
|
||||
try: self.__progress_callback(self)
|
||||
except Exception as e:
|
||||
RNS.log("Error while executing progress callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
@@ -1066,7 +1080,13 @@ class Resource:
|
||||
Cancels transferring the resource.
|
||||
"""
|
||||
if self.next_segment: self.next_segment.cancel()
|
||||
if self.status < Resource.COMPLETE:
|
||||
|
||||
if self.status == Resource.CORRUPT:
|
||||
self.link.cancel_incoming_resource(self)
|
||||
self.reject(self.advertisement_packet)
|
||||
self.link.teardown()
|
||||
|
||||
elif self.status < Resource.COMPLETE:
|
||||
self.status = Resource.FAILED
|
||||
if self.initiator:
|
||||
if self.link.status == RNS.Link.ACTIVE:
|
||||
@@ -1104,6 +1124,7 @@ class Resource:
|
||||
|
||||
def progress_callback(self, callback):
|
||||
self.__progress_callback = callback
|
||||
if self.next_segment: self.next_segment.progress_callback(callback)
|
||||
|
||||
def get_progress(self):
|
||||
"""
|
||||
|
||||
+59
-20
@@ -47,6 +47,7 @@ else:
|
||||
from RNS.Interfaces import *
|
||||
|
||||
from RNS.vendor.configobj import ConfigObj
|
||||
from threading import Lock
|
||||
import configparser
|
||||
import multiprocessing.connection
|
||||
import importlib.util
|
||||
@@ -171,6 +172,8 @@ class Reticulum:
|
||||
cachepath = ""
|
||||
interfacepath = ""
|
||||
|
||||
gracious_persist_lock = Lock()
|
||||
|
||||
__instance = None
|
||||
|
||||
__interface_detach_ran = False
|
||||
@@ -183,13 +186,11 @@ class Reticulum:
|
||||
# out cleanup operations.
|
||||
if not Reticulum.__exit_handler_ran:
|
||||
Reticulum.__exit_handler_ran = True
|
||||
if not Reticulum.__interface_detach_ran:
|
||||
RNS.Transport.detach_interfaces()
|
||||
if not Reticulum.__interface_detach_ran: RNS.Transport.detach_interfaces()
|
||||
RNS.Transport.exit_handler()
|
||||
RNS.Identity.exit_handler()
|
||||
|
||||
if RNS.Profiler.ran():
|
||||
RNS.Profiler.results()
|
||||
if RNS.Profiler.ran(): RNS.Profiler.results()
|
||||
|
||||
RNS.loglevel = -1
|
||||
|
||||
@@ -238,7 +239,7 @@ class Reticulum:
|
||||
|
||||
if logdest == RNS.LOG_FILE:
|
||||
RNS.logdest = RNS.LOG_FILE
|
||||
RNS.logfile = Reticulum.configdir+"/logfile"
|
||||
RNS.logfile = RNS.logfile or Reticulum.configdir+"/logfile"
|
||||
elif callable(logdest):
|
||||
RNS.logdest = RNS.LOG_CALLBACK
|
||||
RNS.logcall = logdest
|
||||
@@ -322,6 +323,7 @@ class Reticulum:
|
||||
RNS.log(f"Configuration loaded from {self.configpath}", RNS.LOG_VERBOSE)
|
||||
|
||||
RNS.Identity.load_known_destinations()
|
||||
if not self.is_connected_to_shared_instance: RNS.Identity._clean_ratchets()
|
||||
RNS.Transport.start(self)
|
||||
|
||||
if self.use_af_unix:
|
||||
@@ -351,7 +353,6 @@ class Reticulum:
|
||||
|
||||
def __start_jobs(self):
|
||||
if self.jobs_thread == None:
|
||||
RNS.Identity._clean_ratchets()
|
||||
self.jobs_thread = threading.Thread(target=self.__jobs)
|
||||
self.jobs_thread.daemon = True
|
||||
self.jobs_thread.start()
|
||||
@@ -361,11 +362,11 @@ class Reticulum:
|
||||
now = time.time()
|
||||
|
||||
if now > self.last_cache_clean+Reticulum.CLEAN_INTERVAL:
|
||||
self.__clean_caches()
|
||||
self.__clean_caches(background=True)
|
||||
self.last_cache_clean = time.time()
|
||||
|
||||
if now > self.last_data_persist+Reticulum.PERSIST_INTERVAL:
|
||||
self.__persist_data()
|
||||
self.__persist_data(background=True)
|
||||
|
||||
time.sleep(Reticulum.JOB_INTERVAL)
|
||||
|
||||
@@ -960,7 +961,7 @@ class Reticulum:
|
||||
interface.optimise_mtu()
|
||||
|
||||
if ifac_size != None: interface.ifac_size = ifac_size
|
||||
else: interface.ifac_size = 8
|
||||
else: interface.ifac_size = interface.DEFAULT_IFAC_SIZE
|
||||
|
||||
interface.announce_cap = announce_cap if announce_cap != None else Reticulum.ANNOUNCE_CAP/100.0
|
||||
interface.announce_rate_target = announce_rate_target
|
||||
@@ -993,16 +994,20 @@ class Reticulum:
|
||||
RNS.Transport.interfaces.append(interface)
|
||||
interface.final_init()
|
||||
|
||||
def _should_persist_data(self):
|
||||
def _should_persist_data(self, background=False):
|
||||
if time.time() > self.last_data_persist+Reticulum.GRACIOUS_PERSIST_INTERVAL:
|
||||
self.__persist_data()
|
||||
def job(): self.__persist_data(background=background)
|
||||
if background: threading.Thread(target=job, daemon=True).start()
|
||||
else: job()
|
||||
|
||||
def __persist_data(self):
|
||||
RNS.Transport.persist_data()
|
||||
RNS.Identity.persist_data()
|
||||
self.last_data_persist = time.time()
|
||||
def __persist_data(self, background=False):
|
||||
if Reticulum.gracious_persist_lock.locked(): return
|
||||
with Reticulum.gracious_persist_lock:
|
||||
RNS.Transport.persist_data(background=background)
|
||||
RNS.Identity.persist_data(background=background)
|
||||
self.last_data_persist = time.time()
|
||||
|
||||
def __clean_caches(self):
|
||||
def __clean_caches(self, background=False):
|
||||
RNS.log("Cleaning resource and packet caches...", RNS.LOG_EXTREME)
|
||||
now = time.time()
|
||||
|
||||
@@ -1013,8 +1018,8 @@ class Reticulum:
|
||||
filepath = self.resourcepath + "/" + filename
|
||||
mtime = os.path.getmtime(filepath)
|
||||
age = now - mtime
|
||||
if age > Reticulum.RESOURCE_CACHE:
|
||||
os.unlink(filepath)
|
||||
if age > Reticulum.RESOURCE_CACHE: os.unlink(filepath)
|
||||
if background: time.sleep(0.001)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Error while cleaning resources cache, the contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
@@ -1026,8 +1031,8 @@ class Reticulum:
|
||||
filepath = self.cachepath + "/" + filename
|
||||
mtime = os.path.getmtime(filepath)
|
||||
age = now - mtime
|
||||
if age > RNS.Transport.DESTINATION_TIMEOUT:
|
||||
os.unlink(filepath)
|
||||
if age > RNS.Transport.DESTINATION_TIMEOUT: os.unlink(filepath)
|
||||
if background: time.sleep(0.001)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Error while cleaning resources cache, the contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
@@ -1080,6 +1085,13 @@ class Reticulum:
|
||||
identity_hash = call["unblackhole_identity"]
|
||||
rpc_connection.send(self.unblackhole_identity(identity_hash))
|
||||
|
||||
if "destination_data" in call:
|
||||
operation = call["destination_data"]
|
||||
destination_hash = call["destination_hash"]
|
||||
if operation == "used": rpc_connection.send(self._used_destination_data(destination_hash))
|
||||
elif operation == "retain": rpc_connection.send(self._retain_destination_data(destination_hash))
|
||||
elif operation == "unretain": rpc_connection.send(self._unretain_destination_data(destination_hash))
|
||||
|
||||
rpc_connection.close()
|
||||
|
||||
except Exception as e:
|
||||
@@ -1087,6 +1099,33 @@ class Reticulum:
|
||||
|
||||
def get_rpc_client(self): return multiprocessing.connection.Client(self.rpc_addr, family=self.rpc_type, authkey=self.rpc_key)
|
||||
|
||||
def _used_destination_data(self, destination_hash):
|
||||
if self.is_connected_to_shared_instance:
|
||||
rpc_connection = self.get_rpc_client()
|
||||
rpc_connection.send({"destination_data": "used", "destination_hash": destination_hash})
|
||||
response = rpc_connection.recv()
|
||||
return response
|
||||
|
||||
else: return RNS.Identity._used_destination_data(destination_hash)
|
||||
|
||||
def _retain_destination_data(self, destination_hash):
|
||||
if self.is_connected_to_shared_instance:
|
||||
rpc_connection = self.get_rpc_client()
|
||||
rpc_connection.send({"destination_data": "retain", "destination_hash": destination_hash})
|
||||
response = rpc_connection.recv()
|
||||
return response
|
||||
|
||||
else: return RNS.Identity._retain_destination_data(destination_hash)
|
||||
|
||||
def _unretain_destination_data(self, destination_hash):
|
||||
if self.is_connected_to_shared_instance:
|
||||
rpc_connection = self.get_rpc_client()
|
||||
rpc_connection.send({"destination_data": "unretain", "destination_hash": destination_hash})
|
||||
response = rpc_connection.recv()
|
||||
return response
|
||||
|
||||
else: return RNS.Identity._unretain_destination_data(destination_hash)
|
||||
|
||||
def get_interface_stats(self):
|
||||
if self.is_connected_to_shared_instance:
|
||||
rpc_connection = self.get_rpc_client()
|
||||
|
||||
+420
-230
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,39 @@
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2026 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
APP_NAME = "git"
|
||||
|
||||
import os
|
||||
import glob
|
||||
|
||||
py_modules = glob.glob(os.path.dirname(__file__)+"/*.py")
|
||||
pyc_modules = glob.glob(os.path.dirname(__file__)+"/*.pyc")
|
||||
modules = py_modules+pyc_modules
|
||||
__all__ = list(set([os.path.basename(f).replace(".pyc", "").replace(".py", "") for f in modules if not (f.endswith("__init__.py") or f.endswith("__init__.pyc"))]))
|
||||
@@ -0,0 +1,674 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2026 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import RNS
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import shutil
|
||||
import threading
|
||||
import subprocess
|
||||
|
||||
from RNS._version import __version__
|
||||
from RNS.Utilities.rngit import APP_NAME
|
||||
|
||||
from RNS.vendor.configobj import ConfigObj
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
def program_setup(configdir, rnsconfigdir, destination_hexhash, group_name, repo_name):
|
||||
git_client = ReticulumGitClient(configdir=configdir, rnsconfigdir=rnsconfigdir, destination_hexhash=destination_hexhash,
|
||||
group_name=group_name, repo_name=repo_name)
|
||||
|
||||
if not git_client.ready: sys.exit(1)
|
||||
else: git_client.run()
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 3:
|
||||
print("Usage: git-remote-rns <remote-name> <url>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
url = sys.argv[2]
|
||||
if not url.startswith("rns://"):
|
||||
print("Invalid URL scheme. Must be rns://", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
parts = url[6:].split("/", 2)
|
||||
destination_hexhash = parts[0]
|
||||
group_name = parts[1]
|
||||
repo_name = parts[2]
|
||||
|
||||
except IndexError: print("Invalid URL format. Use rns://<hash>/<group>/<repo>", file=sys.stderr); sys.exit(1)
|
||||
|
||||
configdir = os.environ.get("RNGIT_CONFIG", None)
|
||||
rnsconfigdir = os.environ.get("RNS_CONFIG", None)
|
||||
|
||||
program_setup(configdir, rnsconfigdir, destination_hexhash, group_name, repo_name)
|
||||
exit(0)
|
||||
|
||||
|
||||
class ReticulumGitClient():
|
||||
PATH_LIST = "/git/list"
|
||||
PATH_FETCH = "/git/fetch"
|
||||
PATH_PUSH = "/git/push"
|
||||
PATH_DELETE = "/git/delete"
|
||||
|
||||
RES_DISALLOWED = 0x01
|
||||
RES_INVALID_REQ = 0x02
|
||||
RES_NOT_FOUND = 0x03
|
||||
RES_REMOTE_FAIL = 0xFF
|
||||
|
||||
IDX_REPOSITORY = 0x00
|
||||
IDX_RESULT_CODE = 0x01
|
||||
|
||||
REF_BATCH_SIZE = 25
|
||||
PATH_TIMEOUT = 15
|
||||
LINK_TIMEOUT = 15
|
||||
|
||||
def __init__(self, configdir, rnsconfigdir, destination_hexhash, group_name, repo_name):
|
||||
# Client state and configuration
|
||||
self.identity = None
|
||||
self.userdir = os.path.expanduser("~")
|
||||
self.config = None
|
||||
self.ready = False
|
||||
|
||||
self.remote_identity = None
|
||||
self.destination = None
|
||||
self.link = None
|
||||
self.link_ready = False
|
||||
self.link_failed = False
|
||||
self.link_timeout = self.LINK_TIMEOUT
|
||||
self.path_timeout = self.PATH_TIMEOUT
|
||||
|
||||
self.destination_hexhash = destination_hexhash
|
||||
self.group_name = group_name
|
||||
self.repo_name = repo_name
|
||||
self.repo_path = f"{group_name}/{repo_name}"
|
||||
|
||||
self.tmp_dir = TemporaryDirectory()
|
||||
self.request_event = threading.Event()
|
||||
self.request_response = None
|
||||
self.response_metadata = None
|
||||
|
||||
self.ref_batch_size = self.REF_BATCH_SIZE
|
||||
self.remote_refs = {}
|
||||
|
||||
self.response_progress = 0
|
||||
self.previous_progress = 0
|
||||
self.response_size = None
|
||||
self.response_transfer_size = None
|
||||
self.progress_updated_at = None
|
||||
self.progress_enabled = False
|
||||
|
||||
if configdir != None: self.configdir = configdir
|
||||
else:
|
||||
if os.path.isdir(self.userdir+"/.config/rngit") and os.path.isfile(self.userdir+"/.config/rngit/config"): self.configdir = self.userdir+"/.rngit/reticulum"
|
||||
else: self.configdir = self.userdir+"/.rngit"
|
||||
|
||||
self.logfile = self.configdir+"/client_log"
|
||||
self.configpath = self.configdir+"/client_config"
|
||||
self.identitypath = self.configdir+"/client_identity"
|
||||
|
||||
RNS.logfile = self.logfile
|
||||
try: self.reticulum = RNS.Reticulum(configdir=rnsconfigdir, logdest=RNS.LOG_FILE)
|
||||
except Exception as e:
|
||||
print(f"Failed to initialize Reticulum: {e}", file=sys.stderr)
|
||||
return
|
||||
|
||||
if os.path.isfile(self.configpath):
|
||||
try: self.config = ConfigObj(self.configpath)
|
||||
except Exception as e:
|
||||
RNS.log("Could not parse the configuration at "+self.configpath, RNS.LOG_ERROR)
|
||||
return
|
||||
|
||||
else: self.__create_default_config()
|
||||
|
||||
self.__apply_config()
|
||||
self.ready = True
|
||||
|
||||
def __create_default_config(self):
|
||||
self.config = ConfigObj(__default_rngit_config__)
|
||||
self.config.filename = self.configpath
|
||||
if not os.path.isdir(self.configdir): os.makedirs(self.configdir)
|
||||
self.config.write()
|
||||
|
||||
def __apply_config(self):
|
||||
if "logging" in self.config:
|
||||
section = self.config["logging"]
|
||||
if "loglevel" in section: RNS.loglevel = max(RNS.LOG_NONE, min(RNS.LOG_EXTREME, section.as_int("loglevel")))
|
||||
|
||||
if "client" in self.config:
|
||||
section = self.config["client"]
|
||||
if "ref_batch_size" in section: self.ref_batch_size = max(0, min(1024, section.as_int("ref_batch_size")))
|
||||
|
||||
if not os.path.isfile(self.identitypath):
|
||||
identity = RNS.Identity()
|
||||
identity.to_file(self.identitypath)
|
||||
RNS.log(f"Client identity generated and persisted to {self.identitypath}", RNS.LOG_VERBOSE)
|
||||
|
||||
else:
|
||||
identity = RNS.Identity.from_file(self.identitypath)
|
||||
RNS.log(f"Client identity loaded from {self.identitypath}", RNS.LOG_VERBOSE)
|
||||
|
||||
if not identity:
|
||||
RNS.log("Could not initialize client identity.", RNS.LOG_ERROR)
|
||||
self.ready = False
|
||||
|
||||
else: self.identity = identity
|
||||
|
||||
def abort(self, reason=None, code=255):
|
||||
if not reason: reason = "Unknown reason"
|
||||
print(f"git-remote-rns failed: {reason}", file=sys.stderr)
|
||||
if self.link: self.link.teardown()
|
||||
sys.exit(code)
|
||||
|
||||
def connect_server(self):
|
||||
try: destination_hash = bytes.fromhex(self.destination_hexhash)
|
||||
except Exception as e: self.abort(f"Invalid destination hash: {e}")
|
||||
|
||||
RNS.log(f"Requesting path to {RNS.prettyhexrep(destination_hash)}", RNS.LOG_DEBUG)
|
||||
sys.stderr.write(f"Requesting path..."); sys.stderr.flush()
|
||||
if not RNS.Transport.await_path(destination_hash, timeout=self.path_timeout):
|
||||
sys.stderr.write(f"\n"); sys.stderr.flush()
|
||||
self.abort(f"Could not resolve path to {RNS.prettyhexrep(destination_hash)}")
|
||||
|
||||
else:
|
||||
RNS.log(f"Path to {RNS.prettyhexrep(destination_hash)} resolved", RNS.LOG_DEBUG);
|
||||
sys.stderr.write(f"\rPath resolved "); sys.stderr.flush()
|
||||
|
||||
self.remote_identity = RNS.Identity.recall(destination_hash)
|
||||
if not self.remote_identity: self.abort("Could not recall remote identity. Is the server announcing?")
|
||||
|
||||
sys.stderr.write(f"\rEstablishing link..."); sys.stderr.flush()
|
||||
self.destination = RNS.Destination(self.remote_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, APP_NAME, "repositories")
|
||||
self.link = RNS.Link(self.destination)
|
||||
self.link.set_link_established_callback(self.link_established)
|
||||
self.link.set_link_closed_callback(self.link_closed)
|
||||
|
||||
def link_established(self, link):
|
||||
RNS.log(f"Link established, identifying...", RNS.LOG_DEBUG)
|
||||
sys.stderr.write(f"\rLink established with remote\n"); sys.stderr.flush()
|
||||
link.identify(self.identity)
|
||||
self.link_ready = True
|
||||
|
||||
def link_closed(self, link):
|
||||
RNS.log(f"Link was closed", RNS.LOG_DEBUG)
|
||||
if not self.link_ready: self.link_failed = True
|
||||
|
||||
def _on_progress(self, transfer_instance):
|
||||
if hasattr(transfer_instance, "progress"):
|
||||
self.response_progress = transfer_instance.progress
|
||||
self.response_size = transfer_instance.response_size
|
||||
self.response_transfer_size = transfer_instance.response_transfer_size
|
||||
|
||||
elif hasattr(transfer_instance, "get_progress") and callable(transfer_instance.get_progress):
|
||||
self.response_progress = transfer_instance.get_progress()
|
||||
self.response_size = transfer_instance.total_size
|
||||
self.response_transfer_size = transfer_instance.size
|
||||
|
||||
now = time.time()
|
||||
if self.progress_updated_at == None: self.progress_updated_at = now
|
||||
|
||||
if now > self.progress_updated_at+1:
|
||||
td = now - self.progress_updated_at
|
||||
pd = self.response_progress - self.previous_progress
|
||||
bd = pd*self.response_size if self.response_size else 0
|
||||
self.response_speed = (bd/td)*8 if td > 0 else 0
|
||||
self.previous_progress = self.response_progress
|
||||
self.progress_updated_at = now
|
||||
|
||||
# Report progress to git via stderr
|
||||
if self.progress_enabled and self.response_size:
|
||||
percent = round(self.response_progress * 100, 1)
|
||||
size = self.response_size
|
||||
rxd = size*self.response_progress
|
||||
speed_kbps = (self.response_speed / 1000) if hasattr(self, 'response_speed') else 0
|
||||
sys.stderr.write(f"Transferring: {percent}% ({RNS.prettysize(rxd)}/{RNS.prettysize(size)}) {RNS.prettyspeed(self.response_speed)} \r")
|
||||
sys.stderr.flush()
|
||||
|
||||
################################
|
||||
# Synchronous Request Wrappers #
|
||||
################################
|
||||
|
||||
def _response_ready(self, request_receipt):
|
||||
self.request_response = request_receipt.response
|
||||
self.response_metadata = request_receipt.metadata
|
||||
|
||||
if hasattr(self.request_response, "read") and callable(self.request_response.read):
|
||||
response_path = self.request_response.name
|
||||
base_name = os.path.basename(response_path)
|
||||
retained_path = os.path.join(self.tmp_dir.name, base_name)
|
||||
shutil.move(response_path, retained_path)
|
||||
self.request_response = open(retained_path, "rb")
|
||||
|
||||
self.request_event.set()
|
||||
|
||||
def _response_failed(self, request_receipt=None):
|
||||
self.request_response = None
|
||||
self.request_event.set()
|
||||
|
||||
def send_request(self, path, data, timeout=7200):
|
||||
if not self.link_ready: self.abort("Link not ready for request")
|
||||
|
||||
self.request_event.clear()
|
||||
self.request_response = None
|
||||
self.response_metadata = None
|
||||
self.previous_progress = 0
|
||||
self.progress_updated_at = None
|
||||
|
||||
RNS.log(f"Sending request: {path}", RNS.LOG_DEBUG)
|
||||
request_receipt = self.link.request(path, data, progress_callback=self._on_progress, response_callback=self._response_ready, failed_callback=self._response_failed, timeout=timeout)
|
||||
if request_receipt.resource: request_receipt.resource.progress_callback(self._on_progress)
|
||||
self.request_event.wait(timeout=timeout)
|
||||
|
||||
if self.request_response is None: self.abort("Request failed or timed out")
|
||||
RNS.log(f"Got response for: {path}", RNS.LOG_DEBUG)
|
||||
|
||||
return self.request_response, self.response_metadata
|
||||
|
||||
#############################
|
||||
# Git Helper Protocol Logic #
|
||||
#############################
|
||||
|
||||
def _detach_stdout(self):
|
||||
sys.stdout = open(os.devnull, "w")
|
||||
sys.stderr = open(os.devnull, "w")
|
||||
|
||||
def run(self):
|
||||
try: self.connect_server()
|
||||
except Exception as e: self.abort(str(e))
|
||||
|
||||
timeout = self.link_timeout
|
||||
while not self.link_ready and not self.link_failed and timeout > 0:
|
||||
time.sleep(0.5)
|
||||
timeout -= 1
|
||||
|
||||
if not self.link_ready: self.abort("Failed to establish link")
|
||||
|
||||
self.progress_enabled = False
|
||||
|
||||
git_stdin = sys.stdin
|
||||
git_stdout = sys.stdout
|
||||
git_stderr = sys.stderr
|
||||
|
||||
fetch_queue = []
|
||||
push_queue = []
|
||||
|
||||
while True:
|
||||
line = git_stdin.readline()
|
||||
if not line: break
|
||||
|
||||
line = line.strip()
|
||||
if line == "capabilities":
|
||||
git_stdout.write("list\n")
|
||||
git_stdout.write("fetch\n")
|
||||
git_stdout.write("push\n")
|
||||
git_stdout.write("option\n")
|
||||
git_stdout.write("\n")
|
||||
git_stdout.flush()
|
||||
|
||||
elif line == "list": self.handle_git_list(git_stdout)
|
||||
|
||||
elif line.startswith("list "): self.handle_git_list(git_stdout, for_push=True) # List for push
|
||||
|
||||
elif line.startswith("option"):
|
||||
# Line format: option <name> <value>
|
||||
parts = line.split(maxsplit=2)
|
||||
opt_name = parts[1] if len(parts) > 1 else ""
|
||||
opt_value = parts[2] if len(parts) > 2 else ""
|
||||
|
||||
if opt_name == "progress": self.progress_enabled = opt_value.lower() in ("true", "1", "yes"); git_stdout.write("ok\n")
|
||||
else: git_stdout.write("unsupported\n")
|
||||
|
||||
git_stdout.flush()
|
||||
|
||||
elif line.startswith("fetch"):
|
||||
# Line format: fetch <sha> <ref>
|
||||
parts = line.split()
|
||||
sha = parts[1]
|
||||
ref = parts[2]
|
||||
# Avoid duplicates in the same batch - TODO: Re-evaluate this
|
||||
if (sha, ref) not in fetch_queue: fetch_queue.append((sha, ref))
|
||||
push_queue = []
|
||||
|
||||
elif line.startswith("push"):
|
||||
# Line format: push <local_ref>:<remote_ref>
|
||||
parts = line.split()
|
||||
refspec = parts[1]
|
||||
local_ref, remote_ref = refspec.split(":", 1)
|
||||
push_queue.append((local_ref, remote_ref))
|
||||
fetch_queue = []
|
||||
|
||||
elif line == "": # End of batch
|
||||
try:
|
||||
self.process_fetch_queue(fetch_queue, git_stdout, self.progress_enabled, self.ref_batch_size)
|
||||
self.process_push_queue(push_queue, git_stdout, git_stderr, self.progress_enabled)
|
||||
fetch_queue = []
|
||||
push_queue = []
|
||||
git_stdout.write("\n")
|
||||
git_stdout.flush()
|
||||
|
||||
except BrokenPipeError:
|
||||
self._detach_stdout()
|
||||
RNS.log("Git closed connection, exiting", RNS.LOG_DEBUG)
|
||||
break
|
||||
|
||||
else: self.abort(f"Unknown Git command: {line}")
|
||||
|
||||
try: sys.stdout.flush()
|
||||
except BrokenPipeError: pass
|
||||
|
||||
if self.link: self.link.teardown()
|
||||
|
||||
def handle_git_list(self, git_stdout, for_push=False):
|
||||
RNS.log("Handle git list" + (" for-push" if for_push else ""), RNS.LOG_DEBUG)
|
||||
request_data = {self.IDX_REPOSITORY: self.repo_path, "for_push": for_push}
|
||||
response, metadata = self.send_request(self.PATH_LIST, request_data)
|
||||
|
||||
if not response or not isinstance(response, bytes): self.abort("Invalid list response from server")
|
||||
|
||||
status_byte = response[0]
|
||||
payload = response[1:]
|
||||
|
||||
if status_byte != 0: self.abort(f"Server refused list: {payload.decode('utf-8', errors='ignore')}")
|
||||
|
||||
response_text = payload.decode("utf-8")
|
||||
|
||||
self.remote_refs = {}
|
||||
for line in response_text.split("\n"):
|
||||
line = line.strip()
|
||||
if not line: continue
|
||||
parts = line.split(" ", 1)
|
||||
if len(parts) == 2:
|
||||
sha, ref_name = parts
|
||||
if ref_name == "HEAD": continue
|
||||
self.remote_refs[ref_name] = sha
|
||||
|
||||
git_stdout.write(response_text)
|
||||
git_stdout.write("\n") # Required to terminate list
|
||||
git_stdout.flush()
|
||||
|
||||
def escape_for_stdout(self, value):
|
||||
if isinstance(value, bytes): value = value.decode('utf-8', errors='replace')
|
||||
|
||||
escaped = '"'
|
||||
for char in value:
|
||||
if char == '\\': escaped += '\\\\'
|
||||
elif char == '"': escaped += '\\"'
|
||||
elif char == '\n': escaped += '\\n'
|
||||
elif char == '\t': escaped += '\\t'
|
||||
elif char == '\r': escaped += '\\r'
|
||||
elif ord(char) < 32 or ord(char) > 126: escaped += f'\\x{ord(char):02x}'
|
||||
else: escaped += char
|
||||
|
||||
return escaped + '"'
|
||||
|
||||
def process_fetch_queue(self, fetch_queue, git_stdout, progress_enabled=False, ref_batch_size=REF_BATCH_SIZE):
|
||||
import tempfile
|
||||
import subprocess
|
||||
|
||||
if not fetch_queue: return
|
||||
|
||||
# Build a global have list from all remote refs that the client already has objects for
|
||||
have_shas = []
|
||||
for sha in self.remote_refs.values():
|
||||
try:
|
||||
result = subprocess.run(["git", "cat-file", "-t", sha], capture_output=True, check=False)
|
||||
if result.returncode == 0: have_shas.append(sha)
|
||||
|
||||
except Exception as e: RNS.log(f"Could not verify remote SHA {sha} locally: {e}", RNS.LOG_WARNING)
|
||||
|
||||
while fetch_queue:
|
||||
batch = fetch_queue[:ref_batch_size]
|
||||
fetch_queue = fetch_queue[ref_batch_size:]
|
||||
|
||||
refs_list = []
|
||||
for sha, ref in batch:
|
||||
ref_entry = {"sha": sha, "ref": ref}
|
||||
try:
|
||||
# Attempt to get local ref SHA for incremental bundle generation on remote
|
||||
result = subprocess.run(["git", "rev-parse", ref], capture_output=True, text=True, check=False)
|
||||
if result.returncode == 0:
|
||||
local_sha = result.stdout.strip()
|
||||
if local_sha != sha: ref_entry["have"] = local_sha
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Could not resolve local SHA for {ref} during fetch enumeration, getting full history for this ref: {e}", RNS.LOG_WARNING)
|
||||
|
||||
refs_list.append(ref_entry)
|
||||
|
||||
ref_names = [ref for _, ref in batch]
|
||||
RNS.log(f"Fetching batch of {len(refs_list)} refs: {ref_names} (have {len(have_shas)} common objects)", RNS.LOG_DEBUG)
|
||||
|
||||
request_data = { self.IDX_REPOSITORY: self.repo_path, "refs": refs_list }
|
||||
if have_shas: request_data["have"] = have_shas
|
||||
|
||||
response, metadata = self.send_request(self.PATH_FETCH, request_data)
|
||||
|
||||
if not response: self.abort(f"No data in fetch response for batch")
|
||||
if not metadata:
|
||||
if not isinstance(response, bytes): self.abort(f"Invalid fetch response for batch")
|
||||
status_byte = response[0]
|
||||
|
||||
if status_byte == 0:
|
||||
RNS.log(f"Server returned empty bundle, all objects already exist locally", RNS.LOG_DEBUG)
|
||||
continue
|
||||
|
||||
else:
|
||||
error_msg = response[1:].decode('utf-8', errors='ignore')
|
||||
self.abort(f"Fetch failed for batch: {error_msg}")
|
||||
|
||||
else:
|
||||
if not self.IDX_RESULT_CODE in metadata: self.abort(f"No result metadata on bundle response")
|
||||
status_byte = metadata[self.IDX_RESULT_CODE]
|
||||
if status_byte == 0: bundle_path = response.name
|
||||
else: self.abort(f"Unknown remote state for batch ref fetch")
|
||||
|
||||
if progress_enabled:
|
||||
size = os.stat(bundle_path).st_size
|
||||
sys.stderr.write(f"Transferring: 100% ({RNS.prettysize(size)}). \n")
|
||||
sys.stderr.flush()
|
||||
|
||||
stderr_arg = sys.stderr if progress_enabled else subprocess.DEVNULL
|
||||
|
||||
verify_cmd = ["git", "bundle", "verify", "-q", bundle_path]
|
||||
verify_result = subprocess.run(verify_cmd, stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL)
|
||||
|
||||
if verify_result.returncode != 0: self.abort(f"Bundle verification failed for batch")
|
||||
|
||||
unbundle_cmd = ["git", "bundle", "unbundle"]
|
||||
if progress_enabled: unbundle_cmd.append("--progress")
|
||||
unbundle_cmd.append(bundle_path)
|
||||
|
||||
unbundle_result = subprocess.run(unbundle_cmd, stderr=stderr_arg, stdout=subprocess.DEVNULL)
|
||||
|
||||
if unbundle_result.returncode != 0: self.abort(f"Bundle unbundle failed for batch: Non-zero return code")
|
||||
|
||||
def process_push_queue(self, push_queue, git_stdout, git_stderr, progress_enabled=False):
|
||||
import tempfile
|
||||
import subprocess
|
||||
|
||||
for local_ref, remote_ref in push_queue:
|
||||
RNS.log(f"Pushing {local_ref} to {remote_ref}", RNS.LOG_DEBUG)
|
||||
|
||||
# Handle potential deletions
|
||||
if not local_ref or local_ref == "":
|
||||
request_data = { self.IDX_REPOSITORY: self.repo_path, "ref": remote_ref }
|
||||
response, metadata = self.send_request(self.PATH_DELETE, request_data)
|
||||
|
||||
if not response or not isinstance(response, bytes):
|
||||
git_stdout.write(f"error {remote_ref} {self.escape_for_stdout('No response from server')}\n")
|
||||
git_stdout.flush()
|
||||
continue
|
||||
|
||||
status_byte = response[0]
|
||||
if status_byte != 0:
|
||||
error_msg = response[1:].decode("utf-8", errors="ignore")
|
||||
git_stdout.write(f"error {remote_ref} {self.escape_for_stdout(error_msg)}\n")
|
||||
git_stdout.flush()
|
||||
continue
|
||||
|
||||
git_stdout.write(f"ok {remote_ref}\n")
|
||||
git_stdout.flush()
|
||||
continue
|
||||
|
||||
force = local_ref.startswith("+")
|
||||
if force: local_ref = local_ref[1:]
|
||||
|
||||
stderr_arg = sys.stderr if progress_enabled else subprocess.DEVNULL
|
||||
|
||||
# Resolve the SHA that local_ref points to
|
||||
sha_result = subprocess.run(["git", "rev-parse", local_ref], capture_output=True, text=True, check=False)
|
||||
if sha_result.returncode != 0:
|
||||
error_msg = f"Could not resolve local ref {local_ref}"
|
||||
git_stdout.write(f"error {remote_ref} {self.escape_for_stdout(error_msg)}\n")
|
||||
git_stdout.flush()
|
||||
continue
|
||||
|
||||
local_sha = sha_result.stdout.strip()
|
||||
|
||||
bundle_empty = False
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
bundle_path = tmpdir + "/push.bundle"
|
||||
|
||||
create_cmd = ["git", "bundle", "create", bundle_path, local_ref]
|
||||
|
||||
# Exclude all remote ref SHAs that exist locally, so the
|
||||
# bundle only contains objects the remote doesn't already have
|
||||
exclude_count = 0
|
||||
for sha in self.remote_refs.values():
|
||||
try:
|
||||
# We need to verify each SHA actually exists locally, since git
|
||||
# bundle create will fail if a ^<sha> argument references an object
|
||||
# not present in the local repository.
|
||||
result = subprocess.run(["git", "cat-file", "-t", sha], capture_output=True, check=False)
|
||||
if result.returncode == 0:
|
||||
create_cmd.append(f"^{sha}")
|
||||
exclude_count += 1
|
||||
|
||||
except Exception as e: RNS.log(f"Could not verify remote SHA {sha} locally: {e}", RNS.LOG_WARNING)
|
||||
|
||||
RNS.log(f"Excluding {exclude_count}/{len(self.remote_refs)} remote refs for {local_ref}", RNS.LOG_DEBUG)
|
||||
|
||||
if progress_enabled: create_cmd.insert(3, "--progress")
|
||||
|
||||
create_result = subprocess.run(create_cmd, capture_output=True, text=True, check=False)
|
||||
|
||||
if create_result.returncode == 0:
|
||||
if create_result.stderr:
|
||||
# git_stderr.write(create_result.stderr)
|
||||
pass
|
||||
else:
|
||||
if "empty bundle" in create_result.stderr.lower():
|
||||
# All objects reachable from local_ref already exist on
|
||||
# the remote. In this case, no bundle is needed and we can
|
||||
# update the ref directly via the operations path instead.
|
||||
bundle_empty = True
|
||||
RNS.log(f"Empty bundle for {local_ref}, all objects already on remote", RNS.LOG_DEBUG)
|
||||
|
||||
else:
|
||||
if progress_enabled and create_result.stderr: git_stderr.write(create_result.stderr)
|
||||
error_msg = "Bundle creation failed"
|
||||
git_stdout.write(f"error {remote_ref} {self.escape_for_stdout(error_msg)}\n")
|
||||
git_stdout.flush()
|
||||
continue
|
||||
|
||||
if not bundle_empty:
|
||||
with open(bundle_path, "rb") as f: bundle_data = f.read()
|
||||
|
||||
request_data = { self.IDX_REPOSITORY: self.repo_path, "local_ref": local_ref, "remote_ref": remote_ref,
|
||||
"force": force, "bundle": bundle_data }
|
||||
|
||||
response, metadata = self.send_request(self.PATH_PUSH, request_data)
|
||||
|
||||
if not response or not isinstance(response, bytes):
|
||||
git_stdout.write(f"error {remote_ref} {self.escape_for_stdout('No response from server')}\n")
|
||||
git_stdout.flush()
|
||||
continue
|
||||
|
||||
status_byte = response[0]
|
||||
if status_byte != 0:
|
||||
error_msg = response[1:].decode('utf-8', errors='ignore')
|
||||
git_stdout.write(f"error {remote_ref} {self.escape_for_stdout(error_msg)}\n")
|
||||
git_stdout.flush()
|
||||
continue
|
||||
|
||||
# When all reachable objects already exist on the remote, send a
|
||||
# direct ref update operation instead of a bundle.
|
||||
if bundle_empty:
|
||||
operation = {"action": "update_ref", "ref": remote_ref, "sha": local_sha, "force": force}
|
||||
request_data = { self.IDX_REPOSITORY: self.repo_path,
|
||||
"operations": [operation] }
|
||||
|
||||
response, metadata = self.send_request(self.PATH_PUSH, request_data)
|
||||
|
||||
if not response or not isinstance(response, bytes):
|
||||
git_stdout.write(f"error {remote_ref} {self.escape_for_stdout('No response from server')}\n")
|
||||
git_stdout.flush()
|
||||
continue
|
||||
|
||||
status_byte = response[0]
|
||||
if status_byte != 0:
|
||||
error_msg = response[1:].decode('utf-8', errors='ignore')
|
||||
git_stdout.write(f"error {remote_ref} {self.escape_for_stdout(error_msg)}\n")
|
||||
git_stdout.flush()
|
||||
continue
|
||||
|
||||
git_stdout.write(f"ok {remote_ref}\n")
|
||||
git_stdout.flush()
|
||||
|
||||
|
||||
__default_rngit_config__ = '''# This is the default rngit client config file.
|
||||
|
||||
[client]
|
||||
|
||||
# You can control the batch size of ref transfers
|
||||
# using the ref_batch_size directive:
|
||||
|
||||
ref_batch_size = 25
|
||||
|
||||
[logging]
|
||||
# Valid log levels are 0 through 7:
|
||||
# 0: Log only critical information
|
||||
# 1: Log errors and lower log levels
|
||||
# 2: Log warnings and lower log levels
|
||||
# 3: Log notices and lower log levels
|
||||
# 4: Log info and lower (this is the default)
|
||||
# 5: Verbose logging
|
||||
# 6: Debug logging
|
||||
# 7: Extreme logging
|
||||
|
||||
loglevel = 4
|
||||
|
||||
'''.splitlines()
|
||||
|
||||
if __name__ == "__main__": main()
|
||||
@@ -0,0 +1,379 @@
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2026 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import os
|
||||
import io
|
||||
import RNS
|
||||
|
||||
class SyntaxHighlighter:
|
||||
|
||||
def __init__(self, theme=None):
|
||||
self.pygments_available = False
|
||||
self.pygments = None
|
||||
self._lexer_cache = {}
|
||||
self._check_pygments()
|
||||
self.theme = theme or self._get_default_theme()
|
||||
|
||||
def _get_default_theme(self):
|
||||
return {
|
||||
# Control flow - warm coral-red
|
||||
"keyword": "ff7b72",
|
||||
"keyword_constant": "ff7b72",
|
||||
"keyword_control": "ff7b72",
|
||||
"keyword_declaration": "ff7b72",
|
||||
|
||||
# Function definitions - bright sky blue
|
||||
"function_def": "79c0ff",
|
||||
"function_magic": "ff7b72",
|
||||
|
||||
# Function calls - soft lavender
|
||||
"function_call": "d2a8ff",
|
||||
"function_builtin": "ffa657", # amber
|
||||
|
||||
# Class definitions - fresh mint green
|
||||
"class_def": "7ee787",
|
||||
"class_ref": "56d364", # muted when referenced
|
||||
|
||||
# Instance context - soft pink
|
||||
"self": "ff9bce",
|
||||
"cls": "ff9bce",
|
||||
|
||||
# Data literals - cool, calm ice blue
|
||||
"string": "a5d6ff",
|
||||
"string_quoted": "a5d6ff",
|
||||
"string_doc": "8b949e", # docstrings - like comments
|
||||
"string_interpol": "ffd700", # f-string braces - gold
|
||||
"string_escape": "ffea00", # escape sequences - bright yellow
|
||||
|
||||
# Numbers - same as function def
|
||||
"number": "79c0ff",
|
||||
"number_float": "79c0ff",
|
||||
"number_integer": "79c0ff",
|
||||
"number_hex": "79c0ff",
|
||||
|
||||
# Comments - muted gray
|
||||
"comment": "8b949e",
|
||||
"comment_doc": "8b949e",
|
||||
"comment_preproc": "ff7b72", # preprocessor directives
|
||||
|
||||
# Operators - distinct pink/red for visibility
|
||||
"operator": "ff7b72", # General operators - coral
|
||||
"operator_arithmetic": "ff7b72", # +, -, *, /, etc.
|
||||
"operator_comparison": "ff7b72", # ==, !=, <, >, etc.
|
||||
"operator_assignment": "ff7b72", # =, +=, -=, etc.
|
||||
"operator_word": "ff7b72", # and, or, not, in, is
|
||||
"operator_dot": "c9d1d9", # . - subtle for attribute access
|
||||
|
||||
# Punctuation - neutral
|
||||
"punctuation": "b4b4b4",
|
||||
"punctuation_brace": "b4b4b4", # [, ], {, }
|
||||
"punctuation_paren": "b4b4b4", # (, )
|
||||
"punctuation_colon": "b4b4b4", # :, ;
|
||||
"punctuation_comma": "8b949e", # , - slightly dimmed
|
||||
|
||||
# Decorators - burnt orange
|
||||
"decorator": "f0883e",
|
||||
|
||||
# Constants - same as keywords
|
||||
"constant": "ff7b72",
|
||||
"constant_builtin": "ff7b72", # True, False, None
|
||||
|
||||
# Type hints and annotations - amber
|
||||
"type_hint": "ffa657",
|
||||
"type_builtin": "ffa657",
|
||||
|
||||
# Exception handling - alert red
|
||||
"exception": "f85149",
|
||||
"exception_builtin": "f85149",
|
||||
|
||||
# Names and attributes - near-white for readability
|
||||
"name": "e6edf3",
|
||||
"attribute": "e6edf3",
|
||||
"attribute_call": "d2a8ff", # Function/method calls after dot - lavender
|
||||
"variable": "e6edf3",
|
||||
"parameter": "e6edf3",
|
||||
|
||||
# Namespaces and modules
|
||||
"namespace": "7ee787",
|
||||
"module": "a5d6ff",
|
||||
|
||||
# Generic tokens
|
||||
"generic_heading": "c9d1d9",
|
||||
"generic_subheading": "c9d1d9",
|
||||
"generic_prompt": "8b949e",
|
||||
"generic_error": "f85149",
|
||||
"generic_deleted": "f85149",
|
||||
"generic_inserted": "7ee787",
|
||||
"generic_output": "e6edf3",
|
||||
|
||||
# Text and whitespace - no color (None means no color tag)
|
||||
"text": None,
|
||||
"whitespace": None,
|
||||
}
|
||||
|
||||
def _check_pygments(self):
|
||||
try:
|
||||
import pygments
|
||||
from pygments.lexers import get_lexer_for_filename, guess_lexer, get_lexer_by_name
|
||||
from pygments.formatter import Formatter
|
||||
from pygments.token import Token
|
||||
|
||||
self.pygments = pygments
|
||||
self.pygments_available = True
|
||||
RNS.log("Pygments syntax highlighting available", RNS.LOG_DEBUG)
|
||||
|
||||
except ImportError:
|
||||
self.pygments_available = False
|
||||
RNS.log("Pygments not available, using plain text rendering", RNS.LOG_DEBUG)
|
||||
|
||||
def highlight(self, content, filename=None, language=None):
|
||||
if not content: return self._plain_text(content)
|
||||
|
||||
if self.pygments_available:
|
||||
try:
|
||||
highlighted = self._highlight_pygments(content, filename, language)
|
||||
# Fix pygments insisting on trailing newlines
|
||||
if highlighted.endswith("\n") and not content.endswith("\n"): highlighted = highlighted[:-1]
|
||||
return highlighted
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Pygments highlighting failed, falling back: {e}", RNS.LOG_WARNING)
|
||||
return self._plain_text(content)
|
||||
|
||||
# TODO: Implement Python tokenize fallback for .py files.
|
||||
# For now, route to plain text
|
||||
if filename and filename.endswith(".py"):
|
||||
return self._plain_text(content)
|
||||
|
||||
# Universal fallback
|
||||
return self._plain_text(content)
|
||||
|
||||
def _highlight_pygments(self, content, filename=None, language=None):
|
||||
from pygments.lexers import get_lexer_for_filename, guess_lexer, get_lexer_by_name
|
||||
from pygments.util import ClassNotFound
|
||||
|
||||
lexer = None
|
||||
if language:
|
||||
if language == "env": language = "bash"
|
||||
if language == "environment": language = "bash"
|
||||
try: lexer = get_lexer_by_name(language)
|
||||
except ClassNotFound: pass
|
||||
|
||||
if lexer is None and filename:
|
||||
try: lexer = get_lexer_for_filename(filename)
|
||||
except ClassNotFound: pass
|
||||
|
||||
if lexer is None:
|
||||
try:
|
||||
if len(content) > 20: lexer = guess_lexer(content)
|
||||
except ClassNotFound: pass
|
||||
|
||||
if lexer is None: return self._plain_text(content)
|
||||
|
||||
formatter = MicronFormatter(theme=self.theme)
|
||||
result = self.pygments.highlight(content, lexer, formatter)
|
||||
return result
|
||||
|
||||
def _plain_text(self, content):
|
||||
escaped = self._escape_micron(content)
|
||||
return f"`=\n{escaped}\n`="
|
||||
|
||||
@staticmethod
|
||||
def _escape_micron(text): return text.replace("`", "\\`")
|
||||
|
||||
|
||||
class MicronFormatter:
|
||||
def __init__(self, theme, **options):
|
||||
self.theme = theme
|
||||
self.options = options
|
||||
|
||||
def format(self, tokensource, outfile):
|
||||
output_parts = []
|
||||
prev_was_dot = False
|
||||
|
||||
for ttype, value in tokensource:
|
||||
is_dot = (str(ttype) == "Token.Operator" and value == ".")
|
||||
|
||||
# If previous token was a dot and this is a Name, treat as attribute/function call
|
||||
# TODO: Improve this if we can check next token as parantheses or something.
|
||||
if prev_was_dot and str(ttype).startswith("Token.Name") and value:
|
||||
color = self._get_color_from_key("attribute_call")
|
||||
if color:
|
||||
escaped = self._escape_value(value)
|
||||
output_parts.append(f"`FT{color}{escaped}`f")
|
||||
else:
|
||||
output_parts.append(self._escape_value(value))
|
||||
|
||||
else:
|
||||
color_key = self._get_color_key_for_token(ttype)
|
||||
color = self._get_color_from_key(color_key)
|
||||
|
||||
if color and value:
|
||||
escaped = self._escape_value(value)
|
||||
output_parts.append(f"`FT{color}{escaped}`f")
|
||||
|
||||
else: output_parts.append(self._escape_value(value))
|
||||
|
||||
prev_was_dot = is_dot
|
||||
|
||||
outfile.write("".join(output_parts))
|
||||
|
||||
def _get_color_key_for_token(self, ttype):
|
||||
token_parts = []
|
||||
current = ttype
|
||||
while current:
|
||||
token_parts.insert(0, current[0] if isinstance(current, tuple) else str(current).split(".")[-1])
|
||||
current = current.parent if hasattr(current, "parent") else None
|
||||
|
||||
token_str = ".".join(["Token"] + token_parts[1:] if len(token_parts) > 1 else token_parts)
|
||||
|
||||
current_type = ttype
|
||||
while current_type:
|
||||
token_key = str(current_type)
|
||||
if token_key in granular_token_map: return granular_token_map[token_key]
|
||||
|
||||
# Move to parent
|
||||
current_type = current_type.parent if hasattr(current_type, "parent") else None
|
||||
|
||||
return None
|
||||
|
||||
def _get_color_from_key(self, color_key):
|
||||
if color_key and color_key in self.theme: return self.theme[color_key]
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _escape_value(value: str) -> str: return value.replace("`", "\\`")
|
||||
|
||||
# Required by Pygments formatter API, returns None for Micron
|
||||
def get_style_defs(self, arg=None): return None
|
||||
|
||||
|
||||
# Convenience function for direct use
|
||||
def highlight_code(content: str, filename: str = None, language: str = None, theme=None) -> str:
|
||||
highlighter = SyntaxHighlighter(theme=theme)
|
||||
return highlighter.highlight(content, filename, language)
|
||||
|
||||
granular_token_map = {
|
||||
# Keywords with semantic distinction
|
||||
"Token.Keyword": "keyword",
|
||||
"Token.Keyword.Constant": "keyword_constant",
|
||||
"Token.Keyword.Declaration": "keyword_declaration",
|
||||
"Token.Keyword.Namespace": "keyword_control",
|
||||
"Token.Keyword.Pseudo": "keyword_control",
|
||||
"Token.Keyword.Reserved": "keyword_control",
|
||||
"Token.Keyword.Type": "type_builtin",
|
||||
|
||||
# Names - functions with definition vs call distinction
|
||||
"Token.Name.Function": "function_call",
|
||||
"Token.Name.Function.Magic": "function_magic",
|
||||
"Token.Name.Class": "class_ref",
|
||||
"Token.Name.Builtin": "function_builtin",
|
||||
"Token.Name.Builtin.Pseudo": "constant_builtin",
|
||||
"Token.Name.Exception": "exception_builtin",
|
||||
"Token.Name.Decorator": "decorator",
|
||||
"Token.Name.Namespace": "namespace",
|
||||
"Token.Name.Attribute": "attribute",
|
||||
"Token.Name.Variable": "variable",
|
||||
"Token.Name.Variable.Magic": "function_magic",
|
||||
"Token.Name.Other": "name",
|
||||
"Token.Name": "name",
|
||||
"Token.Name.Tag": "keyword", # HTML/XML tags
|
||||
"Token.Name.Constant": "constant",
|
||||
"Token.Name.Label": "name",
|
||||
"Token.Name.Entity": "name",
|
||||
|
||||
# Literals - strings with detailed handling
|
||||
"Token.Literal.String": "string",
|
||||
"Token.Literal.String.Affix": "string", # f, r, b prefixes
|
||||
"Token.Literal.String.Backtick": "string",
|
||||
"Token.Literal.String.Char": "string",
|
||||
"Token.Literal.String.Delimiter": "string",
|
||||
"Token.Literal.String.Doc": "string_doc",
|
||||
"Token.Literal.String.Double": "string_quoted",
|
||||
"Token.Literal.String.Escape": "string_escape",
|
||||
"Token.Literal.String.Heredoc": "string",
|
||||
"Token.Literal.String.Interpol": "string_interpol",
|
||||
"Token.Literal.String.Other": "string",
|
||||
"Token.Literal.String.Regex": "string",
|
||||
"Token.Literal.String.Single": "string_quoted",
|
||||
"Token.Literal.String.Symbol": "string",
|
||||
|
||||
# Numbers
|
||||
"Token.Literal.Number": "number",
|
||||
"Token.Literal.Number.Bin": "number",
|
||||
"Token.Literal.Number.Float": "number_float",
|
||||
"Token.Literal.Number.Hex": "number_hex",
|
||||
"Token.Literal.Number.Integer": "number_integer",
|
||||
"Token.Literal.Number.Integer.Long": "number_integer",
|
||||
"Token.Literal.Number.Oct": "number",
|
||||
"Token.Literal": "string",
|
||||
"Token.Literal.Date": "string",
|
||||
|
||||
# Operators - all operators get distinct coloring
|
||||
"Token.Operator": "operator",
|
||||
"Token.Operator.Word": "operator_word",
|
||||
"Token.Operator.Comparison": "operator_comparison",
|
||||
"Token.Operator.Assignment": "operator_assignment",
|
||||
"Token.Operator.Arithmetic": "operator_arithmetic",
|
||||
|
||||
# Punctuation - braces, parens, colons, commas
|
||||
"Token.Punctuation": "punctuation",
|
||||
"Token.Punctuation.Marker": "punctuation",
|
||||
"Token.Punctuation.Brace": "punctuation_brace",
|
||||
"Token.Punctuation.Bracket": "punctuation_brace",
|
||||
"Token.Punctuation.Parenthesis": "punctuation_paren",
|
||||
"Token.Punctuation.Colon": "punctuation_colon",
|
||||
"Token.Punctuation.Comma": "punctuation_comma",
|
||||
|
||||
# Comments
|
||||
"Token.Comment": "comment",
|
||||
"Token.Comment.Hashbang": "comment",
|
||||
"Token.Comment.Multiline": "comment_doc",
|
||||
"Token.Comment.Preproc": "comment_preproc",
|
||||
"Token.Comment.Single": "comment",
|
||||
"Token.Comment.Special": "comment",
|
||||
|
||||
# Generic tokens
|
||||
"Token.Generic.Deleted": "generic_deleted",
|
||||
"Token.Generic.Emph": "text",
|
||||
"Token.Generic.Error": "generic_error",
|
||||
"Token.Generic.Heading": "generic_heading",
|
||||
"Token.Generic.Inserted": "generic_inserted",
|
||||
"Token.Generic.Output": "generic_output",
|
||||
"Token.Generic.Prompt": "generic_prompt",
|
||||
"Token.Generic.Strong": "text",
|
||||
"Token.Generic.Subheading": "generic_subheading",
|
||||
"Token.Generic.Traceback": "generic_error",
|
||||
"Token.Generic": "text",
|
||||
|
||||
# Text and whitespace
|
||||
"Token.Text": "text",
|
||||
"Token.Text.Whitespace": "whitespace",
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2026 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import sys
|
||||
from RNS.Utilities.rngit import client, server
|
||||
|
||||
if __name__ == "__main__":
|
||||
cmd = sys.argv[0]
|
||||
if cmd == "rngit": ec = server.main()
|
||||
elif cmd == "git-remote-rns": ec = client.main()
|
||||
else: raise NotImplementedError(f"The {cmd} executable entrypoint is not yet implemented in rngit")
|
||||
|
||||
sys.exit(ec)
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,588 @@
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2026 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import re
|
||||
import RNS
|
||||
|
||||
class MarkdownToMicron:
|
||||
BOLD = "`!"
|
||||
BOLD_END = "`!"
|
||||
ITALIC = "`*"
|
||||
ITALIC_END = "`*"
|
||||
UNDERLINE = "`_"
|
||||
UNDERLINE_END = "`_"
|
||||
|
||||
CODE_BG = "`BT282828"
|
||||
CODE_BG_INLINE = "`BT383838"
|
||||
CODE_FG = "`Fddd"
|
||||
CODE_RESET = "`f`b"
|
||||
|
||||
LITERAL_START = "`="
|
||||
LITERAL_END = "`="
|
||||
|
||||
BULLET = "•"
|
||||
|
||||
# Regex patterns for markdown elements
|
||||
HEADER_RE = re.compile(r'^(#{1,6})\s+(.+)$')
|
||||
CODE_FENCE_RE = re.compile(r'^(\s*)```(.*)$')
|
||||
HORIZONTAL_RULE_RE = re.compile(r'^(\s*)(---+|===+|\*\*\*+|___+)\s*$')
|
||||
UNORDERED_LIST_RE = re.compile(r'^(\s*)([-*+])\s+(.+)$')
|
||||
|
||||
# Table patterns
|
||||
TABLE_ROW_RE = re.compile(r'^\s*\|?(.+?)\|?\s*$')
|
||||
TABLE_SEP_RE = re.compile(r'^\s*\|?(?:\s*:?-+:?\s*\|)+\s*$')
|
||||
|
||||
# Inline patterns (processed in order of specificity)
|
||||
LINK_RE = re.compile(r'\[([^\]]+)\]\(([^)]+)\)')
|
||||
INLINE_CODE_RE = re.compile(r'`([^`]+)`')
|
||||
BOLD_RE = re.compile(r'\*\*(.+?)\*\*|__(.+?)__')
|
||||
ITALIC_RE = re.compile(r'\*(.+?)\*|_(.+?)_')
|
||||
|
||||
TABLE_H = "─"
|
||||
TABLE_V = "│"
|
||||
TABLE_TL = "┌"
|
||||
TABLE_TR = "┐"
|
||||
TABLE_BL = "└"
|
||||
TABLE_BR = "┘"
|
||||
TABLE_ML = "├"
|
||||
TABLE_MR = "┤"
|
||||
TABLE_TM = "┬"
|
||||
TABLE_BM = "┴"
|
||||
TABLE_MM = "┼"
|
||||
|
||||
TABLE_MIN_COL_WIDTH = 3
|
||||
|
||||
def __init__(self, max_width=100, syntax_highlighter=None):
|
||||
self.max_width = max_width
|
||||
self.syntax_highlighter = syntax_highlighter
|
||||
self.wcwidth = None
|
||||
|
||||
try:
|
||||
import wcwidth
|
||||
self.wcwidth = wcwidth
|
||||
|
||||
except: RNS.log(f"The wcwidth module is unavailable, display width calculations for some glyphs will be incorrect", RNS.LOG_WARNING)
|
||||
|
||||
def display_width(self, text):
|
||||
if not self.wcwidth: return len(text)
|
||||
else:
|
||||
# wcswidth returns -1 for non-printable strings,
|
||||
# fallback to len in this case
|
||||
w = self.wcwidth.wcswidth(text)
|
||||
return w if w is not None and w >= 0 else len(text)
|
||||
|
||||
def format_block(self, text):
|
||||
lines = text.split('\n')
|
||||
result_lines = []
|
||||
in_code_block = False
|
||||
code_block_lang = None
|
||||
code_buffer = []
|
||||
in_table = False
|
||||
table_buffer = []
|
||||
|
||||
def flush_table_buffer():
|
||||
nonlocal result_lines, table_buffer, in_table
|
||||
if not table_buffer:
|
||||
in_table = False
|
||||
return
|
||||
|
||||
if len(table_buffer) >= 2 and self._is_table_separator(table_buffer[1]):
|
||||
formatted_lines = self.format_table(table_buffer)
|
||||
result_lines.extend(formatted_lines)
|
||||
|
||||
else:
|
||||
for line in table_buffer: result_lines.append(self.format_line(line))
|
||||
|
||||
table_buffer = []
|
||||
in_table = False
|
||||
|
||||
def flush_code_block():
|
||||
nonlocal result_lines, code_buffer, code_block_lang
|
||||
if not code_buffer:
|
||||
return
|
||||
|
||||
code_content = '\n'.join(code_buffer)
|
||||
|
||||
if self.syntax_highlighter and code_block_lang:
|
||||
try:
|
||||
highlighted = self.syntax_highlighter.highlight(code_content, language=code_block_lang)
|
||||
result_lines.append(f"{self.CODE_BG}{self.CODE_FG}")
|
||||
result_lines.append(highlighted)
|
||||
result_lines.append(self.CODE_RESET)
|
||||
|
||||
except Exception:
|
||||
# Fallback to plain literal block on any error
|
||||
result_lines.append(f"{self.CODE_BG}{self.CODE_FG}")
|
||||
result_lines.append(self.LITERAL_START)
|
||||
result_lines.append(self._escape_literals(code_content))
|
||||
result_lines.append(self.LITERAL_END)
|
||||
result_lines.append(self.CODE_RESET)
|
||||
else:
|
||||
result_lines.append(f"{self.CODE_BG}{self.CODE_FG}")
|
||||
result_lines.append(self.LITERAL_START)
|
||||
result_lines.append(self._escape_literals(code_content))
|
||||
result_lines.append(self.LITERAL_END)
|
||||
result_lines.append(self.CODE_RESET)
|
||||
|
||||
code_buffer = []
|
||||
|
||||
for line in lines:
|
||||
is_fence, lang_hint = self._detect_code_fence(line)
|
||||
|
||||
if is_fence:
|
||||
# Flush any pending table before code fence
|
||||
flush_table_buffer()
|
||||
|
||||
if not in_code_block:
|
||||
# Opening fence, start buffering
|
||||
in_code_block = True
|
||||
code_block_lang = lang_hint.strip() if lang_hint else None
|
||||
code_buffer = []
|
||||
|
||||
else:
|
||||
# Closing fence, flush highlighted code
|
||||
flush_code_block()
|
||||
in_code_block = False
|
||||
code_block_lang = None
|
||||
|
||||
else:
|
||||
if in_code_block:
|
||||
# Buffer code lines for later highlighting
|
||||
code_buffer.append(line)
|
||||
else:
|
||||
if self._is_table_row(line):
|
||||
if not in_table:
|
||||
in_table = True
|
||||
table_buffer = [line]
|
||||
|
||||
else: table_buffer.append(line)
|
||||
|
||||
else:
|
||||
# Line breaks table, flush buffer
|
||||
if in_table: flush_table_buffer()
|
||||
formatted = self.format_line(line)
|
||||
result_lines.append(formatted)
|
||||
|
||||
# Handle unclosed structures
|
||||
if in_table: flush_table_buffer()
|
||||
if in_code_block:
|
||||
# Unclosed code block, flush what we have
|
||||
flush_code_block()
|
||||
|
||||
return '\n'.join(result_lines)
|
||||
|
||||
def format_line(self, line, mode="normal"):
|
||||
if mode == "codeblock": return self._escape_literals(line)
|
||||
|
||||
if self.HORIZONTAL_RULE_RE.match(line): return self._format_horizontal_rule()
|
||||
|
||||
header_match = self.HEADER_RE.match(line)
|
||||
if header_match: return self._format_header(header_match)
|
||||
|
||||
list_match = self.UNORDERED_LIST_RE.match(line)
|
||||
if list_match: return self._format_list_item(list_match)
|
||||
|
||||
line = self._format_inline(line)
|
||||
|
||||
return line
|
||||
|
||||
def _format_inline(self, text):
|
||||
code_blocks = []
|
||||
def extract_code(match):
|
||||
code_blocks.append(match.group(1))
|
||||
return f"\x00CODE{len(code_blocks)-1}\x00"
|
||||
|
||||
links = []
|
||||
def extract_link(match):
|
||||
links.append((match.group(1), match.group(2)))
|
||||
return f"\x00LINK{len(links)-1}\x00"
|
||||
|
||||
text = self.INLINE_CODE_RE.sub(extract_code, text)
|
||||
text = self.LINK_RE.sub(extract_link, text)
|
||||
text = self.BOLD_RE.sub(self._bold_sub, text)
|
||||
text = self.ITALIC_RE.sub(self._italic_sub, text)
|
||||
|
||||
def restore_link(match):
|
||||
idx = int(match.group(1))
|
||||
text, url = links[idx]
|
||||
text = text.replace('`', '')
|
||||
return f"`!`[{text}`{url}]`!"
|
||||
|
||||
text = re.sub(r'\x00LINK(\d+)\x00', restore_link, text)
|
||||
|
||||
def restore_code(match):
|
||||
idx = int(match.group(1))
|
||||
content = code_blocks[idx]
|
||||
|
||||
# Disabled for now
|
||||
# highlighted = self._highlight_inline_code(content)
|
||||
# if highlighted: return highlighted
|
||||
|
||||
# Use plain inline code formatting
|
||||
content = content.replace('`', '\\`')
|
||||
return f"{self.CODE_BG_INLINE}{self.CODE_FG}{content}{self.CODE_RESET}"
|
||||
|
||||
text = re.sub(r'\x00CODE(\d+)\x00', restore_code, text)
|
||||
return text
|
||||
|
||||
def _highlight_inline_code(self, content):
|
||||
if not self.syntax_highlighter: return None
|
||||
return self.syntax_highlighter.highlight(content, language=None)
|
||||
|
||||
def _bold_sub(self, match):
|
||||
content = match.group(1) or match.group(2)
|
||||
return f"{self.BOLD}{content}{self.BOLD_END}"
|
||||
|
||||
def _italic_sub(self, match):
|
||||
content = match.group(1) or match.group(2)
|
||||
return f"{self.ITALIC}{content}{self.ITALIC_END}"
|
||||
|
||||
def _format_header(self, match):
|
||||
hashes = match.group(1)
|
||||
content = match.group(2)
|
||||
level = len(hashes)
|
||||
prefix = ">" * min(level, 6)
|
||||
return f"{prefix}{content}"
|
||||
|
||||
def _format_list_item(self, match):
|
||||
indent = match.group(1)
|
||||
content = match.group(3)
|
||||
content = self._format_inline(content)
|
||||
return f"{indent} {self.BULLET} {content}"
|
||||
|
||||
def _format_horizontal_rule(self):
|
||||
return "-"
|
||||
|
||||
def _detect_code_fence(self, line):
|
||||
match = self.CODE_FENCE_RE.match(line)
|
||||
if match:
|
||||
# match.group(2) contains everything after the backticks (language hint)
|
||||
return True, match.group(2)
|
||||
return False, ""
|
||||
|
||||
def _is_table_row(self, line):
|
||||
if '|' not in line: return False
|
||||
match = self.TABLE_ROW_RE.match(line)
|
||||
if match is None: return False
|
||||
content = match.group(1)
|
||||
return '|' in content or line.strip().startswith('|')
|
||||
|
||||
def _is_table_separator(self, line):
|
||||
if '|' not in line: return False
|
||||
match = self.TABLE_SEP_RE.match(line)
|
||||
return match is not None
|
||||
|
||||
def _escape_literals(self, text):
|
||||
return text.replace('`', '\\`')
|
||||
|
||||
def format_table(self, rows, align="c"):
|
||||
if len(rows) < 2: return rows
|
||||
|
||||
# Parse header and separator
|
||||
header_cells = self._parse_table_row(rows[0])
|
||||
alignments = self._parse_table_alignments(rows[1])
|
||||
|
||||
# Ensure alignment count matches header cells
|
||||
while len(alignments) < len(header_cells): alignments.append('left')
|
||||
alignments = alignments[:len(header_cells)]
|
||||
|
||||
# Parse data rows
|
||||
data_rows = []
|
||||
for i in range(2, len(rows)):
|
||||
cells = self._parse_table_row(rows[i])
|
||||
while len(cells) < len(header_cells): cells.append("")
|
||||
cells = cells[:len(header_cells)]
|
||||
data_rows.append(cells)
|
||||
|
||||
# Calculate column widths based on content
|
||||
num_cols = len(header_cells)
|
||||
col_widths = [0] * num_cols
|
||||
|
||||
all_rows = [header_cells] + data_rows
|
||||
for row in all_rows:
|
||||
for i, cell in enumerate(row):
|
||||
formatted = self._format_inline(cell)
|
||||
width = self._visible_width(formatted)
|
||||
col_widths[i] = max(col_widths[i], width)
|
||||
|
||||
# Apply minimum width and calculate total
|
||||
col_widths = [max(w, self.TABLE_MIN_COL_WIDTH) for w in col_widths]
|
||||
|
||||
# Check max_width constraint
|
||||
# Total = sum of columns + 3 chars per column (space + 2 borders) + 1 for final border
|
||||
total_width = sum(col_widths) + (num_cols * 3) + 1
|
||||
|
||||
if total_width > self.max_width:
|
||||
# Reduce widest columns proportionally
|
||||
excess = total_width - self.max_width
|
||||
indexed_widths = [(i, w) for i, w in enumerate(col_widths)]
|
||||
indexed_widths.sort(key=lambda x: -x[1])
|
||||
|
||||
for i, w in indexed_widths:
|
||||
if excess <= 0: break
|
||||
reduction = min(excess, w - self.TABLE_MIN_COL_WIDTH)
|
||||
col_widths[i] -= reduction
|
||||
excess -= reduction
|
||||
|
||||
# Build formatted table
|
||||
result = []
|
||||
|
||||
# Alignment start
|
||||
if align: result.append(f"`{align}")
|
||||
|
||||
# Top border
|
||||
border = self.TABLE_TL
|
||||
for i, w in enumerate(col_widths):
|
||||
border += self.TABLE_H * (w + 2)
|
||||
if i < len(col_widths) - 1: border += self.TABLE_TM
|
||||
else: border += self.TABLE_TR
|
||||
|
||||
result.append(self._escape_literals(border))
|
||||
|
||||
# Header row
|
||||
header_line = self.TABLE_V
|
||||
for i, cell in enumerate(header_cells):
|
||||
formatted = self._format_inline(cell)
|
||||
padded = self._pad_cell(formatted, col_widths[i], 'left')
|
||||
header_line += f" {padded} {self.TABLE_V}"
|
||||
result.append(self._escape_literals(header_line))
|
||||
|
||||
# Separator row
|
||||
sep_line = self.TABLE_ML
|
||||
for i, w in enumerate(col_widths):
|
||||
cell_width = w + 2
|
||||
sep_line += self.TABLE_H * cell_width
|
||||
|
||||
if i < len(col_widths) - 1: sep_line += self.TABLE_MM
|
||||
else: sep_line += self.TABLE_MR
|
||||
|
||||
result.append(self._escape_literals(sep_line))
|
||||
|
||||
# Data rows
|
||||
for row in data_rows:
|
||||
row_line = self.TABLE_V
|
||||
for i, cell in enumerate(row):
|
||||
formatted = self._format_inline(cell)
|
||||
padded = self._pad_cell(formatted, col_widths[i], alignments[i])
|
||||
row_line += f" {padded} {self.TABLE_V}"
|
||||
|
||||
result.append(row_line)
|
||||
|
||||
# Bottom border
|
||||
border = self.TABLE_BL
|
||||
for i, w in enumerate(col_widths):
|
||||
border += self.TABLE_H * (w + 2)
|
||||
if i < len(col_widths) - 1: border += self.TABLE_BM
|
||||
else: border += self.TABLE_BR
|
||||
|
||||
result.append(self._escape_literals(border))
|
||||
|
||||
# End alignment
|
||||
if align: result.append("`a")
|
||||
|
||||
return result
|
||||
|
||||
def format_table_raw(self, rows, align="c"):
|
||||
if len(rows) < 2: return rows
|
||||
|
||||
# Parse header and separator
|
||||
header_cells = self._parse_table_row(rows[0])
|
||||
alignments = self._parse_table_alignments(rows[1])
|
||||
|
||||
# Ensure alignment count matches header cells
|
||||
while len(alignments) < len(header_cells): alignments.append('left')
|
||||
alignments = alignments[:len(header_cells)]
|
||||
|
||||
# Parse data rows
|
||||
data_rows = []
|
||||
for i in range(2, len(rows)):
|
||||
cells = self._parse_table_row(rows[i])
|
||||
while len(cells) < len(header_cells): cells.append("")
|
||||
cells = cells[:len(header_cells)]
|
||||
data_rows.append(cells)
|
||||
|
||||
# Calculate column widths based on raw content
|
||||
num_cols = len(header_cells)
|
||||
col_widths = [0] * num_cols
|
||||
|
||||
all_rows = [header_cells] + data_rows
|
||||
for row in all_rows:
|
||||
for i, cell in enumerate(row):
|
||||
width = self._visible_width(cell)
|
||||
col_widths[i] = max(col_widths[i], width)
|
||||
|
||||
# Apply minimum width and calculate total
|
||||
col_widths = [max(w, self.TABLE_MIN_COL_WIDTH) for w in col_widths]
|
||||
|
||||
# Check max_width constraint
|
||||
total_width = sum(col_widths) + (num_cols * 3) + 1
|
||||
|
||||
if total_width > self.max_width:
|
||||
# Reduce widest columns proportionally
|
||||
excess = total_width - self.max_width
|
||||
indexed_widths = [(i, w) for i, w in enumerate(col_widths)]
|
||||
indexed_widths.sort(key=lambda x: -x[1])
|
||||
|
||||
for i, w in indexed_widths:
|
||||
if excess <= 0: break
|
||||
reduction = min(excess, w - self.TABLE_MIN_COL_WIDTH)
|
||||
col_widths[i] -= reduction
|
||||
excess -= reduction
|
||||
|
||||
# Build formatted table
|
||||
result = []
|
||||
|
||||
# Alignment start
|
||||
if align: result.append(f"`{align}")
|
||||
|
||||
# Top border
|
||||
border = self.TABLE_TL
|
||||
for i, w in enumerate(col_widths):
|
||||
border += self.TABLE_H * (w + 2)
|
||||
if i < len(col_widths) - 1: border += self.TABLE_TM
|
||||
else: border += self.TABLE_TR
|
||||
|
||||
result.append(self._escape_literals(border))
|
||||
|
||||
# Header row
|
||||
header_line = self.TABLE_V
|
||||
for i, cell in enumerate(header_cells):
|
||||
padded = self._pad_cell(cell, col_widths[i], 'left')
|
||||
header_line += f" {padded} {self.TABLE_V}"
|
||||
result.append(header_line)
|
||||
|
||||
# Separator row - clean horizontal lines without alignment markers
|
||||
sep_line = self.TABLE_ML
|
||||
for i, w in enumerate(col_widths):
|
||||
cell_width = w + 2
|
||||
sep_line += self.TABLE_H * cell_width
|
||||
|
||||
if i < len(col_widths) - 1: sep_line += self.TABLE_MM
|
||||
else: sep_line += self.TABLE_MR
|
||||
|
||||
result.append(self._escape_literals(sep_line))
|
||||
|
||||
# Data rows (with alignment)
|
||||
for row in data_rows:
|
||||
row_line = self.TABLE_V
|
||||
for i, cell in enumerate(row):
|
||||
padded = self._pad_cell(cell, col_widths[i], alignments[i])
|
||||
row_line += f" {padded} {self.TABLE_V}"
|
||||
|
||||
result.append(row_line)
|
||||
|
||||
# Bottom border
|
||||
border = self.TABLE_BL
|
||||
for i, w in enumerate(col_widths):
|
||||
border += self.TABLE_H * (w + 2)
|
||||
if i < len(col_widths) - 1: border += self.TABLE_BM
|
||||
else: border += self.TABLE_BR
|
||||
|
||||
result.append(self._escape_literals(border))
|
||||
|
||||
# End alignment
|
||||
if align: result.append("`a")
|
||||
|
||||
return result
|
||||
|
||||
def _parse_table_row(self, line):
|
||||
line = line.strip()
|
||||
if line.startswith('|'): line = line[1:]
|
||||
if line.endswith('|'): line = line[:-1]
|
||||
|
||||
cells = []
|
||||
current = ""
|
||||
escaped = False
|
||||
for char in line:
|
||||
if escaped:
|
||||
current += char
|
||||
escaped = False
|
||||
elif char == '\\':
|
||||
escaped = True
|
||||
elif char == '|':
|
||||
cells.append(current.strip())
|
||||
current = ""
|
||||
else:
|
||||
current += char
|
||||
|
||||
cells.append(current.strip())
|
||||
return cells
|
||||
|
||||
def _parse_table_alignments(self, line):
|
||||
cells = self._parse_table_row(line)
|
||||
alignments = []
|
||||
for cell in cells:
|
||||
cell = cell.strip()
|
||||
if cell.startswith(':') and cell.endswith(':'): alignments.append('center')
|
||||
elif cell.endswith(':'): alignments.append('right')
|
||||
else: alignments.append('left')
|
||||
|
||||
return alignments
|
||||
|
||||
def _visible_width(self, text):
|
||||
text = re.sub(r'`[FB][0-9a-fA-F]{3}', '', text)
|
||||
text = re.sub(r'`[FB]T[0-9a-fA-F]{6}', '', text)
|
||||
text = re.sub(r'`[!*_=]', '', text)
|
||||
text = re.sub(r'`f`b', '', text)
|
||||
text = re.sub(r'`f', '', text)
|
||||
text = re.sub(r'`b', '', text)
|
||||
return self.display_width(text)
|
||||
|
||||
def _pad_cell(self, text, width, align):
|
||||
text = self._truncate_cell(text, width)
|
||||
text_width = self._visible_width(text)
|
||||
padding = width - text_width
|
||||
|
||||
if align == 'right':
|
||||
return " " * padding + text
|
||||
elif align == 'center':
|
||||
left = padding // 2
|
||||
right = padding - left
|
||||
return " " * left + text + " " * right
|
||||
else:
|
||||
return text + " " * padding
|
||||
|
||||
def _truncate_cell(self, text, width):
|
||||
if self._visible_width(text) <= width: return text
|
||||
|
||||
stripped = text
|
||||
stripped = re.sub(r'`[FB][0-9a-fA-F]{3}', '', stripped)
|
||||
stripped = re.sub(r'`[!*_]', '', stripped)
|
||||
stripped = re.sub(r'`f`b', '', stripped)
|
||||
|
||||
if len(stripped) <= width - 1: return text
|
||||
|
||||
truncated = stripped[:width - 1] + "…"
|
||||
return truncated
|
||||
|
||||
|
||||
def convert_markdown_to_micron(text):
|
||||
converter = MarkdownToMicron()
|
||||
return converter.format_block(text)
|
||||
@@ -0,0 +1,41 @@
|
||||
# Based on the original rnsh program by Aaron Heise (@acehoss)
|
||||
# https://github.com/acehoss/rnsh - MIT License - Copyright (c) 2023 Aaron Heise
|
||||
# This version of rnsh is included in RNS under the Reticulum License
|
||||
#
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2026 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
from ._version import __version__
|
||||
|
||||
import os
|
||||
module_abs_filename = os.path.abspath(__file__)
|
||||
module_dir = os.path.dirname(module_abs_filename)
|
||||
|
||||
def _get_version(): return __version__
|
||||
@@ -0,0 +1 @@
|
||||
__version__ = "0.2.0"
|
||||
@@ -0,0 +1,93 @@
|
||||
# Based on the original rnsh program by Aaron Heise (@acehoss)
|
||||
# https://github.com/acehoss/rnsh - MIT License - Copyright (c) 2023 Aaron Heise
|
||||
# This version of rnsh is included in RNS under the Reticulum License
|
||||
#
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2026 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
from RNS.Utilities.rnsh._version import __version__ as __rnsh_version__
|
||||
from RNS._version import __version__
|
||||
|
||||
DEFAULT_SERVICE_NAME = "default"
|
||||
|
||||
def setup_argument_parser():
|
||||
parser = argparse.ArgumentParser(description="Reticulum Remote Shell Utility", epilog="When specifying a command to execute, separate rnsh\noptions from the command and its arguments with --\n\nFor example:\n rnsh -l -- /bin/bash --login\n rnsh <destination> -- ls -la /tmp", formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
|
||||
# Common options
|
||||
parser.add_argument("--config", "-c", action="store", default=None, help="path to alternative Reticulum config directory", type=str)
|
||||
parser.add_argument("--identity", "-i", action="store", default=None, help="path to identity file to use", type=str)
|
||||
parser.add_argument("-v", "--verbose", action="count", default=0, help="increase verbosity")
|
||||
parser.add_argument("-q", "--quiet", action="count", default=0, help="decrease verbosity")
|
||||
parser.add_argument("-p", "--print-identity", action="store_true", default=False, help="print identity and destination info and exit")
|
||||
parser.add_argument("--version", action="version", version="rnsh {rv} (protocol {pv})".format(rv=__version__, pv=__rnsh_version__))
|
||||
|
||||
# Listener options
|
||||
parser.add_argument("-l", "--listen", action="store_true", default=False, help="listen (server) mode; any command specified after -- will be used as the default command when the initiator does not provide one or when remote command execution is disabled; if no command is specified, the default shell of the user running rnsh will be used")
|
||||
parser.add_argument("-s", "--service", action="store", default=None, help="service name for identity file if not the default", type=str)
|
||||
parser.add_argument("-b", "--announce",action="store", default=None,help="announce on startup and every PERIOD seconds; specify 0 to announce on startup only",metavar="PERIOD", type=int)
|
||||
parser.add_argument("-a", "--allowed", action="append", default=None, metavar="HASH", type=str, help="allow this identity to connect (may be specified multiple times); allowed identities can also be specified in ~/.rnsh/allowed_identities or ~/.config/rnsh/allowed_identities, one hash per line")
|
||||
parser.add_argument("-n", "--no-auth", action="store_true", default=False, help="disable authentication (allow any identity to connect)")
|
||||
parser.add_argument("-A", "--remote-command-as-args", action="store_true", default=False, help="concatenate remote command to the argument list of the default program or shell")
|
||||
parser.add_argument("-C", "--no-remote-command", action="store_true", default=False, help="disable executing command lines received from the remote initiator")
|
||||
|
||||
# Initiator options
|
||||
parser.add_argument("-N", "--no-id", action="store_true", default=False, help="disable identity announcement on connect")
|
||||
parser.add_argument("-m", "--mirror", action="store_true", default=False, help="return with the exit code of the remote process")
|
||||
parser.add_argument("-w", "--timeout", action="store", default=None, help="connect and request timeout in seconds", metavar="SECONDS", type=float)
|
||||
|
||||
parser.add_argument("destination", nargs="?", default=None, help="hexadecimal hash of the destination to connect to", type=str)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def parse_arguments(argv=None):
|
||||
if argv is None: argv = sys.argv[1:]
|
||||
|
||||
# Split at -- to separate rnsh options from the command to execute.
|
||||
# Everything before -- (or the entire argv if no --) goes to argparse.
|
||||
# Everything after -- becomes the command list.
|
||||
try:
|
||||
split_idx = argv.index("--")
|
||||
rnsh_argv = argv[:split_idx]
|
||||
command = argv[split_idx + 1:]
|
||||
except ValueError:
|
||||
rnsh_argv = argv
|
||||
command = []
|
||||
|
||||
parser = setup_argument_parser()
|
||||
args = parser.parse_args(rnsh_argv)
|
||||
args.command = command
|
||||
|
||||
if args.listen and not args.service: args.service = DEFAULT_SERVICE_NAME
|
||||
|
||||
return args, parser
|
||||
@@ -0,0 +1,60 @@
|
||||
# Based on the original rnsh program by Aaron Heise (@acehoss)
|
||||
# https://github.com/acehoss/rnsh - MIT License - Copyright (c) 2023 Aaron Heise
|
||||
# This version of rnsh is included in RNS under the Reticulum License
|
||||
#
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2026 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import contextlib
|
||||
from contextlib import AbstractContextManager
|
||||
import logging
|
||||
import sys
|
||||
|
||||
|
||||
class permit(AbstractContextManager):
|
||||
"""Context manager to allow specified exceptions
|
||||
|
||||
The specified exceptions will be allowed to bubble up. Other
|
||||
exceptions are suppressed.
|
||||
|
||||
After a non-matching exception is suppressed, execution proceeds
|
||||
with the next statement following the with statement.
|
||||
|
||||
with allow(KeyboardInterrupt):
|
||||
time.sleep(300)
|
||||
# Execution still resumes here if no KeyboardInterrupt
|
||||
"""
|
||||
|
||||
def __init__(self, *exceptions): self._exceptions = exceptions
|
||||
|
||||
def __enter__(self): pass
|
||||
|
||||
def __exit__(self, exctype, excinst, exctb):
|
||||
return exctype is not None and not issubclass(exctype, self._exceptions)
|
||||
@@ -0,0 +1,59 @@
|
||||
# Based on the original rnsh program by Aaron Heise (@acehoss)
|
||||
# https://github.com/acehoss/rnsh - MIT License - Copyright (c) 2023 Aaron Heise
|
||||
# This version of rnsh is included in RNS under the Reticulum License
|
||||
#
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2026 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
def bitwise_or_if(value: int, condition: bool, orval: int):
|
||||
if not condition: return value
|
||||
return value | orval
|
||||
|
||||
def check_and(value: int, andval: int) -> bool:
|
||||
return (value & andval) > 0
|
||||
|
||||
class SleepRate:
|
||||
def __init__(self, target_period: float):
|
||||
self.target_period = target_period
|
||||
self.last_wake = time.time()
|
||||
|
||||
def next_sleep_time(self) -> float:
|
||||
old_last_wake = self.last_wake
|
||||
self.last_wake = time.time()
|
||||
next_wake = max(old_last_wake + 0.01, self.last_wake)
|
||||
sleep_for = next_wake - self.last_wake
|
||||
return sleep_for if sleep_for > 0 else 0
|
||||
|
||||
async def sleep_async(self): await asyncio.sleep(self.next_sleep_time())
|
||||
|
||||
def sleep_block(self): time.sleep(self.next_sleep_time())
|
||||
@@ -0,0 +1,484 @@
|
||||
# Based on the original rnsh program by Aaron Heise (@acehoss)
|
||||
# https://github.com/acehoss/rnsh - MIT License - Copyright (c) 2023 Aaron Heise
|
||||
# This version of rnsh is included in RNS under the Reticulum License
|
||||
#
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2026 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
import enum
|
||||
import functools
|
||||
import os
|
||||
import queue
|
||||
import shlex
|
||||
import signal
|
||||
import sys
|
||||
import termios
|
||||
import threading
|
||||
import time
|
||||
import tty
|
||||
from typing import Callable, TypeVar
|
||||
import RNS
|
||||
import RNS.Utilities.rnsh.exception as exception
|
||||
import RNS.Utilities.rnsh.process as process
|
||||
import RNS.Utilities.rnsh.retry as retry
|
||||
import RNS.Utilities.rnsh.session as session
|
||||
import re
|
||||
import contextlib
|
||||
|
||||
import pwd
|
||||
import bz2
|
||||
import RNS.Utilities.rnsh.protocol as protocol
|
||||
import RNS.Utilities.rnsh.helpers as helpers
|
||||
import RNS.Utilities.rnsh.rnsh as rnsh
|
||||
|
||||
_identity = None
|
||||
_reticulum = None
|
||||
_cmd: [str] | None = None
|
||||
DATA_AVAIL_MSG = "data available"
|
||||
_finished: asyncio.Event = None
|
||||
_retry_timer: retry.RetryThread | None = None
|
||||
_destination: RNS.Destination | None = None
|
||||
_loop: asyncio.AbstractEventLoop | None = None
|
||||
|
||||
|
||||
async def _check_finished(timeout: float = 0):
|
||||
return _finished is not None and await process.event_wait(_finished, timeout=timeout)
|
||||
|
||||
def _sigint_handler(sig, loop):
|
||||
global _finished
|
||||
RNS.log(f"{signal.Signals(sig).name}", RNS.LOG_DEBUG)
|
||||
if _finished is not None: _finished.set()
|
||||
else: raise KeyboardInterrupt()
|
||||
|
||||
async def _spin_tty(until=None, msg=None, timeout=None):
|
||||
i = 0
|
||||
syms = "⢄⢂⢁⡁⡈⡐⡠"
|
||||
if timeout != None: timeout = time.time()+timeout
|
||||
|
||||
print(msg+" ", end=" ")
|
||||
while (timeout == None or time.time()<timeout) and not until():
|
||||
await asyncio.sleep(0.1)
|
||||
print(("\b\b"+syms[i]+" "), end="")
|
||||
sys.stdout.flush()
|
||||
i = (i+1)%len(syms)
|
||||
|
||||
print("\r"+" "*len(msg)+" \r", end="")
|
||||
|
||||
if timeout != None and time.time() > timeout: return False
|
||||
else: return True
|
||||
|
||||
|
||||
async def _spin_pipe(until: callable = None, msg=None, timeout: float | None = None) -> bool:
|
||||
if timeout is not None: timeout += time.time()
|
||||
|
||||
while (timeout is None or time.time() < timeout) and not until():
|
||||
if await _check_finished(0.1): raise asyncio.CancelledError()
|
||||
|
||||
if timeout is not None and time.time() > timeout: return False
|
||||
else: return True
|
||||
|
||||
async def _spin(until: callable = None, msg=None, timeout: float | None = None, quiet: bool = False) -> bool:
|
||||
if not quiet and os.isatty(1): return await _spin_tty(until, msg, timeout)
|
||||
else: return await _spin_pipe(until, msg, timeout)
|
||||
|
||||
_link: RNS.Link | None = None
|
||||
_remote_exec_grace = 2.0
|
||||
_pq = queue.Queue()
|
||||
|
||||
|
||||
class InitiatorState(enum.IntEnum):
|
||||
IS_INITIAL = 0
|
||||
IS_LINKED = 1
|
||||
IS_WAIT_VERS = 2
|
||||
IS_RUNNING = 3
|
||||
IS_TERMINATE = 4
|
||||
IS_TEARDOWN = 5
|
||||
|
||||
def _client_link_closed(link):
|
||||
if _finished: _finished.set()
|
||||
|
||||
def _client_message_handler(message: RNS.MessageBase): _pq.put(message)
|
||||
|
||||
def compute_target_rns_loglevel(verbosity: int, quietness: int, base_level: int = RNS.LOG_INFO) -> int:
|
||||
try:
|
||||
target = int(base_level) + int(verbosity) - int(quietness)
|
||||
if target < RNS.LOG_CRITICAL: target = RNS.LOG_CRITICAL
|
||||
if target > RNS.LOG_DEBUG: target = RNS.LOG_DEBUG
|
||||
return target
|
||||
|
||||
except Exception: return base_level
|
||||
|
||||
|
||||
class RemoteExecutionError(Exception):
|
||||
def __init__(self, msg): self.msg = msg
|
||||
|
||||
|
||||
async def _initiate_link(configdir, rnsconfigdir, identitypath=None, verbosity=0, quietness=0, noid=False, destination=None,
|
||||
timeout=RNS.Transport.PATH_REQUEST_TIMEOUT):
|
||||
global _identity, _reticulum, _link, _destination, _remote_exec_grace
|
||||
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH // 8) * 2
|
||||
if len(destination) != dest_len:
|
||||
raise RemoteExecutionError(
|
||||
"Allowed destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(
|
||||
hex=dest_len, byte=dest_len // 2))
|
||||
try:
|
||||
destination_hash = bytes.fromhex(destination)
|
||||
except Exception as e:
|
||||
raise RemoteExecutionError("Invalid destination entered. Check your input.")
|
||||
|
||||
if _reticulum is None:
|
||||
targetloglevel = compute_target_rns_loglevel(verbosity, quietness, RNS.LOG_ERROR)
|
||||
RNS.logfile = os.path.join(configdir, "logfile")
|
||||
_reticulum = RNS.Reticulum(configdir=rnsconfigdir, loglevel=targetloglevel, logdest=RNS.LOG_FILE)
|
||||
|
||||
if _identity is None:
|
||||
_identity = rnsh.prepare_identity(identitypath)
|
||||
|
||||
if not RNS.Transport.has_path(destination_hash):
|
||||
RNS.Transport.request_path(destination_hash)
|
||||
RNS.log(f"Requesting path...", RNS.LOG_INFO)
|
||||
if not await _spin(until=lambda: RNS.Transport.has_path(destination_hash), msg="Requesting path...",
|
||||
timeout=timeout, quiet=quietness > 0):
|
||||
raise RemoteExecutionError("Path not found")
|
||||
|
||||
if _destination is None:
|
||||
listener_identity = RNS.Identity.recall(destination_hash)
|
||||
_destination = RNS.Destination(
|
||||
listener_identity,
|
||||
RNS.Destination.OUT,
|
||||
RNS.Destination.SINGLE,
|
||||
rnsh.APP_NAME
|
||||
)
|
||||
|
||||
if _link is None or _link.status == RNS.Link.PENDING:
|
||||
RNS.log("No link", RNS.LOG_DEBUG)
|
||||
_link = RNS.Link(_destination)
|
||||
_link.did_identify = False
|
||||
|
||||
_link.set_link_closed_callback(_client_link_closed)
|
||||
|
||||
RNS.log(f"Establishing link...", RNS.LOG_VERBOSE)
|
||||
if not await _spin(until=lambda: _link.status == RNS.Link.ACTIVE, msg="Establishing link...",
|
||||
timeout=timeout, quiet=quietness > 0):
|
||||
raise RemoteExecutionError("Could not establish link with " + RNS.prettyhexrep(destination_hash))
|
||||
|
||||
RNS.log("Have link", RNS.LOG_DEBUG)
|
||||
if not noid and not _link.did_identify:
|
||||
# Delay a tiny bit to allow listener to fully enter WAIT_IDENT state
|
||||
await asyncio.sleep(min(1, _link.rtt * 1.1 + 0.05))
|
||||
_link.identify(_identity)
|
||||
_link.did_identify = True
|
||||
|
||||
|
||||
async def _handle_error(errmsg: RNS.MessageBase):
|
||||
if isinstance(errmsg, protocol.ErrorMessage):
|
||||
with contextlib.suppress(Exception):
|
||||
if _link and _link.status == RNS.Link.ACTIVE:
|
||||
_link.teardown()
|
||||
await asyncio.sleep(0.1)
|
||||
raise RemoteExecutionError(f"Remote error: {errmsg.msg}")
|
||||
|
||||
|
||||
async def initiate(configdir: str, rnsconfigdir:str, identitypath: str, verbosity: int, quietness: int, noid: bool, destination: str,
|
||||
timeout: float, command: [str] | None = None):
|
||||
global _finished, _link
|
||||
with process.TTYRestorer(sys.stdin.fileno()) as ttyRestorer:
|
||||
loop = asyncio.get_running_loop()
|
||||
state = InitiatorState.IS_INITIAL
|
||||
data_buffer = bytearray(sys.stdin.buffer.read()) if not os.isatty(sys.stdin.fileno()) else bytearray()
|
||||
line_buffer = bytearray()
|
||||
|
||||
await _initiate_link(configdir=configdir,
|
||||
rnsconfigdir=rnsconfigdir,
|
||||
identitypath=identitypath,
|
||||
verbosity=verbosity,
|
||||
quietness=quietness,
|
||||
noid=noid,
|
||||
destination=destination,
|
||||
timeout=timeout)
|
||||
|
||||
if not _link or _link.status not in [RNS.Link.ACTIVE, RNS.Link.PENDING]:
|
||||
return 255
|
||||
|
||||
state = InitiatorState.IS_LINKED
|
||||
outlet = session.RNSOutlet(_link)
|
||||
channel = _link.get_channel()
|
||||
protocol.register_message_types(channel)
|
||||
channel.add_message_handler(_client_message_handler)
|
||||
|
||||
# Next step after linking and identifying: send version
|
||||
# if not await _spin(lambda: messenger.is_outlet_ready(outlet), timeout=5, quiet=quietness > 0):
|
||||
# print("Error bringing up link")
|
||||
# return 253
|
||||
|
||||
channel.send(protocol.VersionInfoMessage())
|
||||
try:
|
||||
vm = _pq.get(timeout=max(outlet.rtt * 20, 5))
|
||||
await _handle_error(vm)
|
||||
if not isinstance(vm, protocol.VersionInfoMessage):
|
||||
raise Exception("Invalid message received")
|
||||
RNS.log(f"Server version info: sw {vm.sw_version} prot {vm.protocol_version}", RNS.LOG_DEBUG)
|
||||
state = InitiatorState.IS_RUNNING
|
||||
except queue.Empty:
|
||||
print("Protocol error")
|
||||
return 254
|
||||
|
||||
winch = False
|
||||
def sigwinch_handler():
|
||||
nonlocal winch
|
||||
winch = True
|
||||
|
||||
esc = False
|
||||
pre_esc = True
|
||||
line_mode = False
|
||||
line_flush = False
|
||||
blind_write_count = 0
|
||||
flush_chars = ["\x01", "\x03", "\x04", "\x05", "\x0c", "\x11", "\x13", "\x15", "\x19", "\t", "\x1A", "\x1B"]
|
||||
def handle_escape(b):
|
||||
nonlocal line_mode
|
||||
if b == "?":
|
||||
os.write(1, "\n\r\n\rSupported rnsh escape sequences:".encode("utf-8"))
|
||||
os.write(1, "\n\r ~~ Send the escape character by typing it twice".encode("utf-8"))
|
||||
os.write(1, "\n\r ~. Terminate session and exit immediately".encode("utf-8"))
|
||||
os.write(1, "\n\r ~L Toggle line-interactive mode".encode("utf-8"))
|
||||
os.write(1, "\n\r ~? Display this quick reference\n\r".encode("utf-8"))
|
||||
os.write(1, "\n\r(Escape sequences are only recognized immediately after newline)\n\r".encode("utf-8"))
|
||||
return None
|
||||
elif b == ".":
|
||||
_link.teardown()
|
||||
return None
|
||||
elif b == "L":
|
||||
line_mode = not line_mode
|
||||
if line_mode:
|
||||
os.write(1, "\n\rLine-interactive mode enabled\n\r".encode("utf-8"))
|
||||
else:
|
||||
os.write(1, "\n\rLine-interactive mode disabled\n\r".encode("utf-8"))
|
||||
return None
|
||||
|
||||
return b
|
||||
|
||||
stdin_eof = False
|
||||
def stdin():
|
||||
nonlocal stdin_eof, pre_esc, esc, line_mode
|
||||
nonlocal line_flush, blind_write_count
|
||||
try:
|
||||
in_data = process.tty_read(sys.stdin.fileno())
|
||||
if in_data is not None:
|
||||
data = bytearray()
|
||||
for b in bytes(in_data):
|
||||
c = chr(b)
|
||||
if c == "\r":
|
||||
pre_esc = True
|
||||
line_flush = True
|
||||
data.append(b)
|
||||
elif line_mode and c in flush_chars:
|
||||
pre_esc = False
|
||||
line_flush = True
|
||||
data.append(b)
|
||||
elif line_mode and (c == "\b" or c == "\x7f"):
|
||||
pre_esc = False
|
||||
if len(line_buffer)>0:
|
||||
line_buffer.pop(-1)
|
||||
blind_write_count -= 1
|
||||
os.write(1, "\b \b".encode("utf-8"))
|
||||
elif pre_esc == True and c == "~":
|
||||
pre_esc = False
|
||||
esc = True
|
||||
elif esc == True:
|
||||
ret = handle_escape(c)
|
||||
if ret != None:
|
||||
if ret != "~":
|
||||
data.append(ord("~"))
|
||||
data.append(ord(ret))
|
||||
esc = False
|
||||
else:
|
||||
pre_esc = False
|
||||
data.append(b)
|
||||
|
||||
if not line_mode:
|
||||
data_buffer.extend(data)
|
||||
else:
|
||||
line_buffer.extend(data)
|
||||
if line_flush:
|
||||
data_buffer.extend(line_buffer)
|
||||
line_buffer.clear()
|
||||
os.write(1, ("\b \b"*blind_write_count).encode("utf-8"))
|
||||
line_flush = False
|
||||
blind_write_count = 0
|
||||
else:
|
||||
os.write(1, data)
|
||||
blind_write_count += len(data)
|
||||
|
||||
except EOFError:
|
||||
if os.isatty(0):
|
||||
data_buffer.extend(process.CTRL_D)
|
||||
stdin_eof = True
|
||||
process.tty_unset_reader_callbacks(sys.stdin.fileno())
|
||||
|
||||
process.tty_add_reader_callback(sys.stdin.fileno(), stdin)
|
||||
|
||||
tcattr = None
|
||||
rows, cols, hpix, vpix = (None, None, None, None)
|
||||
try:
|
||||
tcattr = termios.tcgetattr(0)
|
||||
rows, cols, hpix, vpix = process.tty_get_winsize(0)
|
||||
except:
|
||||
try:
|
||||
tcattr = termios.tcgetattr(1)
|
||||
rows, cols, hpix, vpix = process.tty_get_winsize(1)
|
||||
except:
|
||||
try:
|
||||
tcattr = termios.tcgetattr(2)
|
||||
rows, cols, hpix, vpix = process.tty_get_winsize(2)
|
||||
except:
|
||||
pass
|
||||
|
||||
await _spin(lambda: channel.is_ready_to_send(), "Waiting for channel...", 1, quietness > 0)
|
||||
channel.send(protocol.ExecuteCommandMesssage(cmdline=command,
|
||||
pipe_stdin=not os.isatty(0),
|
||||
pipe_stdout=not os.isatty(1),
|
||||
pipe_stderr=not os.isatty(2),
|
||||
tcflags=tcattr,
|
||||
term=os.environ.get("TERM", None),
|
||||
rows=rows,
|
||||
cols=cols,
|
||||
hpix=hpix,
|
||||
vpix=vpix))
|
||||
|
||||
loop.add_signal_handler(signal.SIGWINCH, sigwinch_handler)
|
||||
_finished = asyncio.Event()
|
||||
loop.add_signal_handler(signal.SIGINT, functools.partial(_sigint_handler, signal.SIGINT, loop))
|
||||
loop.add_signal_handler(signal.SIGTERM, functools.partial(_sigint_handler, signal.SIGTERM, loop))
|
||||
mdu = _link.MDU - 16
|
||||
sent_eof = False
|
||||
last_winch = time.time()
|
||||
sleeper = helpers.SleepRate(0.01)
|
||||
processed = False
|
||||
while not await _check_finished() and state in [InitiatorState.IS_RUNNING]:
|
||||
try:
|
||||
try:
|
||||
message = _pq.get(timeout=sleeper.next_sleep_time() if not processed else 0.0005)
|
||||
await _handle_error(message)
|
||||
processed = True
|
||||
if isinstance(message, protocol.StreamDataMessage):
|
||||
if message.stream_id == protocol.StreamDataMessage.STREAM_ID_STDOUT:
|
||||
if message.data and len(message.data) > 0:
|
||||
ttyRestorer.raw()
|
||||
RNS.log(f"stdout: {message.data}", RNS.LOG_DEBUG)
|
||||
os.write(1, message.data)
|
||||
sys.stdout.flush()
|
||||
if message.eof:
|
||||
os.close(1)
|
||||
if message.stream_id == protocol.StreamDataMessage.STREAM_ID_STDERR:
|
||||
if message.data and len(message.data) > 0:
|
||||
ttyRestorer.raw()
|
||||
RNS.log(f"stdout: {message.data}", RNS.LOG_DEBUG)
|
||||
os.write(2, message.data)
|
||||
sys.stderr.flush()
|
||||
if message.eof:
|
||||
os.close(2)
|
||||
elif isinstance(message, protocol.CommandExitedMessage):
|
||||
RNS.log(f"received return code {message.return_code}, exiting", RNS.LOG_DEBUG)
|
||||
return message.return_code
|
||||
elif isinstance(message, protocol.ErrorMessage):
|
||||
RNS.log(f"Remote error: {message.data}", RNS.LOG_ERROR)
|
||||
if message.fatal:
|
||||
_link.teardown()
|
||||
return 200
|
||||
|
||||
except queue.Empty:
|
||||
processed = False
|
||||
|
||||
if channel.is_ready_to_send():
|
||||
def compress_adaptive(buf: bytes):
|
||||
comp_tries = RNS.RawChannelWriter.COMPRESSION_TRIES
|
||||
comp_try = 1
|
||||
comp_success = False
|
||||
|
||||
chunk_len = len(buf)
|
||||
if chunk_len > RNS.RawChannelWriter.MAX_CHUNK_LEN:
|
||||
chunk_len = RNS.RawChannelWriter.MAX_CHUNK_LEN
|
||||
chunk_segment = None
|
||||
|
||||
chunk_segment = None
|
||||
max_data_len = channel.mdu - protocol.StreamDataMessage.OVERHEAD
|
||||
while chunk_len > 32 and comp_try < comp_tries:
|
||||
chunk_segment_length = int(chunk_len/comp_try)
|
||||
compressed_chunk = bz2.compress(buf[:chunk_segment_length])
|
||||
compressed_length = len(compressed_chunk)
|
||||
if compressed_length < max_data_len and compressed_length < chunk_segment_length:
|
||||
comp_success = True
|
||||
break
|
||||
else:
|
||||
comp_try += 1
|
||||
|
||||
if comp_success:
|
||||
diff = max_data_len - len(compressed_chunk)
|
||||
chunk = compressed_chunk
|
||||
processed_length = chunk_segment_length
|
||||
else:
|
||||
chunk = bytes(buf[:max_data_len])
|
||||
processed_length = len(chunk)
|
||||
|
||||
return comp_success, processed_length, chunk
|
||||
|
||||
comp_success, processed_length, chunk = compress_adaptive(data_buffer)
|
||||
stdin = chunk
|
||||
data_buffer = data_buffer[processed_length:]
|
||||
eof = not sent_eof and stdin_eof and len(stdin) == 0
|
||||
if len(stdin) > 0 or eof:
|
||||
channel.send(protocol.StreamDataMessage(protocol.StreamDataMessage.STREAM_ID_STDIN, stdin, eof, comp_success))
|
||||
sent_eof = eof
|
||||
processed = True
|
||||
|
||||
# send window change, but rate limited
|
||||
if winch and time.time() - last_winch > _link.rtt * 25:
|
||||
last_winch = time.time()
|
||||
winch = False
|
||||
with contextlib.suppress(Exception):
|
||||
r, c, h, v = process.tty_get_winsize(0)
|
||||
channel.send(protocol.WindowSizeMessage(r, c, h, v))
|
||||
processed = True
|
||||
except RemoteExecutionError as e:
|
||||
print(e.msg)
|
||||
return 255
|
||||
except Exception as ex:
|
||||
print(f"Client exception: {ex}")
|
||||
if _link and _link.status != RNS.Link.CLOSED:
|
||||
_link.teardown()
|
||||
return 127
|
||||
|
||||
RNS.log("Main loop done", RNS.LOG_DEBUG)
|
||||
return 0
|
||||
@@ -0,0 +1,229 @@
|
||||
# Based on the original rnsh program by Aaron Heise (@acehoss)
|
||||
# https://github.com/acehoss/rnsh - MIT License - Copyright (c) 2023 Aaron Heise
|
||||
# This version of rnsh is included in RNS under the Reticulum License
|
||||
#
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2026 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import queue
|
||||
import shlex
|
||||
import signal
|
||||
import sys
|
||||
import termios
|
||||
import threading
|
||||
import time
|
||||
import tty
|
||||
from typing import Callable, TypeVar
|
||||
import RNS
|
||||
import RNS.Utilities.rnsh.exception as exception
|
||||
import RNS.Utilities.rnsh.process as process
|
||||
import RNS.Utilities.rnsh.retry as retry
|
||||
import RNS.Utilities.rnsh.session as session
|
||||
import re
|
||||
import contextlib
|
||||
|
||||
import pwd
|
||||
import RNS.Utilities.rnsh.protocol as protocol
|
||||
import RNS.Utilities.rnsh.helpers as helpers
|
||||
import RNS.Utilities.rnsh.rnsh as rnsh
|
||||
|
||||
|
||||
_identity = None
|
||||
_reticulum = None
|
||||
_allow_all = False
|
||||
_allowed_file = None
|
||||
_allowed_identity_hashes = []
|
||||
_allowed_file_identity_hashes = []
|
||||
_cmd: [str] | None = None
|
||||
DATA_AVAIL_MSG = "data available"
|
||||
_finished: asyncio.Event = None
|
||||
_retry_timer: retry.RetryThread | None = None
|
||||
_destination: RNS.Destination | None = None
|
||||
_loop: asyncio.AbstractEventLoop | None = None
|
||||
_no_remote_command = True
|
||||
_remote_cmd_as_args = False
|
||||
|
||||
|
||||
async def _check_finished(timeout: float = 0):
|
||||
return await process.event_wait(_finished, timeout=timeout)
|
||||
|
||||
|
||||
def _sigint_handler(sig, loop):
|
||||
global _finished
|
||||
RNS.log(f"Signal: {signal.Signals(sig).name}", RNS.LOG_DEBUG)
|
||||
if _finished is not None: _finished.set()
|
||||
else: raise KeyboardInterrupt()
|
||||
|
||||
def _reload_allowed_file():
|
||||
global _allowed_file, _allowed_file_identity_hashes
|
||||
if _allowed_file != None:
|
||||
try:
|
||||
with open(_allowed_file, "r") as file:
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH // 8) * 2
|
||||
added = 0
|
||||
line = 0
|
||||
_allowed_file_identity_hashes = []
|
||||
for allow in file.read().replace("\r", "").split("\n"):
|
||||
line += 1
|
||||
if len(allow) == dest_len:
|
||||
try:
|
||||
destination_hash = bytes.fromhex(allow)
|
||||
_allowed_file_identity_hashes.append(destination_hash)
|
||||
added += 1
|
||||
except Exception:
|
||||
RNS.log(f"Discarded invalid Identity hash in {_allowed_file} at line {line}", RNS.LOG_DEBUG)
|
||||
|
||||
ms = "y" if added == 1 else "ies"
|
||||
RNS.log(f"Loaded {added} allowed identit{ms} from "+str(_allowed_file), RNS.LOG_DEBUG)
|
||||
|
||||
except Exception as e: RNS.log(f"Error while reloading allowed indetities file: {e}", RNS.LOG_ERROR)
|
||||
|
||||
def compute_target_rns_loglevel(verbosity: int, quietness: int, base_level: int = RNS.LOG_INFO) -> int:
|
||||
try:
|
||||
target = int(base_level) + int(verbosity) - int(quietness)
|
||||
if target < RNS.LOG_CRITICAL: target = RNS.LOG_CRITICAL
|
||||
if target > RNS.LOG_DEBUG: target = RNS.LOG_DEBUG
|
||||
return target
|
||||
|
||||
except Exception: return base_level
|
||||
|
||||
async def listen(configdir, rnsconfigdir, command, identitypath=None, service_name=None, verbosity=0, quietness=0, allowed=None,
|
||||
allowed_file=None, disable_auth=None, announce_period=900, no_remote_command=True, remote_cmd_as_args=False,
|
||||
loop: asyncio.AbstractEventLoop = None):
|
||||
global _identity, _allow_all, _allowed_identity_hashes, _allowed_file, _allowed_file_identity_hashes
|
||||
global _reticulum, _cmd, _destination, _no_remote_command, _remote_cmd_as_args, _finished
|
||||
|
||||
if not loop: loop = asyncio.get_running_loop()
|
||||
if service_name is None or len(service_name) == 0:
|
||||
service_name = "default"
|
||||
|
||||
RNS.log(f"Using service name {service_name}", RNS.LOG_INFO)
|
||||
|
||||
# More -v should increase verbosity (higher RNS.loglevel); -q should decrease it
|
||||
targetloglevel = compute_target_rns_loglevel(verbosity, quietness, RNS.LOG_INFO)
|
||||
_reticulum = RNS.Reticulum(configdir=rnsconfigdir, loglevel=targetloglevel)
|
||||
_identity = rnsh.prepare_identity(identitypath, service_name)
|
||||
_destination = RNS.Destination(_identity, RNS.Destination.IN, RNS.Destination.SINGLE, rnsh.APP_NAME)
|
||||
|
||||
RNS.log(f"rnsh listening for commands on {RNS.prettyhexrep(_destination.hash)}", RNS.LOG_NOTICE)
|
||||
|
||||
_cmd = command
|
||||
if _cmd is None or len(_cmd) == 0:
|
||||
shell = None
|
||||
try: shell = pwd.getpwuid(os.getuid()).pw_shell
|
||||
except Exception as e: RNS.log(f"Error looking up shell: {e}", RNS.LOG_ERROR)
|
||||
RNS.log(f"Using {shell} for default command.", RNS.LOG_INFO)
|
||||
|
||||
# Ensure a sane shell default. Fall back to /bin/sh if lookup fails.
|
||||
if not shell or len(shell) == 0: shell = "/bin/sh"
|
||||
_cmd = [shell]
|
||||
|
||||
else: RNS.log(f"Using command {shlex.join(_cmd)}", RNS.LOG_INFO)
|
||||
|
||||
_no_remote_command = no_remote_command
|
||||
session.ListenerSession.allow_remote_command = not no_remote_command
|
||||
_remote_cmd_as_args = remote_cmd_as_args
|
||||
if (_cmd is None or len(_cmd) == 0 or _cmd[0] is None or len(_cmd[0]) == 0) \
|
||||
and (_no_remote_command or _remote_cmd_as_args):
|
||||
raise Exception(f"Unable to look up shell for {os.getlogin}, cannot proceed with -A or -C and no <program>.")
|
||||
|
||||
session.ListenerSession.default_command = _cmd
|
||||
session.ListenerSession.remote_cmd_as_args = _remote_cmd_as_args
|
||||
|
||||
if disable_auth:
|
||||
_allow_all = True
|
||||
session.ListenerSession.allow_all = True
|
||||
else:
|
||||
if allowed_file is not None:
|
||||
_allowed_file = allowed_file
|
||||
_reload_allowed_file()
|
||||
|
||||
if allowed is not None:
|
||||
for a in allowed:
|
||||
try:
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH // 8) * 2
|
||||
if len(a) != dest_len:
|
||||
raise ValueError(
|
||||
"Allowed destination length is invalid, must be {hex} hexadecimal " +
|
||||
"characters ({byte} bytes).".format(
|
||||
hex=dest_len, byte=dest_len // 2))
|
||||
try:
|
||||
destination_hash = bytes.fromhex(a)
|
||||
_allowed_identity_hashes.append(destination_hash)
|
||||
session.ListenerSession.allowed_identity_hashes.append(destination_hash)
|
||||
except Exception:
|
||||
raise ValueError("Invalid destination entered. Check your input.")
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Unhandled error: {e}", RNS.LOG_ERROR)
|
||||
RNS.trace_exception(e)
|
||||
exit(1)
|
||||
|
||||
if (len(_allowed_identity_hashes) < 1 and len(_allowed_file_identity_hashes) < 1) and not disable_auth:
|
||||
RNS.log("Warning: No allowed identities configured, rnsh will not accept any connections!", RNS.LOG_WARNING)
|
||||
|
||||
def link_established(lnk: RNS.Link):
|
||||
_reload_allowed_file()
|
||||
session.ListenerSession.allowed_file_identity_hashes = _allowed_file_identity_hashes
|
||||
session.ListenerSession(session.RNSOutlet.get_outlet(lnk), lnk.get_channel(), loop)
|
||||
_destination.set_link_established_callback(link_established)
|
||||
|
||||
_finished = asyncio.Event()
|
||||
signal.signal(signal.SIGINT, _sigint_handler)
|
||||
|
||||
if announce_period is not None: _destination.announce()
|
||||
|
||||
last_announce = time.time()
|
||||
sleeper = helpers.SleepRate(0.01)
|
||||
|
||||
try:
|
||||
while not await _check_finished():
|
||||
if announce_period and 0 < announce_period < time.time() - last_announce:
|
||||
last_announce = time.time()
|
||||
_destination.announce()
|
||||
if len(session.ListenerSession.sessions) > 0:
|
||||
# no sleep if there's work to do
|
||||
if not await session.ListenerSession.pump_all():
|
||||
await sleeper.sleep_async()
|
||||
else:
|
||||
await asyncio.sleep(0.25)
|
||||
finally:
|
||||
RNS.log("Shutting down", RNS.LOG_NOTICE)
|
||||
await session.ListenerSession.terminate_all("Shutting down")
|
||||
await asyncio.sleep(1)
|
||||
links_still_active = list(filter(lambda l: l.status != RNS.Link.CLOSED, _destination.links))
|
||||
for link in links_still_active:
|
||||
if link.status not in [RNS.Link.CLOSED]:
|
||||
link.teardown()
|
||||
await asyncio.sleep(0.01)
|
||||
@@ -0,0 +1,46 @@
|
||||
# Based on the original rnsh program by Aaron Heise (@acehoss)
|
||||
# https://github.com/acehoss/rnsh - MIT License - Copyright (c) 2023 Aaron Heise
|
||||
# This version of rnsh is included in RNS under the Reticulum License
|
||||
#
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2026 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import asyncio
|
||||
import functools
|
||||
from typing import Callable
|
||||
|
||||
def sig_handler_sys_to_loop(handler: Callable[[int, any], None]) -> Callable[[int, asyncio.AbstractEventLoop], None]:
|
||||
def wrapped(cb: Callable[[int, any], None], signal: int, loop: asyncio.AbstractEventLoop): cb(signal, None)
|
||||
return functools.partial(wrapped, handler)
|
||||
|
||||
def loop_set_signal(sig, handler: Callable[[int, asyncio.AbstractEventLoop], None], loop: asyncio.AbstractEventLoop = None):
|
||||
if loop is None: loop = asyncio.get_running_loop()
|
||||
loop.remove_signal_handler(sig)
|
||||
loop.add_signal_handler(sig, functools.partial(handler, sig, loop))
|
||||
@@ -0,0 +1,785 @@
|
||||
# Based on the original rnsh program by Aaron Heise (@acehoss)
|
||||
# https://github.com/acehoss/rnsh - MIT License - Copyright (c) 2023 Aaron Heise
|
||||
# This version of rnsh is included in RNS under the Reticulum License
|
||||
#
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2026 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
from __future__ import annotations
|
||||
import asyncio
|
||||
import contextlib
|
||||
import copy
|
||||
import errno
|
||||
import fcntl
|
||||
import functools
|
||||
import os
|
||||
import pty
|
||||
import select
|
||||
import signal
|
||||
import struct
|
||||
import sys
|
||||
import termios
|
||||
import threading
|
||||
import tty
|
||||
import types
|
||||
import typing
|
||||
import RNS
|
||||
|
||||
import RNS.Utilities.rnsh.exception as exception
|
||||
|
||||
CTRL_C = "\x03".encode("utf-8")
|
||||
CTRL_D = "\x04".encode("utf-8")
|
||||
|
||||
def tty_add_reader_callback(fd: int, callback: callable, loop: asyncio.AbstractEventLoop = None):
|
||||
"""
|
||||
Add an async reader callback for a tty file descriptor.
|
||||
|
||||
Example usage:
|
||||
|
||||
def reader():
|
||||
data = tty_read(fd)
|
||||
# do something with data
|
||||
|
||||
tty_add_reader_callback(self._child_fd, reader, self._loop)
|
||||
|
||||
:param fd: file descriptor
|
||||
:param callback: callback function
|
||||
:param loop: asyncio event loop to which the reader should be added. If None, use the currently-running loop.
|
||||
"""
|
||||
if loop is None:
|
||||
loop = asyncio.get_running_loop()
|
||||
loop.add_reader(fd, callback)
|
||||
|
||||
|
||||
def tty_read(fd: int) -> bytes:
|
||||
"""
|
||||
Read available bytes from a tty file descriptor. When used in a callback added to a file descriptor using
|
||||
tty_add_reader_callback(...), this function creates a solution for non-blocking reads from ttys.
|
||||
:param fd: tty file descriptor
|
||||
:return: bytes read
|
||||
"""
|
||||
if fd_is_closed(fd):
|
||||
raise EOFError
|
||||
|
||||
try:
|
||||
run = True
|
||||
result = bytearray()
|
||||
while not fd_is_closed(fd):
|
||||
ready, _, _ = select.select([fd], [], [], 0)
|
||||
if len(ready) == 0:
|
||||
break
|
||||
for f in ready:
|
||||
try:
|
||||
data = os.read(f, 4096)
|
||||
except OSError as e:
|
||||
if e.errno != errno.EIO and e.errno != errno.EWOULDBLOCK:
|
||||
raise
|
||||
else:
|
||||
if not data: # EOF
|
||||
if data is not None and len(data) > 0:
|
||||
result.extend(data)
|
||||
return result
|
||||
elif len(result) > 0:
|
||||
return result
|
||||
else:
|
||||
raise EOFError
|
||||
if data is not None and len(data) > 0:
|
||||
result.extend(data)
|
||||
return result
|
||||
|
||||
except EOFError: raise
|
||||
except Exception as e: RNS.log(f"TTY read error: {e}", RNS.LOG_ERROR)
|
||||
|
||||
|
||||
def tty_read_poll(fd: int) -> bytes:
|
||||
"""
|
||||
Read available bytes from a tty file descriptor. When used in a callback added to a file descriptor using
|
||||
tty_add_reader_callback(...), this function creates a solution for non-blocking reads from ttys.
|
||||
:param fd: tty file descriptor
|
||||
:return: bytes read
|
||||
"""
|
||||
if fd_is_closed(fd):
|
||||
raise EOFError
|
||||
|
||||
result = bytearray()
|
||||
try:
|
||||
flags = fcntl.fcntl(fd, fcntl.F_GETFL)
|
||||
fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
|
||||
while True:
|
||||
try:
|
||||
data = os.read(fd, 4096)
|
||||
if not data:
|
||||
# EOF
|
||||
if len(result) > 0:
|
||||
return result
|
||||
raise EOFError
|
||||
result.extend(data)
|
||||
# continue loop to drain
|
||||
except OSError as e:
|
||||
if e.errno in (errno.EWOULDBLOCK, errno.EAGAIN):
|
||||
break
|
||||
if e.errno == errno.EIO:
|
||||
if len(result) > 0:
|
||||
return result
|
||||
raise EOFError
|
||||
raise
|
||||
except EOFError: raise
|
||||
except Exception as e: RNS.log(f"TTY read error: {e}", RNS.LOG_ERROR)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def fd_is_closed(fd: int) -> bool:
|
||||
"""
|
||||
Check if file descriptor is closed
|
||||
:param fd: file descriptor
|
||||
:return: True if file descriptor is closed
|
||||
"""
|
||||
try:
|
||||
fcntl.fcntl(fd, fcntl.F_GETFL) < 0
|
||||
except OSError as ose:
|
||||
return ose.errno == errno.EBADF
|
||||
|
||||
|
||||
def tty_unset_reader_callbacks(fd: int, loop: asyncio.AbstractEventLoop = None):
|
||||
"""
|
||||
Remove async reader callbacks for file descriptor.
|
||||
:param fd: file descriptor
|
||||
:param loop: asyncio event loop from which to remove callbacks
|
||||
"""
|
||||
with exception.permit(SystemExit):
|
||||
if loop is None:
|
||||
loop = asyncio.get_running_loop()
|
||||
loop.remove_reader(fd)
|
||||
|
||||
|
||||
def tty_get_winsize(fd: int) -> [int, int, int, int]:
|
||||
"""
|
||||
Ge the window size of a tty.
|
||||
:param fd: file descriptor of tty
|
||||
:return: (rows, cols, h_pixels, v_pixels)
|
||||
"""
|
||||
packed = fcntl.ioctl(fd, termios.TIOCGWINSZ, struct.pack('HHHH', 0, 0, 0, 0))
|
||||
rows, cols, h_pixels, v_pixels = struct.unpack('HHHH', packed)
|
||||
return rows, cols, h_pixels, v_pixels
|
||||
|
||||
|
||||
def tty_set_winsize(fd: int, rows: int, cols: int, h_pixels: int, v_pixels: int):
|
||||
"""
|
||||
Set the window size on a tty.
|
||||
:param fd: file descriptor of tty
|
||||
:param rows: number of visible rows
|
||||
:param cols: number of visible columns
|
||||
:param h_pixels: number of visible horizontal pixels
|
||||
:param v_pixels: number of visible vertical pixels
|
||||
"""
|
||||
if fd < 0:
|
||||
return
|
||||
packed = struct.pack('HHHH', rows, cols, h_pixels, v_pixels)
|
||||
fcntl.ioctl(fd, termios.TIOCSWINSZ, packed)
|
||||
|
||||
|
||||
def process_exists(pid) -> bool:
|
||||
"""
|
||||
Check For the existence of a unix pid.
|
||||
:param pid: process id to check
|
||||
:return: True if process exists
|
||||
"""
|
||||
try:
|
||||
os.kill(pid, 0)
|
||||
except OSError:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
class TTYRestorer(contextlib.AbstractContextManager):
|
||||
# Indexes of flags within the attrs array
|
||||
ATTR_IDX_IFLAG = 0
|
||||
ATTR_IDX_OFLAG = 1
|
||||
ATTR_IDX_CFLAG = 2
|
||||
ATTR_IDX_LFLAG = 4
|
||||
ATTR_IDX_CC = 5
|
||||
|
||||
def __init__(self, fd: int, suppress_logs=False):
|
||||
"""
|
||||
Saves termios attributes for a tty for later restoration.
|
||||
|
||||
The attributes are an array of values with the following meanings.
|
||||
|
||||
tcflag_t c_iflag; /* input modes */
|
||||
tcflag_t c_oflag; /* output modes */
|
||||
tcflag_t c_cflag; /* control modes */
|
||||
tcflag_t c_lflag; /* local modes */
|
||||
cc_t c_cc[NCCS]; /* special characters */
|
||||
|
||||
:param fd: file descriptor of tty
|
||||
"""
|
||||
self._fd = fd
|
||||
self._tattr = None
|
||||
self._suppress_logs = suppress_logs
|
||||
self._tattr = self.current_attr()
|
||||
if not self._tattr and not self._suppress_logs: RNS.log(f"Could not get attrs for fd {fd}", RNS.LOG_DEBUG)
|
||||
|
||||
def raw(self):
|
||||
"""
|
||||
Set raw mode on tty
|
||||
"""
|
||||
if self._fd is None:
|
||||
return
|
||||
with contextlib.suppress(termios.error):
|
||||
tty.setraw(self._fd, termios.TCSANOW)
|
||||
|
||||
def original_attr(self) -> [any]:
|
||||
return copy.deepcopy(self._tattr)
|
||||
|
||||
def current_attr(self) -> [any]:
|
||||
"""
|
||||
Get the current termios attributes for the wrapped fd.
|
||||
:return: attribute array
|
||||
"""
|
||||
if self._fd is None:
|
||||
return None
|
||||
|
||||
with contextlib.suppress(termios.error):
|
||||
return copy.deepcopy(termios.tcgetattr(self._fd))
|
||||
return None
|
||||
|
||||
def set_attr(self, attr: [any], when: int = termios.TCSADRAIN):
|
||||
"""
|
||||
Set termios attributes
|
||||
:param attr: attribute list to set
|
||||
:param when: when attributes should be applied (termios.TCSANOW, termios.TCSADRAIN, termios.TCSAFLUSH)
|
||||
"""
|
||||
if not attr or self._fd is None:
|
||||
return
|
||||
|
||||
with contextlib.suppress(termios.error):
|
||||
termios.tcsetattr(self._fd, when, attr)
|
||||
|
||||
def isatty(self):
|
||||
return os.isatty(self._fd) if self._fd is not None else None
|
||||
|
||||
def restore(self):
|
||||
"""
|
||||
Restore termios settings to state captured in constructor.
|
||||
"""
|
||||
self.set_attr(self._tattr, termios.TCSADRAIN)
|
||||
|
||||
def __exit__(self, __exc_type: typing.Type[BaseException], __exc_value: BaseException,
|
||||
__traceback: types.TracebackType) -> bool:
|
||||
self.restore()
|
||||
return False #__exc_type is not None and issubclass(__exc_type, termios.error)
|
||||
|
||||
|
||||
def _task_from_event(evt: asyncio.Event, loop: asyncio.AbstractEventLoop = None):
|
||||
if not loop:
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
#TODO: this is hacky
|
||||
async def wait():
|
||||
while not evt.is_set():
|
||||
await asyncio.sleep(0.1)
|
||||
return True
|
||||
|
||||
return loop.create_task(wait())
|
||||
|
||||
|
||||
class AggregateException(Exception):
|
||||
def __init__(self, inner_exceptions: [Exception]):
|
||||
super().__init__()
|
||||
self.inner_exceptions = inner_exceptions
|
||||
|
||||
def __str__(self):
|
||||
return "Multiple exceptions encountered: \n\n" + "\n\n".join(map(lambda e: str(e), self.inner_exceptions))
|
||||
|
||||
|
||||
async def event_wait_any(evts: [asyncio.Event], timeout: float = None) -> (any, any):
|
||||
tasks = list(map(lambda evt: (evt, _task_from_event(evt)), evts))
|
||||
try:
|
||||
finished, unfinished = await asyncio.wait(map(lambda t: t[1], tasks),
|
||||
timeout=timeout,
|
||||
return_when=asyncio.FIRST_COMPLETED)
|
||||
|
||||
if len(unfinished) > 0:
|
||||
for task in unfinished:
|
||||
task.cancel()
|
||||
await asyncio.wait(unfinished)
|
||||
|
||||
exceptions = []
|
||||
|
||||
for f in finished:
|
||||
ex = f.exception()
|
||||
if ex and not isinstance(ex, asyncio.CancelledError) and not isinstance(ex, TimeoutError):
|
||||
exceptions.append(ex)
|
||||
|
||||
if len(exceptions) > 0:
|
||||
raise AggregateException(exceptions)
|
||||
|
||||
return next(map(lambda t: next(map(lambda tt: tt[0], tasks)), finished), None)
|
||||
finally:
|
||||
unfinished = []
|
||||
for task in map(lambda t: t[1], tasks):
|
||||
if task.done():
|
||||
if not task.cancelled():
|
||||
task.exception()
|
||||
else:
|
||||
task.cancel()
|
||||
unfinished.append(task)
|
||||
if len(unfinished) > 0:
|
||||
await asyncio.wait(unfinished)
|
||||
|
||||
|
||||
async def event_wait(evt: asyncio.Event, timeout: float) -> bool:
|
||||
"""
|
||||
Wait for event to be set, or timeout to expire.
|
||||
:param evt: asyncio.Event to wait on
|
||||
:param timeout: maximum number of seconds to wait.
|
||||
:return: True if event was set, False if timeout expired
|
||||
"""
|
||||
await event_wait_any([evt], timeout=timeout)
|
||||
return evt.is_set()
|
||||
|
||||
|
||||
def _launch_child(cmd_line: list[str], env: dict[str, str], stdin_is_pipe: bool, stdout_is_pipe: bool,
|
||||
stderr_is_pipe: bool) -> tuple[int, int, int, int]:
|
||||
# Set up PTY and/or pipes
|
||||
child_fd = parent_fd = None
|
||||
if not (stdin_is_pipe and stdout_is_pipe and stderr_is_pipe):
|
||||
parent_fd, child_fd = pty.openpty()
|
||||
child_stdin, parent_stdin = (os.pipe() if stdin_is_pipe else (child_fd, parent_fd))
|
||||
parent_stdout, child_stdout = (os.pipe() if stdout_is_pipe else (parent_fd, child_fd))
|
||||
parent_stderr, child_stderr = (os.pipe() if stderr_is_pipe else (parent_fd, child_fd))
|
||||
|
||||
# Fork
|
||||
pid = os.fork()
|
||||
|
||||
if pid == 0:
|
||||
try:
|
||||
# We are in the child process, so close all open sockets and pipes except for the PTY and/or pipes
|
||||
max_fd = os.sysconf("SC_OPEN_MAX")
|
||||
for fd in range(3, max_fd):
|
||||
if fd not in (child_stdin, child_stdout, child_stderr):
|
||||
try:
|
||||
os.close(fd)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# Set up PTY and/or pipes
|
||||
os.dup2(child_stdin, 0)
|
||||
os.dup2(child_stdout, 1)
|
||||
os.dup2(child_stderr, 2)
|
||||
# Make PTY controlling if necessary so that CTRL_C/CTRL_D behave as expected
|
||||
if child_fd is not None:
|
||||
os.setsid()
|
||||
try:
|
||||
tty_fd = 0 if not stdin_is_pipe else (1 if not stdout_is_pipe else 2)
|
||||
# Set controlling TTY for this session
|
||||
fcntl.ioctl(tty_fd, termios.TIOCSCTTY, 0)
|
||||
except Exception:
|
||||
pass
|
||||
# Ensure the child is the foreground process group for the TTY
|
||||
try:
|
||||
os.setpgid(0, 0)
|
||||
pgid = os.getpgrp()
|
||||
import struct as _struct
|
||||
fcntl.ioctl(tty_fd, termios.TIOCSPGRP, _struct.pack('i', pgid))
|
||||
except Exception:
|
||||
pass
|
||||
# Ensure canonical input with signals and local echo enabled
|
||||
try:
|
||||
tty_fd = 0 if not stdin_is_pipe else (1 if not stdout_is_pipe else 2)
|
||||
attrs = termios.tcgetattr(tty_fd)
|
||||
lflag = attrs[3]
|
||||
lflag |= termios.ICANON | termios.ISIG | termios.ECHO
|
||||
attrs[3] = lflag
|
||||
termios.tcsetattr(tty_fd, termios.TCSANOW, attrs)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Execute the command
|
||||
os.execvpe(cmd_line[0], cmd_line, env)
|
||||
except Exception as err:
|
||||
exc_type, exc_obj, exc_tb = sys.exc_info()
|
||||
fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
|
||||
print(f"Unable to start {cmd_line[0]}: {err} ({fname}:{exc_tb.tb_lineno})")
|
||||
sys.stdout.flush()
|
||||
# don't let any other modules get in our way, do an immediate silent exit.
|
||||
os._exit(255)
|
||||
|
||||
else:
|
||||
# We are in the parent process, so close the child-side of the PTY and/or pipes
|
||||
if child_fd is not None:
|
||||
os.close(child_fd)
|
||||
if child_stdin != child_fd:
|
||||
os.close(child_stdin)
|
||||
if child_stdout != child_fd:
|
||||
os.close(child_stdout)
|
||||
if child_stderr != child_fd:
|
||||
os.close(child_stderr)
|
||||
# # Close the write end of the pipe if a pipe is used for standard input
|
||||
# if not stdin_is_pipe:
|
||||
# os.close(parent_stdin)
|
||||
# Return the child PID and the file descriptors for the PTY and/or pipes
|
||||
return pid, parent_stdin, parent_stdout, parent_stderr
|
||||
|
||||
|
||||
class CallbackSubprocess:
|
||||
# time between checks of child process
|
||||
PROCESS_POLL_TIME: float = 0.1
|
||||
# Close pipes soon after process exit to avoid scheduling on closed event loops
|
||||
PROCESS_PIPE_TIME: int = 1
|
||||
|
||||
def __init__(self, argv: [str], env: dict, loop: asyncio.AbstractEventLoop, stdout_callback: callable,
|
||||
stderr_callback: callable, terminated_callback: callable, stdin_is_pipe: bool, stdout_is_pipe: bool,
|
||||
stderr_is_pipe: bool):
|
||||
"""
|
||||
Fork a child process and generate callbacks with output from the process.
|
||||
:param argv: the command line, tokenized. The first element must be the absolute path to an executable file.
|
||||
:param env: environment variables to override
|
||||
:param loop: the asyncio event loop to use
|
||||
:param stdout_callback: callback for data, e.g. def callback(data:bytes) -> None
|
||||
:param terminated_callback: callback for termination/return code, e.g. def callback(return_code:int) -> None
|
||||
"""
|
||||
assert loop is not None, "loop should not be None"
|
||||
assert stdout_callback is not None, "stdout_callback should not be None"
|
||||
assert terminated_callback is not None, "terminated_callback should not be None"
|
||||
|
||||
self._command: [str] = argv
|
||||
self._env = env or {}
|
||||
self._loop = loop
|
||||
self._stdout_cb = stdout_callback
|
||||
self._stderr_cb = stderr_callback
|
||||
self._terminated_cb = terminated_callback
|
||||
self._pid: int = None
|
||||
self._child_stdin: int = None
|
||||
self._child_stdout: int = None
|
||||
self._child_stderr: int = None
|
||||
self._return_code: int = None
|
||||
self._stdout_eof: bool = False
|
||||
self._stderr_eof: bool = False
|
||||
self._stdin_is_pipe = stdin_is_pipe
|
||||
self._stdout_is_pipe = stdout_is_pipe
|
||||
self._stderr_is_pipe = stderr_is_pipe
|
||||
self._at_line_start: bool = True
|
||||
self._tty_line_buffer: bytearray = bytearray()
|
||||
|
||||
def _ensure_pipes_closed(self):
|
||||
stdin = self._child_stdin
|
||||
stdout = self._child_stdout
|
||||
stderr = self._child_stderr
|
||||
fds = set(filter(lambda x: x is not None, list({stdin, stdout, stderr})))
|
||||
RNS.log(f"Queuing close of pipes for ended process (fds: {fds})", RNS.LOG_DEBUG)
|
||||
|
||||
def ensure_pipes_closed_inner():
|
||||
RNS.log(f"Ensuring pipes are closed (fds: {fds})", RNS.LOG_DEBUG)
|
||||
for fd in fds:
|
||||
RNS.log(f"Closing fd {fd}", RNS.LOG_DEBUG)
|
||||
with contextlib.suppress(OSError): tty_unset_reader_callbacks(fd)
|
||||
with contextlib.suppress(OSError): os.close(fd)
|
||||
|
||||
self._child_stdin = None
|
||||
self._child_stdout = None
|
||||
self._child_stderr = None
|
||||
|
||||
# Avoid scheduling on a closed loop
|
||||
if self._loop.is_closed(): ensure_pipes_closed_inner()
|
||||
else: self._loop.call_later(CallbackSubprocess.PROCESS_PIPE_TIME, ensure_pipes_closed_inner)
|
||||
|
||||
def terminate(self, kill_delay: float = 1.0):
|
||||
"""
|
||||
Terminate child process if running
|
||||
:param kill_delay: if after kill_delay seconds the child process has not exited, escalate to SIGHUP and SIGKILL
|
||||
"""
|
||||
|
||||
RNS.log("terminate()", RNS.LOG_EXTREME)
|
||||
if not self.running: return
|
||||
|
||||
with exception.permit(SystemExit): os.kill(self._pid, signal.SIGTERM)
|
||||
|
||||
def kill():
|
||||
if process_exists(self._pid):
|
||||
RNS.log("kill()", RNS.LOG_EXTREME)
|
||||
with exception.permit(SystemExit):
|
||||
os.kill(self._pid, signal.SIGHUP)
|
||||
os.kill(self._pid, signal.SIGKILL)
|
||||
|
||||
self._loop.call_later(kill_delay, kill)
|
||||
|
||||
def wait():
|
||||
RNS.log("wait()", RNS.LOG_EXTREME)
|
||||
with contextlib.suppress(OSError): os.waitpid(self._pid, 0)
|
||||
self._ensure_pipes_closed()
|
||||
RNS.log("wait() finish", RNS.LOG_EXTREME)
|
||||
|
||||
threading.Thread(target=wait, daemon=True).start()
|
||||
|
||||
def close_stdin(self):
|
||||
with contextlib.suppress(Exception):
|
||||
os.close(self._child_stdin)
|
||||
# Encourage prompt shutdown if child lingers after stdin close
|
||||
def _ensure_terminate():
|
||||
if self.running:
|
||||
self.terminate(kill_delay=0.2)
|
||||
if not self._loop.is_closed():
|
||||
self._loop.call_later(0.05, _ensure_terminate)
|
||||
|
||||
@property
|
||||
def started(self) -> bool:
|
||||
"""
|
||||
:return: True if child process has been started
|
||||
"""
|
||||
return self._pid is not None
|
||||
|
||||
@property
|
||||
def running(self) -> bool:
|
||||
"""
|
||||
:return: True if child process is still running
|
||||
"""
|
||||
return self._pid is not None and process_exists(self._pid)
|
||||
|
||||
def write(self, data: bytes):
|
||||
"""
|
||||
Write bytes to the stdin of the child process.
|
||||
:param data: bytes to write
|
||||
"""
|
||||
|
||||
os.write(self._child_stdin, data)
|
||||
|
||||
# TODO: Check what this is actually supposed to solve.
|
||||
#
|
||||
# For pipe-in + TTY-out, echo should be visible immediately
|
||||
if self._stdin_is_pipe and not self._stdout_is_pipe and self._stdout_cb is not None and data not in (CTRL_C, CTRL_D):
|
||||
try: self._stdout_cb(data)
|
||||
except Exception: pass
|
||||
|
||||
def set_winsize(self, r: int, c: int, h: int, v: int):
|
||||
"""
|
||||
Set the window size on the tty of the child process.
|
||||
:param r: rows visible
|
||||
:param c: columns visible
|
||||
:param h: horizontal pixels visible
|
||||
:param v: vertical pixels visible
|
||||
:return:
|
||||
"""
|
||||
RNS.log(f"set_winsize({r},{c},{h},{v}", RNS.LOG_DEBUG)
|
||||
tty_set_winsize(self._child_stdout, r, c, h, v)
|
||||
|
||||
def copy_winsize(self, fromfd: int):
|
||||
"""
|
||||
Copy window size from one tty to another.
|
||||
:param fromfd: source tty file descriptor
|
||||
"""
|
||||
r, c, h, v = tty_get_winsize(fromfd)
|
||||
self.set_winsize(r, c, h, v)
|
||||
|
||||
def tcsetattr(self, when: int, attr: list[any]): # actual type is list[int | list[int | bytes]]
|
||||
"""
|
||||
Set tty attributes.
|
||||
:param when: when to apply change: termios.TCSANOW or termios.TCSADRAIN or termios.TCSAFLUSH
|
||||
:param attr: attributes to set
|
||||
"""
|
||||
termios.tcsetattr(self._child_stdin, when, attr)
|
||||
|
||||
def tcgetattr(self) -> list[any]: # actual type is list[int | list[int | bytes]]
|
||||
"""
|
||||
Get tty attributes.
|
||||
:return: tty attributes value
|
||||
"""
|
||||
return termios.tcgetattr(self._child_stdout)
|
||||
|
||||
def ttysetraw(self):
|
||||
tty.setraw(self._child_stdout, termios.TCSADRAIN)
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
Start the child process.
|
||||
"""
|
||||
RNS.log("start()", RNS.LOG_EXTREME)
|
||||
|
||||
# # Using the parent environment seems to do some weird stuff, at least on macOS
|
||||
# parentenv = os.environ.copy()
|
||||
# env = {"HOME": parentenv["HOME"],
|
||||
# "PATH": parentenv["PATH"],
|
||||
# "TERM": self._term if self._term is not None else parentenv.get("TERM", "xterm"),
|
||||
# "LANG": parentenv.get("LANG"),
|
||||
# "SHELL": self._command[0]}
|
||||
|
||||
env = os.environ.copy()
|
||||
for key in self._env:
|
||||
env[key] = self._env[key]
|
||||
|
||||
program = self._command[0]
|
||||
assert isinstance(program, str)
|
||||
|
||||
# match = re.search("^/bin/(.*sh)$", program)
|
||||
# if match:
|
||||
# self._command[0] = "-" + match.group(1)
|
||||
# env["SHELL"] = program
|
||||
# self._log.debug(f"set login shell {self._command}")
|
||||
|
||||
self._pid, \
|
||||
self._child_stdin, \
|
||||
self._child_stdout, \
|
||||
self._child_stderr = _launch_child(self._command, env, self._stdin_is_pipe, self._stdout_is_pipe,
|
||||
self._stderr_is_pipe)
|
||||
RNS.log(f"Started pid {self.pid}, fds: {self._child_stdin}, {self._child_stdout}, {self._child_stderr}", RNS.LOG_DEBUG)
|
||||
|
||||
def poll():
|
||||
try:
|
||||
pid, self._return_code = os.waitpid(self._pid, os.WNOHANG)
|
||||
if self._return_code is not None:
|
||||
self._return_code = self._return_code & 0xff
|
||||
if self._return_code is not None and not process_exists(self._pid):
|
||||
RNS.log(f"polled return code {self._return_code}", RNS.LOG_DEBUG)
|
||||
self._terminated_cb(self._return_code)
|
||||
if self.running:
|
||||
self._loop.call_later(CallbackSubprocess.PROCESS_POLL_TIME, poll)
|
||||
else:
|
||||
self._ensure_pipes_closed()
|
||||
except Exception as e:
|
||||
if not hasattr(e, "errno") or e.errno != errno.ECHILD:
|
||||
RNS.log(f"Error in process poll: {e}", RNS.LOG_DEBUG)
|
||||
|
||||
self._loop.call_later(CallbackSubprocess.PROCESS_POLL_TIME, poll)
|
||||
|
||||
def stdout():
|
||||
try:
|
||||
with exception.permit(SystemExit):
|
||||
data = tty_read_poll(self._child_stdout)
|
||||
if data is not None and len(data) > 0:
|
||||
self._stdout_cb(data)
|
||||
# Opportunistically drain shortly after to coalesce immediate follow-up output
|
||||
if not self._loop.is_closed():
|
||||
self._loop.call_later(0.01, stdout)
|
||||
except EOFError:
|
||||
self._stdout_eof = True
|
||||
tty_unset_reader_callbacks(self._child_stdout)
|
||||
self._stdout_cb(bytearray())
|
||||
|
||||
def stderr():
|
||||
try:
|
||||
with exception.permit(SystemExit):
|
||||
data = tty_read_poll(self._child_stderr)
|
||||
if data is not None and len(data) > 0:
|
||||
self._stderr_cb(data)
|
||||
if not self._loop.is_closed():
|
||||
self._loop.call_later(0.01, stderr)
|
||||
except EOFError:
|
||||
self._stderr_eof = True
|
||||
tty_unset_reader_callbacks(self._child_stderr)
|
||||
self._stderr_cb(bytearray())
|
||||
|
||||
tty_add_reader_callback(self._child_stdout, stdout, self._loop)
|
||||
if self._child_stderr != self._child_stdout:
|
||||
tty_add_reader_callback(self._child_stderr, stderr, self._loop)
|
||||
|
||||
@property
|
||||
def stdout_eof(self):
|
||||
return self._stdout_eof or not self.running
|
||||
|
||||
@property
|
||||
def stderr_eof(self):
|
||||
return self._stderr_eof or not self.running
|
||||
|
||||
|
||||
@property
|
||||
def return_code(self) -> int:
|
||||
return self._return_code
|
||||
|
||||
@property
|
||||
def pid(self) -> int:
|
||||
return self._pid
|
||||
|
||||
|
||||
async def main():
|
||||
"""
|
||||
A test driver for the CallbackProcess class.
|
||||
python ./process.py /bin/zsh --login
|
||||
"""
|
||||
|
||||
if len(sys.argv) <= 1:
|
||||
print(f"Usage: {sys.argv} <absolute_path_to_child_executable> [child_arg ...]")
|
||||
exit(1)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
# asyncio.set_event_loop(loop)
|
||||
retcode = loop.create_future()
|
||||
|
||||
def stdout(data: bytes): os.write(sys.stdout.fileno(), data)
|
||||
|
||||
def terminated(rc: int): retcode.set_result(rc)
|
||||
|
||||
process = CallbackSubprocess(argv=sys.argv[1:],
|
||||
env={"TERM": os.environ.get("TERM", "xterm")},
|
||||
loop=loop,
|
||||
stdout_callback=stdout,
|
||||
terminated_callback=terminated)
|
||||
|
||||
def sigint_handler(sig, frame):
|
||||
if process is None or process.started and not process.running:
|
||||
raise KeyboardInterrupt
|
||||
elif process.running:
|
||||
process.write("\x03".encode("utf-8"))
|
||||
|
||||
def sigwinch_handler(sig, frame):
|
||||
process.copy_winsize(sys.stdin.fileno())
|
||||
|
||||
signal.signal(signal.SIGINT, sigint_handler)
|
||||
signal.signal(signal.SIGWINCH, sigwinch_handler)
|
||||
|
||||
def stdin():
|
||||
try:
|
||||
data = tty_read(sys.stdin.fileno())
|
||||
if data is not None:
|
||||
process.write(data)
|
||||
|
||||
except EOFError:
|
||||
tty_unset_reader_callbacks(sys.stdin.fileno())
|
||||
process.write(CTRL_D)
|
||||
|
||||
tty_add_reader_callback(sys.stdin.fileno(), stdin)
|
||||
process.start()
|
||||
# call_soon called it too soon, not sure why.
|
||||
loop.call_later(0.001, functools.partial(process.copy_winsize, sys.stdin.fileno()))
|
||||
|
||||
val = await retcode
|
||||
RNS.log(f"Got return code {val}", RNS.LOG_DEBUG)
|
||||
return val
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
tr = TTYRestorer(sys.stdin.fileno())
|
||||
try:
|
||||
tr.raw()
|
||||
asyncio.run(main())
|
||||
finally:
|
||||
tty_unset_reader_callbacks(sys.stdin.fileno())
|
||||
tr.restore()
|
||||
@@ -0,0 +1,149 @@
|
||||
# Based on the original rnsh program by Aaron Heise (@acehoss)
|
||||
# https://github.com/acehoss/rnsh - MIT License - Copyright (c) 2023 Aaron Heise
|
||||
# This version of rnsh is included in RNS under the Reticulum License
|
||||
#
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2026 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import RNS
|
||||
from RNS.vendor import umsgpack
|
||||
from RNS.Buffer import StreamDataMessage as RNSStreamDataMessage
|
||||
import RNS.Utilities.rnsh.retry
|
||||
import abc
|
||||
import contextlib
|
||||
import struct
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
MSG_MAGIC = 0xac
|
||||
PROTOCOL_VERSION = 1
|
||||
|
||||
def _make_MSGTYPE(val: int):
|
||||
return ((MSG_MAGIC << 8) & 0xff00) | (val & 0x00ff)
|
||||
|
||||
|
||||
class NoopMessage(RNS.MessageBase):
|
||||
MSGTYPE = _make_MSGTYPE(0)
|
||||
def pack(self) -> bytes: return bytes()
|
||||
def unpack(self, raw): pass
|
||||
|
||||
|
||||
class WindowSizeMessage(RNS.MessageBase):
|
||||
MSGTYPE = _make_MSGTYPE(2)
|
||||
|
||||
def __init__(self, rows: int = None, cols: int = None, hpix: int = None, vpix: int = None):
|
||||
super().__init__()
|
||||
self.rows = rows
|
||||
self.cols = cols
|
||||
self.hpix = hpix
|
||||
self.vpix = vpix
|
||||
|
||||
def pack(self) -> bytes: return umsgpack.packb((self.rows, self.cols, self.hpix, self.vpix))
|
||||
def unpack(self, raw): self.rows, self.cols, self.hpix, self.vpix = umsgpack.unpackb(raw)
|
||||
|
||||
|
||||
class ExecuteCommandMesssage(RNS.MessageBase):
|
||||
MSGTYPE = _make_MSGTYPE(3)
|
||||
|
||||
def __init__(self, cmdline: [str] = None, pipe_stdin: bool = False, pipe_stdout: bool = False,
|
||||
pipe_stderr: bool = False, tcflags: [any] = None, term: str | None = None, rows: int = None,
|
||||
cols: int = None, hpix: int = None, vpix: int = None):
|
||||
|
||||
super().__init__()
|
||||
self.cmdline = cmdline
|
||||
self.pipe_stdin = pipe_stdin
|
||||
self.pipe_stdout = pipe_stdout
|
||||
self.pipe_stderr = pipe_stderr
|
||||
self.tcflags = tcflags
|
||||
self.term = term
|
||||
self.rows = rows
|
||||
self.cols = cols
|
||||
self.hpix = hpix
|
||||
self.vpix = vpix
|
||||
|
||||
def pack(self) -> bytes:
|
||||
return umsgpack.packb((self.cmdline, self.pipe_stdin, self.pipe_stdout, self.pipe_stderr,
|
||||
self.tcflags, self.term, self.rows, self.cols, self.hpix, self.vpix))
|
||||
|
||||
def unpack(self, raw):
|
||||
self.cmdline, self.pipe_stdin, self.pipe_stdout, self.pipe_stderr, self.tcflags, self.term, self.rows, \
|
||||
self.cols, self.hpix, self.vpix = umsgpack.unpackb(raw)
|
||||
|
||||
|
||||
# Create a version of RNS.Buffer.StreamDataMessage that we control
|
||||
class StreamDataMessage(RNSStreamDataMessage):
|
||||
MSGTYPE = _make_MSGTYPE(4)
|
||||
STREAM_ID_STDIN = 0
|
||||
STREAM_ID_STDOUT = 1
|
||||
STREAM_ID_STDERR = 2
|
||||
|
||||
|
||||
class VersionInfoMessage(RNS.MessageBase):
|
||||
MSGTYPE = _make_MSGTYPE(5)
|
||||
|
||||
def __init__(self, sw_version: str = None):
|
||||
super().__init__()
|
||||
self.sw_version = sw_version or RNS.Utilities.rnsh.__version__
|
||||
self.protocol_version = PROTOCOL_VERSION
|
||||
|
||||
def pack(self) -> bytes: return umsgpack.packb((self.sw_version, self.protocol_version))
|
||||
def unpack(self, raw): self.sw_version, self.protocol_version = umsgpack.unpackb(raw)
|
||||
|
||||
|
||||
class ErrorMessage(RNS.MessageBase):
|
||||
MSGTYPE = _make_MSGTYPE(6)
|
||||
|
||||
def __init__(self, msg: str = None, fatal: bool = False, data: dict = None):
|
||||
super().__init__()
|
||||
self.msg = msg
|
||||
self.fatal = fatal
|
||||
self.data = data
|
||||
|
||||
def pack(self) -> bytes: return umsgpack.packb((self.msg, self.fatal, self.data))
|
||||
def unpack(self, raw: bytes): self.msg, self.fatal, self.data = umsgpack.unpackb(raw)
|
||||
|
||||
|
||||
class CommandExitedMessage(RNS.MessageBase):
|
||||
MSGTYPE = _make_MSGTYPE(7)
|
||||
|
||||
def __init__(self, return_code: int = None):
|
||||
super().__init__()
|
||||
self.return_code = return_code
|
||||
|
||||
def pack(self) -> bytes: return umsgpack.packb(self.return_code)
|
||||
def unpack(self, raw: bytes): self.return_code = umsgpack.unpackb(raw)
|
||||
|
||||
|
||||
message_types = [NoopMessage, VersionInfoMessage, WindowSizeMessage, ExecuteCommandMesssage, StreamDataMessage,
|
||||
CommandExitedMessage, ErrorMessage]
|
||||
|
||||
def register_message_types(channel: RNS.Channel.Channel):
|
||||
for message_type in message_types: channel.register_message_type(message_type)
|
||||
@@ -0,0 +1,201 @@
|
||||
# Based on the original rnsh program by Aaron Heise (@acehoss)
|
||||
# https://github.com/acehoss/rnsh - MIT License - Copyright (c) 2023 Aaron Heise
|
||||
# This version of rnsh is included in RNS under the Reticulum License
|
||||
#
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2026 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import asyncio
|
||||
import threading
|
||||
import time
|
||||
import RNS.Utilities.rnsh.exception as exception
|
||||
from typing import Callable
|
||||
from contextlib import AbstractContextManager
|
||||
import types
|
||||
import typing
|
||||
|
||||
|
||||
class RetryStatus:
|
||||
def __init__(self, tag: any, try_limit: int, wait_delay: float, retry_callback: Callable[[any, int], any],
|
||||
timeout_callback: Callable[[any, int], None], tries: int = 1):
|
||||
|
||||
self.tag = tag
|
||||
self.try_limit = try_limit
|
||||
self.tries = tries
|
||||
self.wait_delay = wait_delay
|
||||
self.retry_callback = retry_callback
|
||||
self.timeout_callback = timeout_callback
|
||||
self.try_time = time.time()
|
||||
self.completed = False
|
||||
|
||||
@property
|
||||
def ready(self):
|
||||
ready = time.time() > self.try_time + self.wait_delay
|
||||
RNS.log(f"ready check {self.tag} try_time {self.try_time} wait_delay {self.wait_delay} " +
|
||||
f"next_try {self.try_time + self.wait_delay} now {time.time()} " +
|
||||
f"exceeded {time.time() - self.try_time - self.wait_delay} ready {ready}", RNS.LOG_DEBUG)
|
||||
return ready
|
||||
|
||||
@property
|
||||
def timed_out(self):
|
||||
return self.ready and self.tries >= self.try_limit
|
||||
|
||||
def timeout(self):
|
||||
self.completed = True
|
||||
self.timeout_callback(self.tag, self.tries)
|
||||
|
||||
def retry(self) -> any:
|
||||
self.tries = self.tries + 1
|
||||
self.try_time = time.time()
|
||||
return self.retry_callback(self.tag, self.tries)
|
||||
|
||||
|
||||
class RetryThread(AbstractContextManager):
|
||||
def __init__(self, loop_period: float = 0.25, name: str = "retry thread"):
|
||||
self._loop_period = loop_period
|
||||
self._statuses: list[RetryStatus] = []
|
||||
self._tag_counter = 0
|
||||
self._lock = threading.RLock()
|
||||
self._run = True
|
||||
self._finished: asyncio.Future = None
|
||||
self._thread = threading.Thread(name=name, target=self._thread_run, daemon=True)
|
||||
self._thread.start()
|
||||
|
||||
def is_alive(self):
|
||||
return self._thread.is_alive()
|
||||
|
||||
def close(self, loop: asyncio.AbstractEventLoop = None) -> asyncio.Future:
|
||||
RNS.log("Stopping timer thread", RNS.LOG_DEBUG)
|
||||
if loop is None:
|
||||
self._run = False
|
||||
self._thread.join()
|
||||
return None
|
||||
else:
|
||||
self._finished = loop.create_future()
|
||||
return self._finished
|
||||
|
||||
def wait(self, timeout: float = None):
|
||||
if timeout:
|
||||
timeout = timeout + time.time()
|
||||
|
||||
while timeout is None or time.time() < timeout:
|
||||
with self._lock:
|
||||
task_count = len(self._statuses)
|
||||
if task_count == 0:
|
||||
return
|
||||
time.sleep(0.1)
|
||||
|
||||
|
||||
def _thread_run(self):
|
||||
while self._run and self._finished is None:
|
||||
time.sleep(self._loop_period)
|
||||
ready: list[RetryStatus] = []
|
||||
prune: list[RetryStatus] = []
|
||||
with self._lock: ready.extend(list(filter(lambda s: s.ready, self._statuses)))
|
||||
|
||||
for retry in ready:
|
||||
try:
|
||||
if not retry.completed:
|
||||
if retry.timed_out:
|
||||
RNS.log(f"Timed out {retry.tag} after {retry.try_limit} tries", RNS.LOG_DEBUG)
|
||||
retry.timeout()
|
||||
prune.append(retry)
|
||||
elif retry.ready:
|
||||
RNS.log(f"Retrying {retry.tag}, try {retry.tries + 1}/{retry.try_limit}", RNS.LOG_DEBUG)
|
||||
should_continue = retry.retry()
|
||||
if not should_continue: self.complete(retry.tag)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error processing retry id {retry.tag}: {e}", RNS.LOG_ERROR)
|
||||
prune.append(retry)
|
||||
|
||||
with self._lock:
|
||||
for retry in prune:
|
||||
RNS.log(f"pruned retry {retry.tag}, retry count {retry.tries}/{retry.try_limit}", RNS.LOG_DEBUG)
|
||||
with exception.permit(SystemExit): self._statuses.remove(retry)
|
||||
|
||||
if self._finished is not None: self._finished.set_result(None)
|
||||
|
||||
def _get_next_tag(self):
|
||||
self._tag_counter += 1
|
||||
return self._tag_counter
|
||||
|
||||
def has_tag(self, tag: any) -> bool:
|
||||
with self._lock: return next(filter(lambda s: s.tag == tag, self._statuses), None) is not None
|
||||
|
||||
def begin(self, try_limit: int, wait_delay: float, try_callback: Callable[[any, int], any],
|
||||
timeout_callback: Callable[[any, int], None]) -> any:
|
||||
|
||||
RNS.log(f"Running first try", RNS.LOG_DEBUG)
|
||||
tag = try_callback(None, 1)
|
||||
RNS.log(f"First try got id {tag}", RNS.LOG_DEBUG)
|
||||
|
||||
if not tag:
|
||||
RNS.log(f"Callback returned None/False/0, considering complete.", RNS.LOG_DEBUG)
|
||||
return None
|
||||
|
||||
with self._lock:
|
||||
if tag is None: tag = self._get_next_tag()
|
||||
self.complete(tag)
|
||||
|
||||
self._statuses.append(RetryStatus(tag=tag,
|
||||
tries=1,
|
||||
try_limit=try_limit,
|
||||
wait_delay=wait_delay,
|
||||
retry_callback=try_callback,
|
||||
timeout_callback=timeout_callback))
|
||||
|
||||
RNS.log(f"Added retry timer for {tag}", RNS.LOG_DEBUG)
|
||||
return tag
|
||||
|
||||
def complete(self, tag: any):
|
||||
assert tag is not None
|
||||
with self._lock:
|
||||
status = next(filter(lambda l: l.tag == tag, self._statuses), None)
|
||||
if status is not None:
|
||||
status.completed = True
|
||||
self._statuses.remove(status)
|
||||
RNS.log(f"completed {tag}", RNS.LOG_DEBUG)
|
||||
return
|
||||
|
||||
RNS.log(f"status not found to complete {tag}", RNS.LOG_DEBUG)
|
||||
|
||||
def complete_all(self):
|
||||
with self._lock:
|
||||
for status in self._statuses:
|
||||
status.completed = True
|
||||
RNS.log(f"completed {status.tag}", RNS.LOG_DEBUG)
|
||||
|
||||
self._statuses.clear()
|
||||
|
||||
def __exit__(self, __exc_type: typing.Type[BaseException], __exc_value: BaseException,
|
||||
__traceback: types.TracebackType) -> bool:
|
||||
self.close()
|
||||
return False
|
||||
@@ -0,0 +1,174 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Based on the original rnsh program by Aaron Heise (@acehoss)
|
||||
# https://github.com/acehoss/rnsh - MIT License - Copyright (c) 2023 Aaron Heise
|
||||
# This version of rnsh is included in RNS under the Reticulum License
|
||||
#
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2026 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
|
||||
import re
|
||||
import os
|
||||
import sys
|
||||
|
||||
import RNS
|
||||
import RNS.Utilities.rnsh.process as process
|
||||
import RNS.Utilities.rnsh.session as session
|
||||
import RNS.Utilities.rnsh.args
|
||||
import RNS.Utilities.rnsh.loop
|
||||
import RNS.Utilities.rnsh.listener as listener
|
||||
import RNS.Utilities.rnsh.initiator as initiator
|
||||
from RNS.Utilities.rnsh.args import parse_arguments
|
||||
|
||||
APP_NAME = "rnsh"
|
||||
loop: asyncio.AbstractEventLoop | None = None
|
||||
|
||||
def _sanitize_service_name(service_name:str) -> str: return re.sub(r'\W+', '', service_name)
|
||||
|
||||
def prepare_identity(identity_path, service_name: str = None) -> tuple[RNS.Identity]:
|
||||
service_name = _sanitize_service_name(service_name or "")
|
||||
if identity_path is None:
|
||||
identity_path = RNS.Reticulum.identitypath + "/" + APP_NAME + \
|
||||
(f".{service_name}" if service_name and len(service_name) > 0 else "")
|
||||
|
||||
identity = None
|
||||
if os.path.isfile(identity_path):
|
||||
identity = RNS.Identity.from_file(identity_path)
|
||||
|
||||
if identity is None:
|
||||
RNS.log("No valid saved identity found, creating new...", RNS.LOG_INFO)
|
||||
identity = RNS.Identity()
|
||||
identity.to_file(identity_path)
|
||||
|
||||
return identity
|
||||
|
||||
|
||||
def print_identity(configdir, identitypath, service_name, include_destination: bool):
|
||||
reticulum = RNS.Reticulum(configdir=configdir, loglevel=RNS.LOG_INFO)
|
||||
if service_name and len(service_name) > 0:
|
||||
print(f"Using service name \"{service_name}\"")
|
||||
identity = prepare_identity(identitypath, service_name)
|
||||
destination = RNS.Destination(identity, RNS.Destination.IN, RNS.Destination.SINGLE, APP_NAME)
|
||||
print("Identity : " + str(identity))
|
||||
if include_destination:
|
||||
print("Listening on : " + RNS.prettyhexrep(destination.hash))
|
||||
|
||||
exit(0)
|
||||
|
||||
verbose_set = False
|
||||
|
||||
def ensure_config_directory():
|
||||
if os.path.isdir(os.path.expanduser("~/.config/rnsh")): return os.path.expanduser("~/.config/rnsh")
|
||||
elif os.path.isdir(os.path.expanduser("~/.rnsh")): return os.path.expanduser("~/.rnsh")
|
||||
else:
|
||||
try:
|
||||
os.makedirs(os.path.expanduser("~/.rnsh"))
|
||||
return os.path.expanduser("~/.rnsh")
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Could not get or create rnsh configuration directory, aborting", RNS.LOG_CRITICAL)
|
||||
os._exit(1)
|
||||
|
||||
|
||||
async def _rnsh_cli_main():
|
||||
global verbose_set
|
||||
args, parser = parse_arguments()
|
||||
verbose_set = args.verbose > 0
|
||||
|
||||
configdir = ensure_config_directory()
|
||||
|
||||
if args.print_identity:
|
||||
print_identity(args.config, args.identity, args.service, args.listen)
|
||||
return 0
|
||||
|
||||
if args.listen:
|
||||
allowed_file = None
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
|
||||
if os.path.isfile(os.path.expanduser("~/.config/rnsh/allowed_identities")):
|
||||
allowed_file = os.path.expanduser("~/.config/rnsh/allowed_identities")
|
||||
elif os.path.isfile(os.path.expanduser("~/.rnsh/allowed_identities")):
|
||||
allowed_file = os.path.expanduser("~/.rnsh/allowed_identities")
|
||||
|
||||
await listener.listen(configdir=configdir,
|
||||
rnsconfigdir=args.config,
|
||||
command=args.command,
|
||||
identitypath=args.identity,
|
||||
service_name=args.service,
|
||||
verbosity=args.verbose,
|
||||
quietness=args.quiet,
|
||||
allowed=args.allowed or [],
|
||||
allowed_file=allowed_file,
|
||||
disable_auth=args.no_auth,
|
||||
announce_period=args.announce,
|
||||
no_remote_command=args.no_remote_command,
|
||||
remote_cmd_as_args=args.remote_command_as_args)
|
||||
return 0
|
||||
|
||||
if args.destination is not None:
|
||||
return_code = await initiator.initiate(configdir=configdir,
|
||||
rnsconfigdir=args.config,
|
||||
identitypath=args.identity,
|
||||
verbosity=args.verbose,
|
||||
quietness=args.quiet,
|
||||
noid=args.no_id,
|
||||
destination=args.destination,
|
||||
timeout=args.timeout,
|
||||
command=args.command
|
||||
)
|
||||
return return_code if args.mirror else 0
|
||||
else:
|
||||
print("")
|
||||
parser.print_help()
|
||||
print("")
|
||||
return 1
|
||||
|
||||
|
||||
def main():
|
||||
global verbose_set
|
||||
return_code = 1
|
||||
exc = None
|
||||
try: return_code = asyncio.run(_rnsh_cli_main())
|
||||
except SystemExit: pass
|
||||
except KeyboardInterrupt: pass
|
||||
except Exception as e:
|
||||
print(f"{e}")
|
||||
exc = e
|
||||
|
||||
process.tty_unset_reader_callbacks(0)
|
||||
if verbose_set and exc: raise exc
|
||||
sys.exit(return_code if return_code is not None else 255)
|
||||
|
||||
|
||||
if __name__ == "__main__": main()
|
||||
@@ -0,0 +1,441 @@
|
||||
# Based on the original rnsh program by Aaron Heise (@acehoss)
|
||||
# https://github.com/acehoss/rnsh - MIT License - Copyright (c) 2023 Aaron Heise
|
||||
# This version of rnsh is included in RNS under the Reticulum License
|
||||
#
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2026 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
from __future__ import annotations
|
||||
import contextlib
|
||||
import functools
|
||||
import asyncio
|
||||
import RNS.Utilities.rnsh.exception as exception
|
||||
import RNS.Utilities.rnsh.process as process
|
||||
import RNS.Utilities.rnsh.helpers as helpers
|
||||
import RNS.Utilities.rnsh.protocol as protocol
|
||||
import enum
|
||||
from typing import TypeVar, Generic, Callable, List
|
||||
from abc import abstractmethod, ABC
|
||||
from multiprocessing import Manager
|
||||
import os
|
||||
import bz2
|
||||
import RNS
|
||||
|
||||
_TLink = TypeVar("_TLink")
|
||||
_TIdentity = TypeVar("_TIdentity")
|
||||
|
||||
class SEType(enum.IntEnum):
|
||||
SE_LINK_CLOSED = 0
|
||||
|
||||
class SessionException(Exception):
|
||||
def __init__(self, setype: SEType, msg: str, *args):
|
||||
super().__init__(msg, args)
|
||||
self.type = setype
|
||||
|
||||
class LSState(enum.IntEnum):
|
||||
LSSTATE_WAIT_IDENT = 1
|
||||
LSSTATE_WAIT_VERS = 2
|
||||
LSSTATE_WAIT_CMD = 3
|
||||
LSSTATE_RUNNING = 4
|
||||
LSSTATE_ERROR = 5
|
||||
LSSTATE_TEARDOWN = 6
|
||||
|
||||
|
||||
class LSOutletBase(ABC):
|
||||
@abstractmethod
|
||||
def set_initiator_identified_callback(self, cb: Callable[[LSOutletBase, _TIdentity], None]): raise NotImplemented()
|
||||
|
||||
@abstractmethod
|
||||
def set_link_closed_callback(self, cb: Callable[[LSOutletBase], None]): raise NotImplemented()
|
||||
|
||||
@abstractmethod
|
||||
def unset_link_closed_callback(self): raise NotImplemented()
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def rtt(self): raise NotImplemented()
|
||||
|
||||
@abstractmethod
|
||||
def teardown(self): raise NotImplemented()
|
||||
|
||||
|
||||
class ListenerSession:
|
||||
sessions: List[ListenerSession] = []
|
||||
allowed_identity_hashes: [any] = []
|
||||
allowed_file_identity_hashes: [any] = []
|
||||
allow_all: bool = False
|
||||
allow_remote_command: bool = False
|
||||
default_command: [str] = []
|
||||
remote_cmd_as_args = False
|
||||
|
||||
def __init__(self, outlet: LSOutletBase, channel: RNS.Channel.Channel, loop: asyncio.AbstractEventLoop):
|
||||
RNS.log(f"Session started for {outlet}", RNS.LOG_INFO)
|
||||
self.outlet = outlet
|
||||
self.channel = channel
|
||||
self.outlet.set_initiator_identified_callback(self._initiator_identified)
|
||||
self.outlet.set_link_closed_callback(self._link_closed)
|
||||
self.loop = loop
|
||||
self.state: LSState = None
|
||||
self.remote_identity = None
|
||||
self.term: str | None = None
|
||||
self.stdin_is_pipe: bool = False
|
||||
self.stdout_is_pipe: bool = False
|
||||
self.stderr_is_pipe: bool = False
|
||||
self.tcflags: [any] = None
|
||||
self.cmdline: [str] = None
|
||||
self.rows: int = 0
|
||||
self.cols: int = 0
|
||||
self.hpix: int = 0
|
||||
self.vpix: int = 0
|
||||
self.stdout_buf = bytearray()
|
||||
self.stdout_eof_sent = False
|
||||
self.stderr_buf = bytearray()
|
||||
self.stderr_eof_sent = False
|
||||
self.return_code: int | None = None
|
||||
self.return_code_sent = False
|
||||
self.process: process.CallbackSubprocess | None = None
|
||||
|
||||
if self.allow_all: self._set_state(LSState.LSSTATE_WAIT_VERS)
|
||||
else: self._set_state(LSState.LSSTATE_WAIT_IDENT)
|
||||
|
||||
self.sessions.append(self)
|
||||
protocol.register_message_types(self.channel)
|
||||
self.channel.add_message_handler(self._handle_message)
|
||||
|
||||
def _terminated(self, return_code: int):
|
||||
self.return_code = return_code
|
||||
|
||||
def _set_state(self, state: LSState, timeout_factor: float = 10.0):
|
||||
timeout = max(self.outlet.rtt * timeout_factor, max(self.outlet.rtt * 2, 10)) if timeout_factor is not None else None
|
||||
RNS.log(f"Set state: {state.name}, timeout {timeout}", RNS.LOG_DEBUG)
|
||||
orig_state = self.state
|
||||
self.state = state
|
||||
if timeout_factor is not None:
|
||||
self._call(functools.partial(self._check_protocol_timeout, lambda: self.state == orig_state, state.name), timeout)
|
||||
|
||||
def _call(self, func: callable, delay: float = 0):
|
||||
def call_inner():
|
||||
if delay == 0: func()
|
||||
else: self.loop.call_later(delay, func)
|
||||
|
||||
self.loop.call_soon_threadsafe(call_inner)
|
||||
|
||||
def send(self, message: RNS.MessageBase):
|
||||
self.channel.send(message)
|
||||
|
||||
def _protocol_error(self, name: str):
|
||||
self.terminate(f"Protocol error ({name})")
|
||||
|
||||
def _protocol_timeout_error(self, name: str):
|
||||
self.terminate(f"Protocol timeout error: {name}")
|
||||
|
||||
def terminate(self, error: str = None):
|
||||
with contextlib.suppress(Exception):
|
||||
RNS.log("Terminating session" + (f": {error}" if error else ""), RNS.LOG_DEBUG)
|
||||
if error and self.state != LSState.LSSTATE_TEARDOWN:
|
||||
with contextlib.suppress(Exception):
|
||||
self.send(protocol.ErrorMessage(error, True))
|
||||
|
||||
self.state = LSState.LSSTATE_ERROR
|
||||
self._terminate_process()
|
||||
self._call(self._prune, max(self.outlet.rtt * 3, process.CallbackSubprocess.PROCESS_PIPE_TIME+5))
|
||||
|
||||
def _prune(self):
|
||||
self.state = LSState.LSSTATE_TEARDOWN
|
||||
RNS.log("Pruning session", RNS.LOG_DEBUG)
|
||||
with contextlib.suppress(ValueError):
|
||||
self.sessions.remove(self)
|
||||
with contextlib.suppress(Exception):
|
||||
self.outlet.teardown()
|
||||
|
||||
def _check_protocol_timeout(self, fail_condition: Callable[[], bool], name: str):
|
||||
timeout = True
|
||||
try: timeout = self.state != LSState.LSSTATE_TEARDOWN and fail_condition()
|
||||
except Exception as e: RNS.log(f"Error in protocol timeout: {e}", RNS.LOG_ERROR)
|
||||
if timeout: self._protocol_timeout_error(name)
|
||||
|
||||
def _link_closed(self, outlet: LSOutletBase):
|
||||
outlet.unset_link_closed_callback()
|
||||
|
||||
if outlet != self.outlet:
|
||||
RNS.log("Link closed received from incorrect outlet", RNS.LOG_DEBUG)
|
||||
return
|
||||
|
||||
RNS.log(f"link_closed {outlet}", RNS.LOG_DEBUG)
|
||||
self.terminate()
|
||||
|
||||
def _initiator_identified(self, outlet, identity):
|
||||
if outlet != self.outlet:
|
||||
RNS.log("Identity received from incorrect outlet", RNS.LOG_DEBUG)
|
||||
return
|
||||
|
||||
RNS.log(f"initiator_identified {identity} on link {outlet}", RNS.LOG_INFO)
|
||||
if self.state not in [LSState.LSSTATE_WAIT_IDENT, LSState.LSSTATE_WAIT_VERS]:
|
||||
self._protocol_error(LSState.LSSTATE_WAIT_IDENT.name)
|
||||
|
||||
if not self.allow_all and identity.hash not in self.allowed_identity_hashes and identity.hash not in self.allowed_file_identity_hashes:
|
||||
self.terminate("Identity is not allowed.")
|
||||
|
||||
self.remote_identity = identity
|
||||
self._set_state(LSState.LSSTATE_WAIT_VERS)
|
||||
|
||||
@classmethod
|
||||
async def pump_all(cls) -> True:
|
||||
processed_any = False
|
||||
for session in cls.sessions:
|
||||
processed = session.pump()
|
||||
processed_any = processed_any or processed
|
||||
await asyncio.sleep(0)
|
||||
|
||||
|
||||
@classmethod
|
||||
async def terminate_all(cls, reason: str):
|
||||
for session in cls.sessions:
|
||||
session.terminate(reason)
|
||||
await asyncio.sleep(0)
|
||||
|
||||
def pump(self) -> bool:
|
||||
def compress_adaptive(buf: bytes):
|
||||
comp_tries = RNS.RawChannelWriter.COMPRESSION_TRIES
|
||||
comp_try = 1
|
||||
comp_success = False
|
||||
|
||||
chunk_len = len(buf)
|
||||
if chunk_len > RNS.RawChannelWriter.MAX_CHUNK_LEN:
|
||||
chunk_len = RNS.RawChannelWriter.MAX_CHUNK_LEN
|
||||
chunk_segment = None
|
||||
|
||||
chunk_segment = None
|
||||
max_data_len = self.channel.mdu - protocol.StreamDataMessage.OVERHEAD
|
||||
while chunk_len > 32 and comp_try < comp_tries:
|
||||
chunk_segment_length = int(chunk_len/comp_try)
|
||||
compressed_chunk = bz2.compress(buf[:chunk_segment_length])
|
||||
compressed_length = len(compressed_chunk)
|
||||
if compressed_length < max_data_len and compressed_length < chunk_segment_length:
|
||||
comp_success = True
|
||||
break
|
||||
else:
|
||||
comp_try += 1
|
||||
|
||||
if comp_success:
|
||||
diff = max_data_len - len(compressed_chunk)
|
||||
chunk = compressed_chunk
|
||||
processed_length = chunk_segment_length
|
||||
else:
|
||||
chunk = bytes(buf[:max_data_len])
|
||||
processed_length = len(chunk)
|
||||
|
||||
return comp_success, processed_length, chunk
|
||||
|
||||
try:
|
||||
if self.state != LSState.LSSTATE_RUNNING:
|
||||
return False
|
||||
elif not self.channel.is_ready_to_send():
|
||||
return False
|
||||
elif len(self.stderr_buf) > 0:
|
||||
comp_success, processed_length, data = compress_adaptive(self.stderr_buf)
|
||||
self.stderr_buf = self.stderr_buf[processed_length:]
|
||||
send_eof = self.process.stderr_eof and len(data) == 0 and not self.stderr_eof_sent
|
||||
self.stderr_eof_sent = self.stderr_eof_sent or send_eof
|
||||
msg = protocol.StreamDataMessage(protocol.StreamDataMessage.STREAM_ID_STDERR,
|
||||
data, send_eof, comp_success)
|
||||
self.send(msg)
|
||||
if send_eof:
|
||||
self.stderr_eof_sent = True
|
||||
return True
|
||||
elif len(self.stdout_buf) > 0:
|
||||
comp_success, processed_length, data = compress_adaptive(self.stdout_buf)
|
||||
self.stdout_buf = self.stdout_buf[processed_length:]
|
||||
send_eof = self.process.stdout_eof and len(data) == 0 and not self.stdout_eof_sent
|
||||
self.stdout_eof_sent = self.stdout_eof_sent or send_eof
|
||||
msg = protocol.StreamDataMessage(protocol.StreamDataMessage.STREAM_ID_STDOUT,
|
||||
data, send_eof, comp_success)
|
||||
self.send(msg)
|
||||
if send_eof:
|
||||
self.stdout_eof_sent = True
|
||||
return True
|
||||
elif self.return_code is not None and not self.return_code_sent:
|
||||
msg = protocol.CommandExitedMessage(self.return_code)
|
||||
self.send(msg)
|
||||
self.return_code_sent = True
|
||||
self._call(functools.partial(self._check_protocol_timeout,
|
||||
lambda: self.state == LSState.LSSTATE_RUNNING, "CommandExitedMessage"),
|
||||
max(self.outlet.rtt * 5, 10))
|
||||
return False
|
||||
|
||||
except Exception as e: RNS.log(f"Error during pump: {e}", RNS.LOG_ERROR)
|
||||
return False
|
||||
|
||||
def _terminate_process(self):
|
||||
with contextlib.suppress(Exception):
|
||||
if self.process and self.process.running:
|
||||
self.process.terminate()
|
||||
|
||||
def _start_cmd(self, cmdline: [str], pipe_stdin: bool, pipe_stdout: bool, pipe_stderr: bool, tcflags: [any],
|
||||
term: str | None, rows: int, cols: int, hpix: int, vpix: int):
|
||||
|
||||
self.cmdline = self.default_command
|
||||
if not self.allow_remote_command and cmdline and len(cmdline) > 0:
|
||||
self.terminate("Remote command line not allowed by listener")
|
||||
return
|
||||
|
||||
if self.remote_cmd_as_args and cmdline and len(cmdline) > 0:
|
||||
self.cmdline.extend(cmdline)
|
||||
elif cmdline and len(cmdline) > 0:
|
||||
self.cmdline = cmdline
|
||||
|
||||
|
||||
self.stdin_is_pipe = pipe_stdin
|
||||
self.stdout_is_pipe = pipe_stdout
|
||||
self.stderr_is_pipe = pipe_stderr
|
||||
self.tcflags = tcflags
|
||||
self.term = term
|
||||
|
||||
def stdout(data: bytes):
|
||||
self.stdout_buf.extend(data)
|
||||
|
||||
def stderr(data: bytes):
|
||||
self.stderr_buf.extend(data)
|
||||
|
||||
try:
|
||||
self.process = process.CallbackSubprocess(argv=self.cmdline,
|
||||
env={"TERM": self.term or os.environ.get("TERM") or "xterm",
|
||||
"RNS_REMOTE_IDENTITY": (RNS.prettyhexrep(self.remote_identity.hash)
|
||||
if self.remote_identity and self.remote_identity.hash else "")},
|
||||
loop=self.loop,
|
||||
stdout_callback=stdout,
|
||||
stderr_callback=stderr,
|
||||
terminated_callback=self._terminated,
|
||||
stdin_is_pipe=self.stdin_is_pipe,
|
||||
stdout_is_pipe=self.stdout_is_pipe,
|
||||
stderr_is_pipe=self.stderr_is_pipe)
|
||||
self.process.start()
|
||||
self._set_window_size(rows, cols, hpix, vpix)
|
||||
except Exception as e:
|
||||
RNS.log(f"Unable to start process for link {self.outlet}: {e}", RNS.LOG_ERROR)
|
||||
self.terminate("Unable to start process")
|
||||
|
||||
def _set_window_size(self, rows: int, cols: int, hpix: int, vpix: int):
|
||||
self.rows = rows
|
||||
self.cols = cols
|
||||
self.hpix = hpix
|
||||
self.vpix = vpix
|
||||
with contextlib.suppress(Exception):
|
||||
self.process.set_winsize(rows, cols, hpix, vpix)
|
||||
|
||||
def _received_stdin(self, data: bytes, eof: bool):
|
||||
if data and len(data) > 0:
|
||||
self.process.write(data)
|
||||
if eof:
|
||||
self.process.close_stdin()
|
||||
|
||||
def _handle_message(self, message: RNS.MessageBase):
|
||||
if self.state == LSState.LSSTATE_WAIT_IDENT:
|
||||
# Ignore any messages until the initiator has identified to avoid race conditions
|
||||
# between identity announcement and early protocol messages.
|
||||
RNS.log("Ignoring message while waiting for identification", RNS.LOG_DEBUG)
|
||||
return
|
||||
if self.state == LSState.LSSTATE_WAIT_VERS:
|
||||
if not isinstance(message, protocol.VersionInfoMessage):
|
||||
self._protocol_error(self.state.name)
|
||||
return
|
||||
RNS.log(f"Version {message.sw_version}, protocol {message.protocol_version} on link {self.outlet}", RNS.LOG_VERBOSE)
|
||||
if message.protocol_version != protocol.PROTOCOL_VERSION:
|
||||
self.terminate("Incompatible protocol")
|
||||
return
|
||||
self.send(protocol.VersionInfoMessage())
|
||||
self._set_state(LSState.LSSTATE_WAIT_CMD)
|
||||
return
|
||||
elif self.state == LSState.LSSTATE_WAIT_CMD:
|
||||
if not isinstance(message, protocol.ExecuteCommandMesssage):
|
||||
return self._protocol_error(self.state.name)
|
||||
RNS.log(f"Execute command message on link {self.outlet}: {message.cmdline}", RNS.LOG_VERBOSE)
|
||||
self._set_state(LSState.LSSTATE_RUNNING)
|
||||
self._start_cmd(message.cmdline, message.pipe_stdin, message.pipe_stdout, message.pipe_stderr,
|
||||
message.tcflags, message.term, message.rows, message.cols, message.hpix, message.vpix)
|
||||
return
|
||||
elif self.state == LSState.LSSTATE_RUNNING:
|
||||
if isinstance(message, protocol.WindowSizeMessage):
|
||||
self._set_window_size(message.rows, message.cols, message.hpix, message.vpix)
|
||||
elif isinstance(message, protocol.StreamDataMessage):
|
||||
if message.stream_id != protocol.StreamDataMessage.STREAM_ID_STDIN:
|
||||
RNS.log(f"Received stream data for invalid stream {message.stream_id} on link {self.outlet}", RNS.LOG_ERROR)
|
||||
return self._protocol_error(self.state.name)
|
||||
self._received_stdin(message.data, message.eof)
|
||||
return
|
||||
elif isinstance(message, protocol.NoopMessage):
|
||||
# echo noop only on listener--used for keepalive/connectivity check
|
||||
self.send(message)
|
||||
return
|
||||
elif self.state in [LSState.LSSTATE_ERROR, LSState.LSSTATE_TEARDOWN]:
|
||||
RNS.log(f"Received packet, but in state {self.state.name}", RNS.LOG_ERROR)
|
||||
return
|
||||
else:
|
||||
self._protocol_error("unexpected message")
|
||||
return
|
||||
|
||||
|
||||
class RNSOutlet(LSOutletBase):
|
||||
|
||||
def set_initiator_identified_callback(self, cb: Callable[[LSOutletBase, _TIdentity], None]):
|
||||
def inner_cb(link, identity: _TIdentity):
|
||||
cb(self, identity)
|
||||
|
||||
self.link.set_remote_identified_callback(inner_cb)
|
||||
|
||||
def set_link_closed_callback(self, cb: Callable[[LSOutletBase], None]):
|
||||
def inner_cb(link):
|
||||
cb(self)
|
||||
|
||||
self.link.set_link_closed_callback(inner_cb)
|
||||
|
||||
def unset_link_closed_callback(self):
|
||||
self.link.set_link_closed_callback(None)
|
||||
|
||||
def teardown(self):
|
||||
self.link.teardown()
|
||||
|
||||
@property
|
||||
def rtt(self) -> float:
|
||||
return self.link.rtt
|
||||
|
||||
def __str__(self):
|
||||
return f"Outlet RNS Link {self.link}"
|
||||
|
||||
def __init__(self, link: RNS.Link):
|
||||
self.link = link
|
||||
link.lsoutlet = self
|
||||
|
||||
@staticmethod
|
||||
def get_outlet(link: RNS.Link):
|
||||
if hasattr(link, "lsoutlet"):
|
||||
return link.lsoutlet
|
||||
|
||||
return RNSOutlet(link)
|
||||
+31
-9
@@ -149,14 +149,10 @@ def log(msg, level=3, _override_destination = False, pt=False):
|
||||
|
||||
elif (logdest == LOG_FILE and logfile != None):
|
||||
try:
|
||||
file = open(logfile, "a")
|
||||
file.write(logstring+"\n")
|
||||
file.close()
|
||||
|
||||
with open(logfile, "a") as file: file.write(logstring+"\n")
|
||||
if os.path.getsize(logfile) > LOG_MAXSIZE:
|
||||
prevfile = logfile+".1"
|
||||
if os.path.isfile(prevfile):
|
||||
os.unlink(prevfile)
|
||||
if os.path.isfile(prevfile): os.unlink(prevfile)
|
||||
os.rename(logfile, prevfile)
|
||||
|
||||
except Exception as e:
|
||||
@@ -166,8 +162,7 @@ def log(msg, level=3, _override_destination = False, pt=False):
|
||||
log(msg, level)
|
||||
|
||||
elif logdest == LOG_CALLBACK:
|
||||
try:
|
||||
logcall(logstring)
|
||||
try: logcall(logstring)
|
||||
except Exception as e:
|
||||
_always_override_destination = True
|
||||
log("Exception occurred while calling external log handler: "+str(e), LOG_CRITICAL)
|
||||
@@ -202,6 +197,11 @@ def prettyhexrep(data):
|
||||
hexrep = "<"+delimiter.join("{:02x}".format(c) for c in data)+">"
|
||||
return hexrep
|
||||
|
||||
def prettyb256rep(data):
|
||||
delimiter = ""
|
||||
b256rep = "<"+delimiter.join(b256_rep(c) for c in data)+">"
|
||||
return b256rep
|
||||
|
||||
def prettyspeed(num, suffix="b"):
|
||||
return prettysize(num/8, suffix=suffix)+"ps"
|
||||
|
||||
@@ -545,4 +545,26 @@ class Profiler:
|
||||
if tag["super"] == None:
|
||||
print_results_recursive(tag, results)
|
||||
|
||||
profile = Profiler.get_profiler
|
||||
profile = Profiler.get_profiler
|
||||
|
||||
b256 = [
|
||||
# 0 1 2 3 4 5 6 7 8 9 A B C D F F
|
||||
"a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p", # 0x0 Latin & numerals
|
||||
"q","r","s","t","u","v","x","y","z","æ","ø","0","1","2","3","4", # 0x1 Latin & numerals
|
||||
"A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P", # 0x2 Latin & numerals
|
||||
"Q","R","S","T","U","W","X","Y","Z","Æ","Ø","5","6","7","8","9", # 0x3 Latin & numerals
|
||||
"α","β","γ","δ","ε","ζ","η","θ","ι","κ","λ","μ","ν","ξ","π","ρ", # 0x4 Greek
|
||||
"σ","τ","φ","χ","ψ","ω","Γ","Δ","Θ","Λ","Ξ","Π","Σ","Φ","Ψ","Ω", # 0x5 Greek
|
||||
"Б","Д","Ж","З","И","Л","П","Ц","Ч","Ш","Щ","Ъ","Ы","Э","Ю","Я", # 0x6 Cyrillic
|
||||
"б","д","ж","з","и","л","п","ц","ч","ш","щ","ъ","ы","э","ю","я", # 0x7 Cyrillic
|
||||
"Ա","Բ","Գ","Դ","Ե","Զ","Է","Ը","Թ","Ժ","Ի","Խ","Ծ","Կ","Հ","Ձ", # 0x8 Armenian Capitals
|
||||
"Ղ","Ճ","Մ","Յ","Ն","Շ","Ո","Չ","Պ","Ջ","Վ","Ր","Ց","Ւ","Ք","Ֆ", # 0x9 Armenian Captials
|
||||
"ᚠ","ᚢ","ᚦ","ᚱ","ᚹ","ᚺ","ᚾ","ᛈ","ᛇ","ᛉ","ᛊ","ᛏ","ᛒ","ᛖ","ᛗ","ᛟ", # 0xA Elder Futhark
|
||||
"イ","ウ","オ","カ","キ","ケ","サ","シ","ス","セ","タ","チ","テ","ト","ナ","ヌ", # 0xB Katakana
|
||||
"ネ","ヒ","フ","ヘ","ホ","マ","ミ","ム","メ","モ","ヤ","ラ","リ","ル","レ","ワ", # 0xC Katakana
|
||||
"𐑐","𐑑","𐑒","𐑔","𐑕","𐑗","𐑙","𐑳","𐑶","𐑸","𐑹","𐑺","𐑻","𐑽","𐑾","𐑿", # 0xD Shavian
|
||||
"᱑","᱕","᱘","᱙","ᱚ","ᱝ","ᱟ","ᱣ","ᱦ","ᱨ","ᱬ","ᱭ","ᱰ","ᱳ","ᱶ","ᱷ", # 0xE Ol Chiki
|
||||
"𐌳","𐌸","𐌾","𐐀","𐐁","𐐂","𐐆","𐐇","𐐈","𐐉","𐐊","𐐋","𐐌","𐐍","𐐎","𐐏", # 0xF Gothic & Deseret
|
||||
]
|
||||
|
||||
def b256_rep(input_byte): return b256[int(input_byte)]
|
||||
+1
-1
@@ -1 +1 @@
|
||||
__version__ = "1.1.5"
|
||||
__version__ = "1.2.2"
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,4 +1,4 @@
|
||||
# Sphinx build info version 1
|
||||
# This file records the configuration used when building these files. When it is not found, a full rebuild will be done.
|
||||
config: 0a02f7ea18b4c8fc12a9bb273be595c7
|
||||
config: 7f24c66053dcd3623245c5f317b94800
|
||||
tags: 645f666f9bcd5a90fca523b33c5a78b7
|
||||
|
||||
@@ -0,0 +1,404 @@
|
||||
.. _git-main:
|
||||
|
||||
************************
|
||||
Using Git Over Reticulum
|
||||
************************
|
||||
|
||||
A set of utilities for distributed collaborative software development and publishing is included in RNS.
|
||||
|
||||
The system consists of two parts: The ``rngit`` node that hosts repositories, and the ``git-remote-rns`` helper that enables Git to communicate with rngit nodes. As soon as you have RNS installed on your system, you can transparently use Git with Reticulum-hosted repositories just like any other type of remote. Git over Reticulum uses URLs in the following format: ``rns://DESTINATION_HASH/group/repo``.
|
||||
|
||||
If you set a branch to track a Reticulum remote as the default upstream, you can simply use ``git`` as you normally would; all commands work transparently and as expected.
|
||||
|
||||
.. warning::
|
||||
**The rngit program is a new addition to RNS!** This functionality was introduced in RNS 1.2.0. While great care has been taken to design a secure, but highly configurable and flexible permission system for allowing many users to interact with many different repositories on a single node, ``rngit`` has not been tested extensively in the wild! Be careful when hosting repositories, especially if they are public or semi-public.
|
||||
|
||||
The rngit Utility
|
||||
=================
|
||||
|
||||
The ``rngit`` utility provides full Git repository hosting and interaction over Reticulum. It allows you to host and manage Git repositories and releases on Reticulum nodes, and to interact with remote repositories using standard Git commands through the ``rns://`` URL scheme.
|
||||
|
||||
**Usage Examples**
|
||||
|
||||
Run ``rngit`` to start a repository node:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rngit
|
||||
|
||||
[Notice] Starting Reticulum Git Node...
|
||||
[Notice] Reticulum Git Node listening on <0d7334d411d00120cbad24edf355fdd2>
|
||||
|
||||
On the first run, ``rngit`` will create a default configuration file. You will then need to edit this, to point to your repository locations, configure access permissions, and perform any other necessary configuration.
|
||||
|
||||
View your identity and destination hashes:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rngit --print-identity
|
||||
|
||||
Git Peer Identity : <959e10e5efc1bd9d97a4083babe51dea>
|
||||
Repository Node Identity : <153cb870b4665b8c1c348896292b0bad>
|
||||
Repositories Destination : <0d7334d411d00120cbad24edf355fdd2>
|
||||
|
||||
If the page server is enabled, the output will also include the Nomad Network destination hash.
|
||||
|
||||
You can run ``rngit`` in service mode with logging to file:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rngit -s
|
||||
|
||||
Clone a repository from a remote ``rngit`` node:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ git clone rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo
|
||||
|
||||
Add a Reticulum remote to an existing repository:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ git remote add some_remote rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo
|
||||
|
||||
Push changes to the Reticulum remote:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ git push some_remote master
|
||||
|
||||
Get changes from a remote repository:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ git pull rns_remote master
|
||||
|
||||
**All Command-Line Options (rngit)**
|
||||
|
||||
.. code:: text
|
||||
|
||||
usage: rngit.py [-h] [--config CONFIG] [--rnsconfig RNSCONFIG] [-s] [-i] [-v]
|
||||
[-q] [--version]
|
||||
|
||||
Reticulum Git Repository Node
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--config CONFIG path to alternative config directory
|
||||
--rnsconfig RNSCONFIG
|
||||
path to alternative Reticulum config directory
|
||||
-p, --print-identity print identity and destination info and exit
|
||||
-s, --service rngit is running as a service and should log to file
|
||||
-i, --interactive drop into interactive shell after initialisation
|
||||
-v, --verbose increase verbosity
|
||||
-q, --quiet decrease verbosity
|
||||
--version show program's version number and exit
|
||||
|
||||
**All Command-Line Options (git-remote-rns)**
|
||||
|
||||
The ``git-remote-rns`` helper is automatically invoked by Git when interacting with ``rns://`` URLs. It is not typically run directly by users, but accepts the following environment variables for configuration:
|
||||
|
||||
- ``RNGIT_CONFIG`` - Path to alternative client configuration directory
|
||||
- ``RNS_CONFIG`` - Path to alternative Reticulum configuration directory
|
||||
|
||||
The client configuration file is located at ``~/.rngit/client_config`` and allows adjusting parameters such as the reference batch size for transfers.
|
||||
|
||||
|
||||
Repository Structure
|
||||
====================
|
||||
|
||||
The ``rngit`` node organizes repositories into groups. Each group is a directory containing bare Git repositories. The repository path format is ``group_name/repo_name``. For example, a repository at ``/var/git/public/myrepo`` would be accessible as ``public/myrepo`` via the URL ``rns://DESTINATION_HASH/public/myrepo``.
|
||||
|
||||
**Configuration**
|
||||
|
||||
The ``rngit`` node configuration file is located at ``~/.rngit/config`` (or ``/etc/rngit/config`` for system-wide installations). The default configuration includes:
|
||||
|
||||
- Repository group paths defining where to find bare repositories
|
||||
- Access permissions for groups and individual repositories
|
||||
- Announce intervals for network visibility
|
||||
- Optional statistics recording for repository activity
|
||||
|
||||
Access permissions can be configured at the group level in the config file, or per-repository using ``.allowed`` files. Permissions use the format ``permission:target`` where permission is ``r`` (read), ``w`` (write), ``rw`` (read/write), ``c`` (create) or ``s`` (stats) and target is ``all``, ``none``, or a specific identity hash.
|
||||
|
||||
The ``s`` (stats) permission allows viewing repository activity statistics, including views, fetches and pushes over time. To enable statistics recording, set ``record_stats = yes`` in the ``[rngit]`` section of the configuration file. You can also exclude specific identities from statistics by adding their hashes to ``stats_ignore_identities``.
|
||||
|
||||
Repository-specific ``.allowed`` files can be static text files or executable scripts that output permission rules to stdout. A ``group.allowed`` file in a repository group directory applies to all repositories within that group.
|
||||
|
||||
Serving Pages Over Nomad Network
|
||||
================================
|
||||
|
||||
In addition to providing Git repository access via the Git remote helper protocol, ``rngit`` can also run a `Nomad Network <https://github.com/markqvist/nomadnet>`_ compatible page node. This allows users to browse repository information, view file contents, inspect commit history and access repository statistics through any Nomad Network client.
|
||||
|
||||
When enabled, the page node provides a complete interface to your repositories, with automatic Markdown to Micron conversion, syntax-highlighted code browsing, and detailed commit, diff and statistics views.
|
||||
|
||||
**Enabling the Git Page Node**
|
||||
|
||||
To enable the page node, add the following to your ``~/.rngit/config`` file:
|
||||
|
||||
.. code:: text
|
||||
|
||||
[pages]
|
||||
serve_nomadnet = yes
|
||||
|
||||
When the page node is enabled, ``rngit`` will listen on a Nomad Network node destination in addition to the Git repository destination. You can view the destination hash by running:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rngit --print-identity
|
||||
|
||||
Git Peer Identity : <959e10e5efc1bd9d97a4083babe51dea>
|
||||
Repository Node Identity : <153cb870b4665b8c1c348896292b0bad>
|
||||
Repositories Destination : <0d7334d411d00120cbad24edf355fdd2>
|
||||
Nomad Network Destination : <50824b711717f97c2fb1166ceddd5ea9>
|
||||
|
||||
**Accessing Repository Pages**
|
||||
|
||||
Once the page server is running, you can access it from any Nomad Network client by connecting to the Nomad Network destination. The page node provides the following views:
|
||||
|
||||
- **Front Page** - Lists all repository groups accessible to your identity
|
||||
- **Group Page** - Shows all repositories within a group
|
||||
- **Repository Page** - Displays repository overview, description and README
|
||||
- **Releases** - List of releases for the repository, with information and downloads
|
||||
- **File Browser** - Browse directory trees and view and download file contents
|
||||
- **Commits View** - View commit history with pagination
|
||||
- **Commit Details** - Detailed commit information with file changes and diffs
|
||||
- **Refs View** - List branches and tags
|
||||
- **Statistics** - Activity charts showing views, fetches and pushes over time
|
||||
|
||||
All pages respect the same permission system used for Git access. If an identity does not have read access to a repository, they will not be able to view its pages.
|
||||
|
||||
Formatting & Syntax Highlighting
|
||||
================================
|
||||
|
||||
If the ``pygments`` Python module is installed on your system, the page server will automatically apply syntax highlighting to code files. The highlighting supports a wide range of programming languages and uses a color theme optimized for terminal display.
|
||||
|
||||
To enable syntax highlighting, install pygments:
|
||||
|
||||
.. code:: text
|
||||
|
||||
pip install pygments
|
||||
|
||||
**Markdown & Micron Support**
|
||||
|
||||
README files and other Markdown documents are automatically converted to Micron markup for display in Nomad Network clients. You can also write your README files directly in Micron, in which case they will display and render as such in any Nomad Network client. The file browser also supports viewing both rendered and raw Markdown and Micron documents.
|
||||
|
||||
Code blocks in Markdown can include language hints for syntax highlighting:
|
||||
|
||||
.. code:: text
|
||||
|
||||
```python
|
||||
def hello_world():
|
||||
print("Hello, Reticulum!")
|
||||
```
|
||||
|
||||
Customizing Templates
|
||||
=====================
|
||||
|
||||
The page server uses a template system that allows complete customization of the generated pages. Templates are stored in the ``~/.rngit/templates/`` directory as Micron files.
|
||||
|
||||
The following template files are supported:
|
||||
|
||||
- ``base.mu`` - Base template wrapping all pages
|
||||
- ``front.mu`` - Front page listing all groups
|
||||
- ``group.mu`` - Group page listing repositories
|
||||
- ``repo.mu`` - Repository overview page
|
||||
- ``releases.mu`` - Release list page
|
||||
- ``release.mu`` - Release details page
|
||||
- ``tree.mu`` - File browser pages
|
||||
- ``blob.mu`` - File content display
|
||||
- ``commits.mu`` - Commit history listing
|
||||
- ``commit.mu`` - Individual commit detail page
|
||||
- ``refs.mu`` - Branches and tags listing
|
||||
- ``stats.mu`` - Statistics page
|
||||
|
||||
Templates can include the following variables:
|
||||
|
||||
- ``{PAGE_CONTENT}`` - The main content of the page (required)
|
||||
- ``{NODE_NAME}`` - The configured node name
|
||||
- ``{NAVIGATION}`` - Breadcrumb navigation links
|
||||
- ``{VERSION}`` - The rngit version number
|
||||
- ``{GEN_TIME}`` - Page generation time
|
||||
|
||||
**Dynamic Templates**
|
||||
|
||||
Templates can be made executable to generate dynamic content. If a template file has the executable bit set, it will be executed and its stdout used as the template content.
|
||||
|
||||
**Icon Sets**
|
||||
|
||||
By default, the page server uses Nerd Font icons. If you prefer simpler icons or your terminal does not support Nerd Fonts, you can enable Unicode icons instead:
|
||||
|
||||
.. code:: text
|
||||
|
||||
[pages]
|
||||
serve_nomadnet = yes
|
||||
unicode_icons = yes
|
||||
|
||||
**Repository Statistics**
|
||||
|
||||
When statistics recording is enabled (see the ``record_stats`` configuration option), the page server can display activity charts for each repository. The statistics page shows:
|
||||
|
||||
- Total and peak views, fetches and pushes
|
||||
- Daily activity charts over a 90-day period
|
||||
- Combined activity visualization
|
||||
|
||||
To view statistics, a user must have the ``s`` (stats) permission for the repository. See the Access Configuration section for details on setting permissions.
|
||||
|
||||
**Repository Thanks**
|
||||
|
||||
The page server includes a "Thanks" feature that allows users to express appreciation for a repository. On each repository page, a "Thanks" link is displayed showing the current thanks count. Clicking this link registers a thank you for the repository.
|
||||
|
||||
**Configuration Example**
|
||||
|
||||
A complete page server configuration might look like this:
|
||||
|
||||
.. code:: text
|
||||
|
||||
[rngit]
|
||||
node_name = My Git Server
|
||||
announce_interval = 360
|
||||
record_stats = yes
|
||||
|
||||
[repositories]
|
||||
public = /var/git/public
|
||||
internal = /var/git/internal
|
||||
|
||||
[access]
|
||||
public = r:all
|
||||
internal = rw:9710b86ba12c42d1d8f30f74fe509286
|
||||
|
||||
[pages]
|
||||
serve_nomadnet = yes
|
||||
unicode_icons = no
|
||||
|
||||
|
||||
Release Management
|
||||
==================
|
||||
|
||||
In addition to hosting Git repositories, ``rngit`` provides a complete release management system. This allows you to publish versioned releases with associated artifacts, release notes and metadata. Releases are managed through the ``rngit release`` subcommand, and are also viewable through the Nomad Network page interface.
|
||||
|
||||
**The Release Workflow**
|
||||
|
||||
Creating a release involves specifying a Git tag and a directory containing build artifacts or other files to distribute. The ``rngit`` client will open your configured ``$EDITOR`` to compose release notes, then upload all artifacts to the remote repository node.
|
||||
|
||||
To create a release, specify the tag name and path to artifacts:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rngit release create rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo v1.2.0:./dist
|
||||
|
||||
This will:
|
||||
|
||||
1. Verify that the tag ``v1.2.0`` exists in the repository
|
||||
2. Open your editor to write release notes
|
||||
3. Upload all files from the ``./dist`` directory
|
||||
4. Publish the release
|
||||
|
||||
If no ``$EDITOR`` environment variable is set, ``rngit`` will try to use ``nano``, ``vim`` or ``vi``. The editor will show a template with instructions. Lines starting with ``#`` will be ignored, and if the remaining content is empty after stripping comments, the release creation will be cancelled.
|
||||
|
||||
**Release Storage & Structure**
|
||||
|
||||
Releases are stored on the server in a directory named ``repo_name.releases`` next to the bare repository. Each release is a subdirectory containing:
|
||||
|
||||
- ``META`` - Release metadata in ConfigObj format
|
||||
- ``RELEASE.md`` or ``RELEASE.mu`` - Release notes
|
||||
- ``artifacts/`` - All uploaded files
|
||||
- ``THANKS`` - Appreciation count from users
|
||||
|
||||
**Listing Releases**
|
||||
|
||||
To view all releases for a repository:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rngit release list rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo
|
||||
|
||||
Tag Status Created Objs Notes
|
||||
------------------------------------------------------------------
|
||||
v1.2.0 published 2025-01-15 14:32 3 Another release
|
||||
v1.1.0 published 2024-12-03 09:15 2 Bug fix release
|
||||
v1.0.0 published 2024-10-20 16:45 2 Initial release
|
||||
|
||||
**Viewing Release Details**
|
||||
|
||||
To see full information about a specific release:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rngit release view rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo v1.2.0
|
||||
|
||||
Release : 0.9.2
|
||||
Status : published
|
||||
Created : 2026-05-04 23:53:09
|
||||
Thanks : 5
|
||||
|
||||
Release Notes
|
||||
=============
|
||||
|
||||
Version 1.2.0 release notes...
|
||||
|
||||
Artifacts (4)
|
||||
=============
|
||||
- myapp-1.2.0.tar.gz (1.5 MB)
|
||||
- myapp-1.2.0.zip (1.6 MB)
|
||||
- checksums.txt (256 B)
|
||||
|
||||
**Deleting Releases**
|
||||
|
||||
To remove a release:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rngit release delete rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo v1.2.0
|
||||
|
||||
Are you sure you want to delete release 'v1.2.0'? [y/N]: y
|
||||
Release v1.2.0 deleted
|
||||
|
||||
**Requirements & Validation**
|
||||
|
||||
- The specified tag must exist in the remote repository
|
||||
- You must have ``release`` permission for the repository
|
||||
- The target artifacts directory must exist and contain at least one file
|
||||
- Release notes cannot be empty
|
||||
|
||||
**Permissions**
|
||||
|
||||
Release management requires the ``release`` permission, configured the same way as other repository permissions. In the config file or ``.allowed`` files, use ``rel:target`` to grant release management rights:
|
||||
|
||||
.. code:: text
|
||||
|
||||
# In .allowed file or config
|
||||
rel:all # Allow everyone
|
||||
rel:9710b86... # Allow specific identity
|
||||
rel:none # Deny everyone
|
||||
|
||||
**Nomad Network Interface**
|
||||
|
||||
When the Nomad Network page server is enabled, releases are displayed on a dedicated releases page for each repository. Each release is listed with its tag, creation date, artifact count and a preview of the release notes. Clicking a release shows the full details including formatted release notes and a listing of all artifacts with their sizes.
|
||||
|
||||
Only releases with ``published`` status are visible through the Nomad Network interface. Draft releases (if supported in future implementations) would only be visible through the command-line interface.
|
||||
|
||||
**All Command-Line Options (rngit release)**
|
||||
|
||||
.. code:: text
|
||||
|
||||
usage: rngit release [-h] [--config CONFIG] [--rnsconfig RNSCONFIG]
|
||||
[-i IDENTITY] [-v] [-q] [--version]
|
||||
operation repository [target]
|
||||
|
||||
Reticulum Git Release Manager
|
||||
|
||||
positional arguments:
|
||||
operation list, view, create or delete
|
||||
repository URL of remote repository (rns://hash/group/repo)
|
||||
target tag or tag:path for create, tag for view/delete
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--config CONFIG path to alternative config directory
|
||||
--rnsconfig RNSCONFIG
|
||||
path to alternative Reticulum config directory
|
||||
-i IDENTITY, --identity IDENTITY
|
||||
path to release identity
|
||||
-v, --verbose increase verbosity
|
||||
-q, --quiet decrease verbosity
|
||||
--version show program's version number and exit
|
||||
@@ -27,6 +27,7 @@ to participate in the development of Reticulum itself.
|
||||
hardware
|
||||
interfaces
|
||||
networks
|
||||
git
|
||||
support
|
||||
examples
|
||||
license
|
||||
|
||||
@@ -680,6 +680,21 @@ another one, which will be created if it does not already exist
|
||||
--version show program's version number and exit
|
||||
|
||||
|
||||
The rngit Utility
|
||||
=================
|
||||
|
||||
The ``rngit`` utility provides full Git repository hosting and interaction over Reticulum, as well as many other useful features for software development, collaboration and publishing. It allows you to host Git repositories on Reticulum nodes, interact with remote repositories using standard Git commands through the ``rns://`` URL scheme, and to publish software releases.
|
||||
|
||||
The system consists of two parts: The ``rngit`` node that hosts and manages repositories, and the ``git-remote-rns`` helper that enables Git to communicate with rngit nodes. As soon as you have RNS installed on your system, you can transparently use Git with Reticulum-hosted repositories just like any other type of remote. Git over Reticulum uses URLs in the following format: ``rns://DESTINATION_HASH/group/repo``.
|
||||
|
||||
If you set a branch to track a Reticulum remote as the default upstream, you can simply use ``git`` as you normally would; all commands work transparently and as expected.
|
||||
|
||||
.. warning::
|
||||
**The rngit program is a new addition to RNS!** This functionality was introduced in RNS 1.2.0. While great care has been taken to design a secure, but highly configurable and flexible permission system for allowing many users to interact with many different repositories on a single node, ``rngit`` has not been tested extensively in the wild! Be careful when hosting repositories, especially if they are public or semi-public.
|
||||
|
||||
For the full documentation on the `rngit` system, see the :ref:`Using Git Over Reticulum<git-main>` chapter of this manual.
|
||||
|
||||
|
||||
The rnx Utility
|
||||
================
|
||||
|
||||
@@ -752,6 +767,282 @@ another one, which will be created if it does not already exist
|
||||
--version show program's version number and exit
|
||||
|
||||
|
||||
The rnsh Utility
|
||||
================
|
||||
|
||||
The ``rnsh`` utility provides a fully interactive remote shell over Reticulum.
|
||||
It allows you to establish encrypted, authenticated shell sessions on remote
|
||||
systems, complete with terminal emulation, pipe support, and window resizing.
|
||||
|
||||
While the ``rnx`` utility is useful for simple remote command execution and
|
||||
retrieving output, ``rnsh`` provides a complete interactive terminal experience,
|
||||
making it ideal for remote administration and management tasks that require
|
||||
real-time interaction, just like SSH does for IP networks.
|
||||
|
||||
``rnsh`` operates in two modes: a *listener* mode that accepts incoming
|
||||
connections, and an *initiator* mode that connects to a remote listener. Both
|
||||
sides authenticate using Reticulum Identities, ensuring that only authorised
|
||||
peers can establish sessions.
|
||||
|
||||
.. note::
|
||||
``rnsh`` provides a genuine interactive terminal over Reticulum. It supports
|
||||
full terminal emulation including escape sequences, window resizing, signal
|
||||
forwarding, and piping of standard input, output and error streams. This
|
||||
makes it suitable for running text editors, terminal multiplexers, and any
|
||||
other interactive programs on remote systems.
|
||||
|
||||
**Usage Examples**
|
||||
|
||||
Start ``rnsh`` in listener mode, accepting connections from specific identities:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh -l -a 941bed5e228775e5a8079fc38b1ccf3f -a 1b03013c25f1c2ca068a4f080b844a10
|
||||
|
||||
You can also specify allowed identity hashes (one per line) in the file
|
||||
``~/.rnsh/allowed_identities`` or ``~/.config/rnsh/allowed_identities``, and
|
||||
simply run the program in listener mode:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh -l
|
||||
|
||||
Connect to a remote listener from another system:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh 7a55144adf826958a9529a3bcf08b149
|
||||
|
||||
Specify a command to run on the remote system, separating ``rnsh`` options from
|
||||
the remote command with ``--``:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh 7a55144adf826958a9529a3bcf08b149 -- top
|
||||
|
||||
Set a default command for the listener, in case the initiator does not supply
|
||||
one, or when remote command execution is disabled:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh -l -- /bin/bash --login
|
||||
|
||||
Use the ``-m`` flag to mirror the exit code of the remote process:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh -m 7a55144adf826958a9529a3bcf08b149 -- /usr/local/bin/check-status
|
||||
|
||||
Use the ``-p`` flag to display the identity and destination hash for a listener:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh -l -p
|
||||
|
||||
Identity : <984b74a3f768bef236af4371e6f248cd>
|
||||
Listening on : 7a55144adf826958a9529a3bcf08b149
|
||||
|
||||
Use a specific identity file rather than the default:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh -l -i /path/to/identity
|
||||
|
||||
Announce the listener destination on startup, and periodically:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh -l -b 900
|
||||
|
||||
The ``-b`` option specifies the announce period in seconds. Use ``0`` to
|
||||
announce only once at startup.
|
||||
|
||||
**Authentication & Authorisation**
|
||||
|
||||
By default, ``rnsh`` requires that connecting initiators identify themselves
|
||||
with a Reticulum Identity whose hash is present in the list of allowed
|
||||
identities. Allowed identities can be specified on the command line with the
|
||||
``-a`` option, and can be used multiple times:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh -l -a 941bed5e228775e5a8079fc38b1ccf3f -a 1b03013c25f1c2ca068a4f080b844a10
|
||||
|
||||
You can also maintain a list of allowed identity hashes in the file
|
||||
``~/.rnsh/allowed_identities`` or ``~/.config/rnsh/allowed_identities``,
|
||||
with one hex hash per line. This file is reloaded every time a new connection
|
||||
is received, so changes take effect immediately without restarting ``rnsh``.
|
||||
|
||||
If you want to accept connections from any identity (for testing or in fully
|
||||
trusted environments), you can disable authentication with the ``-n`` option:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh -l -n
|
||||
|
||||
.. warning::
|
||||
Disabling authentication with ``-n`` means that **any** Reticulum peer that
|
||||
can reach your listener will be able to execute commands on your system. Only
|
||||
use this option if you *really* know what you're doing.
|
||||
|
||||
**Remote Command Control**
|
||||
|
||||
When running in listener mode, ``rnsh`` allows you to control how remote
|
||||
commands are handled:
|
||||
|
||||
- By default, the listener accepts the command sent by the initiator. If the
|
||||
initiator does not supply a command, the listener's default shell is used.
|
||||
|
||||
- Use ``-C`` (``--no-remote-command``) to disable execution of commands received
|
||||
from the initiator. Only the listener's default command (or the command
|
||||
specified after ``--``) will be executed:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh -l -C -- /usr/local/bin/safe-script
|
||||
|
||||
- Use ``-A`` (``--remote-command-as-args``) to append the initiator's command
|
||||
to the listener's default command instead of replacing it. This can be useful
|
||||
for restricting the remote to a specific program while still allowing the
|
||||
initiator to pass arguments:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh -l -A -- /usr/bin/top
|
||||
|
||||
**Service Names**
|
||||
|
||||
When running in listener mode, ``rnsh`` uses a service name to differentiate
|
||||
between multiple listener instances that may share the same identity. By
|
||||
default, the service name is ``default``. You can specify a different service
|
||||
name with the ``-s`` option:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh -l -s monitoring
|
||||
|
||||
This allows you to run multiple listeners on the same node, each with a
|
||||
different service name and purpose.
|
||||
|
||||
**Initiator Options**
|
||||
|
||||
When connecting to a remote listener, several options are available:
|
||||
|
||||
- Use ``-N`` (``--no-id``) to disable sending your identity to the remote
|
||||
listener. Note that the listener must have authentication disabled (``-n``)
|
||||
for the connection to succeed in this case.
|
||||
|
||||
- Use ``-m`` (``--mirror``) to make the initiator return with the exit code of
|
||||
the remote process, rather than always returning ``0``.
|
||||
|
||||
- Use ``-w`` (``--timeout``) to specify the connection and request timeout in
|
||||
seconds. By default, the timeout matches the Reticulum path request timeout.
|
||||
|
||||
**Identity & Destination**
|
||||
|
||||
The default identity file for ``rnsh`` is stored at
|
||||
``~/.reticulum/identities/rnsh``, but you can specify a different one with the
|
||||
``-i`` option, which will be created if it does not already exist:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh -l -i /path/to/identity
|
||||
|
||||
To display the identity and destination information for a listener, use the
|
||||
``-p`` option. When combined with ``-l``, both the identity and the listening
|
||||
destination hash are displayed:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh -p
|
||||
|
||||
Identity : <984b74a3f768bef236af4371e6f248cd>
|
||||
|
||||
$ rnsh -l -p
|
||||
|
||||
Identity : <984b74a3f768bef236af4371e6f248cd>
|
||||
Listening on : 7a55144adf826958a9529a3bcf08b149
|
||||
|
||||
**Verbosity**
|
||||
|
||||
Like other Reticulum utilities, ``rnsh`` supports the ``-v`` and ``-q`` flags
|
||||
to increase or decrease logging verbosity. Multiple flags can be specified to
|
||||
further adjust the log level. The default log level is ``INFO`` for listeners
|
||||
and ``ERROR`` for initiators.
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh -l -vv # Listener with debug-level output
|
||||
$ rnsh -q 7a55144adf826958a9529a3bcf08b149 # Quiet initiator
|
||||
|
||||
By default, all log output is routed to ``~/.rnsh/logfile`` for initiators.
|
||||
|
||||
**Escape Sequences**
|
||||
|
||||
During an active ``rnsh`` session, the following escape sequences are
|
||||
available. These are only recognised immediately after a newline character:
|
||||
|
||||
- ``~~`` - Send a literal tilde character
|
||||
- ``~.`` - Terminate the session and exit immediately
|
||||
- ``~L`` - Toggle line-interactive mode
|
||||
- ``~?`` - Display the escape sequence quick reference
|
||||
|
||||
**All Command-Line Options**
|
||||
|
||||
.. code:: text
|
||||
|
||||
usage: rnsh [-h] [--config CONFIG] [--identity IDENTITY] [-v] [-q] [-p]
|
||||
[--version] [-l] [-s SERVICE] [-b PERIOD] [-a HASH] [-n] [-A] [-C]
|
||||
[-N] [-m] [-w SECONDS]
|
||||
[destination]
|
||||
|
||||
Reticulum Remote Shell Utility
|
||||
|
||||
positional arguments:
|
||||
destination hexadecimal hash of the destination to connect to
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--config, -c CONFIG path to alternative Reticulum config directory
|
||||
--identity, -i IDENTITY
|
||||
path to identity file to use
|
||||
-v, --verbose increase verbosity
|
||||
-q, --quiet decrease verbosity
|
||||
-p, --print-identity print identity and destination info and exit
|
||||
--version show program's version number and exit
|
||||
-l, --listen listen (server) mode; any command specified after --
|
||||
will be used as the default command when the initiator
|
||||
does not provide one or when remote command execution
|
||||
is disabled; if no command is specified, the default
|
||||
shell of the user running rnsh will be used
|
||||
-s, --service SERVICE
|
||||
service name for identity file if not the default
|
||||
-b, --announce PERIOD
|
||||
announce on startup and every PERIOD seconds; specify
|
||||
0 to announce on startup only
|
||||
-a, --allowed HASH allow this identity to connect (may be specified
|
||||
multiple times); allowed identities can also be
|
||||
specified in ~/.rnsh/allowed_identities or
|
||||
~/.config/rnsh/allowed_identities, one hash per line
|
||||
-n, --no-auth disable authentication (allow any identity to connect)
|
||||
-A, --remote-command-as-args
|
||||
concatenate remote command to the argument list of the
|
||||
default program or shell
|
||||
-C, --no-remote-command
|
||||
disable executing command lines received from the
|
||||
remote initiator
|
||||
-N, --no-id disable identity announcement on connect
|
||||
-m, --mirror return with the exit code of the remote process
|
||||
-w, --timeout SECONDS
|
||||
connect and request timeout in seconds
|
||||
|
||||
When specifying a command to execute, separate rnsh options from the command
|
||||
and its arguments with --. For example:
|
||||
|
||||
rnsh -l -- /bin/bash --login
|
||||
rnsh <destination> -- ls -la /tmp
|
||||
|
||||
|
||||
The rnodeconf Utility
|
||||
=====================
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const DOCUMENTATION_OPTIONS = {
|
||||
VERSION: '1.1.5',
|
||||
VERSION: '1.2.2',
|
||||
LANGUAGE: 'en',
|
||||
COLLAPSE_INDEX: false,
|
||||
BUILDER: 'html',
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
|
||||
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
|
||||
<title>Code Examples - Reticulum Network Stack 1.1.5 documentation</title>
|
||||
<title>Code Examples - Reticulum Network Stack 1.2.2 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.5 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.2 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.5 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.2 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -222,6 +222,7 @@
|
||||
<li class="toctree-l1"><a class="reference internal" href="hardware.html">Communications Hardware</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="interfaces.html">Configuring Interfaces</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="networks.html">Building Networks</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="git.html">Using Git Over Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="support.html">Support Reticulum</a></li>
|
||||
<li class="toctree-l1 current current-page"><a class="current reference internal" href="#">Code Examples</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="license.html">Reticulum License</a></li>
|
||||
@@ -3663,7 +3664,7 @@ will be fully on-par with natively included interfaces, including all supported
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=a48ae3df"></script>
|
||||
</div><script src="_static/documentation_options.js?v=fd7cadf9"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
|
||||
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
|
||||
<title>An Explanation of Reticulum for Human Beings - Reticulum Network Stack 1.1.5 documentation</title>
|
||||
<title>An Explanation of Reticulum for Human Beings - Reticulum Network Stack 1.2.2 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.5 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.2 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.5 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.2 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -222,6 +222,7 @@
|
||||
<li class="toctree-l1"><a class="reference internal" href="hardware.html">Communications Hardware</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="interfaces.html">Configuring Interfaces</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="networks.html">Building Networks</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="git.html">Using Git Over Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="support.html">Support Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="examples.html">Code Examples</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="license.html">Reticulum License</a></li>
|
||||
@@ -294,7 +295,7 @@
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=a48ae3df"></script>
|
||||
</div><script src="_static/documentation_options.js?v=fd7cadf9"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="#"><link rel="search" title="Search" href="search.html">
|
||||
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
|
||||
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 --><title>Index - Reticulum Network Stack 1.1.5 documentation</title>
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 --><title>Index - Reticulum Network Stack 1.2.2 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -178,7 +178,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.5 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.2 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -202,7 +202,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.5 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.2 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -220,6 +220,7 @@
|
||||
<li class="toctree-l1"><a class="reference internal" href="hardware.html">Communications Hardware</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="interfaces.html">Configuring Interfaces</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="networks.html">Building Networks</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="git.html">Using Git Over Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="support.html">Support Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="examples.html">Code Examples</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="license.html">Reticulum License</a></li>
|
||||
@@ -836,7 +837,7 @@
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=a48ae3df"></script>
|
||||
</div><script src="_static/documentation_options.js?v=fd7cadf9"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
|
||||
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
|
||||
<title>Getting Started Fast - Reticulum Network Stack 1.1.5 documentation</title>
|
||||
<title>Getting Started Fast - Reticulum Network Stack 1.2.2 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.5 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.2 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.5 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.2 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -222,6 +222,7 @@
|
||||
<li class="toctree-l1"><a class="reference internal" href="hardware.html">Communications Hardware</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="interfaces.html">Configuring Interfaces</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="networks.html">Building Networks</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="git.html">Using Git Over Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="support.html">Support Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="examples.html">Code Examples</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="license.html">Reticulum License</a></li>
|
||||
@@ -966,7 +967,7 @@ All other available modules will still be loaded when needed.</p>
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=a48ae3df"></script>
|
||||
</div><script src="_static/documentation_options.js?v=fd7cadf9"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
|
||||
|
||||
@@ -0,0 +1,662 @@
|
||||
<!doctype html>
|
||||
<html class="no-js" lang="en" data-content_root="./">
|
||||
<head><meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="color-scheme" content="light dark"><meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="index" title="Index" href="genindex.html"><link rel="search" title="Search" href="search.html"><link rel="next" title="Support Reticulum" href="support.html"><link rel="prev" title="Building Networks" href="networks.html">
|
||||
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
|
||||
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
|
||||
<title>Using Git Over Reticulum - Reticulum Network Stack 1.2.2 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo-extensions.css?v=8dab3a3b" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/custom.css?v=bb3cebc5" />
|
||||
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
body {
|
||||
--color-code-background: #f2f2f2;
|
||||
--color-code-foreground: #1e1e1e;
|
||||
|
||||
}
|
||||
@media not print {
|
||||
body[data-theme="dark"] {
|
||||
--color-code-background: #202020;
|
||||
--color-code-foreground: #d0d0d0;
|
||||
--color-background-primary: #202b38;
|
||||
--color-background-secondary: #161f27;
|
||||
--color-foreground-primary: #dbdbdb;
|
||||
--color-foreground-secondary: #a9b1ba;
|
||||
--color-brand-primary: #41adff;
|
||||
--color-background-hover: #161f27;
|
||||
--color-api-name: #ffbe85;
|
||||
--color-api-pre-name: #efae75;
|
||||
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body:not([data-theme="light"]) {
|
||||
--color-code-background: #202020;
|
||||
--color-code-foreground: #d0d0d0;
|
||||
--color-background-primary: #202b38;
|
||||
--color-background-secondary: #161f27;
|
||||
--color-foreground-primary: #dbdbdb;
|
||||
--color-foreground-secondary: #a9b1ba;
|
||||
--color-brand-primary: #41adff;
|
||||
--color-background-hover: #161f27;
|
||||
--color-api-name: #ffbe85;
|
||||
--color-api-pre-name: #efae75;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
</style></head>
|
||||
<body>
|
||||
|
||||
<script>
|
||||
document.body.dataset.theme = localStorage.getItem("theme") || "auto";
|
||||
</script>
|
||||
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
|
||||
<symbol id="svg-toc" viewBox="0 0 24 24">
|
||||
<title>Contents</title>
|
||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 1024 1024">
|
||||
<path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM115.4 518.9L271.7 642c5.8 4.6 14.4.5 14.4-6.9V388.9c0-7.4-8.5-11.5-14.4-6.9L115.4 505.1a8.74 8.74 0 0 0 0 13.8z"/>
|
||||
</svg>
|
||||
</symbol>
|
||||
<symbol id="svg-menu" viewBox="0 0 24 24">
|
||||
<title>Menu</title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather-menu">
|
||||
<line x1="3" y1="12" x2="21" y2="12"></line>
|
||||
<line x1="3" y1="6" x2="21" y2="6"></line>
|
||||
<line x1="3" y1="18" x2="21" y2="18"></line>
|
||||
</svg>
|
||||
</symbol>
|
||||
<symbol id="svg-arrow-right" viewBox="0 0 24 24">
|
||||
<title>Expand</title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather-chevron-right">
|
||||
<polyline points="9 18 15 12 9 6"></polyline>
|
||||
</svg>
|
||||
</symbol>
|
||||
<symbol id="svg-sun" viewBox="0 0 24 24">
|
||||
<title>Light mode</title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="feather-sun">
|
||||
<circle cx="12" cy="12" r="5"></circle>
|
||||
<line x1="12" y1="1" x2="12" y2="3"></line>
|
||||
<line x1="12" y1="21" x2="12" y2="23"></line>
|
||||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
|
||||
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
|
||||
<line x1="1" y1="12" x2="3" y2="12"></line>
|
||||
<line x1="21" y1="12" x2="23" y2="12"></line>
|
||||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
|
||||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
|
||||
</svg>
|
||||
</symbol>
|
||||
<symbol id="svg-moon" viewBox="0 0 24 24">
|
||||
<title>Dark mode</title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-moon">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1 -8.313 -12.454z" />
|
||||
</svg>
|
||||
</symbol>
|
||||
<symbol id="svg-sun-with-moon" viewBox="0 0 24 24">
|
||||
<title>Auto light/dark, in light mode</title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="1" stroke-linecap="round" stroke-linejoin="round"
|
||||
class="icon-custom-derived-from-feather-sun-and-tabler-moon">
|
||||
<path style="opacity: 50%" d="M 5.411 14.504 C 5.471 14.504 5.532 14.504 5.591 14.504 C 3.639 16.319 4.383 19.569 6.931 20.352 C 7.693 20.586 8.512 20.551 9.25 20.252 C 8.023 23.207 4.056 23.725 2.11 21.184 C 0.166 18.642 1.702 14.949 4.874 14.536 C 5.051 14.512 5.231 14.5 5.411 14.5 L 5.411 14.504 Z"/>
|
||||
<line x1="14.5" y1="3.25" x2="14.5" y2="1.25"/>
|
||||
<line x1="14.5" y1="15.85" x2="14.5" y2="17.85"/>
|
||||
<line x1="10.044" y1="5.094" x2="8.63" y2="3.68"/>
|
||||
<line x1="19" y1="14.05" x2="20.414" y2="15.464"/>
|
||||
<line x1="8.2" y1="9.55" x2="6.2" y2="9.55"/>
|
||||
<line x1="20.8" y1="9.55" x2="22.8" y2="9.55"/>
|
||||
<line x1="10.044" y1="14.006" x2="8.63" y2="15.42"/>
|
||||
<line x1="19" y1="5.05" x2="20.414" y2="3.636"/>
|
||||
<circle cx="14.5" cy="9.55" r="3.6"/>
|
||||
</svg>
|
||||
</symbol>
|
||||
<symbol id="svg-moon-with-sun" viewBox="0 0 24 24">
|
||||
<title>Auto light/dark, in dark mode</title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="1" stroke-linecap="round" stroke-linejoin="round"
|
||||
class="icon-custom-derived-from-feather-sun-and-tabler-moon">
|
||||
<path d="M 8.282 7.007 C 8.385 7.007 8.494 7.007 8.595 7.007 C 5.18 10.184 6.481 15.869 10.942 17.24 C 12.275 17.648 13.706 17.589 15 17.066 C 12.851 22.236 5.91 23.143 2.505 18.696 C -0.897 14.249 1.791 7.786 7.342 7.063 C 7.652 7.021 7.965 7 8.282 7 L 8.282 7.007 Z"/>
|
||||
<line style="opacity: 50%" x1="18" y1="3.705" x2="18" y2="2.5"/>
|
||||
<line style="opacity: 50%" x1="18" y1="11.295" x2="18" y2="12.5"/>
|
||||
<line style="opacity: 50%" x1="15.316" y1="4.816" x2="14.464" y2="3.964"/>
|
||||
<line style="opacity: 50%" x1="20.711" y1="10.212" x2="21.563" y2="11.063"/>
|
||||
<line style="opacity: 50%" x1="14.205" y1="7.5" x2="13.001" y2="7.5"/>
|
||||
<line style="opacity: 50%" x1="21.795" y1="7.5" x2="23" y2="7.5"/>
|
||||
<line style="opacity: 50%" x1="15.316" y1="10.184" x2="14.464" y2="11.036"/>
|
||||
<line style="opacity: 50%" x1="20.711" y1="4.789" x2="21.563" y2="3.937"/>
|
||||
<circle style="opacity: 50%" cx="18" cy="7.5" r="2.169"/>
|
||||
</svg>
|
||||
</symbol>
|
||||
<symbol id="svg-pencil" viewBox="0 0 24 24">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-pencil-code">
|
||||
<path d="M4 20h4l10.5 -10.5a2.828 2.828 0 1 0 -4 -4l-10.5 10.5v4" />
|
||||
<path d="M13.5 6.5l4 4" />
|
||||
<path d="M20 21l2 -2l-2 -2" />
|
||||
<path d="M17 17l-2 2l2 2" />
|
||||
</svg>
|
||||
</symbol>
|
||||
<symbol id="svg-eye" viewBox="0 0 24 24">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-eye-code">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
|
||||
<path
|
||||
d="M11.11 17.958c-3.209 -.307 -5.91 -2.293 -8.11 -5.958c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6c-.21 .352 -.427 .688 -.647 1.008" />
|
||||
<path d="M20 21l2 -2l-2 -2" />
|
||||
<path d="M17 17l-2 2l2 2" />
|
||||
</svg>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
||||
<input type="checkbox" class="sidebar-toggle" name="__navigation" id="__navigation" aria-label="Toggle site navigation sidebar">
|
||||
<input type="checkbox" class="sidebar-toggle" name="__toc" id="__toc" aria-label="Toggle table of contents sidebar">
|
||||
<label class="overlay sidebar-overlay" for="__navigation"></label>
|
||||
<label class="overlay toc-overlay" for="__toc"></label>
|
||||
|
||||
<a class="skip-to-content muted-link" href="#furo-main-content">Skip to content</a>
|
||||
|
||||
|
||||
|
||||
<div class="page">
|
||||
<header class="mobile-header">
|
||||
<div class="header-left">
|
||||
<label class="nav-overlay-icon" for="__navigation">
|
||||
<span class="icon"><svg><use href="#svg-menu"></use></svg></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.2 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
<button class="theme-toggle" aria-label="Toggle Light / Dark / Auto color theme">
|
||||
<svg class="theme-icon-when-auto-light"><use href="#svg-sun-with-moon"></use></svg>
|
||||
<svg class="theme-icon-when-auto-dark"><use href="#svg-moon-with-sun"></use></svg>
|
||||
<svg class="theme-icon-when-dark"><use href="#svg-moon"></use></svg>
|
||||
<svg class="theme-icon-when-light"><use href="#svg-sun"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
<label class="toc-overlay-icon toc-header-icon" for="__toc">
|
||||
<span class="icon"><svg><use href="#svg-toc"></use></svg></span>
|
||||
</label>
|
||||
</div>
|
||||
</header>
|
||||
<aside class="sidebar-drawer">
|
||||
<div class="sidebar-container">
|
||||
|
||||
<div class="sidebar-sticky"><a class="sidebar-brand" href="index.html">
|
||||
<div class="sidebar-logo-container">
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.2 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
<input type="hidden" name="check_keywords" value="yes">
|
||||
<input type="hidden" name="area" value="default">
|
||||
</form>
|
||||
<div id="searchbox"></div><div class="sidebar-scroll"><div class="sidebar-tree">
|
||||
<ul class="current">
|
||||
<li class="toctree-l1"><a class="reference internal" href="whatis.html">What is Reticulum?</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="gettingstartedfast.html">Getting Started Fast</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="zen.html">Zen of Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="software.html">Programs Using Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="using.html">Using Reticulum on Your System</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="understanding.html">Understanding Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="hardware.html">Communications Hardware</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="interfaces.html">Configuring Interfaces</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="networks.html">Building Networks</a></li>
|
||||
<li class="toctree-l1 current current-page"><a class="current reference internal" href="#">Using Git Over Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="support.html">Support Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="examples.html">Code Examples</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="license.html">Reticulum License</a></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li class="toctree-l1"><a class="reference internal" href="reference.html">API Reference</a></li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</aside>
|
||||
<div class="main">
|
||||
<div class="content">
|
||||
<div class="article-container">
|
||||
<a href="#" class="back-to-top muted-link">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path d="M13 20h-2V8l-5.5 5.5-1.42-1.42L12 4.16l7.92 7.92-1.42 1.42L13 8v12z"></path>
|
||||
</svg>
|
||||
<span>Back to top</span>
|
||||
</a>
|
||||
<div class="content-icon-container">
|
||||
<div class="theme-toggle-container theme-toggle-content">
|
||||
<button class="theme-toggle" aria-label="Toggle Light / Dark / Auto color theme">
|
||||
<svg class="theme-icon-when-auto-light"><use href="#svg-sun-with-moon"></use></svg>
|
||||
<svg class="theme-icon-when-auto-dark"><use href="#svg-moon-with-sun"></use></svg>
|
||||
<svg class="theme-icon-when-dark"><use href="#svg-moon"></use></svg>
|
||||
<svg class="theme-icon-when-light"><use href="#svg-sun"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
<label class="toc-overlay-icon toc-content-icon" for="__toc">
|
||||
<span class="icon"><svg><use href="#svg-toc"></use></svg></span>
|
||||
</label>
|
||||
</div>
|
||||
<article role="main" id="furo-main-content">
|
||||
<section id="using-git-over-reticulum">
|
||||
<span id="git-main"></span><h1>Using Git Over Reticulum<a class="headerlink" href="#using-git-over-reticulum" title="Link to this heading">¶</a></h1>
|
||||
<p>A set of utilities for distributed collaborative software development and publishing is included in RNS.</p>
|
||||
<p>The system consists of two parts: The <code class="docutils literal notranslate"><span class="pre">rngit</span></code> node that hosts repositories, and the <code class="docutils literal notranslate"><span class="pre">git-remote-rns</span></code> helper that enables Git to communicate with rngit nodes. As soon as you have RNS installed on your system, you can transparently use Git with Reticulum-hosted repositories just like any other type of remote. Git over Reticulum uses URLs in the following format: <code class="docutils literal notranslate"><span class="pre">rns://DESTINATION_HASH/group/repo</span></code>.</p>
|
||||
<p>If you set a branch to track a Reticulum remote as the default upstream, you can simply use <code class="docutils literal notranslate"><span class="pre">git</span></code> as you normally would; all commands work transparently and as expected.</p>
|
||||
<div class="admonition warning">
|
||||
<p class="admonition-title">Warning</p>
|
||||
<p><strong>The rngit program is a new addition to RNS!</strong> This functionality was introduced in RNS 1.2.0. While great care has been taken to design a secure, but highly configurable and flexible permission system for allowing many users to interact with many different repositories on a single node, <code class="docutils literal notranslate"><span class="pre">rngit</span></code> has not been tested extensively in the wild! Be careful when hosting repositories, especially if they are public or semi-public.</p>
|
||||
</div>
|
||||
<section id="the-rngit-utility">
|
||||
<h2>The rngit Utility<a class="headerlink" href="#the-rngit-utility" title="Link to this heading">¶</a></h2>
|
||||
<p>The <code class="docutils literal notranslate"><span class="pre">rngit</span></code> utility provides full Git repository hosting and interaction over Reticulum. It allows you to host and manage Git repositories and releases on Reticulum nodes, and to interact with remote repositories using standard Git commands through the <code class="docutils literal notranslate"><span class="pre">rns://</span></code> URL scheme.</p>
|
||||
<p><strong>Usage Examples</strong></p>
|
||||
<p>Run <code class="docutils literal notranslate"><span class="pre">rngit</span></code> to start a repository node:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rngit
|
||||
|
||||
[Notice] Starting Reticulum Git Node...
|
||||
[Notice] Reticulum Git Node listening on <0d7334d411d00120cbad24edf355fdd2>
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>On the first run, <code class="docutils literal notranslate"><span class="pre">rngit</span></code> will create a default configuration file. You will then need to edit this, to point to your repository locations, configure access permissions, and perform any other necessary configuration.</p>
|
||||
<p>View your identity and destination hashes:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rngit --print-identity
|
||||
|
||||
Git Peer Identity : <959e10e5efc1bd9d97a4083babe51dea>
|
||||
Repository Node Identity : <153cb870b4665b8c1c348896292b0bad>
|
||||
Repositories Destination : <0d7334d411d00120cbad24edf355fdd2>
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>If the page server is enabled, the output will also include the Nomad Network destination hash.</p>
|
||||
<p>You can run <code class="docutils literal notranslate"><span class="pre">rngit</span></code> in service mode with logging to file:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rngit -s
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>Clone a repository from a remote <code class="docutils literal notranslate"><span class="pre">rngit</span></code> node:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ git clone rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>Add a Reticulum remote to an existing repository:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ git remote add some_remote rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>Push changes to the Reticulum remote:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ git push some_remote master
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>Get changes from a remote repository:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ git pull rns_remote master
|
||||
</pre></div>
|
||||
</div>
|
||||
<p><strong>All Command-Line Options (rngit)</strong></p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>usage: rngit.py [-h] [--config CONFIG] [--rnsconfig RNSCONFIG] [-s] [-i] [-v]
|
||||
[-q] [--version]
|
||||
|
||||
Reticulum Git Repository Node
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--config CONFIG path to alternative config directory
|
||||
--rnsconfig RNSCONFIG
|
||||
path to alternative Reticulum config directory
|
||||
-p, --print-identity print identity and destination info and exit
|
||||
-s, --service rngit is running as a service and should log to file
|
||||
-i, --interactive drop into interactive shell after initialisation
|
||||
-v, --verbose increase verbosity
|
||||
-q, --quiet decrease verbosity
|
||||
--version show program's version number and exit
|
||||
</pre></div>
|
||||
</div>
|
||||
<p><strong>All Command-Line Options (git-remote-rns)</strong></p>
|
||||
<p>The <code class="docutils literal notranslate"><span class="pre">git-remote-rns</span></code> helper is automatically invoked by Git when interacting with <code class="docutils literal notranslate"><span class="pre">rns://</span></code> URLs. It is not typically run directly by users, but accepts the following environment variables for configuration:</p>
|
||||
<ul class="simple">
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">RNGIT_CONFIG</span></code> - Path to alternative client configuration directory</p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">RNS_CONFIG</span></code> - Path to alternative Reticulum configuration directory</p></li>
|
||||
</ul>
|
||||
<p>The client configuration file is located at <code class="docutils literal notranslate"><span class="pre">~/.rngit/client_config</span></code> and allows adjusting parameters such as the reference batch size for transfers.</p>
|
||||
</section>
|
||||
<section id="repository-structure">
|
||||
<h2>Repository Structure<a class="headerlink" href="#repository-structure" title="Link to this heading">¶</a></h2>
|
||||
<p>The <code class="docutils literal notranslate"><span class="pre">rngit</span></code> node organizes repositories into groups. Each group is a directory containing bare Git repositories. The repository path format is <code class="docutils literal notranslate"><span class="pre">group_name/repo_name</span></code>. For example, a repository at <code class="docutils literal notranslate"><span class="pre">/var/git/public/myrepo</span></code> would be accessible as <code class="docutils literal notranslate"><span class="pre">public/myrepo</span></code> via the URL <code class="docutils literal notranslate"><span class="pre">rns://DESTINATION_HASH/public/myrepo</span></code>.</p>
|
||||
<p><strong>Configuration</strong></p>
|
||||
<p>The <code class="docutils literal notranslate"><span class="pre">rngit</span></code> node configuration file is located at <code class="docutils literal notranslate"><span class="pre">~/.rngit/config</span></code> (or <code class="docutils literal notranslate"><span class="pre">/etc/rngit/config</span></code> for system-wide installations). The default configuration includes:</p>
|
||||
<ul class="simple">
|
||||
<li><p>Repository group paths defining where to find bare repositories</p></li>
|
||||
<li><p>Access permissions for groups and individual repositories</p></li>
|
||||
<li><p>Announce intervals for network visibility</p></li>
|
||||
<li><p>Optional statistics recording for repository activity</p></li>
|
||||
</ul>
|
||||
<p>Access permissions can be configured at the group level in the config file, or per-repository using <code class="docutils literal notranslate"><span class="pre">.allowed</span></code> files. Permissions use the format <code class="docutils literal notranslate"><span class="pre">permission:target</span></code> where permission is <code class="docutils literal notranslate"><span class="pre">r</span></code> (read), <code class="docutils literal notranslate"><span class="pre">w</span></code> (write), <code class="docutils literal notranslate"><span class="pre">rw</span></code> (read/write), <code class="docutils literal notranslate"><span class="pre">c</span></code> (create) or <code class="docutils literal notranslate"><span class="pre">s</span></code> (stats) and target is <code class="docutils literal notranslate"><span class="pre">all</span></code>, <code class="docutils literal notranslate"><span class="pre">none</span></code>, or a specific identity hash.</p>
|
||||
<p>The <code class="docutils literal notranslate"><span class="pre">s</span></code> (stats) permission allows viewing repository activity statistics, including views, fetches and pushes over time. To enable statistics recording, set <code class="docutils literal notranslate"><span class="pre">record_stats</span> <span class="pre">=</span> <span class="pre">yes</span></code> in the <code class="docutils literal notranslate"><span class="pre">[rngit]</span></code> section of the configuration file. You can also exclude specific identities from statistics by adding their hashes to <code class="docutils literal notranslate"><span class="pre">stats_ignore_identities</span></code>.</p>
|
||||
<p>Repository-specific <code class="docutils literal notranslate"><span class="pre">.allowed</span></code> files can be static text files or executable scripts that output permission rules to stdout. A <code class="docutils literal notranslate"><span class="pre">group.allowed</span></code> file in a repository group directory applies to all repositories within that group.</p>
|
||||
</section>
|
||||
<section id="serving-pages-over-nomad-network">
|
||||
<h2>Serving Pages Over Nomad Network<a class="headerlink" href="#serving-pages-over-nomad-network" title="Link to this heading">¶</a></h2>
|
||||
<p>In addition to providing Git repository access via the Git remote helper protocol, <code class="docutils literal notranslate"><span class="pre">rngit</span></code> can also run a <a class="reference external" href="https://github.com/markqvist/nomadnet">Nomad Network</a> compatible page node. This allows users to browse repository information, view file contents, inspect commit history and access repository statistics through any Nomad Network client.</p>
|
||||
<p>When enabled, the page node provides a complete interface to your repositories, with automatic Markdown to Micron conversion, syntax-highlighted code browsing, and detailed commit, diff and statistics views.</p>
|
||||
<p><strong>Enabling the Git Page Node</strong></p>
|
||||
<p>To enable the page node, add the following to your <code class="docutils literal notranslate"><span class="pre">~/.rngit/config</span></code> file:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>[pages]
|
||||
serve_nomadnet = yes
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>When the page node is enabled, <code class="docutils literal notranslate"><span class="pre">rngit</span></code> will listen on a Nomad Network node destination in addition to the Git repository destination. You can view the destination hash by running:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rngit --print-identity
|
||||
|
||||
Git Peer Identity : <959e10e5efc1bd9d97a4083babe51dea>
|
||||
Repository Node Identity : <153cb870b4665b8c1c348896292b0bad>
|
||||
Repositories Destination : <0d7334d411d00120cbad24edf355fdd2>
|
||||
Nomad Network Destination : <50824b711717f97c2fb1166ceddd5ea9>
|
||||
</pre></div>
|
||||
</div>
|
||||
<p><strong>Accessing Repository Pages</strong></p>
|
||||
<p>Once the page server is running, you can access it from any Nomad Network client by connecting to the Nomad Network destination. The page node provides the following views:</p>
|
||||
<ul class="simple">
|
||||
<li><p><strong>Front Page</strong> - Lists all repository groups accessible to your identity</p></li>
|
||||
<li><p><strong>Group Page</strong> - Shows all repositories within a group</p></li>
|
||||
<li><p><strong>Repository Page</strong> - Displays repository overview, description and README</p></li>
|
||||
<li><p><strong>Releases</strong> - List of releases for the repository, with information and downloads</p></li>
|
||||
<li><p><strong>File Browser</strong> - Browse directory trees and view and download file contents</p></li>
|
||||
<li><p><strong>Commits View</strong> - View commit history with pagination</p></li>
|
||||
<li><p><strong>Commit Details</strong> - Detailed commit information with file changes and diffs</p></li>
|
||||
<li><p><strong>Refs View</strong> - List branches and tags</p></li>
|
||||
<li><p><strong>Statistics</strong> - Activity charts showing views, fetches and pushes over time</p></li>
|
||||
</ul>
|
||||
<p>All pages respect the same permission system used for Git access. If an identity does not have read access to a repository, they will not be able to view its pages.</p>
|
||||
</section>
|
||||
<section id="formatting-syntax-highlighting">
|
||||
<h2>Formatting & Syntax Highlighting<a class="headerlink" href="#formatting-syntax-highlighting" title="Link to this heading">¶</a></h2>
|
||||
<p>If the <code class="docutils literal notranslate"><span class="pre">pygments</span></code> Python module is installed on your system, the page server will automatically apply syntax highlighting to code files. The highlighting supports a wide range of programming languages and uses a color theme optimized for terminal display.</p>
|
||||
<p>To enable syntax highlighting, install pygments:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>pip install pygments
|
||||
</pre></div>
|
||||
</div>
|
||||
<p><strong>Markdown & Micron Support</strong></p>
|
||||
<p>README files and other Markdown documents are automatically converted to Micron markup for display in Nomad Network clients. You can also write your README files directly in Micron, in which case they will display and render as such in any Nomad Network client. The file browser also supports viewing both rendered and raw Markdown and Micron documents.</p>
|
||||
<p>Code blocks in Markdown can include language hints for syntax highlighting:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>```python
|
||||
def hello_world():
|
||||
print("Hello, Reticulum!")
|
||||
```
|
||||
</pre></div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="customizing-templates">
|
||||
<h2>Customizing Templates<a class="headerlink" href="#customizing-templates" title="Link to this heading">¶</a></h2>
|
||||
<p>The page server uses a template system that allows complete customization of the generated pages. Templates are stored in the <code class="docutils literal notranslate"><span class="pre">~/.rngit/templates/</span></code> directory as Micron files.</p>
|
||||
<p>The following template files are supported:</p>
|
||||
<ul class="simple">
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">base.mu</span></code> - Base template wrapping all pages</p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">front.mu</span></code> - Front page listing all groups</p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">group.mu</span></code> - Group page listing repositories</p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">repo.mu</span></code> - Repository overview page</p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">releases.mu</span></code> - Release list page</p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">release.mu</span></code> - Release details page</p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">tree.mu</span></code> - File browser pages</p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">blob.mu</span></code> - File content display</p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">commits.mu</span></code> - Commit history listing</p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">commit.mu</span></code> - Individual commit detail page</p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">refs.mu</span></code> - Branches and tags listing</p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">stats.mu</span></code> - Statistics page</p></li>
|
||||
</ul>
|
||||
<p>Templates can include the following variables:</p>
|
||||
<ul class="simple">
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">{PAGE_CONTENT}</span></code> - The main content of the page (required)</p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">{NODE_NAME}</span></code> - The configured node name</p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">{NAVIGATION}</span></code> - Breadcrumb navigation links</p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">{VERSION}</span></code> - The rngit version number</p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">{GEN_TIME}</span></code> - Page generation time</p></li>
|
||||
</ul>
|
||||
<p><strong>Dynamic Templates</strong></p>
|
||||
<p>Templates can be made executable to generate dynamic content. If a template file has the executable bit set, it will be executed and its stdout used as the template content.</p>
|
||||
<p><strong>Icon Sets</strong></p>
|
||||
<p>By default, the page server uses Nerd Font icons. If you prefer simpler icons or your terminal does not support Nerd Fonts, you can enable Unicode icons instead:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>[pages]
|
||||
serve_nomadnet = yes
|
||||
unicode_icons = yes
|
||||
</pre></div>
|
||||
</div>
|
||||
<p><strong>Repository Statistics</strong></p>
|
||||
<p>When statistics recording is enabled (see the <code class="docutils literal notranslate"><span class="pre">record_stats</span></code> configuration option), the page server can display activity charts for each repository. The statistics page shows:</p>
|
||||
<ul class="simple">
|
||||
<li><p>Total and peak views, fetches and pushes</p></li>
|
||||
<li><p>Daily activity charts over a 90-day period</p></li>
|
||||
<li><p>Combined activity visualization</p></li>
|
||||
</ul>
|
||||
<p>To view statistics, a user must have the <code class="docutils literal notranslate"><span class="pre">s</span></code> (stats) permission for the repository. See the Access Configuration section for details on setting permissions.</p>
|
||||
<p><strong>Repository Thanks</strong></p>
|
||||
<p>The page server includes a “Thanks” feature that allows users to express appreciation for a repository. On each repository page, a “Thanks” link is displayed showing the current thanks count. Clicking this link registers a thank you for the repository.</p>
|
||||
<p><strong>Configuration Example</strong></p>
|
||||
<p>A complete page server configuration might look like this:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>[rngit]
|
||||
node_name = My Git Server
|
||||
announce_interval = 360
|
||||
record_stats = yes
|
||||
|
||||
[repositories]
|
||||
public = /var/git/public
|
||||
internal = /var/git/internal
|
||||
|
||||
[access]
|
||||
public = r:all
|
||||
internal = rw:9710b86ba12c42d1d8f30f74fe509286
|
||||
|
||||
[pages]
|
||||
serve_nomadnet = yes
|
||||
unicode_icons = no
|
||||
</pre></div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="release-management">
|
||||
<h2>Release Management<a class="headerlink" href="#release-management" title="Link to this heading">¶</a></h2>
|
||||
<p>In addition to hosting Git repositories, <code class="docutils literal notranslate"><span class="pre">rngit</span></code> provides a complete release management system. This allows you to publish versioned releases with associated artifacts, release notes and metadata. Releases are managed through the <code class="docutils literal notranslate"><span class="pre">rngit</span> <span class="pre">release</span></code> subcommand, and are also viewable through the Nomad Network page interface.</p>
|
||||
<p><strong>The Release Workflow</strong></p>
|
||||
<p>Creating a release involves specifying a Git tag and a directory containing build artifacts or other files to distribute. The <code class="docutils literal notranslate"><span class="pre">rngit</span></code> client will open your configured <code class="docutils literal notranslate"><span class="pre">$EDITOR</span></code> to compose release notes, then upload all artifacts to the remote repository node.</p>
|
||||
<p>To create a release, specify the tag name and path to artifacts:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rngit release create rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo v1.2.0:./dist
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>This will:</p>
|
||||
<ol class="arabic simple">
|
||||
<li><p>Verify that the tag <code class="docutils literal notranslate"><span class="pre">v1.2.0</span></code> exists in the repository</p></li>
|
||||
<li><p>Open your editor to write release notes</p></li>
|
||||
<li><p>Upload all files from the <code class="docutils literal notranslate"><span class="pre">./dist</span></code> directory</p></li>
|
||||
<li><p>Publish the release</p></li>
|
||||
</ol>
|
||||
<p>If no <code class="docutils literal notranslate"><span class="pre">$EDITOR</span></code> environment variable is set, <code class="docutils literal notranslate"><span class="pre">rngit</span></code> will try to use <code class="docutils literal notranslate"><span class="pre">nano</span></code>, <code class="docutils literal notranslate"><span class="pre">vim</span></code> or <code class="docutils literal notranslate"><span class="pre">vi</span></code>. The editor will show a template with instructions. Lines starting with <code class="docutils literal notranslate"><span class="pre">#</span></code> will be ignored, and if the remaining content is empty after stripping comments, the release creation will be cancelled.</p>
|
||||
<p><strong>Release Storage & Structure</strong></p>
|
||||
<p>Releases are stored on the server in a directory named <code class="docutils literal notranslate"><span class="pre">repo_name.releases</span></code> next to the bare repository. Each release is a subdirectory containing:</p>
|
||||
<ul class="simple">
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">META</span></code> - Release metadata in ConfigObj format</p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">RELEASE.md</span></code> or <code class="docutils literal notranslate"><span class="pre">RELEASE.mu</span></code> - Release notes</p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">artifacts/</span></code> - All uploaded files</p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">THANKS</span></code> - Appreciation count from users</p></li>
|
||||
</ul>
|
||||
<p><strong>Listing Releases</strong></p>
|
||||
<p>To view all releases for a repository:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rngit release list rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo
|
||||
|
||||
Tag Status Created Objs Notes
|
||||
------------------------------------------------------------------
|
||||
v1.2.0 published 2025-01-15 14:32 3 Another release
|
||||
v1.1.0 published 2024-12-03 09:15 2 Bug fix release
|
||||
v1.0.0 published 2024-10-20 16:45 2 Initial release
|
||||
</pre></div>
|
||||
</div>
|
||||
<p><strong>Viewing Release Details</strong></p>
|
||||
<p>To see full information about a specific release:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rngit release view rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo v1.2.0
|
||||
|
||||
Release : 0.9.2
|
||||
Status : published
|
||||
Created : 2026-05-04 23:53:09
|
||||
Thanks : 5
|
||||
|
||||
Release Notes
|
||||
=============
|
||||
|
||||
Version 1.2.0 release notes...
|
||||
|
||||
Artifacts (4)
|
||||
=============
|
||||
- myapp-1.2.0.tar.gz (1.5 MB)
|
||||
- myapp-1.2.0.zip (1.6 MB)
|
||||
- checksums.txt (256 B)
|
||||
</pre></div>
|
||||
</div>
|
||||
<p><strong>Deleting Releases</strong></p>
|
||||
<p>To remove a release:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rngit release delete rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo v1.2.0
|
||||
|
||||
Are you sure you want to delete release 'v1.2.0'? [y/N]: y
|
||||
Release v1.2.0 deleted
|
||||
</pre></div>
|
||||
</div>
|
||||
<p><strong>Requirements & Validation</strong></p>
|
||||
<ul class="simple">
|
||||
<li><p>The specified tag must exist in the remote repository</p></li>
|
||||
<li><p>You must have <code class="docutils literal notranslate"><span class="pre">release</span></code> permission for the repository</p></li>
|
||||
<li><p>The target artifacts directory must exist and contain at least one file</p></li>
|
||||
<li><p>Release notes cannot be empty</p></li>
|
||||
</ul>
|
||||
<p><strong>Permissions</strong></p>
|
||||
<p>Release management requires the <code class="docutils literal notranslate"><span class="pre">release</span></code> permission, configured the same way as other repository permissions. In the config file or <code class="docutils literal notranslate"><span class="pre">.allowed</span></code> files, use <code class="docutils literal notranslate"><span class="pre">rel:target</span></code> to grant release management rights:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span># In .allowed file or config
|
||||
rel:all # Allow everyone
|
||||
rel:9710b86... # Allow specific identity
|
||||
rel:none # Deny everyone
|
||||
</pre></div>
|
||||
</div>
|
||||
<p><strong>Nomad Network Interface</strong></p>
|
||||
<p>When the Nomad Network page server is enabled, releases are displayed on a dedicated releases page for each repository. Each release is listed with its tag, creation date, artifact count and a preview of the release notes. Clicking a release shows the full details including formatted release notes and a listing of all artifacts with their sizes.</p>
|
||||
<p>Only releases with <code class="docutils literal notranslate"><span class="pre">published</span></code> status are visible through the Nomad Network interface. Draft releases (if supported in future implementations) would only be visible through the command-line interface.</p>
|
||||
<p><strong>All Command-Line Options (rngit release)</strong></p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>usage: rngit release [-h] [--config CONFIG] [--rnsconfig RNSCONFIG]
|
||||
[-i IDENTITY] [-v] [-q] [--version]
|
||||
operation repository [target]
|
||||
|
||||
Reticulum Git Release Manager
|
||||
|
||||
positional arguments:
|
||||
operation list, view, create or delete
|
||||
repository URL of remote repository (rns://hash/group/repo)
|
||||
target tag or tag:path for create, tag for view/delete
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--config CONFIG path to alternative config directory
|
||||
--rnsconfig RNSCONFIG
|
||||
path to alternative Reticulum config directory
|
||||
-i IDENTITY, --identity IDENTITY
|
||||
path to release identity
|
||||
-v, --verbose increase verbosity
|
||||
-q, --quiet decrease verbosity
|
||||
--version show program's version number and exit
|
||||
</pre></div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
</article>
|
||||
</div>
|
||||
<footer>
|
||||
|
||||
<div class="related-pages">
|
||||
<a class="next-page" href="support.html">
|
||||
<div class="page-info">
|
||||
<div class="context">
|
||||
<span>Next</span>
|
||||
</div>
|
||||
<div class="title">Support Reticulum</div>
|
||||
</div>
|
||||
<svg class="furo-related-icon"><use href="#svg-arrow-right"></use></svg>
|
||||
</a>
|
||||
<a class="prev-page" href="networks.html">
|
||||
<svg class="furo-related-icon"><use href="#svg-arrow-right"></use></svg>
|
||||
<div class="page-info">
|
||||
<div class="context">
|
||||
<span>Previous</span>
|
||||
</div>
|
||||
|
||||
<div class="title">Building Networks</div>
|
||||
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="bottom-of-page">
|
||||
<div class="left-details">
|
||||
<div class="copyright">
|
||||
Copyright © 2025, Mark Qvist
|
||||
</div>
|
||||
Generated with <a href="https://www.sphinx-doc.org/">Sphinx</a> and
|
||||
<a href="https://github.com/pradyunsg/furo">Furo</a>
|
||||
|
||||
</div>
|
||||
<div class="right-details">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</footer>
|
||||
</div>
|
||||
<aside class="toc-drawer">
|
||||
|
||||
|
||||
<div class="toc-sticky toc-scroll">
|
||||
<div class="toc-title-container">
|
||||
<span class="toc-title">
|
||||
On this page
|
||||
</span>
|
||||
</div>
|
||||
<div class="toc-tree-container">
|
||||
<div class="toc-tree">
|
||||
<ul>
|
||||
<li><a class="reference internal" href="#">Using Git Over Reticulum</a><ul>
|
||||
<li><a class="reference internal" href="#the-rngit-utility">The rngit Utility</a></li>
|
||||
<li><a class="reference internal" href="#repository-structure">Repository Structure</a></li>
|
||||
<li><a class="reference internal" href="#serving-pages-over-nomad-network">Serving Pages Over Nomad Network</a></li>
|
||||
<li><a class="reference internal" href="#formatting-syntax-highlighting">Formatting & Syntax Highlighting</a></li>
|
||||
<li><a class="reference internal" href="#customizing-templates">Customizing Templates</a></li>
|
||||
<li><a class="reference internal" href="#release-management">Release Management</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=fd7cadf9"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
|
||||
<script src="_static/clipboard.min.js?v=a7894cd8"></script>
|
||||
<script src="_static/copybutton.js?v=f281be69"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -7,7 +7,7 @@
|
||||
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
|
||||
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
|
||||
<title>Communications Hardware - Reticulum Network Stack 1.1.5 documentation</title>
|
||||
<title>Communications Hardware - Reticulum Network Stack 1.2.2 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.5 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.2 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.5 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.2 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -222,6 +222,7 @@
|
||||
<li class="toctree-l1 current current-page"><a class="current reference internal" href="#">Communications Hardware</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="interfaces.html">Configuring Interfaces</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="networks.html">Building Networks</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="git.html">Using Git Over Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="support.html">Support Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="examples.html">Code Examples</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="license.html">Reticulum License</a></li>
|
||||
@@ -674,7 +675,7 @@ can be used with Reticulum. This includes virtual software modems such as
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=a48ae3df"></script>
|
||||
</div><script src="_static/documentation_options.js?v=fd7cadf9"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
|
||||
|
||||
+16
-4
@@ -7,7 +7,7 @@
|
||||
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
|
||||
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
|
||||
<title>Reticulum Network Stack 1.1.5 documentation</title>
|
||||
<title>Reticulum Network Stack 1.2.2 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="#"><div class="brand">Reticulum Network Stack 1.1.5 documentation</div></a>
|
||||
<a href="#"><div class="brand">Reticulum Network Stack 1.2.2 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.5 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.2 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -222,6 +222,7 @@
|
||||
<li class="toctree-l1"><a class="reference internal" href="hardware.html">Communications Hardware</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="interfaces.html">Configuring Interfaces</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="networks.html">Building Networks</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="git.html">Using Git Over Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="support.html">Support Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="examples.html">Code Examples</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="license.html">Reticulum License</a></li>
|
||||
@@ -409,7 +410,9 @@ to participate in the development of Reticulum itself.</p>
|
||||
<li class="toctree-l3"><a class="reference internal" href="using.html#the-rnpath-utility">The rnpath Utility</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="using.html#the-rnprobe-utility">The rnprobe Utility</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="using.html#the-rncp-utility">The rncp Utility</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="using.html#the-rngit-utility">The rngit Utility</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="using.html#the-rnx-utility">The rnx Utility</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="using.html#the-rnsh-utility">The rnsh Utility</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="using.html#the-rnodeconf-utility">The rnodeconf Utility</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
@@ -521,6 +524,15 @@ to participate in the development of Reticulum itself.</p>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="git.html">Using Git Over Reticulum</a><ul>
|
||||
<li class="toctree-l2"><a class="reference internal" href="git.html#the-rngit-utility">The rngit Utility</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="git.html#repository-structure">Repository Structure</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="git.html#serving-pages-over-nomad-network">Serving Pages Over Nomad Network</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="git.html#formatting-syntax-highlighting">Formatting & Syntax Highlighting</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="git.html#customizing-templates">Customizing Templates</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="git.html#release-management">Release Management</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="support.html">Support Reticulum</a><ul>
|
||||
<li class="toctree-l2"><a class="reference internal" href="support.html#donations">Donations</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="support.html#provide-feedback">Provide Feedback</a></li>
|
||||
@@ -631,7 +643,7 @@ to participate in the development of Reticulum itself.</p>
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=a48ae3df"></script>
|
||||
</div><script src="_static/documentation_options.js?v=fd7cadf9"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
|
||||
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
|
||||
<title>Configuring Interfaces - Reticulum Network Stack 1.1.5 documentation</title>
|
||||
<title>Configuring Interfaces - Reticulum Network Stack 1.2.2 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.5 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.2 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.5 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.2 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -222,6 +222,7 @@
|
||||
<li class="toctree-l1"><a class="reference internal" href="hardware.html">Communications Hardware</a></li>
|
||||
<li class="toctree-l1 current current-page"><a class="current reference internal" href="#">Configuring Interfaces</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="networks.html">Building Networks</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="git.html">Using Git Over Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="support.html">Support Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="examples.html">Code Examples</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="license.html">Reticulum License</a></li>
|
||||
@@ -1684,7 +1685,7 @@ to <code class="docutils literal notranslate"><span class="pre">30</span></code>
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=a48ae3df"></script>
|
||||
</div><script src="_static/documentation_options.js?v=fd7cadf9"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
|
||||
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
|
||||
<title>Reticulum License - Reticulum Network Stack 1.1.5 documentation</title>
|
||||
<title>Reticulum License - Reticulum Network Stack 1.2.2 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.5 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.2 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.5 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.2 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -222,6 +222,7 @@
|
||||
<li class="toctree-l1"><a class="reference internal" href="hardware.html">Communications Hardware</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="interfaces.html">Configuring Interfaces</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="networks.html">Building Networks</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="git.html">Using Git Over Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="support.html">Support Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="examples.html">Code Examples</a></li>
|
||||
<li class="toctree-l1 current current-page"><a class="current reference internal" href="#">Reticulum License</a></li>
|
||||
@@ -343,7 +344,7 @@ SOFTWARE.
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=a48ae3df"></script>
|
||||
</div><script src="_static/documentation_options.js?v=fd7cadf9"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
<head><meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="color-scheme" content="light dark"><meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="index" title="Index" href="genindex.html"><link rel="search" title="Search" href="search.html"><link rel="next" title="Support Reticulum" href="support.html"><link rel="prev" title="Configuring Interfaces" href="interfaces.html">
|
||||
<link rel="index" title="Index" href="genindex.html"><link rel="search" title="Search" href="search.html"><link rel="next" title="Using Git Over Reticulum" href="git.html"><link rel="prev" title="Configuring Interfaces" href="interfaces.html">
|
||||
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
|
||||
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
|
||||
<title>Building Networks - Reticulum Network Stack 1.1.5 documentation</title>
|
||||
<title>Building Networks - Reticulum Network Stack 1.2.2 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.5 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.2 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.5 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.2 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -222,6 +222,7 @@
|
||||
<li class="toctree-l1"><a class="reference internal" href="hardware.html">Communications Hardware</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="interfaces.html">Configuring Interfaces</a></li>
|
||||
<li class="toctree-l1 current current-page"><a class="current reference internal" href="#">Building Networks</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="git.html">Using Git Over Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="support.html">Support Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="examples.html">Code Examples</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="license.html">Reticulum License</a></li>
|
||||
@@ -593,12 +594,12 @@ differently than a mobile device roaming between radio cells.</p>
|
||||
<footer>
|
||||
|
||||
<div class="related-pages">
|
||||
<a class="next-page" href="support.html">
|
||||
<a class="next-page" href="git.html">
|
||||
<div class="page-info">
|
||||
<div class="context">
|
||||
<span>Next</span>
|
||||
</div>
|
||||
<div class="title">Support Reticulum</div>
|
||||
<div class="title">Using Git Over Reticulum</div>
|
||||
</div>
|
||||
<svg class="furo-related-icon"><use href="#svg-arrow-right"></use></svg>
|
||||
</a>
|
||||
@@ -662,7 +663,7 @@ differently than a mobile device roaming between radio cells.</p>
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=a48ae3df"></script>
|
||||
</div><script src="_static/documentation_options.js?v=fd7cadf9"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
|
||||
|
||||
Binary file not shown.
@@ -7,7 +7,7 @@
|
||||
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
|
||||
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
|
||||
<title>API Reference - Reticulum Network Stack 1.1.5 documentation</title>
|
||||
<title>API Reference - Reticulum Network Stack 1.2.2 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.5 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.2 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.5 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.2 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -222,6 +222,7 @@
|
||||
<li class="toctree-l1"><a class="reference internal" href="hardware.html">Communications Hardware</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="interfaces.html">Configuring Interfaces</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="networks.html">Building Networks</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="git.html">Using Git Over Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="support.html">Support Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="examples.html">Code Examples</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="license.html">Reticulum License</a></li>
|
||||
@@ -506,7 +507,7 @@ for addressable hashes and other purposes. Non-configurable.</p>
|
||||
|
||||
<dl class="py method">
|
||||
<dt class="sig sig-object py" id="RNS.Identity.recall">
|
||||
<em class="property"><span class="k"><span class="pre">static</span></span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">recall</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">target_hash</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">from_identity_hash</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">False</span></span></em><span class="sig-paren">)</span><a class="headerlink" href="#RNS.Identity.recall" title="Link to this definition">¶</a></dt>
|
||||
<em class="property"><span class="k"><span class="pre">static</span></span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">recall</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">target_hash</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">from_identity_hash</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">False</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">_no_use</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">False</span></span></em><span class="sig-paren">)</span><a class="headerlink" href="#RNS.Identity.recall" title="Link to this definition">¶</a></dt>
|
||||
<dd><p>Recall identity for a destination or identity hash. By default, this function
|
||||
will return the identity associated with a given <em>destination</em> hash. As an
|
||||
example, if you know the <code class="docutils literal notranslate"><span class="pre">lxmf.delivery</span></code> destination hash of an endpoint,
|
||||
@@ -528,7 +529,7 @@ search for an identity from a known <em>identity hash</em>, by setting the
|
||||
|
||||
<dl class="py method">
|
||||
<dt class="sig sig-object py" id="RNS.Identity.recall_app_data">
|
||||
<em class="property"><span class="k"><span class="pre">static</span></span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">recall_app_data</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">destination_hash</span></span></em><span class="sig-paren">)</span><a class="headerlink" href="#RNS.Identity.recall_app_data" title="Link to this definition">¶</a></dt>
|
||||
<em class="property"><span class="k"><span class="pre">static</span></span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">recall_app_data</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">destination_hash</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">_no_use</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">False</span></span></em><span class="sig-paren">)</span><a class="headerlink" href="#RNS.Identity.recall_app_data" title="Link to this definition">¶</a></dt>
|
||||
<dd><p>Recall last heard app_data for a destination hash.</p>
|
||||
<dl class="field-list simple">
|
||||
<dt class="field-odd">Parameters<span class="colon">:</span></dt>
|
||||
@@ -2472,7 +2473,7 @@ will announce it.</p>
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=a48ae3df"></script>
|
||||
</div><script src="_static/documentation_options.js?v=fd7cadf9"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
|
||||
<meta name="robots" content="noindex" />
|
||||
<title>Search - Reticulum Network Stack 1.1.5 documentation</title><link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<title>Search - Reticulum Network Stack 1.2.2 documentation</title><link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo-extensions.css?v=8dab3a3b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.5 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.2 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.5 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.2 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="#" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -222,6 +222,7 @@
|
||||
<li class="toctree-l1"><a class="reference internal" href="hardware.html">Communications Hardware</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="interfaces.html">Configuring Interfaces</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="networks.html">Building Networks</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="git.html">Using Git Over Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="support.html">Support Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="examples.html">Code Examples</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="license.html">Reticulum License</a></li>
|
||||
@@ -302,7 +303,7 @@
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=a48ae3df"></script>
|
||||
</div><script src="_static/documentation_options.js?v=fd7cadf9"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -7,7 +7,7 @@
|
||||
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
|
||||
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
|
||||
<title>Programs Using Reticulum - Reticulum Network Stack 1.1.5 documentation</title>
|
||||
<title>Programs Using Reticulum - Reticulum Network Stack 1.2.2 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.5 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.2 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.5 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.2 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -222,6 +222,7 @@
|
||||
<li class="toctree-l1"><a class="reference internal" href="hardware.html">Communications Hardware</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="interfaces.html">Configuring Interfaces</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="networks.html">Building Networks</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="git.html">Using Git Over Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="support.html">Support Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="examples.html">Code Examples</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="license.html">Reticulum License</a></li>
|
||||
@@ -533,7 +534,7 @@ using LXMF.</p>
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=a48ae3df"></script>
|
||||
</div><script src="_static/documentation_options.js?v=fd7cadf9"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
<head><meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="color-scheme" content="light dark"><meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="index" title="Index" href="genindex.html"><link rel="search" title="Search" href="search.html"><link rel="next" title="Code Examples" href="examples.html"><link rel="prev" title="Building Networks" href="networks.html">
|
||||
<link rel="index" title="Index" href="genindex.html"><link rel="search" title="Search" href="search.html"><link rel="next" title="Code Examples" href="examples.html"><link rel="prev" title="Using Git Over Reticulum" href="git.html">
|
||||
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
|
||||
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
|
||||
<title>Support Reticulum - Reticulum Network Stack 1.1.5 documentation</title>
|
||||
<title>Support Reticulum - Reticulum Network Stack 1.2.2 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.5 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.2 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.5 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.2 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -222,6 +222,7 @@
|
||||
<li class="toctree-l1"><a class="reference internal" href="hardware.html">Communications Hardware</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="interfaces.html">Configuring Interfaces</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="networks.html">Building Networks</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="git.html">Using Git Over Reticulum</a></li>
|
||||
<li class="toctree-l1 current current-page"><a class="current reference internal" href="#">Support Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="examples.html">Code Examples</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="license.html">Reticulum License</a></li>
|
||||
@@ -327,14 +328,14 @@ circumstances, so we rely on old-fashioned human feedback.</p>
|
||||
</div>
|
||||
<svg class="furo-related-icon"><use href="#svg-arrow-right"></use></svg>
|
||||
</a>
|
||||
<a class="prev-page" href="networks.html">
|
||||
<a class="prev-page" href="git.html">
|
||||
<svg class="furo-related-icon"><use href="#svg-arrow-right"></use></svg>
|
||||
<div class="page-info">
|
||||
<div class="context">
|
||||
<span>Previous</span>
|
||||
</div>
|
||||
|
||||
<div class="title">Building Networks</div>
|
||||
<div class="title">Using Git Over Reticulum</div>
|
||||
|
||||
</div>
|
||||
</a>
|
||||
@@ -381,7 +382,7 @@ circumstances, so we rely on old-fashioned human feedback.</p>
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=a48ae3df"></script>
|
||||
</div><script src="_static/documentation_options.js?v=fd7cadf9"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
|
||||
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
|
||||
<title>Understanding Reticulum - Reticulum Network Stack 1.1.5 documentation</title>
|
||||
<title>Understanding Reticulum - Reticulum Network Stack 1.2.2 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.5 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.2 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.5 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.2 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -222,6 +222,7 @@
|
||||
<li class="toctree-l1"><a class="reference internal" href="hardware.html">Communications Hardware</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="interfaces.html">Configuring Interfaces</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="networks.html">Building Networks</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="git.html">Using Git Over Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="support.html">Support Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="examples.html">Code Examples</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="license.html">Reticulum License</a></li>
|
||||
@@ -1336,7 +1337,7 @@ those risks are acceptable to you.</p>
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=a48ae3df"></script>
|
||||
</div><script src="_static/documentation_options.js?v=fd7cadf9"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
|
||||
|
||||
+244
-4
@@ -7,7 +7,7 @@
|
||||
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
|
||||
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
|
||||
<title>Using Reticulum on Your System - Reticulum Network Stack 1.1.5 documentation</title>
|
||||
<title>Using Reticulum on Your System - Reticulum Network Stack 1.2.2 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.5 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.2 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.5 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.2 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -222,6 +222,7 @@
|
||||
<li class="toctree-l1"><a class="reference internal" href="hardware.html">Communications Hardware</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="interfaces.html">Configuring Interfaces</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="networks.html">Building Networks</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="git.html">Using Git Over Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="support.html">Support Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="examples.html">Code Examples</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="license.html">Reticulum License</a></li>
|
||||
@@ -850,6 +851,17 @@ options:
|
||||
</pre></div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="the-rngit-utility">
|
||||
<h3>The rngit Utility<a class="headerlink" href="#the-rngit-utility" title="Link to this heading">¶</a></h3>
|
||||
<p>The <code class="docutils literal notranslate"><span class="pre">rngit</span></code> utility provides full Git repository hosting and interaction over Reticulum, as well as many other useful features for software development, collaboration and publishing. It allows you to host Git repositories on Reticulum nodes, interact with remote repositories using standard Git commands through the <code class="docutils literal notranslate"><span class="pre">rns://</span></code> URL scheme, and to publish software releases.</p>
|
||||
<p>The system consists of two parts: The <code class="docutils literal notranslate"><span class="pre">rngit</span></code> node that hosts and manages repositories, and the <code class="docutils literal notranslate"><span class="pre">git-remote-rns</span></code> helper that enables Git to communicate with rngit nodes. As soon as you have RNS installed on your system, you can transparently use Git with Reticulum-hosted repositories just like any other type of remote. Git over Reticulum uses URLs in the following format: <code class="docutils literal notranslate"><span class="pre">rns://DESTINATION_HASH/group/repo</span></code>.</p>
|
||||
<p>If you set a branch to track a Reticulum remote as the default upstream, you can simply use <code class="docutils literal notranslate"><span class="pre">git</span></code> as you normally would; all commands work transparently and as expected.</p>
|
||||
<div class="admonition warning">
|
||||
<p class="admonition-title">Warning</p>
|
||||
<p><strong>The rngit program is a new addition to RNS!</strong> This functionality was introduced in RNS 1.2.0. While great care has been taken to design a secure, but highly configurable and flexible permission system for allowing many users to interact with many different repositories on a single node, <code class="docutils literal notranslate"><span class="pre">rngit</span></code> has not been tested extensively in the wild! Be careful when hosting repositories, especially if they are public or semi-public.</p>
|
||||
</div>
|
||||
<p>For the full documentation on the <cite>rngit</cite> system, see the <a class="reference internal" href="git.html#git-main"><span class="std std-ref">Using Git Over Reticulum</span></a> chapter of this manual.</p>
|
||||
</section>
|
||||
<section id="the-rnx-utility">
|
||||
<h3>The rnx Utility<a class="headerlink" href="#the-rnx-utility" title="Link to this heading">¶</a></h3>
|
||||
<p>The <code class="docutils literal notranslate"><span class="pre">rnx</span></code> utility is a basic remote command execution program. It allows you to
|
||||
@@ -909,6 +921,232 @@ optional arguments:
|
||||
</pre></div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="the-rnsh-utility">
|
||||
<h3>The rnsh Utility<a class="headerlink" href="#the-rnsh-utility" title="Link to this heading">¶</a></h3>
|
||||
<p>The <code class="docutils literal notranslate"><span class="pre">rnsh</span></code> utility provides a fully interactive remote shell over Reticulum.
|
||||
It allows you to establish encrypted, authenticated shell sessions on remote
|
||||
systems, complete with terminal emulation, pipe support, and window resizing.</p>
|
||||
<p>While the <code class="docutils literal notranslate"><span class="pre">rnx</span></code> utility is useful for simple remote command execution and
|
||||
retrieving output, <code class="docutils literal notranslate"><span class="pre">rnsh</span></code> provides a complete interactive terminal experience,
|
||||
making it ideal for remote administration and management tasks that require
|
||||
real-time interaction, just like SSH does for IP networks.</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">rnsh</span></code> operates in two modes: a <em>listener</em> mode that accepts incoming
|
||||
connections, and an <em>initiator</em> mode that connects to a remote listener. Both
|
||||
sides authenticate using Reticulum Identities, ensuring that only authorised
|
||||
peers can establish sessions.</p>
|
||||
<div class="admonition note">
|
||||
<p class="admonition-title">Note</p>
|
||||
<p><code class="docutils literal notranslate"><span class="pre">rnsh</span></code> provides a genuine interactive terminal over Reticulum. It supports
|
||||
full terminal emulation including escape sequences, window resizing, signal
|
||||
forwarding, and piping of standard input, output and error streams. This
|
||||
makes it suitable for running text editors, terminal multiplexers, and any
|
||||
other interactive programs on remote systems.</p>
|
||||
</div>
|
||||
<p><strong>Usage Examples</strong></p>
|
||||
<p>Start <code class="docutils literal notranslate"><span class="pre">rnsh</span></code> in listener mode, accepting connections from specific identities:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rnsh -l -a 941bed5e228775e5a8079fc38b1ccf3f -a 1b03013c25f1c2ca068a4f080b844a10
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>You can also specify allowed identity hashes (one per line) in the file
|
||||
<code class="docutils literal notranslate"><span class="pre">~/.rnsh/allowed_identities</span></code> or <code class="docutils literal notranslate"><span class="pre">~/.config/rnsh/allowed_identities</span></code>, and
|
||||
simply run the program in listener mode:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rnsh -l
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>Connect to a remote listener from another system:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rnsh 7a55144adf826958a9529a3bcf08b149
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>Specify a command to run on the remote system, separating <code class="docutils literal notranslate"><span class="pre">rnsh</span></code> options from
|
||||
the remote command with <code class="docutils literal notranslate"><span class="pre">--</span></code>:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rnsh 7a55144adf826958a9529a3bcf08b149 -- top
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>Set a default command for the listener, in case the initiator does not supply
|
||||
one, or when remote command execution is disabled:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rnsh -l -- /bin/bash --login
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>Use the <code class="docutils literal notranslate"><span class="pre">-m</span></code> flag to mirror the exit code of the remote process:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rnsh -m 7a55144adf826958a9529a3bcf08b149 -- /usr/local/bin/check-status
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>Use the <code class="docutils literal notranslate"><span class="pre">-p</span></code> flag to display the identity and destination hash for a listener:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rnsh -l -p
|
||||
|
||||
Identity : <984b74a3f768bef236af4371e6f248cd>
|
||||
Listening on : 7a55144adf826958a9529a3bcf08b149
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>Use a specific identity file rather than the default:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rnsh -l -i /path/to/identity
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>Announce the listener destination on startup, and periodically:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rnsh -l -b 900
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>The <code class="docutils literal notranslate"><span class="pre">-b</span></code> option specifies the announce period in seconds. Use <code class="docutils literal notranslate"><span class="pre">0</span></code> to
|
||||
announce only once at startup.</p>
|
||||
<p><strong>Authentication & Authorisation</strong></p>
|
||||
<p>By default, <code class="docutils literal notranslate"><span class="pre">rnsh</span></code> requires that connecting initiators identify themselves
|
||||
with a Reticulum Identity whose hash is present in the list of allowed
|
||||
identities. Allowed identities can be specified on the command line with the
|
||||
<code class="docutils literal notranslate"><span class="pre">-a</span></code> option, and can be used multiple times:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rnsh -l -a 941bed5e228775e5a8079fc38b1ccf3f -a 1b03013c25f1c2ca068a4f080b844a10
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>You can also maintain a list of allowed identity hashes in the file
|
||||
<code class="docutils literal notranslate"><span class="pre">~/.rnsh/allowed_identities</span></code> or <code class="docutils literal notranslate"><span class="pre">~/.config/rnsh/allowed_identities</span></code>,
|
||||
with one hex hash per line. This file is reloaded every time a new connection
|
||||
is received, so changes take effect immediately without restarting <code class="docutils literal notranslate"><span class="pre">rnsh</span></code>.</p>
|
||||
<p>If you want to accept connections from any identity (for testing or in fully
|
||||
trusted environments), you can disable authentication with the <code class="docutils literal notranslate"><span class="pre">-n</span></code> option:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rnsh -l -n
|
||||
</pre></div>
|
||||
</div>
|
||||
<div class="admonition warning">
|
||||
<p class="admonition-title">Warning</p>
|
||||
<p>Disabling authentication with <code class="docutils literal notranslate"><span class="pre">-n</span></code> means that <strong>any</strong> Reticulum peer that
|
||||
can reach your listener will be able to execute commands on your system. Only
|
||||
use this option if you <em>really</em> know what you’re doing.</p>
|
||||
</div>
|
||||
<p><strong>Remote Command Control</strong></p>
|
||||
<p>When running in listener mode, <code class="docutils literal notranslate"><span class="pre">rnsh</span></code> allows you to control how remote
|
||||
commands are handled:</p>
|
||||
<ul class="simple">
|
||||
<li><p>By default, the listener accepts the command sent by the initiator. If the
|
||||
initiator does not supply a command, the listener’s default shell is used.</p></li>
|
||||
<li><p>Use <code class="docutils literal notranslate"><span class="pre">-C</span></code> (<code class="docutils literal notranslate"><span class="pre">--no-remote-command</span></code>) to disable execution of commands received
|
||||
from the initiator. Only the listener’s default command (or the command
|
||||
specified after <code class="docutils literal notranslate"><span class="pre">--</span></code>) will be executed:</p></li>
|
||||
</ul>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rnsh -l -C -- /usr/local/bin/safe-script
|
||||
</pre></div>
|
||||
</div>
|
||||
<ul class="simple">
|
||||
<li><p>Use <code class="docutils literal notranslate"><span class="pre">-A</span></code> (<code class="docutils literal notranslate"><span class="pre">--remote-command-as-args</span></code>) to append the initiator’s command
|
||||
to the listener’s default command instead of replacing it. This can be useful
|
||||
for restricting the remote to a specific program while still allowing the
|
||||
initiator to pass arguments:</p></li>
|
||||
</ul>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rnsh -l -A -- /usr/bin/top
|
||||
</pre></div>
|
||||
</div>
|
||||
<p><strong>Service Names</strong></p>
|
||||
<p>When running in listener mode, <code class="docutils literal notranslate"><span class="pre">rnsh</span></code> uses a service name to differentiate
|
||||
between multiple listener instances that may share the same identity. By
|
||||
default, the service name is <code class="docutils literal notranslate"><span class="pre">default</span></code>. You can specify a different service
|
||||
name with the <code class="docutils literal notranslate"><span class="pre">-s</span></code> option:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rnsh -l -s monitoring
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>This allows you to run multiple listeners on the same node, each with a
|
||||
different service name and purpose.</p>
|
||||
<p><strong>Initiator Options</strong></p>
|
||||
<p>When connecting to a remote listener, several options are available:</p>
|
||||
<ul class="simple">
|
||||
<li><p>Use <code class="docutils literal notranslate"><span class="pre">-N</span></code> (<code class="docutils literal notranslate"><span class="pre">--no-id</span></code>) to disable sending your identity to the remote
|
||||
listener. Note that the listener must have authentication disabled (<code class="docutils literal notranslate"><span class="pre">-n</span></code>)
|
||||
for the connection to succeed in this case.</p></li>
|
||||
<li><p>Use <code class="docutils literal notranslate"><span class="pre">-m</span></code> (<code class="docutils literal notranslate"><span class="pre">--mirror</span></code>) to make the initiator return with the exit code of
|
||||
the remote process, rather than always returning <code class="docutils literal notranslate"><span class="pre">0</span></code>.</p></li>
|
||||
<li><p>Use <code class="docutils literal notranslate"><span class="pre">-w</span></code> (<code class="docutils literal notranslate"><span class="pre">--timeout</span></code>) to specify the connection and request timeout in
|
||||
seconds. By default, the timeout matches the Reticulum path request timeout.</p></li>
|
||||
</ul>
|
||||
<p><strong>Identity & Destination</strong></p>
|
||||
<p>The default identity file for <code class="docutils literal notranslate"><span class="pre">rnsh</span></code> is stored at
|
||||
<code class="docutils literal notranslate"><span class="pre">~/.reticulum/identities/rnsh</span></code>, but you can specify a different one with the
|
||||
<code class="docutils literal notranslate"><span class="pre">-i</span></code> option, which will be created if it does not already exist:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rnsh -l -i /path/to/identity
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>To display the identity and destination information for a listener, use the
|
||||
<code class="docutils literal notranslate"><span class="pre">-p</span></code> option. When combined with <code class="docutils literal notranslate"><span class="pre">-l</span></code>, both the identity and the listening
|
||||
destination hash are displayed:</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rnsh -p
|
||||
|
||||
Identity : <984b74a3f768bef236af4371e6f248cd>
|
||||
|
||||
$ rnsh -l -p
|
||||
|
||||
Identity : <984b74a3f768bef236af4371e6f248cd>
|
||||
Listening on : 7a55144adf826958a9529a3bcf08b149
|
||||
</pre></div>
|
||||
</div>
|
||||
<p><strong>Verbosity</strong></p>
|
||||
<p>Like other Reticulum utilities, <code class="docutils literal notranslate"><span class="pre">rnsh</span></code> supports the <code class="docutils literal notranslate"><span class="pre">-v</span></code> and <code class="docutils literal notranslate"><span class="pre">-q</span></code> flags
|
||||
to increase or decrease logging verbosity. Multiple flags can be specified to
|
||||
further adjust the log level. The default log level is <code class="docutils literal notranslate"><span class="pre">INFO</span></code> for listeners
|
||||
and <code class="docutils literal notranslate"><span class="pre">ERROR</span></code> for initiators.</p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rnsh -l -vv # Listener with debug-level output
|
||||
$ rnsh -q 7a55144adf826958a9529a3bcf08b149 # Quiet initiator
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>By default, all log output is routed to <code class="docutils literal notranslate"><span class="pre">~/.rnsh/logfile</span></code> for initiators.</p>
|
||||
<p><strong>Escape Sequences</strong></p>
|
||||
<p>During an active <code class="docutils literal notranslate"><span class="pre">rnsh</span></code> session, the following escape sequences are
|
||||
available. These are only recognised immediately after a newline character:</p>
|
||||
<ul class="simple">
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">~~</span></code> - Send a literal tilde character</p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">~.</span></code> - Terminate the session and exit immediately</p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">~L</span></code> - Toggle line-interactive mode</p></li>
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">~?</span></code> - Display the escape sequence quick reference</p></li>
|
||||
</ul>
|
||||
<p><strong>All Command-Line Options</strong></p>
|
||||
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>usage: rnsh [-h] [--config CONFIG] [--identity IDENTITY] [-v] [-q] [-p]
|
||||
[--version] [-l] [-s SERVICE] [-b PERIOD] [-a HASH] [-n] [-A] [-C]
|
||||
[-N] [-m] [-w SECONDS]
|
||||
[destination]
|
||||
|
||||
Reticulum Remote Shell Utility
|
||||
|
||||
positional arguments:
|
||||
destination hexadecimal hash of the destination to connect to
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--config, -c CONFIG path to alternative Reticulum config directory
|
||||
--identity, -i IDENTITY
|
||||
path to identity file to use
|
||||
-v, --verbose increase verbosity
|
||||
-q, --quiet decrease verbosity
|
||||
-p, --print-identity print identity and destination info and exit
|
||||
--version show program's version number and exit
|
||||
-l, --listen listen (server) mode; any command specified after --
|
||||
will be used as the default command when the initiator
|
||||
does not provide one or when remote command execution
|
||||
is disabled; if no command is specified, the default
|
||||
shell of the user running rnsh will be used
|
||||
-s, --service SERVICE
|
||||
service name for identity file if not the default
|
||||
-b, --announce PERIOD
|
||||
announce on startup and every PERIOD seconds; specify
|
||||
0 to announce on startup only
|
||||
-a, --allowed HASH allow this identity to connect (may be specified
|
||||
multiple times); allowed identities can also be
|
||||
specified in ~/.rnsh/allowed_identities or
|
||||
~/.config/rnsh/allowed_identities, one hash per line
|
||||
-n, --no-auth disable authentication (allow any identity to connect)
|
||||
-A, --remote-command-as-args
|
||||
concatenate remote command to the argument list of the
|
||||
default program or shell
|
||||
-C, --no-remote-command
|
||||
disable executing command lines received from the
|
||||
remote initiator
|
||||
-N, --no-id disable identity announcement on connect
|
||||
-m, --mirror return with the exit code of the remote process
|
||||
-w, --timeout SECONDS
|
||||
connect and request timeout in seconds
|
||||
|
||||
When specifying a command to execute, separate rnsh options from the command
|
||||
and its arguments with --. For example:
|
||||
|
||||
rnsh -l -- /bin/bash --login
|
||||
rnsh <destination> -- ls -la /tmp
|
||||
</pre></div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="the-rnodeconf-utility">
|
||||
<h3>The rnodeconf Utility<a class="headerlink" href="#the-rnodeconf-utility" title="Link to this heading">¶</a></h3>
|
||||
<p>The <code class="docutils literal notranslate"><span class="pre">rnodeconf</span></code> utility allows you to inspect and configure existing <a class="reference internal" href="hardware.html#rnode-main"><span class="std std-ref">RNodes</span></a>, and
|
||||
@@ -1363,7 +1601,9 @@ systemctl --user enable rnsd.service
|
||||
<li><a class="reference internal" href="#the-rnpath-utility">The rnpath Utility</a></li>
|
||||
<li><a class="reference internal" href="#the-rnprobe-utility">The rnprobe Utility</a></li>
|
||||
<li><a class="reference internal" href="#the-rncp-utility">The rncp Utility</a></li>
|
||||
<li><a class="reference internal" href="#the-rngit-utility">The rngit Utility</a></li>
|
||||
<li><a class="reference internal" href="#the-rnx-utility">The rnx Utility</a></li>
|
||||
<li><a class="reference internal" href="#the-rnsh-utility">The rnsh Utility</a></li>
|
||||
<li><a class="reference internal" href="#the-rnodeconf-utility">The rnodeconf Utility</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
@@ -1395,7 +1635,7 @@ systemctl --user enable rnsd.service
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=a48ae3df"></script>
|
||||
</div><script src="_static/documentation_options.js?v=fd7cadf9"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
|
||||
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
|
||||
<title>What is Reticulum? - Reticulum Network Stack 1.1.5 documentation</title>
|
||||
<title>What is Reticulum? - Reticulum Network Stack 1.2.2 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.5 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.2 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.5 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.2 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -222,6 +222,7 @@
|
||||
<li class="toctree-l1"><a class="reference internal" href="hardware.html">Communications Hardware</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="interfaces.html">Configuring Interfaces</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="networks.html">Building Networks</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="git.html">Using Git Over Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="support.html">Support Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="examples.html">Code Examples</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="license.html">Reticulum License</a></li>
|
||||
@@ -503,7 +504,7 @@ network, and vice versa.</p>
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=a48ae3df"></script>
|
||||
</div><script src="_static/documentation_options.js?v=fd7cadf9"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
|
||||
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
|
||||
<title>Zen of Reticulum - Reticulum Network Stack 1.1.5 documentation</title>
|
||||
<title>Zen of Reticulum - Reticulum Network Stack 1.2.2 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.5 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.2 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.5 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.2 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -222,6 +222,7 @@
|
||||
<li class="toctree-l1"><a class="reference internal" href="hardware.html">Communications Hardware</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="interfaces.html">Configuring Interfaces</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="networks.html">Building Networks</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="git.html">Using Git Over Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="support.html">Support Reticulum</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="examples.html">Code Examples</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="license.html">Reticulum License</a></li>
|
||||
@@ -675,7 +676,7 @@ Imagine a messaging app. You write it once. It works on a laptop connected to fi
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=a48ae3df"></script>
|
||||
</div><script src="_static/documentation_options.js?v=fd7cadf9"></script>
|
||||
<script src="_static/doctools.js?v=9bcbadda"></script>
|
||||
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
|
||||
<script src="_static/scripts/furo.js?v=46bd48cc"></script>
|
||||
|
||||
@@ -0,0 +1,404 @@
|
||||
.. _git-main:
|
||||
|
||||
************************
|
||||
Using Git Over Reticulum
|
||||
************************
|
||||
|
||||
A set of utilities for distributed collaborative software development and publishing is included in RNS.
|
||||
|
||||
The system consists of two parts: The ``rngit`` node that hosts repositories, and the ``git-remote-rns`` helper that enables Git to communicate with rngit nodes. As soon as you have RNS installed on your system, you can transparently use Git with Reticulum-hosted repositories just like any other type of remote. Git over Reticulum uses URLs in the following format: ``rns://DESTINATION_HASH/group/repo``.
|
||||
|
||||
If you set a branch to track a Reticulum remote as the default upstream, you can simply use ``git`` as you normally would; all commands work transparently and as expected.
|
||||
|
||||
.. warning::
|
||||
**The rngit program is a new addition to RNS!** This functionality was introduced in RNS 1.2.0. While great care has been taken to design a secure, but highly configurable and flexible permission system for allowing many users to interact with many different repositories on a single node, ``rngit`` has not been tested extensively in the wild! Be careful when hosting repositories, especially if they are public or semi-public.
|
||||
|
||||
The rngit Utility
|
||||
=================
|
||||
|
||||
The ``rngit`` utility provides full Git repository hosting and interaction over Reticulum. It allows you to host and manage Git repositories and releases on Reticulum nodes, and to interact with remote repositories using standard Git commands through the ``rns://`` URL scheme.
|
||||
|
||||
**Usage Examples**
|
||||
|
||||
Run ``rngit`` to start a repository node:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rngit
|
||||
|
||||
[Notice] Starting Reticulum Git Node...
|
||||
[Notice] Reticulum Git Node listening on <0d7334d411d00120cbad24edf355fdd2>
|
||||
|
||||
On the first run, ``rngit`` will create a default configuration file. You will then need to edit this, to point to your repository locations, configure access permissions, and perform any other necessary configuration.
|
||||
|
||||
View your identity and destination hashes:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rngit --print-identity
|
||||
|
||||
Git Peer Identity : <959e10e5efc1bd9d97a4083babe51dea>
|
||||
Repository Node Identity : <153cb870b4665b8c1c348896292b0bad>
|
||||
Repositories Destination : <0d7334d411d00120cbad24edf355fdd2>
|
||||
|
||||
If the page server is enabled, the output will also include the Nomad Network destination hash.
|
||||
|
||||
You can run ``rngit`` in service mode with logging to file:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rngit -s
|
||||
|
||||
Clone a repository from a remote ``rngit`` node:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ git clone rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo
|
||||
|
||||
Add a Reticulum remote to an existing repository:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ git remote add some_remote rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo
|
||||
|
||||
Push changes to the Reticulum remote:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ git push some_remote master
|
||||
|
||||
Get changes from a remote repository:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ git pull rns_remote master
|
||||
|
||||
**All Command-Line Options (rngit)**
|
||||
|
||||
.. code:: text
|
||||
|
||||
usage: rngit.py [-h] [--config CONFIG] [--rnsconfig RNSCONFIG] [-s] [-i] [-v]
|
||||
[-q] [--version]
|
||||
|
||||
Reticulum Git Repository Node
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--config CONFIG path to alternative config directory
|
||||
--rnsconfig RNSCONFIG
|
||||
path to alternative Reticulum config directory
|
||||
-p, --print-identity print identity and destination info and exit
|
||||
-s, --service rngit is running as a service and should log to file
|
||||
-i, --interactive drop into interactive shell after initialisation
|
||||
-v, --verbose increase verbosity
|
||||
-q, --quiet decrease verbosity
|
||||
--version show program's version number and exit
|
||||
|
||||
**All Command-Line Options (git-remote-rns)**
|
||||
|
||||
The ``git-remote-rns`` helper is automatically invoked by Git when interacting with ``rns://`` URLs. It is not typically run directly by users, but accepts the following environment variables for configuration:
|
||||
|
||||
- ``RNGIT_CONFIG`` - Path to alternative client configuration directory
|
||||
- ``RNS_CONFIG`` - Path to alternative Reticulum configuration directory
|
||||
|
||||
The client configuration file is located at ``~/.rngit/client_config`` and allows adjusting parameters such as the reference batch size for transfers.
|
||||
|
||||
|
||||
Repository Structure
|
||||
====================
|
||||
|
||||
The ``rngit`` node organizes repositories into groups. Each group is a directory containing bare Git repositories. The repository path format is ``group_name/repo_name``. For example, a repository at ``/var/git/public/myrepo`` would be accessible as ``public/myrepo`` via the URL ``rns://DESTINATION_HASH/public/myrepo``.
|
||||
|
||||
**Configuration**
|
||||
|
||||
The ``rngit`` node configuration file is located at ``~/.rngit/config`` (or ``/etc/rngit/config`` for system-wide installations). The default configuration includes:
|
||||
|
||||
- Repository group paths defining where to find bare repositories
|
||||
- Access permissions for groups and individual repositories
|
||||
- Announce intervals for network visibility
|
||||
- Optional statistics recording for repository activity
|
||||
|
||||
Access permissions can be configured at the group level in the config file, or per-repository using ``.allowed`` files. Permissions use the format ``permission:target`` where permission is ``r`` (read), ``w`` (write), ``rw`` (read/write), ``c`` (create) or ``s`` (stats) and target is ``all``, ``none``, or a specific identity hash.
|
||||
|
||||
The ``s`` (stats) permission allows viewing repository activity statistics, including views, fetches and pushes over time. To enable statistics recording, set ``record_stats = yes`` in the ``[rngit]`` section of the configuration file. You can also exclude specific identities from statistics by adding their hashes to ``stats_ignore_identities``.
|
||||
|
||||
Repository-specific ``.allowed`` files can be static text files or executable scripts that output permission rules to stdout. A ``group.allowed`` file in a repository group directory applies to all repositories within that group.
|
||||
|
||||
Serving Pages Over Nomad Network
|
||||
================================
|
||||
|
||||
In addition to providing Git repository access via the Git remote helper protocol, ``rngit`` can also run a `Nomad Network <https://github.com/markqvist/nomadnet>`_ compatible page node. This allows users to browse repository information, view file contents, inspect commit history and access repository statistics through any Nomad Network client.
|
||||
|
||||
When enabled, the page node provides a complete interface to your repositories, with automatic Markdown to Micron conversion, syntax-highlighted code browsing, and detailed commit, diff and statistics views.
|
||||
|
||||
**Enabling the Git Page Node**
|
||||
|
||||
To enable the page node, add the following to your ``~/.rngit/config`` file:
|
||||
|
||||
.. code:: text
|
||||
|
||||
[pages]
|
||||
serve_nomadnet = yes
|
||||
|
||||
When the page node is enabled, ``rngit`` will listen on a Nomad Network node destination in addition to the Git repository destination. You can view the destination hash by running:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rngit --print-identity
|
||||
|
||||
Git Peer Identity : <959e10e5efc1bd9d97a4083babe51dea>
|
||||
Repository Node Identity : <153cb870b4665b8c1c348896292b0bad>
|
||||
Repositories Destination : <0d7334d411d00120cbad24edf355fdd2>
|
||||
Nomad Network Destination : <50824b711717f97c2fb1166ceddd5ea9>
|
||||
|
||||
**Accessing Repository Pages**
|
||||
|
||||
Once the page server is running, you can access it from any Nomad Network client by connecting to the Nomad Network destination. The page node provides the following views:
|
||||
|
||||
- **Front Page** - Lists all repository groups accessible to your identity
|
||||
- **Group Page** - Shows all repositories within a group
|
||||
- **Repository Page** - Displays repository overview, description and README
|
||||
- **Releases** - List of releases for the repository, with information and downloads
|
||||
- **File Browser** - Browse directory trees and view and download file contents
|
||||
- **Commits View** - View commit history with pagination
|
||||
- **Commit Details** - Detailed commit information with file changes and diffs
|
||||
- **Refs View** - List branches and tags
|
||||
- **Statistics** - Activity charts showing views, fetches and pushes over time
|
||||
|
||||
All pages respect the same permission system used for Git access. If an identity does not have read access to a repository, they will not be able to view its pages.
|
||||
|
||||
Formatting & Syntax Highlighting
|
||||
================================
|
||||
|
||||
If the ``pygments`` Python module is installed on your system, the page server will automatically apply syntax highlighting to code files. The highlighting supports a wide range of programming languages and uses a color theme optimized for terminal display.
|
||||
|
||||
To enable syntax highlighting, install pygments:
|
||||
|
||||
.. code:: text
|
||||
|
||||
pip install pygments
|
||||
|
||||
**Markdown & Micron Support**
|
||||
|
||||
README files and other Markdown documents are automatically converted to Micron markup for display in Nomad Network clients. You can also write your README files directly in Micron, in which case they will display and render as such in any Nomad Network client. The file browser also supports viewing both rendered and raw Markdown and Micron documents.
|
||||
|
||||
Code blocks in Markdown can include language hints for syntax highlighting:
|
||||
|
||||
.. code:: text
|
||||
|
||||
```python
|
||||
def hello_world():
|
||||
print("Hello, Reticulum!")
|
||||
```
|
||||
|
||||
Customizing Templates
|
||||
=====================
|
||||
|
||||
The page server uses a template system that allows complete customization of the generated pages. Templates are stored in the ``~/.rngit/templates/`` directory as Micron files.
|
||||
|
||||
The following template files are supported:
|
||||
|
||||
- ``base.mu`` - Base template wrapping all pages
|
||||
- ``front.mu`` - Front page listing all groups
|
||||
- ``group.mu`` - Group page listing repositories
|
||||
- ``repo.mu`` - Repository overview page
|
||||
- ``releases.mu`` - Release list page
|
||||
- ``release.mu`` - Release details page
|
||||
- ``tree.mu`` - File browser pages
|
||||
- ``blob.mu`` - File content display
|
||||
- ``commits.mu`` - Commit history listing
|
||||
- ``commit.mu`` - Individual commit detail page
|
||||
- ``refs.mu`` - Branches and tags listing
|
||||
- ``stats.mu`` - Statistics page
|
||||
|
||||
Templates can include the following variables:
|
||||
|
||||
- ``{PAGE_CONTENT}`` - The main content of the page (required)
|
||||
- ``{NODE_NAME}`` - The configured node name
|
||||
- ``{NAVIGATION}`` - Breadcrumb navigation links
|
||||
- ``{VERSION}`` - The rngit version number
|
||||
- ``{GEN_TIME}`` - Page generation time
|
||||
|
||||
**Dynamic Templates**
|
||||
|
||||
Templates can be made executable to generate dynamic content. If a template file has the executable bit set, it will be executed and its stdout used as the template content.
|
||||
|
||||
**Icon Sets**
|
||||
|
||||
By default, the page server uses Nerd Font icons. If you prefer simpler icons or your terminal does not support Nerd Fonts, you can enable Unicode icons instead:
|
||||
|
||||
.. code:: text
|
||||
|
||||
[pages]
|
||||
serve_nomadnet = yes
|
||||
unicode_icons = yes
|
||||
|
||||
**Repository Statistics**
|
||||
|
||||
When statistics recording is enabled (see the ``record_stats`` configuration option), the page server can display activity charts for each repository. The statistics page shows:
|
||||
|
||||
- Total and peak views, fetches and pushes
|
||||
- Daily activity charts over a 90-day period
|
||||
- Combined activity visualization
|
||||
|
||||
To view statistics, a user must have the ``s`` (stats) permission for the repository. See the Access Configuration section for details on setting permissions.
|
||||
|
||||
**Repository Thanks**
|
||||
|
||||
The page server includes a "Thanks" feature that allows users to express appreciation for a repository. On each repository page, a "Thanks" link is displayed showing the current thanks count. Clicking this link registers a thank you for the repository.
|
||||
|
||||
**Configuration Example**
|
||||
|
||||
A complete page server configuration might look like this:
|
||||
|
||||
.. code:: text
|
||||
|
||||
[rngit]
|
||||
node_name = My Git Server
|
||||
announce_interval = 360
|
||||
record_stats = yes
|
||||
|
||||
[repositories]
|
||||
public = /var/git/public
|
||||
internal = /var/git/internal
|
||||
|
||||
[access]
|
||||
public = r:all
|
||||
internal = rw:9710b86ba12c42d1d8f30f74fe509286
|
||||
|
||||
[pages]
|
||||
serve_nomadnet = yes
|
||||
unicode_icons = no
|
||||
|
||||
|
||||
Release Management
|
||||
==================
|
||||
|
||||
In addition to hosting Git repositories, ``rngit`` provides a complete release management system. This allows you to publish versioned releases with associated artifacts, release notes and metadata. Releases are managed through the ``rngit release`` subcommand, and are also viewable through the Nomad Network page interface.
|
||||
|
||||
**The Release Workflow**
|
||||
|
||||
Creating a release involves specifying a Git tag and a directory containing build artifacts or other files to distribute. The ``rngit`` client will open your configured ``$EDITOR`` to compose release notes, then upload all artifacts to the remote repository node.
|
||||
|
||||
To create a release, specify the tag name and path to artifacts:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rngit release create rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo v1.2.0:./dist
|
||||
|
||||
This will:
|
||||
|
||||
1. Verify that the tag ``v1.2.0`` exists in the repository
|
||||
2. Open your editor to write release notes
|
||||
3. Upload all files from the ``./dist`` directory
|
||||
4. Publish the release
|
||||
|
||||
If no ``$EDITOR`` environment variable is set, ``rngit`` will try to use ``nano``, ``vim`` or ``vi``. The editor will show a template with instructions. Lines starting with ``#`` will be ignored, and if the remaining content is empty after stripping comments, the release creation will be cancelled.
|
||||
|
||||
**Release Storage & Structure**
|
||||
|
||||
Releases are stored on the server in a directory named ``repo_name.releases`` next to the bare repository. Each release is a subdirectory containing:
|
||||
|
||||
- ``META`` - Release metadata in ConfigObj format
|
||||
- ``RELEASE.md`` or ``RELEASE.mu`` - Release notes
|
||||
- ``artifacts/`` - All uploaded files
|
||||
- ``THANKS`` - Appreciation count from users
|
||||
|
||||
**Listing Releases**
|
||||
|
||||
To view all releases for a repository:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rngit release list rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo
|
||||
|
||||
Tag Status Created Objs Notes
|
||||
------------------------------------------------------------------
|
||||
v1.2.0 published 2025-01-15 14:32 3 Another release
|
||||
v1.1.0 published 2024-12-03 09:15 2 Bug fix release
|
||||
v1.0.0 published 2024-10-20 16:45 2 Initial release
|
||||
|
||||
**Viewing Release Details**
|
||||
|
||||
To see full information about a specific release:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rngit release view rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo v1.2.0
|
||||
|
||||
Release : 0.9.2
|
||||
Status : published
|
||||
Created : 2026-05-04 23:53:09
|
||||
Thanks : 5
|
||||
|
||||
Release Notes
|
||||
=============
|
||||
|
||||
Version 1.2.0 release notes...
|
||||
|
||||
Artifacts (4)
|
||||
=============
|
||||
- myapp-1.2.0.tar.gz (1.5 MB)
|
||||
- myapp-1.2.0.zip (1.6 MB)
|
||||
- checksums.txt (256 B)
|
||||
|
||||
**Deleting Releases**
|
||||
|
||||
To remove a release:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rngit release delete rns://50824b711717f97c2fb1166ceddd5ea9/public/myrepo v1.2.0
|
||||
|
||||
Are you sure you want to delete release 'v1.2.0'? [y/N]: y
|
||||
Release v1.2.0 deleted
|
||||
|
||||
**Requirements & Validation**
|
||||
|
||||
- The specified tag must exist in the remote repository
|
||||
- You must have ``release`` permission for the repository
|
||||
- The target artifacts directory must exist and contain at least one file
|
||||
- Release notes cannot be empty
|
||||
|
||||
**Permissions**
|
||||
|
||||
Release management requires the ``release`` permission, configured the same way as other repository permissions. In the config file or ``.allowed`` files, use ``rel:target`` to grant release management rights:
|
||||
|
||||
.. code:: text
|
||||
|
||||
# In .allowed file or config
|
||||
rel:all # Allow everyone
|
||||
rel:9710b86... # Allow specific identity
|
||||
rel:none # Deny everyone
|
||||
|
||||
**Nomad Network Interface**
|
||||
|
||||
When the Nomad Network page server is enabled, releases are displayed on a dedicated releases page for each repository. Each release is listed with its tag, creation date, artifact count and a preview of the release notes. Clicking a release shows the full details including formatted release notes and a listing of all artifacts with their sizes.
|
||||
|
||||
Only releases with ``published`` status are visible through the Nomad Network interface. Draft releases (if supported in future implementations) would only be visible through the command-line interface.
|
||||
|
||||
**All Command-Line Options (rngit release)**
|
||||
|
||||
.. code:: text
|
||||
|
||||
usage: rngit release [-h] [--config CONFIG] [--rnsconfig RNSCONFIG]
|
||||
[-i IDENTITY] [-v] [-q] [--version]
|
||||
operation repository [target]
|
||||
|
||||
Reticulum Git Release Manager
|
||||
|
||||
positional arguments:
|
||||
operation list, view, create or delete
|
||||
repository URL of remote repository (rns://hash/group/repo)
|
||||
target tag or tag:path for create, tag for view/delete
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--config CONFIG path to alternative config directory
|
||||
--rnsconfig RNSCONFIG
|
||||
path to alternative Reticulum config directory
|
||||
-i IDENTITY, --identity IDENTITY
|
||||
path to release identity
|
||||
-v, --verbose increase verbosity
|
||||
-q, --quiet decrease verbosity
|
||||
--version show program's version number and exit
|
||||
@@ -27,6 +27,7 @@ to participate in the development of Reticulum itself.
|
||||
hardware
|
||||
interfaces
|
||||
networks
|
||||
git
|
||||
support
|
||||
examples
|
||||
license
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 37 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 24 KiB |
@@ -680,6 +680,21 @@ another one, which will be created if it does not already exist
|
||||
--version show program's version number and exit
|
||||
|
||||
|
||||
The rngit Utility
|
||||
=================
|
||||
|
||||
The ``rngit`` utility provides full Git repository hosting and interaction over Reticulum, as well as many other useful features for software development, collaboration and publishing. It allows you to host Git repositories on Reticulum nodes, interact with remote repositories using standard Git commands through the ``rns://`` URL scheme, and to publish software releases.
|
||||
|
||||
The system consists of two parts: The ``rngit`` node that hosts and manages repositories, and the ``git-remote-rns`` helper that enables Git to communicate with rngit nodes. As soon as you have RNS installed on your system, you can transparently use Git with Reticulum-hosted repositories just like any other type of remote. Git over Reticulum uses URLs in the following format: ``rns://DESTINATION_HASH/group/repo``.
|
||||
|
||||
If you set a branch to track a Reticulum remote as the default upstream, you can simply use ``git`` as you normally would; all commands work transparently and as expected.
|
||||
|
||||
.. warning::
|
||||
**The rngit program is a new addition to RNS!** This functionality was introduced in RNS 1.2.0. While great care has been taken to design a secure, but highly configurable and flexible permission system for allowing many users to interact with many different repositories on a single node, ``rngit`` has not been tested extensively in the wild! Be careful when hosting repositories, especially if they are public or semi-public.
|
||||
|
||||
For the full documentation on the `rngit` system, see the :ref:`Using Git Over Reticulum<git-main>` chapter of this manual.
|
||||
|
||||
|
||||
The rnx Utility
|
||||
================
|
||||
|
||||
@@ -752,6 +767,282 @@ another one, which will be created if it does not already exist
|
||||
--version show program's version number and exit
|
||||
|
||||
|
||||
The rnsh Utility
|
||||
================
|
||||
|
||||
The ``rnsh`` utility provides a fully interactive remote shell over Reticulum.
|
||||
It allows you to establish encrypted, authenticated shell sessions on remote
|
||||
systems, complete with terminal emulation, pipe support, and window resizing.
|
||||
|
||||
While the ``rnx`` utility is useful for simple remote command execution and
|
||||
retrieving output, ``rnsh`` provides a complete interactive terminal experience,
|
||||
making it ideal for remote administration and management tasks that require
|
||||
real-time interaction, just like SSH does for IP networks.
|
||||
|
||||
``rnsh`` operates in two modes: a *listener* mode that accepts incoming
|
||||
connections, and an *initiator* mode that connects to a remote listener. Both
|
||||
sides authenticate using Reticulum Identities, ensuring that only authorised
|
||||
peers can establish sessions.
|
||||
|
||||
.. note::
|
||||
``rnsh`` provides a genuine interactive terminal over Reticulum. It supports
|
||||
full terminal emulation including escape sequences, window resizing, signal
|
||||
forwarding, and piping of standard input, output and error streams. This
|
||||
makes it suitable for running text editors, terminal multiplexers, and any
|
||||
other interactive programs on remote systems.
|
||||
|
||||
**Usage Examples**
|
||||
|
||||
Start ``rnsh`` in listener mode, accepting connections from specific identities:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh -l -a 941bed5e228775e5a8079fc38b1ccf3f -a 1b03013c25f1c2ca068a4f080b844a10
|
||||
|
||||
You can also specify allowed identity hashes (one per line) in the file
|
||||
``~/.rnsh/allowed_identities`` or ``~/.config/rnsh/allowed_identities``, and
|
||||
simply run the program in listener mode:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh -l
|
||||
|
||||
Connect to a remote listener from another system:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh 7a55144adf826958a9529a3bcf08b149
|
||||
|
||||
Specify a command to run on the remote system, separating ``rnsh`` options from
|
||||
the remote command with ``--``:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh 7a55144adf826958a9529a3bcf08b149 -- top
|
||||
|
||||
Set a default command for the listener, in case the initiator does not supply
|
||||
one, or when remote command execution is disabled:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh -l -- /bin/bash --login
|
||||
|
||||
Use the ``-m`` flag to mirror the exit code of the remote process:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh -m 7a55144adf826958a9529a3bcf08b149 -- /usr/local/bin/check-status
|
||||
|
||||
Use the ``-p`` flag to display the identity and destination hash for a listener:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh -l -p
|
||||
|
||||
Identity : <984b74a3f768bef236af4371e6f248cd>
|
||||
Listening on : 7a55144adf826958a9529a3bcf08b149
|
||||
|
||||
Use a specific identity file rather than the default:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh -l -i /path/to/identity
|
||||
|
||||
Announce the listener destination on startup, and periodically:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh -l -b 900
|
||||
|
||||
The ``-b`` option specifies the announce period in seconds. Use ``0`` to
|
||||
announce only once at startup.
|
||||
|
||||
**Authentication & Authorisation**
|
||||
|
||||
By default, ``rnsh`` requires that connecting initiators identify themselves
|
||||
with a Reticulum Identity whose hash is present in the list of allowed
|
||||
identities. Allowed identities can be specified on the command line with the
|
||||
``-a`` option, and can be used multiple times:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh -l -a 941bed5e228775e5a8079fc38b1ccf3f -a 1b03013c25f1c2ca068a4f080b844a10
|
||||
|
||||
You can also maintain a list of allowed identity hashes in the file
|
||||
``~/.rnsh/allowed_identities`` or ``~/.config/rnsh/allowed_identities``,
|
||||
with one hex hash per line. This file is reloaded every time a new connection
|
||||
is received, so changes take effect immediately without restarting ``rnsh``.
|
||||
|
||||
If you want to accept connections from any identity (for testing or in fully
|
||||
trusted environments), you can disable authentication with the ``-n`` option:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh -l -n
|
||||
|
||||
.. warning::
|
||||
Disabling authentication with ``-n`` means that **any** Reticulum peer that
|
||||
can reach your listener will be able to execute commands on your system. Only
|
||||
use this option if you *really* know what you're doing.
|
||||
|
||||
**Remote Command Control**
|
||||
|
||||
When running in listener mode, ``rnsh`` allows you to control how remote
|
||||
commands are handled:
|
||||
|
||||
- By default, the listener accepts the command sent by the initiator. If the
|
||||
initiator does not supply a command, the listener's default shell is used.
|
||||
|
||||
- Use ``-C`` (``--no-remote-command``) to disable execution of commands received
|
||||
from the initiator. Only the listener's default command (or the command
|
||||
specified after ``--``) will be executed:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh -l -C -- /usr/local/bin/safe-script
|
||||
|
||||
- Use ``-A`` (``--remote-command-as-args``) to append the initiator's command
|
||||
to the listener's default command instead of replacing it. This can be useful
|
||||
for restricting the remote to a specific program while still allowing the
|
||||
initiator to pass arguments:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh -l -A -- /usr/bin/top
|
||||
|
||||
**Service Names**
|
||||
|
||||
When running in listener mode, ``rnsh`` uses a service name to differentiate
|
||||
between multiple listener instances that may share the same identity. By
|
||||
default, the service name is ``default``. You can specify a different service
|
||||
name with the ``-s`` option:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh -l -s monitoring
|
||||
|
||||
This allows you to run multiple listeners on the same node, each with a
|
||||
different service name and purpose.
|
||||
|
||||
**Initiator Options**
|
||||
|
||||
When connecting to a remote listener, several options are available:
|
||||
|
||||
- Use ``-N`` (``--no-id``) to disable sending your identity to the remote
|
||||
listener. Note that the listener must have authentication disabled (``-n``)
|
||||
for the connection to succeed in this case.
|
||||
|
||||
- Use ``-m`` (``--mirror``) to make the initiator return with the exit code of
|
||||
the remote process, rather than always returning ``0``.
|
||||
|
||||
- Use ``-w`` (``--timeout``) to specify the connection and request timeout in
|
||||
seconds. By default, the timeout matches the Reticulum path request timeout.
|
||||
|
||||
**Identity & Destination**
|
||||
|
||||
The default identity file for ``rnsh`` is stored at
|
||||
``~/.reticulum/identities/rnsh``, but you can specify a different one with the
|
||||
``-i`` option, which will be created if it does not already exist:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh -l -i /path/to/identity
|
||||
|
||||
To display the identity and destination information for a listener, use the
|
||||
``-p`` option. When combined with ``-l``, both the identity and the listening
|
||||
destination hash are displayed:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh -p
|
||||
|
||||
Identity : <984b74a3f768bef236af4371e6f248cd>
|
||||
|
||||
$ rnsh -l -p
|
||||
|
||||
Identity : <984b74a3f768bef236af4371e6f248cd>
|
||||
Listening on : 7a55144adf826958a9529a3bcf08b149
|
||||
|
||||
**Verbosity**
|
||||
|
||||
Like other Reticulum utilities, ``rnsh`` supports the ``-v`` and ``-q`` flags
|
||||
to increase or decrease logging verbosity. Multiple flags can be specified to
|
||||
further adjust the log level. The default log level is ``INFO`` for listeners
|
||||
and ``ERROR`` for initiators.
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnsh -l -vv # Listener with debug-level output
|
||||
$ rnsh -q 7a55144adf826958a9529a3bcf08b149 # Quiet initiator
|
||||
|
||||
By default, all log output is routed to ``~/.rnsh/logfile`` for initiators.
|
||||
|
||||
**Escape Sequences**
|
||||
|
||||
During an active ``rnsh`` session, the following escape sequences are
|
||||
available. These are only recognised immediately after a newline character:
|
||||
|
||||
- ``~~`` - Send a literal tilde character
|
||||
- ``~.`` - Terminate the session and exit immediately
|
||||
- ``~L`` - Toggle line-interactive mode
|
||||
- ``~?`` - Display the escape sequence quick reference
|
||||
|
||||
**All Command-Line Options**
|
||||
|
||||
.. code:: text
|
||||
|
||||
usage: rnsh [-h] [--config CONFIG] [--identity IDENTITY] [-v] [-q] [-p]
|
||||
[--version] [-l] [-s SERVICE] [-b PERIOD] [-a HASH] [-n] [-A] [-C]
|
||||
[-N] [-m] [-w SECONDS]
|
||||
[destination]
|
||||
|
||||
Reticulum Remote Shell Utility
|
||||
|
||||
positional arguments:
|
||||
destination hexadecimal hash of the destination to connect to
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--config, -c CONFIG path to alternative Reticulum config directory
|
||||
--identity, -i IDENTITY
|
||||
path to identity file to use
|
||||
-v, --verbose increase verbosity
|
||||
-q, --quiet decrease verbosity
|
||||
-p, --print-identity print identity and destination info and exit
|
||||
--version show program's version number and exit
|
||||
-l, --listen listen (server) mode; any command specified after --
|
||||
will be used as the default command when the initiator
|
||||
does not provide one or when remote command execution
|
||||
is disabled; if no command is specified, the default
|
||||
shell of the user running rnsh will be used
|
||||
-s, --service SERVICE
|
||||
service name for identity file if not the default
|
||||
-b, --announce PERIOD
|
||||
announce on startup and every PERIOD seconds; specify
|
||||
0 to announce on startup only
|
||||
-a, --allowed HASH allow this identity to connect (may be specified
|
||||
multiple times); allowed identities can also be
|
||||
specified in ~/.rnsh/allowed_identities or
|
||||
~/.config/rnsh/allowed_identities, one hash per line
|
||||
-n, --no-auth disable authentication (allow any identity to connect)
|
||||
-A, --remote-command-as-args
|
||||
concatenate remote command to the argument list of the
|
||||
default program or shell
|
||||
-C, --no-remote-command
|
||||
disable executing command lines received from the
|
||||
remote initiator
|
||||
-N, --no-id disable identity announcement on connect
|
||||
-m, --mirror return with the exit code of the remote process
|
||||
-w, --timeout SECONDS
|
||||
connect and request timeout in seconds
|
||||
|
||||
When specifying a command to execute, separate rnsh options from the command
|
||||
and its arguments with --. For example:
|
||||
|
||||
rnsh -l -- /bin/bash --login
|
||||
rnsh <destination> -- ls -la /tmp
|
||||
|
||||
|
||||
The rnodeconf Utility
|
||||
=====================
|
||||
|
||||
|
||||
@@ -52,6 +52,9 @@ setuptools.setup(
|
||||
'rnx=RNS.Utilities.rnx:main',
|
||||
'rnir=RNS.Utilities.rnir:main',
|
||||
'rnpkg=RNS.Utilities.rnpkg:main',
|
||||
'rnsh=RNS.Utilities.rnsh.rnsh:main',
|
||||
'rngit=RNS.Utilities.rngit.server:main',
|
||||
'git-remote-rns=RNS.Utilities.rngit.client:main',
|
||||
'rnodeconf=RNS.Utilities.rnodeconf:main',
|
||||
]
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user