Compare commits

...

25 Commits

Author SHA1 Message Date
Mark Qvist 95502e2c21 Prepare release 2026-05-14 01:56:30 +02:00
Mark Qvist 3dd4145e62 Updated changelog 2026-05-14 01:53:33 +02:00
Mark Qvist 1d7ddc3f8a Implemented rngit work document signing 2026-05-14 01:51:22 +02:00
Mark Qvist d731b4396c Repo page rendering 2026-05-14 00:32:22 +02:00
Mark Qvist c186a1f6b0 Updated version 2026-05-14 00:16:33 +02:00
Mark Qvist a049ec8b7b Updated changelog 2026-05-14 00:16:28 +02:00
Mark Qvist 4c93f6c7f4 Added local URL resolution to repo frontpage markdown readme renderer 2026-05-13 23:41:07 +02:00
Mark Qvist 35c7a89b19 Fixed typo 2026-05-13 22:58:50 +02:00
Mark Qvist c86b9c9703 Fixed missing none check in interface discovery sanitizer thanks to PAzter1101 2026-05-13 10:34:58 +02:00
Mark Qvist 64ebdd0ee3 Cleanup 2026-05-13 01:19:51 +02:00
Mark Qvist 9179b914d5 Added embedded message signing, validation and viewing to rnid 2026-05-13 01:14:41 +02:00
Mark Qvist eb5d46b20b Added file decryption for multiple file path inputs and shell expansions to rnid 2026-05-12 23:20:28 +02:00
Mark Qvist 54c36f515b Added file encryption for multiple file path inputs and shell expansions to rnid 2026-05-12 23:14:01 +02:00
Mark Qvist 5c5668a4fc Added signature creation for multiple file path inputs and shell expansions to rnid 2026-05-12 23:09:50 +02:00
Mark Qvist eeefb60c89 Added signature validation of multiple file path inputs and shell expansions to rnid 2026-05-12 23:00:06 +02:00
Mark Qvist 018df10a26 Fixed rngit remote helper hanging on startup if no client config had been created previously, and RNS loglevel was configured at debug or higher 2026-05-12 22:21:53 +02:00
Mark Qvist 93ead77435 Added workdoc downloads 2026-05-12 21:47:10 +02:00
Mark Qvist bd0e1ad0ca Better workdoc page handling 2026-05-12 21:05:15 +02:00
Mark Qvist d0ceeacb37 Allow setting title on workdoc edit 2026-05-12 15:04:02 +02:00
Mark Qvist 7d5fb6a13f Cleanup 2026-05-11 23:31:25 +02:00
Mark Qvist 855ef7bfd1 Base256 encoding 2026-05-11 23:22:13 +02:00
Mark Qvist 323890021a Better remote monitor loop 2026-05-11 00:20:02 +02:00
Mark Qvist e004e7592b Added lock to interface discovery 2026-05-10 00:29:48 +02:00
Mark Qvist 0ebec014e5 Improved release page 2026-05-10 00:26:55 +02:00
Mark Qvist 1b624cc0e2 Updated manual 2026-05-09 19:20:38 +02:00
32 changed files with 732 additions and 364 deletions
+30
View File
@@ -1,3 +1,33 @@
### 2026-05-14: RNS 1.2.6
This release adds further improvements to the `rnid` and `rngit` utilities, and includes several bugfixes and other improvements.
**Changes**
- Added embedded message signing, validation and viewing to `rnid`
- Added file encryption for multiple file path inputs and shell expansions to `rnid`
- Added file decryption for multiple file path inputs and shell expansions to `rnid`
- Added signature creation for multiple file path inputs and shell expansions to `rnid`
- Added signature validation of multiple file path inputs and shell expansions to `rnid`
- Added workdoc signing and validation to `rngit`
- Added ability to edit workdoc titles to `rngit`
- Added ability to download workdocs via the `nomadnet` interface to `rngit`
- Added local URL resolution to the `rngit` repository frontpage markdown readme renderer
- Improved `rnstatus` remote monitor loop
- Improved `rngit` workdoc page handling
- Improved `rngit` release page rendering
- Fixed missing none check in interface discovery sanitizer thanks to PAzter1101
- Fixed potential race condition in interface discovery
- Fixed `rngit` remote helper hanging on startup if no client config had been created previously, and RNS loglevel was configured at debug or higher
**Release Signatures**
Release artifacts include `rsg` signature files that can be validated against the RNS release signing identity `<bc7291552be7a58f361522990465165c>` using `rnid`. To verify files, download the `rsg` signatures, make sure they are in the same folder as the release artifact, and run `rnid` signature verification with the release identity as the required signer:
```sh
rnid -i bc7291552be7a58f361522990465165c -V rns*.whl
```
The `rnid` utility will then verify the signatures, and display whether it is valid. If the signature cannot be verified, the file has been tampered with and should be thrown very far away in a jiffy.
### 2026-05-09: RNS 1.2.5
This release brings substantial improvements to path request handling, and should significantly reduce overall network and local transport node processing loads. Path requests are now automatically ingress and egress limited per interface and sub-interface. Although the defaults are effective and sane, and should work right out of the box bring an end to practically all the PR and announce spam going on lately, the backend is fully configurable for both defaults and per interface, if you want to fiddle with the settings.
+44 -37
View File
@@ -6,6 +6,7 @@ import random
import threading
import ipaddress
import subprocess
from threading import Lock
from .vendor import umsgpack as msgpack
NAME = 0xFF
@@ -86,6 +87,7 @@ class InterfaceAnnouncer():
RNS.trace_exception(e)
def sanitize(self, in_str):
if in_str == None: return None
sanitized = in_str.replace("\n", "")
sanitized = sanitized.replace("\r", "")
sanitized = sanitized.strip()
@@ -374,6 +376,8 @@ class InterfaceDiscovery():
AUTOCONNECT_TYPES = ["BackboneInterface", "TCPServerInterface"]
DISCOVERABLE_TYPES = ["BackboneInterface", "TCPServerInterface", "I2PInterface", "RNodeInterface", "WeaveInterface", "KISSInterface"]
discovery_lock = Lock()
def __init__(self, required_value=InterfaceAnnouncer.DEFAULT_STAMP_VALUE, callback=None, discover_interfaces=True):
if not required_value: required_value = InterfaceAnnouncer.DEFAULT_STAMP_VALUE
@@ -401,8 +405,10 @@ class InterfaceDiscovery():
discovery_sources = RNS.Reticulum.interface_discovery_sources()
for filename in os.listdir(self.storagepath):
try:
filepath = os.path.join(self.storagepath, filename)
with open(filepath, "rb") as f: info = msgpack.unpackb(f.read())
with self.discovery_lock:
filepath = os.path.join(self.storagepath, filename)
with open(filepath, "rb") as f: info = msgpack.unpackb(f.read())
should_remove = False
heard_delta = now-info["last_heard"]
info["name"] = InterfaceAnnounceHandler.sanitize_name(info["name"])
@@ -434,8 +440,8 @@ class InterfaceDiscovery():
if should_append: discovered_interfaces.append(info)
except Exception as e:
RNS.log(f"Error while loading discovered interface data: {e}", RNS.LOG_ERROR)
RNS.log(f"The interface data file {os.path.join(self.storagepath, filename)} may be corrupt", RNS.LOG_ERROR)
RNS.log(f"Error while loading discovered interface data: {e}", RNS.LOG_WARNING)
RNS.log(f"The interface data file {os.path.join(self.storagepath, filename)} may be corrupt", RNS.LOG_WARNING)
RNS.trace_exception(e)
discovered_interfaces.sort(key=lambda info: (info["status_code"], info["value"], info["last_heard"]), reverse=True)
@@ -453,44 +459,45 @@ class InterfaceDiscovery():
filename = RNS.hexrep(discovery_hash, delimit=False)
filepath = os.path.join(self.storagepath, filename)
RNS.log(f"Discovered {interface_type} {hops} hop{ms} away with stamp value {value}: {name}", RNS.LOG_DEBUG)
if not os.path.isfile(filepath):
try:
with open(filepath, "wb") as f:
info["discovered"] = info["received"]
info["last_heard"] = info["received"]
info["heard_count"] = 0
f.write(msgpack.packb(info))
except Exception as e:
RNS.log(f"Error while persisting discovered interface data: {e}", RNS.LOG_ERROR)
RNS.trace_exception(e)
return
else:
discovered = None
heard_count = None
try:
with self.discovery_lock:
if not os.path.isfile(filepath):
try:
with open(filepath, "rb") as f:
last_info = msgpack.unpackb(f.read())
discovered = last_info["discovered"]
heard_count = last_info["heard_count"]
with open(filepath, "wb") as f:
info["discovered"] = info["received"]
info["last_heard"] = info["received"]
info["heard_count"] = 0
f.write(msgpack.packb(info))
except Exception as e:
RNS.log(f"Error while persisting discovered interface data: {e}", RNS.LOG_ERROR)
RNS.trace_exception(e)
return
except Exception as e: RNS.log(f"Error while reading existing data for discovered interface, re-creating data", RNS.LOG_ERROR)
else:
discovered = None
heard_count = None
try:
try:
with open(filepath, "rb") as f:
last_info = msgpack.unpackb(f.read())
discovered = last_info["discovered"]
heard_count = last_info["heard_count"]
if discovered == None: discovered = info["received"]
if heard_count == None: heard_count = 0
except Exception as e: RNS.log(f"Error while reading existing data for discovered interface, re-creating data", RNS.LOG_ERROR)
with open(filepath, "wb") as f:
info["discovered"] = discovered
info["last_heard"] = info["received"]
info["heard_count"] = heard_count+1
f.write(msgpack.packb(info))
if discovered == None: discovered = info["received"]
if heard_count == None: heard_count = 0
except Exception as e:
RNS.log(f"Error while persisting discovered interface data: {e}", RNS.LOG_ERROR)
RNS.trace_exception(e)
return
with open(filepath, "wb") as f:
info["discovered"] = discovered
info["last_heard"] = info["received"]
info["heard_count"] = heard_count+1
f.write(msgpack.packb(info))
except Exception as e:
RNS.log(f"Error while persisting discovered interface data: {e}", RNS.LOG_ERROR)
RNS.trace_exception(e)
return
except Exception as e:
RNS.log(f"Error processing discovered interface data: {e}", RNS.LOG_ERROR)
+6 -6
View File
@@ -138,12 +138,6 @@ class ReticulumGitClient():
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:
@@ -152,6 +146,12 @@ class ReticulumGitClient():
else: self.__create_default_config()
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
self.__apply_config()
self.ready = True
+135 -44
View File
@@ -60,8 +60,9 @@ class NomadNetworkNode():
PATH_RELEASE = "/page/release.mu"
PATH_WORK = "/page/work.mu"
PATH_WORK_DOC = "/page/work_doc.mu"
PATH_ARTIFACT = "/file/artifact"
PATH_DOWNLOAD = "/file/download"
FILE_ARTIFACT = "/file/artifact"
FILE_DOWNLOAD = "/file/download"
FILE_WORKDOC = "/file/workdoc"
BLOB_SIZE_LIMIT = 256 * 1024
TREE_ENTRIES_PER_PAGE = 1000
@@ -221,8 +222,9 @@ class NomadNetworkNode():
self.destination.register_request_handler(self.PATH_RELEASE, response_generator=self.serve_release_page, allow=RNS.Destination.ALLOW_ALL)
self.destination.register_request_handler(self.PATH_WORK, response_generator=self.serve_work_page, allow=RNS.Destination.ALLOW_ALL)
self.destination.register_request_handler(self.PATH_WORK_DOC, response_generator=self.serve_work_doc_page, allow=RNS.Destination.ALLOW_ALL)
self.destination.register_request_handler(self.PATH_ARTIFACT, response_generator=self.serve_artifact, allow=RNS.Destination.ALLOW_ALL)
self.destination.register_request_handler(self.PATH_DOWNLOAD, response_generator=self.serve_download, allow=RNS.Destination.ALLOW_ALL)
self.destination.register_request_handler(self.FILE_ARTIFACT, response_generator=self.serve_artifact, allow=RNS.Destination.ALLOW_ALL)
self.destination.register_request_handler(self.FILE_DOWNLOAD, response_generator=self.serve_download, allow=RNS.Destination.ALLOW_ALL)
self.destination.register_request_handler(self.FILE_WORKDOC, response_generator=self.serve_wd_download, allow=RNS.Destination.ALLOW_ALL)
def get_template(self, template):
filename = f"{template}.mu"
@@ -342,7 +344,7 @@ class NomadNetworkNode():
st = time.time()
RNS.log(f"Group page request from {remote_identity}", RNS.LOG_DEBUG)
if not data: data = {}
if not data or not type(data) == dict: data = {}
group_name = data.get("var_g", "") if data else ""
if not group_name:
@@ -385,7 +387,7 @@ class NomadNetworkNode():
st = time.time()
RNS.log(f"Repository page request from {remote_identity}", RNS.LOG_DEBUG)
if not data: data = {}
if not data or not type(data) == dict: data = {}
group_name = data.get("var_g", "") if data else ""
repo_name = data.get("var_r", "") if data else ""
ref = data.get("var_ref", "HEAD") if data else "HEAD"
@@ -454,20 +456,18 @@ class NomadNetworkNode():
content_parts.append(self.m_divider())
if readme_is_markdown:
converted = self.mdc.format_block(readme_content)
url_scope = f":/page/blob.mu`g={group_name}|r={repo_name}|ref={ref}|path="
mdc = MarkdownToMicron(max_width=self.MAX_RENDER_WIDTH, syntax_highlighter=self.highlighter, url_scope=url_scope)
converted = mdc.format_block(readme_content)
content_parts.append(converted)
else: content_parts.append(f"\n{readme_content}\n")
content_parts.append("\n")
content_parts.append(self.m_divider())
else: content_parts.append(f"\n{readme_content}")
else:
content_parts.append(self.m_divider())
content_parts.append("\n")
content_parts.append(self.m_italic("No README file found in this repository."))
content_parts.append("\n")
content_parts.append("\n")
self.owner.view_succeeded(group_name, repo_name, remote_identity)
page_content = "".join(content_parts)
@@ -478,7 +478,7 @@ class NomadNetworkNode():
st = time.time()
RNS.log(f"Tree page request from {remote_identity}", RNS.LOG_DEBUG)
if not data: data = {}
if not data or not type(data) == dict: data = {}
group_name = data.get("var_g", "") if data else ""
repo_name = data.get("var_r", "") if data else ""
ref = data.get("var_ref", "HEAD") if data else "HEAD"
@@ -675,7 +675,7 @@ class NomadNetworkNode():
nav_parts.append(">>\n" + breadcrumb + "\n")
sep = self.icon("sep")
dl_link = self.m_link("Download", self.PATH_DOWNLOAD, g=group_name, r=repo_name, ref=ref, path=file_path)
dl_link = self.m_link("Download", self.FILE_DOWNLOAD, g=group_name, r=repo_name, ref=ref, path=file_path)
if not renderable: nav_parts.append(f"\nDisplaying Raw {sep} {dl_link}\n")
else:
rnd_link = self.m_link("View rendered", self.PATH_BLOB, g=group_name, r=repo_name, ref=ref, path=file_path, render="y")
@@ -742,7 +742,7 @@ class NomadNetworkNode():
st = time.time()
RNS.log(f"Commits page request from {remote_identity}", RNS.LOG_DEBUG)
if not data: data = {}
if not data or not type(data) == dict: data = {}
group_name = data.get("var_g", "") if data else ""
repo_name = data.get("var_r", "") if data else ""
ref = data.get("var_ref", "HEAD") if data else "HEAD"
@@ -825,7 +825,7 @@ class NomadNetworkNode():
st = time.time()
RNS.log(f"Commit page request from {remote_identity}", RNS.LOG_DEBUG)
if not data: data = {}
if not data or not type(data) == dict: data = {}
group_name = data.get("var_g", "") if data else ""
repo_name = data.get("var_r", "") if data else ""
commit_hash = data.get("var_h", "") if data else ""
@@ -960,7 +960,7 @@ class NomadNetworkNode():
st = time.time()
RNS.log(f"Refs page request from {remote_identity}", RNS.LOG_DEBUG)
if not data: data = {}
if not data or not type(data) == dict: data = {}
group_name = data.get("var_g", "") if data else ""
repo_name = data.get("var_r", "") if data else ""
ref_type = data.get("var_type", "") if data else "" # "heads", "tags", or empty for both
@@ -1064,7 +1064,7 @@ class NomadNetworkNode():
st = time.time()
RNS.log(f"Statistics page request from {remote_identity}", RNS.LOG_DEBUG)
if not data: data = {}
if not data or not type(data) == dict: data = {}
group_name = data.get("var_g", "") if data else ""
repo_name = data.get("var_r", "") if data else ""
@@ -1138,7 +1138,7 @@ class NomadNetworkNode():
content_parts.append("\n")
content_parts.append(self.render_combined_chart(stats["views"]["daily"], stats["fetches"]["daily"], stats["pushes"]["daily"], stats["timeline_labels"]))
else: content_parts.append(self.m_italic("\nNo activity recorded for this repository in the selected time period.\n\n"))
else: content_parts.append(self.m_italic("\nNo development activity recorded for this repository in the selected time period.\n\n"))
page_content = "".join(content_parts)
nav_content = "".join(nav_parts)
@@ -1148,7 +1148,7 @@ class NomadNetworkNode():
st = time.time()
RNS.log(f"Releases page request from {remote_identity}", RNS.LOG_DEBUG)
if not data: data = {}
if not data or not type(data) == dict: data = {}
group_name = data.get("var_g", "") if data else ""
repo_name = data.get("var_r", "") if data else ""
@@ -1210,7 +1210,7 @@ class NomadNetworkNode():
st = time.time()
RNS.log(f"Release page request from {remote_identity}", RNS.LOG_DEBUG)
if not data: data = {}
if not data or not type(data) == dict: data = {}
group_name = data.get("var_g", "") if data else ""
repo_name = data.get("var_r", "") if data else ""
tag = data.get("var_t", "") if data else ""
@@ -1260,6 +1260,11 @@ class NomadNetworkNode():
sep = self.icon("sep")
heart = self.icon("heart")
thanks = True if data.get("var_thanks", "") else False
thanks_count = self.release_thanks(release_dir, add=thanks, link_id=link_id)
content_parts.append(f"{self.m_link_r(self.icon('heart')+f' Thanks ({thanks_count})', self.PATH_RELEASE, g=group_name, r=repo_name, t=tag, thanks='y')}\n\n")
created_ts = release_info.get("created", 0)
ts_str = f" {sep} {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(created_ts))}" if created_ts else ""
content_parts.append(self.m_heading(f"Release {tag}{ts_str}", 2))
@@ -1279,7 +1284,7 @@ class NomadNetworkNode():
if artifacts:
content_parts.append(self.m_heading(f"Artifacts ({len(artifacts)})", 2))
content_parts.append("\n")
for art in artifacts:
for art in sorted(artifacts, key=lambda e: e["name"]):
name = art.get("name", "unknown")
size = art.get("size", 0)
size_str = RNS.prettysize(size) if size else "0 B"
@@ -1287,18 +1292,13 @@ class NomadNetworkNode():
lstr_1 = f"{self.icon('file')} {self.m_escape(name)}"
lstr_2 = f"({size_str})"
link_1 = self.m_link_r(lstr_1, self.PATH_ARTIFACT, g=group_name, r=repo_name, t=tag, a=name)
link_2 = self.m_link_r(lstr_2, self.PATH_ARTIFACT, g=group_name, r=repo_name, t=tag, a=name)
link_1 = self.m_link_r(lstr_1, self.FILE_ARTIFACT, g=group_name, r=repo_name, t=tag, a=name)
link_2 = self.m_link_r(lstr_2, self.FILE_ARTIFACT, g=group_name, r=repo_name, t=tag, a=name)
content_parts.append(f"{link_1} {self.CLR_DIM}{link_2}`f\n")
content_parts.append("\n")
else:
content_parts.append(self.m_heading("Artifacts", 2))
content_parts.append("\nNo artifacts for this release.\n\n")
thanks = True if data.get("var_thanks", "") else False
thanks_count = self.release_thanks(release_dir, add=thanks, link_id=link_id)
content_parts.append(f"{self.m_link_r(self.icon('heart')+f' Thanks ({thanks_count})', self.PATH_RELEASE, g=group_name, r=repo_name, t=tag, thanks='y')}\n")
content_parts.append("\n`*No artifacts for this release`*\n")
self.owner.view_succeeded(group_name, repo_name, remote_identity)
page_content = "".join(content_parts)
@@ -1308,7 +1308,7 @@ class NomadNetworkNode():
st = time.time()
RNS.log(f"Work page request from {remote_identity}", RNS.LOG_DEBUG)
if not data: data = {}
if not data or not type(data) == dict: data = {}
group_name = data.get("var_g", "") if data else ""
repo_name = data.get("var_r", "") if data else ""
scope = data.get("var_scope", "active") if data else "active"
@@ -1402,11 +1402,11 @@ class NomadNetworkNode():
st = time.time()
RNS.log(f"Work document page request from {remote_identity}", RNS.LOG_DEBUG)
if not data: data = {}
if not data or not type(data) == dict: data = {}
group_name = data.get("var_g", "") if data else ""
repo_name = data.get("var_r", "") if data else ""
doc_id = data.get("var_id", "") if data else ""
scope = data.get("var_scope", "active") if data else "active"
scope = data.get("var_scope", "all") if data else "all"
if scope not in ["active", "completed", "all"]: scope = "active"
if not group_name or not repo_name or not doc_id:
@@ -1424,7 +1424,19 @@ class NomadNetworkNode():
return self.render_template(content, st=st)
work_path = f"{repo['path']}.work"
doc_dir = os.path.join(work_path, scope, str(doc_id))
active_dir = os.path.join(work_path, "active", str(doc_id))
completed_dir = os.path.join(work_path, "completed", str(doc_id))
if scope == "active": doc_dir = active_dir
elif scope == "completed": doc_dir = completed_dir
elif scope == "all":
if os.path.isdir(active_dir):
doc_dir = active_dir
scope = "active"
else:
doc_dir = completed_dir
scope = "completed"
root_path = os.path.join(doc_dir, "root")
if not os.path.isfile(root_path):
@@ -1440,29 +1452,45 @@ class NomadNetworkNode():
nav_parts = []
# Breadcrumb navigation
dl_link = self.m_link("Download", self.FILE_WORKDOC, g=group_name, r=repo_name, id=doc_id)
breadcrumb = f">>\n{self.m_link('Node', self.PATH_INDEX)} / {self.m_link(group_name, self.PATH_GROUP, g=group_name)} / {self.m_link(repo_name, self.PATH_REPO, g=group_name, r=repo_name)} / {self.m_link('work', self.PATH_WORK, g=group_name, r=repo_name)} / #{doc_id}"
nav_parts.append(breadcrumb + "\n")
nav_parts.append(f"\n{dl_link}\n")
nav_content = "".join(nav_parts)
doc_title = doc['meta'].get('title', 'Untitled')[:64]
doc_title = doc['meta'].get('title', 'Untitled')[:256]
if len(doc_title) < len(doc['meta'].get('title', 'Untitled')): doc_title += ""
meta = doc.get("meta", {})
author = meta.get("author", b"")
author_str = RNS.prettyhexrep(author) if author else "Unknown"
signature = meta.get("signature", None)
pubkey = meta.get("identity", None)
created = meta.get("created", 0)
edited = meta.get("edited", 0)
fmt = meta.get("format", "markdown")
content = doc.get("content", "")
signature_validated = False
signature_str = "Document not signed"
if signature and type(signature) == bytes and len(signature) == RNS.Identity.SIGLENGTH//8:
if pubkey and type(pubkey) == bytes and len(pubkey) == RNS.Identity.KEYSIZE//8:
signature_str = "Not valid"
identity = RNS.Identity(create_keys=False)
identity.load_public_key(pubkey)
signature_validated = identity.validate(signature, content.encode("utf-8"))
if signature_validated: signature_str = "Valid"
# Document header
content_parts.append(self.m_heading(f"{doc_title}", 2))
content_parts.append(f"\n{self.CLR_DIM}Author : {author_str}`f\n")
content_parts.append(f"{self.CLR_DIM}Created : {time.strftime('%Y-%m-%d %H:%M', time.localtime(created)) if created else 'unknown'}`f\n")
content_parts.append(f"\n{self.CLR_DIM}Author : {author_str}`f\n")
content_parts.append(f"{self.CLR_DIM}Signature : {signature_str}`f\n")
content_parts.append(f"{self.CLR_DIM}Created : {time.strftime('%Y-%m-%d %H:%M', time.localtime(created)) if created else 'unknown'}`f\n")
if edited and edited != created:
content_parts.append(f"{self.CLR_DIM}Edited : {time.strftime('%Y-%m-%d %H:%M', time.localtime(edited))}`f\n")
content_parts.append(f"{self.CLR_DIM}Status : {scope.capitalize()}`f\n\n")
content_parts.append(f"{self.CLR_DIM}Edited : {time.strftime('%Y-%m-%d %H:%M', time.localtime(edited))}`f\n")
content_parts.append(f"{self.CLR_DIM}Status : {scope.capitalize()}`f\n\n")
# Document content
content = doc.get("content", "").strip()
content = content.strip()
if content:
if fmt == "micron": content_parts.append(content)
else: content_parts.append(self.mdc.format_block(content))
@@ -1511,7 +1539,7 @@ class NomadNetworkNode():
st = time.time()
RNS.log(f"Artifact file request from {remote_identity}", RNS.LOG_DEBUG)
if not data: data = {}
if not data or not type(data) == dict: data = {}
group_name = data.get("var_g", "") if data else ""
repo_name = data.get("var_r", "") if data else ""
tag = data.get("var_t", "") if data else ""
@@ -1567,6 +1595,7 @@ class NomadNetworkNode():
st = time.time()
RNS.log(f"File download request from {remote_identity}", RNS.LOG_DEBUG)
if not data or not type(data) == dict: data = {}
group_name = data.get("var_g", "") if data else ""
repo_name = data.get("var_r", "") if data else ""
ref = data.get("var_ref", "HEAD") if data else "HEAD"
@@ -1604,6 +1633,68 @@ class NomadNetworkNode():
return None
def serve_wd_download(self, path, data, request_id, link_id, remote_identity, requested_at):
st = time.time()
RNS.log(f"Workdoc download request from {remote_identity}", RNS.LOG_DEBUG)
if not data or not type(data) == dict: data = {}
group_name = data.get("var_g", "") if data else ""
repo_name = data.get("var_r", "") if data else ""
doc_id = data.get("var_id", "") if data else ""
scope = data.get("var_scope", "all") if data else "all"
if scope not in ["active", "completed", "all"]: scope = "active"
if not group_name or not repo_name or not doc_id:
RNS.log(f"Invalid workdoc download request for {group_name[:128]}/{repo_name[:128]}/{doc_id[:128]}", RNS.LOG_WARNING)
return None
try: doc_id = int(doc_id)
except:
RNS.log(f"Could not parse document ID for workdoc download request {group_name[:128]}/{repo_name[:128]}/{doc_id[:128]}", RNS.LOG_WARNING)
return None
repo = self.get_accessible_repository(remote_identity, group_name, repo_name)
if not repo:
RNS.log(f"Repository not found or no access for workdoc download request {group_name[:128]}/{repo_name[:128]}/{doc_id[:128]}", RNS.LOG_WARNING)
return None
work_path = f"{repo['path']}.work"
active_dir = os.path.join(work_path, "active", str(doc_id))
completed_dir = os.path.join(work_path, "completed", str(doc_id))
if scope == "active": doc_dir = active_dir
elif scope == "completed": doc_dir = completed_dir
elif scope == "all":
if os.path.isdir(active_dir):
doc_dir = active_dir
scope = "active"
else:
doc_dir = completed_dir
scope = "completed"
root_path = os.path.join(doc_dir, "root")
if not os.path.isfile(root_path):
RNS.log(f"Document not found for workdoc download request {group_name[:128]}/{repo_name[:128]}/{doc_id[:128]}", RNS.LOG_WARNING)
return None
doc = self.owner._work_load_document(root_path)
if not doc:
RNS.log(f"Could not load document for workdoc download request {group_name[:128]}/{repo_name[:128]}/{doc_id[:128]}", RNS.LOG_WARNING)
return None
meta = doc.get("meta", {})
fmt = meta.get("format", "markdown")
title = meta.get('title', 'Untitled')[:256]
content = doc.get("content", "").strip()
if content:
if fmt == "micron": file_name = f"{title}.mu"
else: file_name = f"{title}.md"
return [file_name, content.encode("utf-8")]
return None
#######################
# Git Data Extraction #
#######################
@@ -2141,7 +2232,7 @@ class NomadNetworkNode():
with open(thanks_path, "wb") as fh: fh.write(mp.packb({"count": thanks_count}))
return thanks_count
except Exception as e: RNS.log(f"Error while processing repository thanks for {group_name}/{repo_name}: {e}", RNS.LOG_ERROR)
except Exception as e: RNS.log(f"Error while processing repository thanks for {repo_path}: {e}", RNS.LOG_ERROR)
return 0
def release_thanks(self, release_path, add=False, link_id=None):
@@ -2166,7 +2257,7 @@ class NomadNetworkNode():
with open(thanks_path, "wb") as fh: fh.write(mp.packb({"count": thanks_count}))
return thanks_count
except Exception as e: RNS.log(f"Error while processing release thanks for {group_name}/{repo_name}: {e}", RNS.LOG_ERROR)
except Exception as e: RNS.log(f"Error while processing release thanks for {release_path}: {e}", RNS.LOG_ERROR)
return 0
###################
+106 -35
View File
@@ -95,7 +95,7 @@ def program_setup(configdir, rnsconfigdir=None, verbosity=0, quietness=0, servic
if operation == "list": git_client.work_list(remote=task["remote"], scope=scope)
elif operation == "view": git_client.work_view(remote=task["remote"], doc_id=doc_id, scope=scope)
elif operation == "create": git_client.work_create(remote=task["remote"], title=title)
elif operation == "edit": git_client.work_edit(remote=task["remote"], doc_id=doc_id, scope=scope)
elif operation == "edit": git_client.work_edit(remote=task["remote"], title=title, doc_id=doc_id, scope=scope)
elif operation == "delete": git_client.work_delete(remote=task["remote"], doc_id=doc_id, scope=scope)
elif operation == "update": git_client.work_comment(remote=task["remote"], doc_id=doc_id, scope=scope)
elif operation == "complete": git_client.work_complete(remote=task["remote"], doc_id=doc_id)
@@ -317,8 +317,7 @@ class ReticulumGitClient():
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
@@ -727,16 +726,32 @@ class ReticulumGitClient():
if len(response) <= 1: self.abort("Empty response from remote")
doc = mp.unpackb(response[1:])
author_str = f"{doc['meta']['author']} (not locally validated)"
signature_str = "Document not signed"
signature = doc["meta"].get("signature", None)
pubkey = doc["meta"].get("identity", None)
content = doc.get("content", "")
if signature and type(signature) == bytes and len(signature) == RNS.Identity.SIGLENGTH//8:
if pubkey and type(pubkey) == bytes and len(pubkey) == RNS.Identity.KEYSIZE//8:
signature_str = "Not valid"
identity = RNS.Identity(create_keys=False)
identity.load_public_key(pubkey)
signature_validated = identity.validate(signature, content.encode("utf-8"))
if signature_validated:
signature_str = "Valid"
author_str = RNS.prettyhexrep(identity.hash)
dt = f"{doc['meta']['title']} (#{doc['id']})"
print(f"{dt}")
print("="*len(dt))
print(f"Author : {doc['meta']['author']}")
print(f"Status : {scope}")
print(f"Created : {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(doc['meta']['created']))}")
print(f"Edited : {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(doc['meta']['edited']))}")
print(f"Format : {doc['meta']['format']}")
print(f"Updates : {len(doc.get('comments', []))}")
print(f"Author : {author_str}")
print(f"Signature : {signature_str}")
print(f"Status : {scope.capitalize()}")
print(f"Created : {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(doc['meta']['created']))}")
print(f"Edited : {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(doc['meta']['edited']))}")
print(f"Format : {doc['meta']['format']}")
print(f"Updates : {len(doc.get('comments', []))}")
print()
print(doc['content'])
@@ -775,9 +790,13 @@ class ReticulumGitClient():
content = self._edit_work_content(title=title)
if content is None: print("Creation cancelled"); return
signature = self.identity.sign(content.encode("utf-8"))
if not signature: self.abort("Could not sign work document")
request_data = { self.IDX_REPOSITORY: repo_path,
"operation": "create", "title": title, "content": content, "format": "markdown" }
request_data = { self.IDX_REPOSITORY: repo_path, "operation": "create",
"title": title, "content": content, "format": "markdown",
"signature": signature }
response, metadata = self.send_request(self.PATH_WORK, request_data, timeout=30)
if not response or not isinstance(response, bytes): self.abort("No response from remote")
@@ -797,7 +816,7 @@ class ReticulumGitClient():
finally:
if self.link: self.link.teardown()
def work_edit(self, remote=None, doc_id=None, scope="active"):
def work_edit(self, remote=None, doc_id=None, title=None, scope="active"):
if not remote: print(f"No remote specified"); exit(1)
if doc_id is None: print(f"No document ID specified"); exit(1)
self.connect_remote(remote)
@@ -830,9 +849,12 @@ class ReticulumGitClient():
content = self._edit_work_content(title=current_title, content=current_content)
if content is None: print("Edit cancelled"); return
title = current_title
request_data = { self.IDX_REPOSITORY: repo_path,
"operation": "edit", "doc_id": doc_id, "scope": scope, "content": content }
signature = self.identity.sign(content.encode("utf-8"))
if not signature: self.abort("Could not sign work document")
title = title or current_title
request_data = { self.IDX_REPOSITORY: repo_path, "operation": "edit", "doc_id": doc_id,
"scope": scope, "content": content, "title": title, "signature": signature }
response, metadata = self.send_request(self.PATH_WORK, request_data, timeout=30)
if not response or not isinstance(response, bytes): self.abort("No response from remote")
@@ -2415,13 +2437,22 @@ class ReticulumGitNode():
def _work_view(self, work_path, data, remote_identity):
doc_id = data.get("doc_id")
scope = data.get("scope", "active")
scope = data.get("scope", "all")
if not scope in ["active", "completed", "all"]: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid request"
if doc_id is None: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"No document ID specified"
try: doc_id = int(doc_id)
except: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid document ID"
scope = None
doc_dir = None
for s in ["active", "completed"]:
d = os.path.join(work_path, s, str(doc_id))
if os.path.isdir(d):
scope = s
doc_dir = d
break
doc_dir = os.path.join(work_path, scope, str(doc_id))
root_path = os.path.join(doc_dir, "root")
@@ -2449,13 +2480,16 @@ class ReticulumGitNode():
comments.sort(key=lambda x: x["id"])
meta = doc.get("meta", {})
result = { "id": doc_id, "scope": scope,
"content": doc.get("content", ""), "comments": comments,
"meta": { "title": doc.get("meta", {}).get("title", "Untitled"),
"created": doc.get("meta", {}).get("created", 0),
"edited": doc.get("meta", {}).get("edited", 0),
"author": RNS.hexrep(doc.get("meta", {}).get("author", b""), delimit=False) if doc.get("meta", {}).get("author") else "",
"format": doc.get("meta", {}).get("format", "markdown") } }
"meta": { "title": meta.get("title", "Untitled"),
"created": meta.get("created", 0),
"edited": meta.get("edited", 0),
"author": RNS.hexrep(meta.get("author", b""), delimit=False) if meta.get("author") else "",
"identity": meta.get("identity", None),
"signature": meta.get("signature", None),
"format": meta.get("format", "markdown") } }
return b"\x00" + mp.packb(result)
@@ -2464,9 +2498,13 @@ class ReticulumGitNode():
content = data.get("content", "").strip()
format_type = data.get("format", "markdown")
signature = data.get("signature", None)
signed_data = content.encode("utf-8")
sig_length = RNS.Identity.SIGLENGTH//8
limit = self.WORK_DOC_LIMIT
if signature and not len(signature) == RNS.Identity.SIGLENGTH: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid signature"
if not signature: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"No signature provided"
if signature and not len(signature) == sig_length: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid signature length"
if not remote_identity.validate(signature, signed_data): return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid signature"
if len(title)+len(content)+len(format_type) > limit: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Content limit exceeded"
if not title: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Title is required"
if not content: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Content is required"
@@ -2480,8 +2518,8 @@ class ReticulumGitNode():
now = time.time()
document = { "content": content,
"meta": { "format": format_type if format_type in ["markdown", "micron"] else "markdown",
"title": title, "created": now, "edited": now,
"signature": signature, "author": remote_identity.hash } }
"title": title, "created": now, "edited": now, "author": remote_identity.hash,
"signature": signature, "identity": remote_identity.get_public_key() } }
root_path = os.path.join(doc_dir, "root")
if not self._work_save_document(root_path, document):
@@ -2495,28 +2533,42 @@ class ReticulumGitNode():
return self.RES_REMOTE_FAIL.to_bytes(1, "big") + b"Remote error"
def _work_edit(self, work_path, data, remote_identity):
doc_id = data.get("doc_id")
scope = data.get("scope", "active")
content = data.get("content")
title = data.get("title")
signature = data.get("signature", None)
limit = self.WORK_DOC_LIMIT
doc_id = data.get("doc_id")
scope = data.get("scope", "active")
content = data.get("content", "")
title = data.get("title", "")
signature = data.get("signature", None)
signed_data = content.encode("utf-8")
sig_length = RNS.Identity.SIGLENGTH//8
limit = self.WORK_DOC_LIMIT
size = 0
if title: size += len(title)
if content: size += len(content)
if not scope in ["active", "completed", "all"]: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid request"
if signature and not len(signature) == RNS.Identity.SIGLENGTH: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid signature"
if not signature: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"No signature provided"
if signature and not len(signature) == sig_length: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid signature length"
if not remote_identity.validate(signature, signed_data): return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid signature"
if size > limit: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Content limit exceeded"
if content is None and title is None: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"No changes specified"
if doc_id is None: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"No document ID specified"
if not content and not title: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"No changes specified"
if not doc_id: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"No document ID specified"
try: doc_id = int(doc_id)
except: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid document ID"
scope = None
doc_dir = None
for s in ["active", "completed"]:
d = os.path.join(work_path, s, str(doc_id))
if os.path.isdir(d):
scope = s
doc_dir = d
break
doc_dir = os.path.join(work_path, scope, str(doc_id))
root_path = os.path.join(doc_dir, "root")
RNS.log(f"PATH: {root_path}")
if not os.path.isfile(root_path): return self.RES_NOT_FOUND.to_bytes(1, "big") + b"Document not found"
@@ -2526,10 +2578,11 @@ class ReticulumGitNode():
if doc.get("meta", {}).get("author") != remote_identity.hash: return self.RES_DISALLOWED.to_bytes(1, "big") + b"No access, not author"
try:
if title is not None: doc["meta"]["title"] = title.strip()
if content is not None: doc["content"] = content.strip()
if title: doc["meta"]["title"] = title.strip()
if content: doc["content"] = content.strip()
doc["meta"]["edited"] = time.time()
doc["meta"]["signature"] = signature
doc["meta"]["identity"] = remote_identity.get_public_key()
if not self._work_save_document(root_path, doc): return self.RES_REMOTE_FAIL.to_bytes(1, "big") + b"Error saving document"
@@ -2549,6 +2602,15 @@ class ReticulumGitNode():
try: doc_id = int(doc_id)
except: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid document ID"
scope = None
doc_dir = None
for s in ["active", "completed"]:
d = os.path.join(work_path, s, str(doc_id))
if os.path.isdir(d):
scope = s
doc_dir = d
break
doc_dir = os.path.join(work_path, scope, str(doc_id))
root_path = os.path.join(doc_dir, "root")
@@ -2586,6 +2648,15 @@ class ReticulumGitNode():
except: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid document ID"
if not content: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Content is required"
scope = None
doc_dir = None
for s in ["active", "completed"]:
d = os.path.join(work_path, s, str(doc_id))
if os.path.isdir(d):
scope = s
doc_dir = d
break
doc_dir = os.path.join(work_path, scope, str(doc_id))
root_path = os.path.join(doc_dir, "root")
+215 -30
View File
@@ -45,10 +45,12 @@ from RNS.Cryptography.Hashes import file_sha256
APP_NAME = "rns"
DEFAULT_ASPECTS = f"{APP_NAME}.id"
NO_MESSAGE = 0x01
PRV_EXT = "rid"
PUB_EXT = "pub"
SIG_EXT = "rsg"
MSG_EXT = "rsm"
ENCRYPT_EXT = "rfe"
CHUNK_BLOCKS = 1024*1024
ENC_CHUNK = CHUNK_BLOCKS*RNS.Identity.AES256_BLOCKSIZE
@@ -69,6 +71,8 @@ R_INVALID_ASPECTS = 9
R_INVALID_SIGNATURE = 10
R_FILE_EXISTS = 11
R_DECRYPT_FAILED = 12
R_INVALID_ARGS = 250
R_SEQUENCE_ERROR = 251
R_READ_ERROR = 252
R_WRITE_ERROR = 253
R_UNKNOWN_ERROR = 254
@@ -78,7 +82,7 @@ reticulum = None
def validate_args(args):
ops = 0;
for o in [args.encrypt, args.decrypt, args.validate, args.sign]:
for o in [args.encrypt, args.decrypt, args.validate, args.sign, args.sign_message]:
if o: ops += 1
if ops > 1: print("This utility currently only supports one of the encrypt, decrypt, sign or verify operations per invocation"); exit(1)
@@ -88,9 +92,9 @@ def validate_args(args):
if g > 1: print("The -i, -g, -m and -M args are mutually exclusive"); exit(1)
g = 0;
for a in [args.base64, args.base32, args.hex]:
for a in [args.base64, args.base32, args.base256, args.hex]:
if a: g += 1
if g > 1: print("The -b, -B and --hex args are mutually exclusive"); exit(1)
if g > 1: print("The -b, -B, --hex and --base256 args are mutually exclusive"); exit(1)
return True
@@ -114,10 +118,11 @@ def main():
# Operations
parser.add_argument("-a", "--announce", metavar="aspects", action="store", nargs="?", const=DEFAULT_ASPECTS, default=None, help="announce a destination based on this Identity")
parser.add_argument("-H", "--hash", metavar="aspects", action="store", default=None, help="show destination hashes for other aspects for this Identity")
parser.add_argument("-d", "--decrypt", metavar="file", action="store", default=None, help="decrypt file")
parser.add_argument("-e", "--encrypt", metavar="file", action="store", default=None, help="encrypt file")
parser.add_argument("-V", "--validate", metavar="path", action="store", default=None, help="validate signature")
parser.add_argument("-s", "--sign", metavar="path", action="store", default=None, help="sign file")
parser.add_argument("-d", "--decrypt", metavar="file", action="store", nargs="*", default=None, help="decrypt file")
parser.add_argument("-e", "--encrypt", metavar="file", action="store", nargs="*", default=None, help="encrypt file")
parser.add_argument("-V", "--validate", metavar="path", action="store", nargs="*", default=None, help="validate signature")
parser.add_argument("-s", "--sign", metavar="path", action="store", nargs="*", default=None, help="sign file")
parser.add_argument("-S", "--sign-message", metavar="path", action="store", nargs="?", const=NO_MESSAGE, default=None, help="create embedded signed message")
parser.add_argument("--raw", action="store_true", default=False, help="sign raw input data instead of hashing first")
# I/O Control
@@ -137,6 +142,7 @@ def main():
parser.add_argument("-b", "--base64", action="store_true", default=False, help="Use base64-encoded input and output")
parser.add_argument("-B", "--base32", action="store_true", default=False, help="Use base32-encoded input and output")
parser.add_argument("--hex", action="store_true", default=False, help="Use hex-encoded input and output")
parser.add_argument("--base256", action="store_true", default=False, help="Use base256-encoded input and output")
parser.add_argument("--version", action="version", version="rnid {version}".format(version=__version__))
@@ -155,6 +161,7 @@ def main():
if args.announce: announce(args, identity); op = True
if args.validate: validate(args, identity or args.identity); op = True
if args.sign: sign(args, identity); op = True
if args.sign_message: sign_message(args, identity); op = True
if args.encrypt: encrypt(args, identity); op = True
if args.decrypt: decrypt(args, identity); op = True
if args.write: write_identity(args, identity); op = True
@@ -391,9 +398,19 @@ def get_rsg_data(rsg):
except: pass
try: rsg_data = bytes.fromhex(rsg.strip(RSG_PADDING))
except: pass
try: rsg_data = RNS.b256_to_bytes(rsg.strip(RSG_PADDING.decode("utf-8")))
except: pass
return rsg_data
def extract_signed_rsg_data(rsg):
siglen = RNS.Identity.SIGLENGTH//8
rsg_data = get_rsg_data(rsg)
envelope = rsg_data[siglen:]
try: return mp.unpackb(envelope)
except: return None
def get_rsg_hash(message):
sha = None
if type(message) == bytes: sha = sha256(message)
@@ -462,16 +479,20 @@ def validate_rsg(rsg, message=None, required_signer=None):
return False, signed_data, signing_identity
def create_rsg(signer_identity, message, note=None, meta=None, output="bin"):
if not output in ["bin", "hex", "base32", "base64"]: raise TypeError(f"Invalid output format for rsg creation")
if not type(signer_identity) == RNS.Identity: raise TypeError(f"{signer_identity} is not a Reticulum Identity")
if not signer_identity.get_private_key(): raise ValueError(f"{signer_identity} does not hold a private key")
def create_rsg(signer_identity, message, embed=False, note=None, meta=None, output="bin"):
if not output in ["bin", "hex", "base32", "base256", "base64"]: raise TypeError(f"Invalid output format for rsg creation")
if not type(signer_identity) == RNS.Identity: raise TypeError(f"{signer_identity} is not a Reticulum Identity")
if not signer_identity.get_private_key(): raise ValueError(f"{signer_identity} does not hold a private key")
signed_data = { "hashtype": "sha256", "hash": get_rsg_hash(message),
"meta": { "signer": signer_identity.hash,
"pubkey": signer_identity.get_public_key(),
"note" : note } }
if embed:
if type(message) == str: message = message.encode("utf-8")
signed_data["message"] = message
if meta and type(meta) == dict:
for key in meta:
if not key in signed_data["meta"]: signed_data["meta"]["key"] = meta["key"]
@@ -480,11 +501,12 @@ def create_rsg(signer_identity, message, note=None, meta=None, output="bin"):
signature = signer_identity.sign(envelope)
rsg_data = signature+envelope
if output == "bin": rsg = rsg_data
elif output == "hex": rsg = RNS.hexrep(rsg_data, delimit=False).encode("ascii")
elif output == "base32": rsg = base64.b32encode(rsg_data)
elif output == "base64": rsg = base64.urlsafe_b64encode(rsg_data)
else: return None
if output == "bin": rsg = rsg_data
elif output == "hex": rsg = RNS.hexrep(rsg_data, delimit=False).encode("ascii")
elif output == "base32": rsg = base64.b32encode(rsg_data)
elif output == "base64": rsg = base64.urlsafe_b64encode(rsg_data)
elif output == "base256": rsg = RNS.b256rep(rsg_data)
else: return None
return rsg
@@ -493,6 +515,7 @@ RSG_ASCII_FOOTER = b" End of rsg data ####"
RSG_ASCII_ROW_WIDTH = 64
RSG_PADDING = b"="
def wrap_rsg(rsg):
if type(rsg) == str: return wrap_rsg_str(rsg)
def pad(chunk): return chunk+(RSG_ASCII_ROW_WIDTH-len(chunk))*RSG_PADDING
header = RSG_ASCII_HEADER+b"#"*(RSG_ASCII_ROW_WIDTH-len(RSG_ASCII_HEADER))
footer = b"#"*(RSG_ASCII_ROW_WIDTH-len(RSG_ASCII_FOOTER))+RSG_ASCII_FOOTER
@@ -507,6 +530,21 @@ def wrap_rsg(rsg):
wrapped += footer
return wrapped.decode("ascii")
def wrap_rsg_str(rsg):
def pad(chunk): return chunk+(RSG_ASCII_ROW_WIDTH-len(chunk))*RSG_PADDING.decode("utf-8")
header = RSG_ASCII_HEADER.decode("utf-8")+"#"*(RSG_ASCII_ROW_WIDTH-len(RSG_ASCII_HEADER.decode("utf-8")))
footer = "#"*(RSG_ASCII_ROW_WIDTH-len(RSG_ASCII_FOOTER.decode("utf-8")))+RSG_ASCII_FOOTER.decode("utf-8")
wrapped = header+"\n"
read = 0
while len(rsg):
chunk = rsg[:RSG_ASCII_ROW_WIDTH]
if len(chunk) < RSG_ASCII_ROW_WIDTH: chunk = pad(chunk)
wrapped += chunk+"\n"; read += len(chunk)
rsg = rsg[len(chunk):]
wrapped += footer
return wrapped
def unwrap_rsg(wrapped_rsg):
unwrapped = ""
if type(wrapped_rsg) == bytes: wrapped_rsg = wrapped_rsg.decode("ascii")
@@ -525,15 +563,30 @@ def unwrap_rsg(wrapped_rsg):
# Signing & Validation Operations #
###################################
def validate(args, identity):
def validate(args, identity, __recursive=False):
if type(args.validate) == list:
paths = args.validate.copy()
validated = 0
for path in paths:
args.validate = path
code = validate(args, identity, __recursive=True)
if code != 0: print(f"Sequence error on recursive signature validation"); exit(R_SEQUENCE_ERROR)
else: validated += 1
if len(paths) != validated: print(f"Sequence error on recursive signature validation"); exit(R_SEQUENCE_ERROR)
else: exit(R_OK)
msg_ext = f".{MSG_EXT}"
sig_ext = f".{SIG_EXT}"
validate_path = os.path.expanduser(args.validate)
path_is_msgfile = validate_path.lower().endswith(msg_ext)
path_is_sigfile = validate_path.lower().endswith(sig_ext)
if path_is_sigfile: signature_path = validate_path; file_path = validate_path[:-len(sig_ext)]
else: signature_path = f"{validate_path}{sig_ext}"; file_path = validate_path
signature_exists = os.path.isfile(signature_path)
file_exists = os.path.isfile(file_path)
if path_is_msgfile: return validate_message(args, identity, __recursive=__recursive)
if not file_exists: print(f"The validation target \"{file_path}\" does not exist"); exit(R_NO_FILE)
if not signature_exists: print(f"No signature file exists for \"{file_path}\""); exit(R_NO_FILE)
@@ -558,7 +611,7 @@ def validate(args, identity):
identity_str = RNS.prettyhexrep(identity) if type(identity) == bytes else f"{identity}"
signer_description = f"\nThis file was NOT signed by {identity_str or signing_identity}" if identity else ""
if not valid: print(f"Invalid signature {signature_path} for file {file_path}{signer_description}"); exit(R_INVALID_SIGNATURE)
else: print(f"Signature is valid, the file {file_path} was signed by {signing_identity}"); exit(R_OK)
else: print(f"Signature is valid, the file {file_path} was signed by {signing_identity}"); return exit(R_OK) if not __recursive else R_OK
except Exception as e: print(f"Error while validating {signature_path}: {e}"); exit(R_UNKNOWN_ERROR)
@@ -577,17 +630,63 @@ def validate(args, identity):
except Exception as e: print(f"Could not validate signature: {e}"); exit(R_READ_ERROR)
def sign(args, identity):
def validate_message(args, identity, __recursive=False):
msg_ext = f".{MSG_EXT}"
validate_path = os.path.expanduser(args.validate)
path_is_msgfile = validate_path.lower().endswith(msg_ext)
if path_is_msgfile: signature_path = validate_path
signature_exists = os.path.isfile(signature_path)
if not signature_exists: print(f"The signature file \"{signature_path}\" does not exist"); exit(R_NO_FILE)
try:
with open(signature_path, "rb") as fh: rsg = fh.read()
except Exception as e: print(f"Could not read rsg: {e}"); exit(R_READ_ERROR)
if type(identity) == str:
if not len(identity) == RNS.Reticulum.TRUNCATED_HASHLENGTH//8*2: print("Invalid identity hash length"); exit(R_INVALID_IDENTITY)
try: identity = bytes.fromhex(identity)
except Exception as e: print(f"Invalid identity hash: {e}"); exit(R_INVALID_IDENTITY)
try:
rsm_contents = extract_signed_rsg_data(rsg)
if not "message" in rsm_contents: print(f"No embedded message in {signature_path}"); exit(R_INVALID_SIGNATURE)
valid, signed_data, signing_identity = validate_rsg(rsg, message=rsm_contents["message"], required_signer=identity)
identity_str = RNS.prettyhexrep(identity) if type(identity) == bytes else f"{identity}"
signer_description = f"\nThe message was NOT signed by {identity_str or signing_identity}" if identity else ""
if not valid: print(f"Invalid signature in {signature_path}{signer_description}"); exit(R_INVALID_SIGNATURE)
else:
print(f"\nSignature is valid, the following message was signed by {signing_identity}:\n")
print(signed_data["message"].decode("utf-8"))
return exit(R_OK) if not __recursive else R_OK
except Exception as e: print(f"Error while validating {signature_path}: {e}"); exit(R_UNKNOWN_ERROR)
def sign(args, identity, __recursive=False):
if type(args.sign) == list:
paths = args.sign.copy()
signed = 0
for path in paths:
args.sign = path
code = sign(args, identity, __recursive=True)
if code != 0: print(f"Sequence error on recursive signature creation"); exit(R_SEQUENCE_ERROR)
else: signed += 1
if len(paths) != signed: print(f"Sequence error on recursive signature creation"); exit(R_SEQUENCE_ERROR)
else: exit(R_OK)
sig_ext = f".{SIG_EXT}"
sign_path = os.path.expanduser(args.sign)
rsg_path = f"{sign_path}{sig_ext}"
file_exists = os.path.isfile(sign_path)
signature_exists = os.path.isfile(rsg_path)
if args.base32: output = "base32"
elif args.base64: output = "base64"
elif args.hex: output = "hex"
else: output = "bin"
if args.base32: output = "base32"
elif args.base64: output = "base64"
elif args.base256: output = "base256"
elif args.hex: output = "hex"
else: output = "bin"
if not identity.get_private_key(): print(f"Cannot sign \"{sign_path}\", the identity does not hold a private key"); exit(R_NO_PRVKEY)
if not file_exists: print(f"The file \"{sign_path}\" does not exist"); exit(R_NO_FILE)
@@ -606,19 +705,66 @@ def sign(args, identity):
if output == "bin":
with open(rsg_path, "wb") as out_file: out_file.write(rsg)
elif output == "base32" or output == "base64" or output == "hex": print(f"\n{wrap_rsg(rsg)}\n")
else: print("No valid output format specified")
elif output in ["base32", "base64", "base256", "hex"]: print(f"\n{wrap_rsg(rsg)}\n")
else: print("No valid output format specified"); exit(R_INVALID_ARGS)
print(f"Signed file {sign_path} with {identity}"); exit(R_OK)
print(f"Signed file {sign_path} with {identity}"); return exit(R_OK) if not __recursive else R_OK
except Exception as e: print(f"Could not sign {sign_path}: {e}"); exit(R_UNKNOWN_ERROR)
def sign_message(args, identity):
message = args.sign_message
if args.base32: output = "base32"
elif args.base64: output = "base64"
elif args.base256: output = "base256"
elif args.hex: output = "hex"
else: output = "bin"
if output == "bin" and not args.write: print("No write path specified"); exit(R_INVALID_ARGS)
if not identity.get_private_key(): print(f"Cannot sign \"{sign_path}\", the identity does not hold a private key"); exit(R_NO_PRVKEY)
if message == NO_MESSAGE: message = get_editor_content()
if not message: print("No message specified"); exit(R_INVALID_ARGS)
try:
rsg = create_rsg(identity, message, embed=True, output=output)
if not rsg: print(f"No signature created, not writing"); exit(R_UNKNOWN_ERROR)
if output == "bin":
sig_ext = f".{MSG_EXT}"
rsg_path = os.path.expanduser(args.write)
rsg_path = f"{rsg_path}{sig_ext}" if not rsg_path.endswith(sig_ext) else rsg_path
signature_exists = os.path.isfile(rsg_path)
if signature_exists and not args.force: print(f"The signature file \"{rsg_path}\" already exists, not overwriting"); exit(R_FILE_EXISTS)
with open(rsg_path, "wb") as out_file: out_file.write(rsg)
print(f"Message signed with {identity} saved to {rsg_path}"); exit(R_OK)
elif output in ["base32", "base64", "base256", "hex"]: print(f"\n{wrap_rsg(rsg)}\n")
else: print("No valid output format specified"); exit(R_INVALID_ARGS)
print(f"Message signed with {identity}"); exit(R_OK)
except Exception as e: print(f"Could not sign message: {e}"); exit(R_UNKNOWN_ERROR)
######################################
# Encryption & Decryption Operations #
######################################
def encrypt(args, identity):
def encrypt(args, identity, __recursive=False):
if type(args.encrypt) == list:
paths = args.encrypt.copy()
encrypted = 0
for path in paths:
args.encrypt = path
code = encrypt(args, identity, __recursive=True)
if code != 0: print(f"Sequence error on recursive file encryption"); exit(R_SEQUENCE_ERROR)
else: encrypted += 1
if len(paths) != encrypted: print(f"Sequence error on recursive file encryption"); exit(R_SEQUENCE_ERROR)
else: exit(R_OK)
enc_ext = f".{ENCRYPT_EXT}"
encrypt_path = os.path.expanduser(args.encrypt)
rfe_path = args.write if args.write else f"{encrypt_path}{enc_ext}"
@@ -647,9 +793,21 @@ def encrypt(args, identity):
except Exception as e: print(f"\nError writing encrypted output to {rfe_path}: {e}"); exit(R_WRITE_ERROR)
except Exception as e: print(f"\nError reading {encrypt_path} for encryption: {e}"); exit(R_WRITE_ERROR)
print(f"\nFile {encrypt_path} encrypted for {identity} to {rfe_path}"); exit(R_OK)
print(f"\nFile {encrypt_path} encrypted for {identity} to {rfe_path}"); return exit(R_OK) if not __recursive else R_OK
def decrypt(args, identity, __recursive=False):
if type(args.decrypt) == list:
paths = args.decrypt.copy()
decrypted = 0
for path in paths:
args.decrypt = path
code = decrypt(args, identity, __recursive=True)
if code != 0: print(f"Sequence error on recursive file decryption"); exit(R_SEQUENCE_ERROR)
else: decrypted += 1
if len(paths) != decrypted: print(f"Sequence error on recursive file decryption"); exit(R_SEQUENCE_ERROR)
else: exit(R_OK)
def decrypt(args, identity):
enc_ext = f".{ENCRYPT_EXT}"
rfe_path = os.path.expanduser(args.decrypt)
if not rfe_path.endswith(enc_ext): print(f"The file {rfe_path} does not appear to be a Reticulum encrypted file"); exit(R_INVALID_FILE)
@@ -686,7 +844,7 @@ def decrypt(args, identity):
except Exception as e: print(f"\nError writing decrypted output to {decrypt_path}: {e}"); exit(R_WRITE_ERROR)
except Exception as e: print(f"\nError reading {rfe_path} for decryption: {e}"); exit(R_WRITE_ERROR)
print(f"\nFile {rfe_path} decrypted to {decrypt_path}"); exit(R_OK)
print(f"\nFile {rfe_path} decrypted to {decrypt_path}"); return exit(R_OK) if not __recursive else R_OK
################
@@ -781,6 +939,33 @@ def export_prv_identity(args, identity):
# Helper & Utility Functions #
##############################
def get_editor_content():
import subprocess
from tempfile import NamedTemporaryFile
template = ""
editor = os.environ.get("EDITOR", "")
if not editor:
for fallback in ["nano", "vim", "vi"]:
try:
subprocess.run(["which", fallback], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
editor = fallback
break
except subprocess.CalledProcessError: continue
if not editor: print("Could not launch editor"); exit(R_READ_ERROR);
try:
with NamedTemporaryFile(mode="w+", suffix=".tmp", delete=False) as tmp:
tmp_path = tmp.name
tmp.write(template)
result = subprocess.run([editor, tmp_path])
if result.returncode != 0: print(f"Editor exited with error code {result.returncode}"); os.unlink(tmp_path); exit(R_READ_ERROR)
with open(tmp_path, "r") as f: content = f.read()
os.unlink(tmp_path)
return content.encode("utf-8")
except Exception as e: print(f"Could not get content from editor: {e}"); exit(R_READ_ERROR)
def spin(until=None, msg=None, timeout=None):
i = 0
syms = "⢄⢂⢁⡁⡈⡐⡠"
+33 -27
View File
@@ -60,8 +60,11 @@ def size_str(num, suffix='B'):
request_result = None
request_concluded = False
first_remote_req = True
remote_destination = None
remote_link = None
def get_remote_status(destination_hash, include_lstats, identity, no_output=False, timeout=RNS.Transport.PATH_REQUEST_TIMEOUT):
global request_result, request_concluded
global request_result, request_concluded, first_remote_req, remote_destination, remote_link
link_count = None
if not RNS.Transport.has_path(destination_hash):
@@ -108,35 +111,41 @@ def get_remote_status(destination_hash, include_lstats, identity, no_output=Fals
response = request_receipt.response
if isinstance(response, list):
status = response[0]
if len(response) > 1:
link_count = response[1]
else:
link_count = None
if len(response) > 1: link_count = response[1]
else: link_count = None
request_result = (status, link_count)
request_concluded = True
def remote_link_established(link):
if not no_output:
global first_remote_req
if not no_output and first_remote_req:
print("\r \r", end="")
print("Sending request...", end=" ")
sys.stdout.flush()
link.identify(identity)
link.request("/status", data = [include_lstats], response_callback = got_response, failed_callback = request_failed)
first_remote_req = False
if not no_output:
if not remote_link and not no_output:
print("\r \r", end="")
print("Establishing link with remote transport instance...", end=" ")
sys.stdout.flush()
remote_destination = RNS.Destination(remote_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "rnstransport", "remote", "management")
link = RNS.Link(remote_destination)
link.set_link_established_callback(remote_link_established)
link.set_link_closed_callback(remote_link_closed)
if not remote_destination:
remote_destination = RNS.Destination(remote_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "rnstransport", "remote", "management")
if remote_link and remote_link.status == RNS.Link.ACTIVE:
request_concluded = False
remote_link.request("/status", data = [include_lstats], response_callback = got_response, failed_callback = request_failed)
while not request_concluded:
time.sleep(0.1)
else:
remote_link = RNS.Link(remote_destination)
remote_link.set_link_established_callback(remote_link_established)
remote_link.set_link_closed_callback(remote_link_closed)
while not request_concluded: time.sleep(0.1)
if request_result != None:
print("\r \r", end="")
@@ -301,28 +310,22 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=
if remote:
try:
if management_identity is None:
raise ValueError("Remote management requires an identity file. Use -i to specify the path to a management identity.")
if management_identity is None: raise ValueError("Remote management requires an identity file. Use -i to specify the path to a management identity.")
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
if len(remote) != dest_len:
raise ValueError("Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
if len(remote) != dest_len: raise ValueError("Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
try:
identity_hash = bytes.fromhex(remote)
destination_hash = RNS.Destination.hash_from_name_and_identity("rnstransport.remote.management", identity_hash)
except Exception as e:
raise ValueError("Invalid destination entered. Check your input.")
except Exception as e: raise ValueError("Invalid destination entered. Check your input.")
identity = RNS.Identity.from_file(os.path.expanduser(management_identity))
if identity == None:
raise ValueError("Could not load management identity from "+str(management_identity))
if identity == None: raise ValueError("Could not load management identity from "+str(management_identity))
try:
remote_status = get_remote_status(destination_hash, lstats, identity, no_output=json, timeout=remote_timeout)
if remote_status != None:
stats, link_count = remote_status
except Exception as e:
raise e
if remote_status != None: stats, link_count = remote_status
except Exception as e: raise e
except Exception as e:
print(str(e))
@@ -717,6 +720,7 @@ def main(must_exit=True, rns_instance=None):
exit(1)
while True:
st = time.time()
buffer = io.StringIO()
old_stdout = sys.stdout
sys.stdout = buffer
@@ -733,8 +737,10 @@ def main(must_exit=True, rns_instance=None):
output = buffer.getvalue()
print("\033[H\033[2J", end="")
print(output, end="", flush=True)
time.sleep(args.monitor_interval)
td = time.time()-st
sleeptime = max(args.monitor_interval-td, 0.2)
time.sleep(sleeptime)
else:
program_setup(configdir = configarg, dispall = args.all, verbosity=args.verbose, name_filter=args.filter, json=args.json,
+88 -110
View File
@@ -94,22 +94,14 @@ _always_override_destination = False
logging_lock = threading.Lock()
def loglevelname(level):
if (level == LOG_CRITICAL):
return "[Critical]"
if (level == LOG_ERROR):
return "[Error] "
if (level == LOG_WARNING):
return "[Warning] "
if (level == LOG_NOTICE):
return "[Notice] "
if (level == LOG_INFO):
return "[Info] "
if (level == LOG_VERBOSE):
return "[Verbose] "
if (level == LOG_DEBUG):
return "[Debug] "
if (level == LOG_EXTREME):
return "[Extra] "
if (level == LOG_CRITICAL): return "[Critical]"
if (level == LOG_ERROR): return "[Error] "
if (level == LOG_WARNING): return "[Warning] "
if (level == LOG_NOTICE): return "[Notice] "
if (level == LOG_INFO): return "[Info] "
if (level == LOG_VERBOSE): return "[Verbose] "
if (level == LOG_DEBUG): return "[Debug] "
if (level == LOG_EXTREME): return "[Extra] "
return "Unknown"
@@ -133,13 +125,10 @@ def log(msg, level=3, _override_destination = False, pt=False):
global _always_override_destination, compact_log_fmt
msg = str(msg)
if loglevel >= level:
if pt:
logstring = "["+precise_timestamp_str(time.time())+"] "+loglevelname(level)+" "+msg
if pt: logstring = "["+precise_timestamp_str(time.time())+"] "+loglevelname(level)+" "+msg
else:
if not compact_log_fmt:
logstring = "["+timestamp_str(time.time())+"] "+loglevelname(level)+" "+msg
else:
logstring = "["+timestamp_str(time.time())+"] "+msg
if not compact_log_fmt: logstring = "["+timestamp_str(time.time())+"] "+loglevelname(level)+" "+msg
else: logstring = "["+timestamp_str(time.time())+"] "+msg
with logging_lock:
if (logdest == LOG_STDOUT or _always_override_destination or _override_destination):
@@ -182,14 +171,11 @@ def trace_exception(e):
log(exception_info, LOG_ERROR)
def hexrep(data, delimit=True):
try:
iter(data)
except TypeError:
data = [data]
try: iter(data)
except TypeError: data = [data]
delimiter = ":"
if not delimit:
delimiter = ""
if not delimit: delimiter = ""
hexrep = delimiter.join("{:02x}".format(c) for c in data)
return hexrep
@@ -198,11 +184,6 @@ 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"
@@ -217,10 +198,8 @@ def prettysize(num, suffix='B'):
for unit in units:
if abs(num) < 1000.0:
if unit == "":
return "%.0f %s%s" % (num, unit, suffix)
else:
return "%.2f %s%s" % (num, unit, suffix)
if unit == "": return "%.0f %s%s" % (num, unit, suffix)
else: return "%.2f %s%s" % (num, unit, suffix)
num /= 1000.0
return "%.2f%s%s" % (num, last_unit, suffix)
@@ -251,8 +230,7 @@ def prettydistance(m, suffix="m"):
if unit == "m": divisor = 10
if unit == "c": divisor = 100
if abs(num) < divisor:
return "%.2f %s%s" % (num, unit, suffix)
if abs(num) < divisor: return "%.2f %s%s" % (num, unit, suffix)
num /= divisor
return "%.2f %s%s" % (num, last_unit, suffix)
@@ -269,10 +247,8 @@ def prettytime(time, verbose=False, compact=False):
time %= 3600
minutes = int(time // 60)
time %= 60
if compact:
seconds = int(time)
else:
seconds = round(time, 2)
if compact: seconds = int(time)
else: seconds = round(time, 2)
ss = "" if seconds == 1 else "s"
sm = "" if minutes == 1 else "s"
@@ -301,22 +277,16 @@ def prettytime(time, verbose=False, compact=False):
tstr = ""
for c in components:
i += 1
if i == 1:
pass
elif i < len(components):
tstr += ", "
elif i == len(components):
tstr += " and "
if i == 1: pass
elif i < len(components): tstr += ", "
elif i == len(components): tstr += " and "
tstr += c
if tstr == "":
return "0s"
if tstr == "": return "0s"
else:
if not neg:
return tstr
else:
return f"-{tstr}"
if not neg: return tstr
else: return f"-{tstr}"
def prettyshorttime(time, verbose=False, compact=False):
neg = False
@@ -328,10 +298,8 @@ def prettyshorttime(time, verbose=False, compact=False):
seconds = int(time // 1e6); time %= 1e6
milliseconds = int(time // 1e3); time %= 1e3
if compact:
microseconds = int(time)
else:
microseconds = round(time, 2)
if compact: microseconds = int(time)
else: microseconds = round(time, 2)
ss = "" if seconds == 1 else "s"
sms = "" if milliseconds == 1 else "s"
@@ -355,22 +323,16 @@ def prettyshorttime(time, verbose=False, compact=False):
tstr = ""
for c in components:
i += 1
if i == 1:
pass
elif i < len(components):
tstr += ", "
elif i == len(components):
tstr += " and "
if i == 1: pass
elif i < len(components): tstr += ", "
elif i == len(components): tstr += " and "
tstr += c
if tstr == "":
return "0us"
if tstr == "": return "0us"
else:
if not neg:
return tstr
else:
return f"-{tstr}"
if not neg: return tstr
else: return f"-{tstr}"
def phyparams():
print("Required Physical Layer MTU : "+str(Reticulum.MTU)+" bytes")
@@ -381,8 +343,7 @@ def phyparams():
print("Link Public Key Size : "+str(Link.ECPUBSIZE*8)+" bits")
print("Link Private Key Size : "+str(Link.KEYSIZE*8)+" bits")
def panic():
os._exit(255)
def panic(): os._exit(255)
exit_called = False
def exit(code=0):
@@ -403,8 +364,7 @@ class Profiler:
@staticmethod
def get_profiler(tag=None, super_tag=None):
if tag in Profiler.profilers:
return Profiler.profilers[tag]
if tag in Profiler.profilers: return Profiler.profilers[tag]
else:
profiler = Profiler(tag, super_tag)
Profiler.profilers[tag] = profiler
@@ -416,13 +376,14 @@ class Profiler:
self.pause_started = None
self.tag = tag
self.super_tag = super_tag
if self.super_tag in Profiler.profilers:
self.super_profiler = Profiler.profilers[self.super_tag]
self.pause_super = self.super_profiler.pause
self.resume_super = self.super_profiler.resume
else:
def noop(self=None):
pass
def noop(self=None): pass
self.super_profiler = None
self.pause_super = noop
self.resume_super = noop
@@ -432,8 +393,7 @@ class Profiler:
tag = self.tag
super_tag = self.super_tag
thread_ident = threading.get_ident()
if not tag in Profiler.tags:
Profiler.tags[tag] = {"threads": {}, "super": super_tag}
if not tag in Profiler.tags: Profiler.tags[tag] = {"threads": {}, "super": super_tag}
if not thread_ident in Profiler.tags[tag]["threads"]:
Profiler.tags[tag]["threads"][thread_ident] = {"current_start": None, "captures": []}
@@ -469,8 +429,7 @@ class Profiler:
self.resume_super()
@staticmethod
def ran():
return Profiler._ran
def ran(): return Profiler._ran
@staticmethod
def results():
@@ -487,41 +446,35 @@ class Profiler:
sample_count = len(thread_captures)
if sample_count > 1:
thread_results = {
"count": sample_count,
"mean": mean(thread_captures),
"median": median(thread_captures),
"stdev": stdev(thread_captures)
}
thread_results = { "count": sample_count,
"mean": mean(thread_captures),
"median": median(thread_captures),
"stdev": stdev(thread_captures) }
elif sample_count == 1:
thread_results = {
"count": sample_count,
"mean": mean(thread_captures),
"median": median(thread_captures),
"stdev": None
}
thread_results = { "count": sample_count,
"mean": mean(thread_captures),
"median": median(thread_captures),
"stdev": None }
tag_captures.extend(thread_captures)
sample_count = len(tag_captures)
if sample_count > 1:
tag_results = {
"name": tag,
"super": tag_entry["super"],
"count": len(tag_captures),
"mean": mean(tag_captures),
"median": median(tag_captures),
"stdev": stdev(tag_captures)
}
tag_results = { "name": tag,
"super": tag_entry["super"],
"count": len(tag_captures),
"mean": mean(tag_captures),
"median": median(tag_captures),
"stdev": stdev(tag_captures) }
elif sample_count == 1:
tag_results = {
"name": tag,
"super": tag_entry["super"],
"count": len(tag_captures),
"mean": mean(tag_captures),
"median": median(tag_captures),
"stdev": None
}
tag_results = { "name": tag,
"super": tag_entry["super"],
"count": len(tag_captures),
"mean": mean(tag_captures),
"median": median(tag_captures),
"stdev": None }
results[tag] = tag_results
@@ -555,6 +508,8 @@ class Profiler:
profile = Profiler.get_profiler
# The base-256 table is likely to change. Currently, it is just
# experimental, so don't count on it too much just yet.
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
@@ -575,4 +530,27 @@ b256 = [
"𐌳","𐌸","𐌾","𐐀","𐐁","𐐂","𐐆","𐐇","𐐈","𐐉","𐐊","𐐋","𐐌","𐐍","𐐎","𐐏", # 0xF Gothic & Deseret
]
def b256_rep(input_byte): return b256[int(input_byte)]
def b256rep(data): return "".join(bytes_to_b256(data))
def prettyb256rep(data): return f"<{b256rep(data)}>"
def b256_to_byte(point):
if not type(point) == str or not len(point) == 1: raise TypeError("Invalid input data for base256 byte decode")
try: return b256.index(point)
except Exception as e: raise ValueError(f"Could not decode base256 byte: {e}")
def b256_to_bytes(b256rep):
if not type(b256rep) == str: raise TypeError("Invalid input data for base256 decode")
try: return bytes([b256.index(c) for c in b256rep])
except Exception as e: raise ValueError(f"Could not decode base256: {e}")
def byte_to_b256(input_byte):
if type(input_byte) == bytes and not len(input_byte) == 1: TypeError("Invalid input data for base256 byte encode")
if type(input_byte) == bytes and len(input_byte) == 1: input_byte = ord(input_byte)
if not type(input_byte) == int: raise TypeError("Invalid input data for base256 byte encode")
try: return b256[int(input_byte)]
except Exception as e: raise TypeError(f"Could not encode byte to base256: {e}")
def bytes_to_b256(data):
if not type(data) == bytes: raise TypeError("Invalid input data for base256 encode")
try: return [byte_to_b256(c) for c in data]
except Exception as e: raise TypeError(f"Could not encode to base256: {e}")
+1 -1
View File
@@ -1 +1 @@
__version__ = "1.2.5"
__version__ = "1.2.6"
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -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: 7c8efb93b69ddda43e6e24cf504eb5f9
config: 6d7f4aac8313ba495ab156ec11ab15c0
tags: 645f666f9bcd5a90fca523b33c5a78b7
+1 -1
View File
@@ -1,5 +1,5 @@
const DOCUMENTATION_OPTIONS = {
VERSION: '1.2.5',
VERSION: '1.2.6',
LANGUAGE: 'en',
COLLAPSE_INDEX: false,
BUILDER: 'html',
+4 -4
View File
@@ -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.2.5 documentation</title>
<title>Code Examples - Reticulum Network Stack 1.2.6 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.2.5 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 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.2.5 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 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">
@@ -3664,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=36f53d34"></script>
</div><script src="_static/documentation_options.js?v=010db75e"></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>
+4 -4
View File
@@ -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.2.5 documentation</title>
<title>An Explanation of Reticulum for Human Beings - Reticulum Network Stack 1.2.6 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.2.5 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 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.2.5 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 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">
@@ -295,7 +295,7 @@
</aside>
</div>
</div><script src="_static/documentation_options.js?v=36f53d34"></script>
</div><script src="_static/documentation_options.js?v=010db75e"></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>
+4 -4
View File
@@ -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.2.5 documentation</title>
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 --><title>Index - Reticulum Network Stack 1.2.6 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.2.5 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 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.2.5 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 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">
@@ -839,7 +839,7 @@
</aside>
</div>
</div><script src="_static/documentation_options.js?v=36f53d34"></script>
</div><script src="_static/documentation_options.js?v=010db75e"></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>
+4 -4
View File
@@ -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.2.5 documentation</title>
<title>Getting Started Fast - Reticulum Network Stack 1.2.6 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.2.5 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 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.2.5 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 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">
@@ -967,7 +967,7 @@ All other available modules will still be loaded when needed.</p>
</aside>
</div>
</div><script src="_static/documentation_options.js?v=36f53d34"></script>
</div><script src="_static/documentation_options.js?v=010db75e"></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>
+4 -4
View File
@@ -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>Git Over Reticulum - Reticulum Network Stack 1.2.5 documentation</title>
<title>Git Over Reticulum - Reticulum Network Stack 1.2.6 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.2.5 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 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.2.5 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 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">
@@ -782,7 +782,7 @@ options:
</aside>
</div>
</div><script src="_static/documentation_options.js?v=36f53d34"></script>
</div><script src="_static/documentation_options.js?v=010db75e"></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>
+4 -4
View File
@@ -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.2.5 documentation</title>
<title>Communications Hardware - Reticulum Network Stack 1.2.6 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.2.5 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 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.2.5 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 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">
@@ -675,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=36f53d34"></script>
</div><script src="_static/documentation_options.js?v=010db75e"></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>
+4 -4
View File
@@ -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.2.5 documentation</title>
<title>Reticulum Network Stack 1.2.6 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.2.5 documentation</div></a>
<a href="#"><div class="brand">Reticulum Network Stack 1.2.6 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.2.5 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 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">
@@ -643,7 +643,7 @@ to participate in the development of Reticulum itself.</p>
</aside>
</div>
</div><script src="_static/documentation_options.js?v=36f53d34"></script>
</div><script src="_static/documentation_options.js?v=010db75e"></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>
+4 -4
View File
@@ -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.2.5 documentation</title>
<title>Configuring Interfaces - Reticulum Network Stack 1.2.6 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.2.5 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 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.2.5 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 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">
@@ -1773,7 +1773,7 @@ interface basis under the relevant interface configuration section.</p>
</aside>
</div>
</div><script src="_static/documentation_options.js?v=36f53d34"></script>
</div><script src="_static/documentation_options.js?v=010db75e"></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>
+4 -4
View File
@@ -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.2.5 documentation</title>
<title>Reticulum License - Reticulum Network Stack 1.2.6 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.2.5 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 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.2.5 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 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">
@@ -344,7 +344,7 @@ SOFTWARE.
</aside>
</div>
</div><script src="_static/documentation_options.js?v=36f53d34"></script>
</div><script src="_static/documentation_options.js?v=010db75e"></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>
+4 -4
View File
@@ -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>Building Networks - Reticulum Network Stack 1.2.5 documentation</title>
<title>Building Networks - Reticulum Network Stack 1.2.6 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.2.5 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 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.2.5 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 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">
@@ -663,7 +663,7 @@ differently than a mobile device roaming between radio cells.</p>
</aside>
</div>
</div><script src="_static/documentation_options.js?v=36f53d34"></script>
</div><script src="_static/documentation_options.js?v=010db75e"></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.
+4 -4
View File
@@ -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.2.5 documentation</title>
<title>API Reference - Reticulum Network Stack 1.2.6 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.2.5 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 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.2.5 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 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">
@@ -2484,7 +2484,7 @@ will announce it.</p>
</aside>
</div>
</div><script src="_static/documentation_options.js?v=36f53d34"></script>
</div><script src="_static/documentation_options.js?v=010db75e"></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>
+4 -4
View File
@@ -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.2.5 documentation</title><link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<title>Search - Reticulum Network Stack 1.2.6 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.2.5 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 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.2.5 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
</a><form class="sidebar-search-container" method="get" action="#" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -303,7 +303,7 @@
</aside>
</div>
</div><script src="_static/documentation_options.js?v=36f53d34"></script>
</div><script src="_static/documentation_options.js?v=010db75e"></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>
+4 -4
View File
@@ -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.2.5 documentation</title>
<title>Programs Using Reticulum - Reticulum Network Stack 1.2.6 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.2.5 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 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.2.5 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 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">
@@ -512,7 +512,7 @@ plugin system for expandability.</p>
</aside>
</div>
</div><script src="_static/documentation_options.js?v=36f53d34"></script>
</div><script src="_static/documentation_options.js?v=010db75e"></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>
+4 -4
View File
@@ -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>Support Reticulum - Reticulum Network Stack 1.2.5 documentation</title>
<title>Support Reticulum - Reticulum Network Stack 1.2.6 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.2.5 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 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.2.5 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 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">
@@ -382,7 +382,7 @@ circumstances, so we rely on old-fashioned human feedback.</p>
</aside>
</div>
</div><script src="_static/documentation_options.js?v=36f53d34"></script>
</div><script src="_static/documentation_options.js?v=010db75e"></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>
+4 -4
View File
@@ -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.2.5 documentation</title>
<title>Understanding Reticulum - Reticulum Network Stack 1.2.6 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.2.5 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 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.2.5 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 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">
@@ -1337,7 +1337,7 @@ those risks are acceptable to you.</p>
</aside>
</div>
</div><script src="_static/documentation_options.js?v=36f53d34"></script>
</div><script src="_static/documentation_options.js?v=010db75e"></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>
+4 -4
View File
@@ -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.2.5 documentation</title>
<title>Using Reticulum on Your System - Reticulum Network Stack 1.2.6 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.2.5 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 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.2.5 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 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">
@@ -1635,7 +1635,7 @@ systemctl --user enable rnsd.service
</aside>
</div>
</div><script src="_static/documentation_options.js?v=36f53d34"></script>
</div><script src="_static/documentation_options.js?v=010db75e"></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>
+4 -4
View File
@@ -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.2.5 documentation</title>
<title>What is Reticulum? - Reticulum Network Stack 1.2.6 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.2.5 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 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.2.5 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 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">
@@ -504,7 +504,7 @@ network, and vice versa.</p>
</aside>
</div>
</div><script src="_static/documentation_options.js?v=36f53d34"></script>
</div><script src="_static/documentation_options.js?v=010db75e"></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>
+4 -4
View File
@@ -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.2.5 documentation</title>
<title>Zen of Reticulum - Reticulum Network Stack 1.2.6 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.2.5 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 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.2.5 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 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">
@@ -676,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=36f53d34"></script>
</div><script src="_static/documentation_options.js?v=010db75e"></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>