refactor(meshchat): streamline transport identity handling and add extra config validation

This commit is contained in:
Ivan
2026-04-29 14:38:44 -05:00
parent 3f6ce06451
commit b454757533
2 changed files with 124 additions and 14 deletions
+68 -14
View File
@@ -4386,22 +4386,13 @@ class ReticulumMeshChat:
},
status=422,
)
transport_identity = data.get("transport_identity")
if (
transport_identity is None
or str(transport_identity).strip() == ""
):
return web.json_response(
{
"message": "Transport identity is required",
},
status=422,
)
interface_details["remote"] = str(remote).strip()
interface_details["target_port"] = interface_target_port
interface_details["transport_identity"] = str(
transport_identity,
).strip()
InterfaceEditor.update_value(
interface_details,
data,
"transport_identity",
)
# handle I2P interface
if interface_type == "I2PInterface":
@@ -4802,6 +4793,42 @@ class ReticulumMeshChat:
interface_details["command"] = interface_command
interface_details["respawn_delay"] = interface_respawn_delay
_builtin_interface_types = frozenset(
{
"AutoInterface",
"TCPClientInterface",
"BackboneInterface",
"I2PInterface",
"TCPServerInterface",
"UDPInterface",
"RNodeInterface",
"RNodeIPInterface",
"RNodeMultiInterface",
"SerialInterface",
"KISSInterface",
"AX25KISSInterface",
"PipeInterface",
},
)
if interface_type not in _builtin_interface_types:
extra = data.get("extra_config")
if extra is None:
extra = {}
if not isinstance(extra, dict):
return web.json_response(
{
"message": "extra_config must be a JSON object",
},
status=422,
)
for key, value in extra.items():
if key in {"name", "type", "allow_overwriting_interface"}:
continue
if value is None or value == "":
interface_details.pop(key, None)
else:
interface_details[key] = value
# interface discovery options
for discovery_key in (
"discoverable",
@@ -13706,6 +13733,27 @@ class ReticulumMeshChat:
)
return
if lu.startswith("meshchatx://docs") or lu.startswith("meshchat://docs"):
from urllib.parse import parse_qsl, urlparse
parsed = urlparse(uri_raw)
q = dict(parse_qsl(parsed.query, keep_blank_values=True))
rel = (q.get("reticulum") or q.get("path") or "").strip()
payload: dict = {
"type": "lxm.ingest_uri.result",
"status": "success",
"message": "Opening documentation.",
"ingest_type": "docs_view",
}
if rel:
payload["docs_query"] = {"reticulum": rel}
AsyncUtils.run_async(
client.send_str(
json.dumps(payload),
),
)
return
# Columba-style contact sharing URI:
# lxma://<destination_hash_hex>:<public_key_hex>
if uri.lower().startswith("lxma://"):
@@ -16697,6 +16745,12 @@ def main():
if args.no_crash_recovery:
recovery.disable()
planned_storage_dir = args.storage_dir or os.path.join("storage")
recovery.update_paths(
storage_dir=planned_storage_dir,
reticulum_config_dir=args.reticulum_config_dir,
)
# check if we want to test exception messages
if args.test_exception_message is not None:
raise Exception(args.test_exception_message)
+56
View File
@@ -308,6 +308,62 @@ async def test_backbone_listener_mode_persists_options(temp_dir):
assert saved["prefer_ipv6"] is True
@pytest.mark.asyncio
async def test_backbone_connector_allows_empty_transport_identity(temp_dir):
config = ConfigDict({"reticulum": {}, "interfaces": {}})
free_port = _free_port("tcp")
async with make_app(temp_dir, config) as handler:
payload = {
"name": "BackboneOut",
"type": "BackboneInterface",
"target_host": "10.0.0.1",
"target_port": str(free_port),
}
response = await handler(make_request(payload))
body = json.loads(response.body)
assert response.status == 200, body
saved = config["interfaces"]["BackboneOut"]
assert saved["remote"] == "10.0.0.1"
assert str(saved["target_port"]) == str(free_port)
assert "transport_identity" not in saved
@pytest.mark.asyncio
async def test_external_interface_merges_extra_config(temp_dir):
config = ConfigDict({"reticulum": {}, "interfaces": {}})
async with make_app(temp_dir, config) as handler:
payload = {
"name": "WeaveTest",
"type": "WeaveInterface",
"extra_config": {"listen_ip": "127.0.0.1", "listen_port": 4242},
}
response = await handler(make_request(payload))
body = json.loads(response.body)
assert response.status == 200, body
saved = config["interfaces"]["WeaveTest"]
assert saved["type"] == "WeaveInterface"
assert saved["listen_ip"] == "127.0.0.1"
assert saved["listen_port"] == 4242
@pytest.mark.asyncio
async def test_external_interface_rejects_non_object_extra_config(temp_dir):
config = ConfigDict({"reticulum": {}, "interfaces": {}})
async with make_app(temp_dir, config) as handler:
payload = {
"name": "Bad",
"type": "FooInterface",
"extra_config": "not-an-object",
}
response = await handler(make_request(payload))
body = json.loads(response.body)
assert response.status == 422
assert "extra_config" in body["message"].lower()
@pytest.mark.asyncio
async def test_backbone_connector_mode_still_requires_remote(temp_dir):
config = ConfigDict({"reticulum": {}, "interfaces": {}})