mirror of
https://github.com/markqvist/Reticulum.git
synced 2026-06-23 04:16:12 -07:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 95502e2c21 | |||
| 3dd4145e62 | |||
| 1d7ddc3f8a | |||
| d731b4396c | |||
| c186a1f6b0 | |||
| a049ec8b7b | |||
| 4c93f6c7f4 | |||
| 35c7a89b19 | |||
| c86b9c9703 | |||
| 64ebdd0ee3 | |||
| 9179b914d5 | |||
| eb5d46b20b | |||
| 54c36f515b | |||
| 5c5668a4fc | |||
| eeefb60c89 | |||
| 018df10a26 | |||
| 93ead77435 | |||
| bd0e1ad0ca | |||
| d0ceeacb37 | |||
| 7d5fb6a13f | |||
| 855ef7bfd1 | |||
| 323890021a | |||
| e004e7592b | |||
| 0ebec014e5 | |||
| 1b624cc0e2 |
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -1 +1 @@
|
||||
__version__ = "1.2.5"
|
||||
__version__ = "1.2.6"
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,4 +1,4 @@
|
||||
# Sphinx build info version 1
|
||||
# This file records the configuration used when building these files. When it is not found, a full rebuild will be done.
|
||||
config: 7c8efb93b69ddda43e6e24cf504eb5f9
|
||||
config: 6d7f4aac8313ba495ab156ec11ab15c0
|
||||
tags: 645f666f9bcd5a90fca523b33c5a78b7
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const DOCUMENTATION_OPTIONS = {
|
||||
VERSION: '1.2.5',
|
||||
VERSION: '1.2.6',
|
||||
LANGUAGE: 'en',
|
||||
COLLAPSE_INDEX: false,
|
||||
BUILDER: 'html',
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
|
||||
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
|
||||
<title>Code Examples - Reticulum Network Stack 1.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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user