mirror of
https://git.quad4.io/RNS-Things/MeshChatX.git
synced 2026-05-10 22:46:54 +00:00
feat(network): implement API for listing host network interfaces and normalize TCP port handling
This commit is contained in:
+66
-2
@@ -237,6 +237,34 @@ def _python_jit_status_line() -> str:
|
||||
return "Python JIT: enabled" if enabled else "Python JIT: disabled"
|
||||
|
||||
|
||||
def list_host_network_interfaces():
|
||||
"""Enumerate kernel network interfaces on the host running MeshChat.
|
||||
|
||||
Uses psutil (Linux, macOS, Windows). Fails soft on restricted environments
|
||||
(e.g. some Android sandboxes) and returns ``([], error)``.
|
||||
|
||||
Reticulum's ``device`` field on server-style interfaces is a *single* interface
|
||||
name, or omitted when binding only via ``listen_ip``.
|
||||
"""
|
||||
try:
|
||||
raw = psutil.net_if_addrs()
|
||||
except Exception as exc:
|
||||
logging.debug("list_host_network_interfaces: net_if_addrs failed: %s", exc)
|
||||
return [], str(exc)
|
||||
out: list[dict[str, object]] = []
|
||||
for name in sorted(raw.keys(), key=lambda n: str(n).lower()):
|
||||
addrs: list[str] = []
|
||||
for addr in raw[name]:
|
||||
if addr.family == socket.AF_INET:
|
||||
addrs.append(addr.address)
|
||||
elif addr.family == socket.AF_INET6:
|
||||
if addr.address.startswith("fe80:"):
|
||||
continue
|
||||
addrs.append(addr.address)
|
||||
out.append({"name": name, "addresses": addrs})
|
||||
return out, None
|
||||
|
||||
|
||||
class ReticulumMeshChat:
|
||||
def __init__(
|
||||
self,
|
||||
@@ -3835,6 +3863,15 @@ class ReticulumMeshChat:
|
||||
},
|
||||
)
|
||||
|
||||
@routes.get("/api/v1/system/network-interfaces")
|
||||
async def system_network_interfaces(request):
|
||||
interfaces, unavailable_reason = list_host_network_interfaces()
|
||||
payload = {
|
||||
"interfaces": interfaces,
|
||||
"unavailable_reason": unavailable_reason,
|
||||
}
|
||||
return web.json_response(payload)
|
||||
|
||||
@routes.get("/api/v1/tools/rnode/latest_release")
|
||||
async def tools_rnode_latest_release(request):
|
||||
"""Proxy GitHub's latest-release JSON for RNode firmware (official repo).
|
||||
@@ -4430,7 +4467,14 @@ class ReticulumMeshChat:
|
||||
if interface_type == "TCPServerInterface":
|
||||
# ensure listen ip provided
|
||||
interface_listen_ip = data.get("listen_ip")
|
||||
if interface_listen_ip is None or interface_listen_ip == "":
|
||||
if (
|
||||
interface_listen_ip is not None
|
||||
and str(interface_listen_ip).strip() != ""
|
||||
):
|
||||
interface_listen_ip = str(interface_listen_ip).strip()
|
||||
else:
|
||||
interface_listen_ip = ""
|
||||
if interface_listen_ip == "":
|
||||
return web.json_response(
|
||||
{
|
||||
"message": "Listen IP is required",
|
||||
@@ -4479,7 +4523,14 @@ class ReticulumMeshChat:
|
||||
if interface_type == "UDPInterface":
|
||||
# ensure listen ip provided
|
||||
interface_listen_ip = data.get("listen_ip")
|
||||
if interface_listen_ip is None or interface_listen_ip == "":
|
||||
if (
|
||||
interface_listen_ip is not None
|
||||
and str(interface_listen_ip).strip() != ""
|
||||
):
|
||||
interface_listen_ip = str(interface_listen_ip).strip()
|
||||
else:
|
||||
interface_listen_ip = ""
|
||||
if interface_listen_ip == "":
|
||||
return web.json_response(
|
||||
{
|
||||
"message": "Listen IP is required",
|
||||
@@ -4559,6 +4610,19 @@ class ReticulumMeshChat:
|
||||
status=422,
|
||||
)
|
||||
|
||||
if str(interface_port).strip().lower().startswith("tcp://"):
|
||||
interface_port = InterfaceEditor.normalize_rnode_tcp_port(
|
||||
str(interface_port),
|
||||
)
|
||||
host_part = str(interface_port)[len("tcp://") :].strip().strip(":")
|
||||
if not host_part:
|
||||
return web.json_response(
|
||||
{
|
||||
"message": "TCP host is required for RNode over IP",
|
||||
},
|
||||
status=422,
|
||||
)
|
||||
|
||||
# ensure frequency provided
|
||||
interface_frequency = data.get("frequency")
|
||||
if interface_frequency is None or interface_frequency == "":
|
||||
|
||||
@@ -1,5 +1,44 @@
|
||||
# SPDX-License-Identifier: 0BSD AND MIT
|
||||
|
||||
import re
|
||||
|
||||
_IPV4_HOST_PORT = re.compile(r"^(\d{1,3}(?:\.\d{1,3}){3}):(\d{1,5})$")
|
||||
|
||||
|
||||
def normalize_rnode_tcp_port(port: str) -> str:
|
||||
"""Normalize RNodeInterface ``port`` when using ``tcp://``.
|
||||
|
||||
Reticulum's ``TCPConnection`` (``RNS/Interfaces/RNodeInterface.py``) calls
|
||||
``socket.getaddrinfo(target_host, 7633)``. The first argument must be a hostname or IP **only**; an embedded ``:port``
|
||||
breaks resolution. Config may list legacy ``tcp://host:7633`` or ``tcp://host:``;
|
||||
strip those so storage matches ``tcp://<host>``.
|
||||
"""
|
||||
raw = str(port).strip()
|
||||
low = raw.lower()
|
||||
scheme = "tcp://"
|
||||
if not low.startswith(scheme):
|
||||
return raw
|
||||
rest = raw[len(scheme) :].strip()
|
||||
while rest.endswith(":"):
|
||||
rest = rest[:-1]
|
||||
if not rest:
|
||||
return scheme
|
||||
if rest.startswith("["):
|
||||
close = rest.find("]")
|
||||
if close != -1 and len(rest) > close + 1 and rest[close + 1] == ":":
|
||||
tail = rest[close + 2 :]
|
||||
if tail.isdigit() and 1 <= int(tail) <= 65535:
|
||||
rest = rest[: close + 1]
|
||||
return scheme + rest
|
||||
m = _IPV4_HOST_PORT.match(rest)
|
||||
if m and int(m.group(2)) <= 65535:
|
||||
rest = m.group(1)
|
||||
elif rest.count(":") == 1:
|
||||
head, tail = rest.split(":", 1)
|
||||
if tail.isdigit() and 1 <= int(tail) <= 65535:
|
||||
rest = head
|
||||
return scheme + rest
|
||||
|
||||
|
||||
def coerce_rnode_frequency_hz(value):
|
||||
"""Return RNode carrier frequency as integer Hz for Reticulum config.
|
||||
@@ -32,6 +71,7 @@ def coerce_rnode_frequency_hz(value):
|
||||
|
||||
class InterfaceEditor:
|
||||
coerce_rnode_frequency_hz = staticmethod(coerce_rnode_frequency_hz)
|
||||
normalize_rnode_tcp_port = staticmethod(normalize_rnode_tcp_port)
|
||||
|
||||
@staticmethod
|
||||
def update_value(interface_details: dict, data: dict, key: str):
|
||||
|
||||
@@ -351,7 +351,9 @@
|
||||
</div>
|
||||
<div v-else class="space-y-4">
|
||||
<div>
|
||||
<FormLabel class="glass-label">Listen IP (Optional)</FormLabel>
|
||||
<FormLabel class="glass-label"
|
||||
>Listen IP (optional if Device is set)</FormLabel
|
||||
>
|
||||
<input
|
||||
v-model="newInterfaceBackboneListenIp"
|
||||
type="text"
|
||||
@@ -373,10 +375,58 @@
|
||||
<input
|
||||
v-model="newInterfaceBackboneListenDevice"
|
||||
type="text"
|
||||
placeholder="e.g. eth0"
|
||||
placeholder="Kernel interface e.g. eth0, wlan0"
|
||||
class="input-field"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
hostKernelInterfacesLoading ||
|
||||
hostKernelInterfaces.length ||
|
||||
hostKernelInterfacesUnavailable
|
||||
"
|
||||
class="rounded-xl border border-gray-100 dark:border-zinc-800 bg-gray-50/50 dark:bg-zinc-900/30 p-3 space-y-2"
|
||||
>
|
||||
<p class="text-xs text-gray-600 dark:text-zinc-400">
|
||||
{{ $t("interfaces.kernel_iface_picker_title") }}
|
||||
</p>
|
||||
<div
|
||||
v-if="hostKernelInterfacesLoading"
|
||||
class="text-xs text-gray-400"
|
||||
>
|
||||
{{ $t("interfaces.kernel_iface_loading") }}
|
||||
</div>
|
||||
<div v-else class="flex flex-wrap gap-1.5 max-h-36 overflow-y-auto">
|
||||
<button
|
||||
v-for="iface in hostKernelInterfaces"
|
||||
:key="'bb-' + iface.name"
|
||||
type="button"
|
||||
class="px-2.5 py-1.5 text-left text-xs rounded-lg border border-gray-200 dark:border-zinc-700 bg-white/90 dark:bg-zinc-900/90 hover:border-blue-400 dark:hover:border-blue-500 transition-colors max-w-full"
|
||||
@click="newInterfaceBackboneListenDevice = iface.name"
|
||||
>
|
||||
<span
|
||||
class="font-mono font-medium text-gray-900 dark:text-zinc-100"
|
||||
>{{ iface.name }}</span
|
||||
>
|
||||
<span
|
||||
v-if="iface.addresses && iface.addresses.length"
|
||||
class="block text-[10px] text-gray-500 dark:text-zinc-500 truncate max-w-[20rem]"
|
||||
>{{
|
||||
iface.addresses.slice(0, 3).join(" \u00b7 ")
|
||||
}}</span
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
v-if="hostKernelInterfacesUnavailable"
|
||||
class="text-xs text-amber-600 dark:text-amber-500"
|
||||
>
|
||||
{{ hostKernelInterfacesUnavailable }}
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 dark:text-zinc-500">
|
||||
{{ $t("interfaces.kernel_iface_picker_help") }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Toggle
|
||||
id="backbone-listen-ipv6"
|
||||
@@ -397,12 +447,13 @@
|
||||
class="space-y-4"
|
||||
>
|
||||
<div>
|
||||
<FormLabel class="glass-label">Listen IP (Optional)</FormLabel>
|
||||
<FormLabel class="glass-label">Listen IP</FormLabel>
|
||||
<input
|
||||
v-model="newInterfaceListenIp"
|
||||
type="text"
|
||||
placeholder="0.0.0.0"
|
||||
class="input-field"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -419,10 +470,53 @@
|
||||
<input
|
||||
v-model="newInterfaceNetworkDevice"
|
||||
type="text"
|
||||
placeholder="e.g. eth0 (overrides Listen IP)"
|
||||
placeholder="e.g. eth0 (real OS interface; leave empty to use Listen IP only)"
|
||||
class="input-field"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
hostKernelInterfacesLoading ||
|
||||
hostKernelInterfaces.length ||
|
||||
hostKernelInterfacesUnavailable
|
||||
"
|
||||
class="rounded-xl border border-gray-100 dark:border-zinc-800 bg-gray-50/50 dark:bg-zinc-900/30 p-3 space-y-2"
|
||||
>
|
||||
<p class="text-xs text-gray-600 dark:text-zinc-400">
|
||||
{{ $t("interfaces.kernel_iface_picker_title") }}
|
||||
</p>
|
||||
<div v-if="hostKernelInterfacesLoading" class="text-xs text-gray-400">
|
||||
{{ $t("interfaces.kernel_iface_loading") }}
|
||||
</div>
|
||||
<div v-else class="flex flex-wrap gap-1.5 max-h-36 overflow-y-auto">
|
||||
<button
|
||||
v-for="iface in hostKernelInterfaces"
|
||||
:key="'srv-' + iface.name"
|
||||
type="button"
|
||||
class="px-2.5 py-1.5 text-left text-xs rounded-lg border border-gray-200 dark:border-zinc-700 bg-white/90 dark:bg-zinc-900/90 hover:border-blue-400 dark:hover:border-blue-500 transition-colors max-w-full"
|
||||
@click="newInterfaceNetworkDevice = iface.name"
|
||||
>
|
||||
<span
|
||||
class="font-mono font-medium text-gray-900 dark:text-zinc-100"
|
||||
>{{ iface.name }}</span
|
||||
>
|
||||
<span
|
||||
v-if="iface.addresses && iface.addresses.length"
|
||||
class="block text-[10px] text-gray-500 dark:text-zinc-500 truncate max-w-[20rem]"
|
||||
>{{ iface.addresses.slice(0, 3).join(" \u00b7 ") }}</span
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
v-if="hostKernelInterfacesUnavailable"
|
||||
class="text-xs text-amber-600 dark:text-amber-500"
|
||||
>
|
||||
{{ hostKernelInterfacesUnavailable }}
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 dark:text-zinc-500">
|
||||
{{ $t("interfaces.kernel_iface_picker_help") }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
v-if="newInterfaceType === 'TCPServerInterface'"
|
||||
class="flex flex-wrap items-center gap-4"
|
||||
@@ -562,10 +656,12 @@
|
||||
|
||||
<div
|
||||
v-if="newInterfaceRNodeUseIP || newInterfaceType === 'RNodeIPInterface'"
|
||||
class="grid grid-cols-2 gap-4"
|
||||
class="space-y-2"
|
||||
>
|
||||
<div>
|
||||
<FormLabel class="glass-label">Host</FormLabel>
|
||||
<FormLabel class="glass-label">{{
|
||||
$t("interfaces.rnode_ip_host_label")
|
||||
}}</FormLabel>
|
||||
<input
|
||||
v-model="newInterfaceRNodeIPHost"
|
||||
type="text"
|
||||
@@ -573,15 +669,9 @@
|
||||
class="input-field"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<FormLabel class="glass-label">Port</FormLabel>
|
||||
<input
|
||||
v-model="newInterfaceRNodeIPPort"
|
||||
type="number"
|
||||
placeholder="7633"
|
||||
class="input-field"
|
||||
/>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-zinc-500 leading-relaxed">
|
||||
{{ $t("interfaces.rnode_tcp_port_fixed_hint") }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
@@ -1697,6 +1787,10 @@ export default {
|
||||
|
||||
comports: [],
|
||||
|
||||
hostKernelInterfaces: [],
|
||||
hostKernelInterfacesLoading: false,
|
||||
hostKernelInterfacesUnavailable: null,
|
||||
|
||||
newInterfaceName: null,
|
||||
newInterfaceType: null,
|
||||
|
||||
@@ -1783,8 +1877,7 @@ export default {
|
||||
newInterfaceRNodeUseIP: false,
|
||||
newInterfaceRNodeUseBle: false,
|
||||
newInterfaceRNodeBlePeer: "",
|
||||
newInterfaceRNodeIPHost: "localhost",
|
||||
newInterfaceRNodeIPPort: "7633",
|
||||
newInterfaceRNodeIPHost: "",
|
||||
RNodeGHzValue: 0,
|
||||
RNodeMHzValue: 0,
|
||||
RNodekHzValue: 0,
|
||||
@@ -1902,6 +1995,7 @@ export default {
|
||||
this.getConfig();
|
||||
this.loadReticulumDiscoveryConfig();
|
||||
this.loadComports();
|
||||
this.loadHostKernelInterfaces();
|
||||
this.loadCommunityInterfaces();
|
||||
|
||||
// check if we are editing an interface
|
||||
@@ -1986,6 +2080,59 @@ export default {
|
||||
console.log(e);
|
||||
}
|
||||
},
|
||||
buildRNodeTcpPort() {
|
||||
let h = String(this.newInterfaceRNodeIPHost ?? "").trim();
|
||||
while (h.endsWith(":")) {
|
||||
h = h.slice(0, -1);
|
||||
}
|
||||
if (!h) {
|
||||
return "";
|
||||
}
|
||||
return `tcp://${h}`;
|
||||
},
|
||||
parseRnodeTcpHostFromPort(portStr) {
|
||||
const s = String(portStr || "");
|
||||
if (!s.startsWith("tcp://")) {
|
||||
return "localhost";
|
||||
}
|
||||
let rest = s.slice(6);
|
||||
while (rest.endsWith(":")) {
|
||||
rest = rest.slice(0, -1);
|
||||
}
|
||||
if (!rest) {
|
||||
return "";
|
||||
}
|
||||
if (rest.startsWith("[")) {
|
||||
const close = rest.indexOf("]");
|
||||
if (close !== -1 && rest[close + 1] === ":") {
|
||||
return rest.slice(0, close + 1);
|
||||
}
|
||||
return rest;
|
||||
}
|
||||
if (rest.includes(":") && rest.indexOf(":") === rest.lastIndexOf(":")) {
|
||||
const idx = rest.indexOf(":");
|
||||
const tail = rest.slice(idx + 1);
|
||||
if (/^\d{1,5}$/.test(tail) && Number(tail) <= 65535) {
|
||||
return rest.slice(0, idx);
|
||||
}
|
||||
}
|
||||
return rest;
|
||||
},
|
||||
async loadHostKernelInterfaces() {
|
||||
this.hostKernelInterfacesLoading = true;
|
||||
this.hostKernelInterfacesUnavailable = null;
|
||||
try {
|
||||
const response = await window.api.get(`/api/v1/system/network-interfaces`);
|
||||
this.hostKernelInterfaces = response.data.interfaces || [];
|
||||
this.hostKernelInterfacesUnavailable = response.data.unavailable_reason || null;
|
||||
} catch (e) {
|
||||
this.hostKernelInterfaces = [];
|
||||
this.hostKernelInterfacesUnavailable = e.response?.data?.message ?? e.message ?? "unavailable";
|
||||
console.log(e);
|
||||
} finally {
|
||||
this.hostKernelInterfacesLoading = false;
|
||||
}
|
||||
},
|
||||
effectiveRNodeBlePort() {
|
||||
let p = (this.newInterfaceRNodeBlePeer || "").trim();
|
||||
if (!p) {
|
||||
@@ -2139,10 +2286,7 @@ export default {
|
||||
this.newInterfaceRNodeUseBle = true;
|
||||
this.newInterfaceRNodeBlePeer = String(iface.port);
|
||||
} else if (iface.port && String(iface.port).startsWith("tcp://")) {
|
||||
const addr = String(iface.port).replace("tcp://", "");
|
||||
const parts = addr.split(":");
|
||||
this.newInterfaceRNodeIPHost = parts[0] || "localhost";
|
||||
this.newInterfaceRNodeIPPort = parts[1] || "7633";
|
||||
this.newInterfaceRNodeIPHost = this.parseRnodeTcpHostFromPort(iface.port);
|
||||
this.newInterfaceRNodeUseIP = true;
|
||||
}
|
||||
this.newInterfaceFrequency = iface.frequency;
|
||||
@@ -2265,10 +2409,7 @@ export default {
|
||||
this.newInterfaceRNodeUseBle = true;
|
||||
this.newInterfaceRNodeBlePeer = config.port;
|
||||
} else if (config.port.startsWith("tcp://")) {
|
||||
const addr = config.port.replace("tcp://", "");
|
||||
const [host, port] = addr.split(":");
|
||||
this.newInterfaceRNodeIPHost = host;
|
||||
this.newInterfaceRNodeIPPort = port || "7633";
|
||||
this.newInterfaceRNodeIPHost = this.parseRnodeTcpHostFromPort(config.port);
|
||||
this.newInterfaceRNodeUseIP = true;
|
||||
}
|
||||
}
|
||||
@@ -2561,6 +2702,24 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
this.newInterfaceType === "RNodeIPInterface" ||
|
||||
(this.newInterfaceType === "RNodeInterface" && this.newInterfaceRNodeUseIP)
|
||||
) {
|
||||
if (!this.buildRNodeTcpPort()) {
|
||||
ToastUtils.error(this.$t("interfaces.rnode_tcp_host_required"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.newInterfaceType === "TCPServerInterface" || this.newInterfaceType === "UDPInterface") {
|
||||
const lip = String(this.newInterfaceListenIp ?? "").trim();
|
||||
if (!lip) {
|
||||
ToastUtils.error(this.$t("interfaces.listen_ip_required"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.newInterfaceType === "__external__") {
|
||||
const typeStr = (this.customExternalTypeName || "").trim();
|
||||
if (!typeStr) {
|
||||
@@ -2636,16 +2795,18 @@ export default {
|
||||
transport_identity: isBackboneListener ? null : this.newInterfaceTransportIdentity,
|
||||
peers: i2pPeers,
|
||||
listen_ip: isBackboneListener
|
||||
? this.newInterfaceBackboneListenIp || null
|
||||
: this.newInterfaceListenIp,
|
||||
? (this.newInterfaceBackboneListenIp || "").trim() || null
|
||||
: this.newInterfaceType === "TCPServerInterface" || this.newInterfaceType === "UDPInterface"
|
||||
? String(this.newInterfaceListenIp ?? "").trim()
|
||||
: this.newInterfaceListenIp,
|
||||
listen_port: isBackboneListener
|
||||
? this.newInterfaceBackboneListenPort || null
|
||||
: this.newInterfaceListenPort,
|
||||
forward_ip: this.newInterfaceForwardIp,
|
||||
forward_port: this.newInterfaceForwardPort,
|
||||
device: isBackboneListener
|
||||
? this.newInterfaceBackboneListenDevice || null
|
||||
: this.newInterfaceNetworkDevice,
|
||||
? (this.newInterfaceBackboneListenDevice || "").trim() || null
|
||||
: (this.newInterfaceNetworkDevice || "").trim() || null,
|
||||
prefer_ipv6: this.newInterfacePreferIPV6 === true,
|
||||
kiss_framing: this.newInterfaceKISSFramingEnabled === true,
|
||||
i2p_tunneled: this.newInterfaceI2PTunnelingEnabled === true,
|
||||
@@ -2659,11 +2820,12 @@ export default {
|
||||
: undefined,
|
||||
connectable:
|
||||
this.newInterfaceType === "I2PInterface" ? this.newInterfaceConnectable === true : null,
|
||||
port: this.newInterfaceRNodeUseIP
|
||||
? `tcp://${this.newInterfaceRNodeIPHost}:${this.newInterfaceRNodeIPPort}`
|
||||
: this.newInterfaceRNodeUseBle
|
||||
? this.effectiveRNodeBlePort()
|
||||
: this.newInterfacePort,
|
||||
port:
|
||||
this.newInterfaceType === "RNodeIPInterface" || this.newInterfaceRNodeUseIP
|
||||
? this.buildRNodeTcpPort()
|
||||
: this.newInterfaceRNodeUseBle
|
||||
? this.effectiveRNodeBlePort()
|
||||
: this.newInterfacePort,
|
||||
frequency: freqHz,
|
||||
bandwidth: this.newInterfaceBandwidth,
|
||||
txpower: this.newInterfaceTxpower,
|
||||
|
||||
@@ -810,6 +810,8 @@
|
||||
"contact_support_title": "Kontakt & Unterstuetzung",
|
||||
"contact_lxmf_address": "LXMF-Adresse",
|
||||
"contact_propagation_hint": "An den Verbreitungsknoten senden, wenn Sie mich nicht direkt erreichen können.",
|
||||
"contact_open_messages": "Unterhaltung oeffnen",
|
||||
"contact_copy_address": "Adresse kopieren",
|
||||
"donate_label": "Spenden",
|
||||
"donate_monero_label": "Monero (XMR)",
|
||||
"donate_copy_monero": "Monero-Adresse kopieren",
|
||||
@@ -951,7 +953,14 @@
|
||||
"rnode_ble_hint": "Reticulum nutzt BLE über bleak auf dem Gerät, auf dem MeshChatX läuft. RNode zuerst im Betriebssystem koppeln oder verbinden. Reine Browser- oder Remote-UI kann kein BLE an Reticulum durchreichen; Electron, Linux, macOS, Windows oder die Android-App verwenden, wo eingebettetes Python bleak enthält. Unter Android gelten weiterhin normale Kopplung, Hardwareunterstützung und Berechtigungsabfragen.",
|
||||
"rnode_ble_peer_label": "BLE-Name oder MAC",
|
||||
"rnode_ble_peer_placeholder": "MyRNode oder aa:bb:cc:dd:ee:ff",
|
||||
"rnode_ble_peer_required": "Anzeigenamen oder MAC-Adresse des RNode eingeben."
|
||||
"rnode_ble_peer_required": "Anzeigenamen oder MAC-Adresse des RNode eingeben.",
|
||||
"rnode_ip_host_label": "Hostname oder IP-Adresse",
|
||||
"rnode_tcp_port_fixed_hint": "Reticulum nutzt TCP-Port 7633 auf dem RNode; nur Hostname oder IP eingeben (kein :Port).",
|
||||
"rnode_tcp_host_required": "Hostname oder IP für RNode über TCP eingeben.",
|
||||
"listen_ip_required": "Listen-IP ist erforderlich.",
|
||||
"kernel_iface_picker_title": "Schnittstellen auf diesem Rechner",
|
||||
"kernel_iface_loading": "Laden…",
|
||||
"kernel_iface_picker_help": "Optional ein Kernel-Interface-Name, oder leer lassen und nur über Listen-IP binden. Klick füllt das Feld."
|
||||
},
|
||||
"map": {
|
||||
"title": "Karte",
|
||||
|
||||
@@ -740,6 +740,8 @@
|
||||
"contact_support_title": "Contact & support",
|
||||
"contact_lxmf_address": "LXMF Address",
|
||||
"contact_propagation_hint": "Send to propagation node if you cant reach me!",
|
||||
"contact_open_messages": "Open conversation",
|
||||
"contact_copy_address": "Copy address",
|
||||
"donate_label": "Donate",
|
||||
"donate_monero_label": "Monero (XMR)",
|
||||
"donate_copy_monero": "Copy Monero address",
|
||||
@@ -899,7 +901,14 @@
|
||||
"rnode_ble_hint": "Reticulum uses BLE via bleak on the device running MeshChatX. Pair or bond the RNode in the OS first. A browser-only or remote UI cannot attach BLE to Reticulum; use Electron, Linux, macOS, Windows, or the Android app, where the bundled Python includes bleak. On Android, Bluetooth still follows normal OS pairing, hardware support, and permission prompts.",
|
||||
"rnode_ble_peer_label": "BLE name or MAC",
|
||||
"rnode_ble_peer_placeholder": "MyRNode or aa:bb:cc:dd:ee:ff",
|
||||
"rnode_ble_peer_required": "Enter a BLE advertised name or MAC address for the RNode."
|
||||
"rnode_ble_peer_required": "Enter a BLE advertised name or MAC address for the RNode.",
|
||||
"rnode_ip_host_label": "Host or IP address",
|
||||
"rnode_tcp_port_fixed_hint": "Reticulum opens TCP port 7633 on the RNode itself; enter only the hostname or IP (do not add :port).",
|
||||
"rnode_tcp_host_required": "Enter the hostname or IP for RNode over TCP.",
|
||||
"listen_ip_required": "Listen IP is required.",
|
||||
"kernel_iface_picker_title": "Interfaces on this host",
|
||||
"kernel_iface_loading": "Loading…",
|
||||
"kernel_iface_picker_help": "Device is optional: one kernel interface name, or leave empty to bind using Listen IP only. Click a row to fill the field."
|
||||
},
|
||||
"map": {
|
||||
"title": "Map",
|
||||
|
||||
@@ -758,6 +758,8 @@
|
||||
"contact_support_title": "Contacto y apoyo",
|
||||
"contact_lxmf_address": "Dirección LXMF",
|
||||
"contact_propagation_hint": "Enviar al nodo de propagación si no puedes alcanzarme directamente.",
|
||||
"contact_open_messages": "Abrir conversación",
|
||||
"contact_copy_address": "Copiar dirección",
|
||||
"donate_label": "Donar",
|
||||
"donate_monero_label": "Monero (XMR)",
|
||||
"donate_copy_monero": "Copiar dirección Monero",
|
||||
@@ -899,7 +901,14 @@
|
||||
"rnode_ble_hint": "Reticulum usa BLE mediante bleak en el dispositivo donde corre MeshChatX. Empareje o vincule el RNode en el sistema primero. Una interfaz solo web o remota no puede conectar BLE a Reticulum; use Electron, Linux, macOS, Windows o la app Android, donde Python integrado incluye bleak. En Android, el Bluetooth sigue las reglas habituales de emparejamiento, hardware y permisos del sistema.",
|
||||
"rnode_ble_peer_label": "Nombre BLE o MAC",
|
||||
"rnode_ble_peer_placeholder": "MyRNode o aa:bb:cc:dd:ee:ff",
|
||||
"rnode_ble_peer_required": "Introduzca el nombre anunciado o la MAC del RNode."
|
||||
"rnode_ble_peer_required": "Introduzca el nombre anunciado o la MAC del RNode.",
|
||||
"rnode_ip_host_label": "Nombre de host o dirección IP",
|
||||
"rnode_tcp_port_fixed_hint": "Reticulum usa el puerto TCP 7633 en el RNode; indique solo el host o la IP (sin :puerto).",
|
||||
"rnode_tcp_host_required": "Indique el host o la IP para RNode por TCP.",
|
||||
"listen_ip_required": "La IP de escucha es obligatoria.",
|
||||
"kernel_iface_picker_title": "Interfaces en este equipo",
|
||||
"kernel_iface_loading": "Cargando…",
|
||||
"kernel_iface_picker_help": "“Dispositivo” es opcional: un nombre de interfaz del kernel, o vacío para enlazar solo con la IP de escucha. Pulse una fila para rellenar."
|
||||
},
|
||||
"map": {
|
||||
"title": "Mapa",
|
||||
|
||||
@@ -758,6 +758,8 @@
|
||||
"contact_support_title": "Contact et soutien",
|
||||
"contact_lxmf_address": "Adresse LXMF",
|
||||
"contact_propagation_hint": "Envoyez au nœud de propagation si vous ne pouvez pas me joindre directement.",
|
||||
"contact_open_messages": "Ouvrir la conversation",
|
||||
"contact_copy_address": "Copier l’adresse",
|
||||
"donate_label": "Faire un don",
|
||||
"donate_monero_label": "Monero (XMR)",
|
||||
"donate_copy_monero": "Copier l’adresse Monero",
|
||||
@@ -899,7 +901,14 @@
|
||||
"rnode_ble_hint": "Reticulum utilise le BLE via bleak sur l'appareil qui exécute MeshChatX. Associez ou appairez d'abord le RNode dans le système. Une interface web seule ou distante ne peut pas raccorder le BLE à Reticulum ; utilisez Electron, Linux, macOS, Windows ou l'application Android, où Python embarqué inclut bleak. Sous Android, le Bluetooth suit l'appairage, le matériel et les demandes de permission habituels du système.",
|
||||
"rnode_ble_peer_label": "Nom BLE ou MAC",
|
||||
"rnode_ble_peer_placeholder": "MyRNode ou aa:bb:cc:dd:ee:ff",
|
||||
"rnode_ble_peer_required": "Entrez le nom annoncé ou l'adresse MAC du RNode."
|
||||
"rnode_ble_peer_required": "Entrez le nom annoncé ou l'adresse MAC du RNode.",
|
||||
"rnode_ip_host_label": "Nom d'hôte ou adresse IP",
|
||||
"rnode_tcp_port_fixed_hint": "Reticulum utilise le port TCP 7633 sur le RNode ; saisissez uniquement le nom d'hôte ou l'IP (sans :port).",
|
||||
"rnode_tcp_host_required": "Saisissez le nom d'hôte ou l'IP pour le RNode en TCP.",
|
||||
"listen_ip_required": "L'adresse IP d'écoute est obligatoire.",
|
||||
"kernel_iface_picker_title": "Interfaces sur cette machine",
|
||||
"kernel_iface_loading": "Chargement…",
|
||||
"kernel_iface_picker_help": "Le champ Périphérique est optionnel : un seul nom d'interface noyau, ou laisser vide pour lier via l'IP d'écoute uniquement. Cliquez sur une ligne pour remplir."
|
||||
},
|
||||
"map": {
|
||||
"title": "Carte",
|
||||
|
||||
@@ -810,6 +810,8 @@
|
||||
"contact_support_title": "Contatto e supporto",
|
||||
"contact_lxmf_address": "Indirizzo LXMF",
|
||||
"contact_propagation_hint": "Invia al nodo di propagazione se non riesci a raggiungermi direttamente.",
|
||||
"contact_open_messages": "Apri conversazione",
|
||||
"contact_copy_address": "Copia indirizzo",
|
||||
"donate_label": "Dona",
|
||||
"donate_monero_label": "Monero (XMR)",
|
||||
"donate_copy_monero": "Copia indirizzo Monero",
|
||||
@@ -951,7 +953,14 @@
|
||||
"rnode_ble_hint": "Reticulum usa il BLE tramite bleak sul dispositivo che esegue MeshChatX. Associare o accoppiare prima l'RNode nel sistema. Solo browser o interfaccia remota non possono collegare il BLE a Reticulum; usare Electron, Linux, macOS, Windows o l'app Android, dove Python integrato include bleak. Su Android valgono come sempre accoppiamento, hardware e richieste di permesso del sistema.",
|
||||
"rnode_ble_peer_label": "Nome BLE o MAC",
|
||||
"rnode_ble_peer_placeholder": "MyRNode o aa:bb:cc:dd:ee:ff",
|
||||
"rnode_ble_peer_required": "Inserire il nome annunciato o il MAC dell'RNode."
|
||||
"rnode_ble_peer_required": "Inserire il nome annunciato o il MAC dell'RNode.",
|
||||
"rnode_ip_host_label": "Nome host o indirizzo IP",
|
||||
"rnode_tcp_port_fixed_hint": "Reticulum usa la porta TCP 7633 sull'RNode; inserire solo host o IP (senza :porta).",
|
||||
"rnode_tcp_host_required": "Inserire host o IP per l'RNode via TCP.",
|
||||
"listen_ip_required": "L'IP di ascolto è obbligatoria.",
|
||||
"kernel_iface_picker_title": "Interfacce su questo host",
|
||||
"kernel_iface_loading": "Caricamento…",
|
||||
"kernel_iface_picker_help": "Il dispositivo è facoltativo: un solo nome di interfaccia del kernel, o lascia vuoto e lega solo con l'IP di ascolto. Clic su una riga per compilare."
|
||||
},
|
||||
"map": {
|
||||
"title": "Mappa",
|
||||
|
||||
@@ -758,6 +758,8 @@
|
||||
"contact_support_title": "Contact & ondersteuning",
|
||||
"contact_lxmf_address": "LXMF-adres",
|
||||
"contact_propagation_hint": "Stuur naar het propagatieknooppunt als je me niet direct kunt bereiken.",
|
||||
"contact_open_messages": "Gesprek openen",
|
||||
"contact_copy_address": "Adres kopiëren",
|
||||
"donate_label": "Doneren",
|
||||
"donate_monero_label": "Monero (XMR)",
|
||||
"donate_copy_monero": "Monero-adres kopiëren",
|
||||
@@ -899,7 +901,14 @@
|
||||
"rnode_ble_hint": "Reticulum gebruikt BLE via bleak op het apparaat waar MeshChatX draait. Koppel of verbind de RNode eerst in het besturingssysteem. Met alleen een browser of een externe UI kunt u geen BLE aan Reticulum koppelen; gebruik Electron, Linux, macOS, Windows of de Android-app, waar ingebed Python bleak bevat. Op Android gelden de gebruikelijke koppeling, hardware en machtigingsvragen van het OS.",
|
||||
"rnode_ble_peer_label": "BLE-naam of MAC",
|
||||
"rnode_ble_peer_placeholder": "MyRNode of aa:bb:cc:dd:ee:ff",
|
||||
"rnode_ble_peer_required": "Voer de aangekondigde naam of MAC van de RNode in."
|
||||
"rnode_ble_peer_required": "Voer de aangekondigde naam of MAC van de RNode in.",
|
||||
"rnode_ip_host_label": "Hostnaam of IP-adres",
|
||||
"rnode_tcp_port_fixed_hint": "Reticulum gebruikt TCP-poort 7633 op de RNode; alleen hostnaam of IP (geen :poort).",
|
||||
"rnode_tcp_host_required": "Voer hostnaam of IP in voor RNode via TCP.",
|
||||
"listen_ip_required": "Listen-IP is verplicht.",
|
||||
"kernel_iface_picker_title": "Interfaces op deze host",
|
||||
"kernel_iface_loading": "Laden…",
|
||||
"kernel_iface_picker_help": "Apparaat is optioneel: één kernelinterfacenaam, of leeg laten en alleen via listen-IP binden. Klik op een regel om in te vullen."
|
||||
},
|
||||
"map": {
|
||||
"title": "Kaart",
|
||||
|
||||
@@ -810,6 +810,8 @@
|
||||
"contact_support_title": "Контакты и поддержка",
|
||||
"contact_lxmf_address": "Адрес LXMF",
|
||||
"contact_propagation_hint": "Отправьте на узел распространения, если не удаётся связаться со мной напрямую.",
|
||||
"contact_open_messages": "Открыть переписку",
|
||||
"contact_copy_address": "Копировать адрес",
|
||||
"donate_label": "Пожертвование",
|
||||
"donate_monero_label": "Monero (XMR)",
|
||||
"donate_copy_monero": "Копировать адрес Monero",
|
||||
@@ -951,7 +953,14 @@
|
||||
"rnode_ble_hint": "Reticulum использует BLE через bleak на устройстве, где запущен MeshChatX. Сначала сопрягите или свяжите RNode в ОС. Только веб или удалённый интерфейс не подключит BLE к Reticulum; используйте Electron, Linux, macOS, Windows или приложение Android, где встроенный Python включает bleak. На Android действуют обычные сопряжение, поддержка оборудования и запросы разрешений ОС.",
|
||||
"rnode_ble_peer_label": "Имя BLE или MAC",
|
||||
"rnode_ble_peer_placeholder": "MyRNode или aa:bb:cc:dd:ee:ff",
|
||||
"rnode_ble_peer_required": "Укажите объявленное имя или MAC-адрес RNode."
|
||||
"rnode_ble_peer_required": "Укажите объявленное имя или MAC-адрес RNode.",
|
||||
"rnode_ip_host_label": "Имя хоста или IP-адрес",
|
||||
"rnode_tcp_port_fixed_hint": "Reticulum подключается к RNode по TCP 7633; укажите только имя хоста или IP (без :порт).",
|
||||
"rnode_tcp_host_required": "Укажите имя хоста или IP для RNode по TCP.",
|
||||
"listen_ip_required": "Укажите IP для прослушивания.",
|
||||
"kernel_iface_picker_title": "Интерфейсы на этом узле",
|
||||
"kernel_iface_loading": "Загрузка…",
|
||||
"kernel_iface_picker_help": "Поле «устройство» необязательно: одно имя интерфейса ядра или пусто — привязка только по Listen IP. Нажмите строку, чтобы подставить имя."
|
||||
},
|
||||
"map": {
|
||||
"title": "Карта",
|
||||
|
||||
@@ -758,6 +758,8 @@
|
||||
"contact_support_title": "联系与支持",
|
||||
"contact_lxmf_address": "LXMF 地址",
|
||||
"contact_propagation_hint": "若无法直接联系我,请发送至传播节点。",
|
||||
"contact_open_messages": "打开对话",
|
||||
"contact_copy_address": "复制地址",
|
||||
"donate_label": "捐赠",
|
||||
"donate_monero_label": "Monero(XMR)",
|
||||
"donate_copy_monero": "复制 Monero 地址",
|
||||
@@ -899,7 +901,14 @@
|
||||
"rnode_ble_hint": "Reticulum 通过 bleak 在运行 MeshChatX 的设备上使用 BLE。请先在系统中配对或绑定 RNode。纯网页或远程界面无法将 BLE 交给 Reticulum;请使用内置 Python 且包含 bleak 的 Electron、Linux、macOS、Windows 或 Android 应用。在 Android 上仍需遵循系统常规的配对、硬件支持与权限提示。",
|
||||
"rnode_ble_peer_label": "BLE 名称或 MAC",
|
||||
"rnode_ble_peer_placeholder": "MyRNode 或 aa:bb:cc:dd:ee:ff",
|
||||
"rnode_ble_peer_required": "请输入 RNode 的广播名称或 MAC 地址。"
|
||||
"rnode_ble_peer_required": "请输入 RNode 的广播名称或 MAC 地址。",
|
||||
"rnode_ip_host_label": "主机名或 IP 地址",
|
||||
"rnode_tcp_port_fixed_hint": "Reticulum 使用 TCP 7633 连接 RNode;只需填写主机名或 IP,不要加 :端口。",
|
||||
"rnode_tcp_host_required": "请填写 RNode TCP 连接的主机名或 IP。",
|
||||
"listen_ip_required": "Listen IP 为必填项。",
|
||||
"kernel_iface_picker_title": "本机网络接口",
|
||||
"kernel_iface_loading": "加载中…",
|
||||
"kernel_iface_picker_help": "“设备”为可选,填写一个内核网卡名,或留空仅按 Listen IP 绑定。点击一行填入输入框。"
|
||||
},
|
||||
"map": {
|
||||
"title": "地图",
|
||||
|
||||
@@ -108,6 +108,10 @@
|
||||
"method": "GET",
|
||||
"path": "/api/v1/comports"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/system/network-interfaces"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/config"
|
||||
|
||||
@@ -44,3 +44,33 @@ def test_coerce_rnode_frequency_hz_integer_mhz():
|
||||
|
||||
def test_coerce_rnode_frequency_hz_leaves_midrange_hz():
|
||||
assert InterfaceEditor.coerce_rnode_frequency_hz(125000) == 125000
|
||||
|
||||
|
||||
def test_normalize_rnode_tcp_port_host_only():
|
||||
assert (
|
||||
InterfaceEditor.normalize_rnode_tcp_port("tcp://10.0.0.5") == "tcp://10.0.0.5"
|
||||
)
|
||||
|
||||
|
||||
def test_normalize_rnode_tcp_port_strips_legacy_ipv4_port():
|
||||
assert (
|
||||
InterfaceEditor.normalize_rnode_tcp_port("tcp://10.0.0.5:7633")
|
||||
== "tcp://10.0.0.5"
|
||||
)
|
||||
|
||||
|
||||
def test_normalize_rnode_tcp_port_strips_trailing_colons():
|
||||
assert (
|
||||
InterfaceEditor.normalize_rnode_tcp_port("tcp://10.0.0.5:") == "tcp://10.0.0.5"
|
||||
)
|
||||
|
||||
|
||||
def test_normalize_rnode_tcp_port_bracket_ipv6_with_port():
|
||||
assert (
|
||||
InterfaceEditor.normalize_rnode_tcp_port("tcp://[2001:db8::1]:7633")
|
||||
== "tcp://[2001:db8::1]"
|
||||
)
|
||||
|
||||
|
||||
def test_normalize_rnode_tcp_port_non_tcp_unchanged():
|
||||
assert InterfaceEditor.normalize_rnode_tcp_port("/dev/ttyUSB0") == "/dev/ttyUSB0"
|
||||
|
||||
@@ -235,6 +235,23 @@ async def test_tcp_server_persists_optional_options(temp_dir):
|
||||
assert saved["i2p_tunneled"] is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tcp_server_rejects_whitespace_listen_ip(temp_dir):
|
||||
config = ConfigDict({"reticulum": {}, "interfaces": {}})
|
||||
|
||||
async with make_app(temp_dir, config) as handler:
|
||||
payload = {
|
||||
"name": "TCPServer",
|
||||
"type": "TCPServerInterface",
|
||||
"listen_ip": " ",
|
||||
"listen_port": _free_port("tcp"),
|
||||
}
|
||||
response = await handler(make_request(payload))
|
||||
body = json.loads(response.body)
|
||||
assert response.status == 422, body
|
||||
assert "Listen IP" in body["message"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tcp_server_rejects_busy_listen_port(temp_dir):
|
||||
config = ConfigDict({"reticulum": {}, "interfaces": {}})
|
||||
@@ -433,6 +450,48 @@ async def test_rnode_ble_uart_port_persisted(temp_dir):
|
||||
assert config["interfaces"]["RadioBLE"]["port"] == "ble://aa:bb:cc:dd:ee:ff"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rnode_tcp_over_ip_normalizes_to_host_only(temp_dir):
|
||||
config = ConfigDict({"reticulum": {}, "interfaces": {}})
|
||||
|
||||
async with make_app(temp_dir, config) as handler:
|
||||
payload = {
|
||||
"name": "RNodeWiFi",
|
||||
"type": "RNodeIPInterface",
|
||||
"port": "tcp://192.168.4.1:7633",
|
||||
"frequency": 868000000,
|
||||
"bandwidth": 125000,
|
||||
"txpower": 7,
|
||||
"spreadingfactor": 8,
|
||||
"codingrate": 5,
|
||||
}
|
||||
response = await handler(make_request(payload))
|
||||
body = json.loads(response.body)
|
||||
assert response.status == 200, body
|
||||
assert config["interfaces"]["RNodeWiFi"]["port"] == "tcp://192.168.4.1"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rnode_tcp_over_ip_rejects_empty_host(temp_dir):
|
||||
config = ConfigDict({"reticulum": {}, "interfaces": {}})
|
||||
|
||||
async with make_app(temp_dir, config) as handler:
|
||||
payload = {
|
||||
"name": "Bad",
|
||||
"type": "RNodeIPInterface",
|
||||
"port": "tcp://",
|
||||
"frequency": 868000000,
|
||||
"bandwidth": 125000,
|
||||
"txpower": 7,
|
||||
"spreadingfactor": 8,
|
||||
"codingrate": 5,
|
||||
}
|
||||
response = await handler(make_request(payload))
|
||||
body = json.loads(response.body)
|
||||
assert response.status == 422, body
|
||||
assert "TCP host" in body["message"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rnode_frequency_mhz_decimal_normalized_to_hz(temp_dir):
|
||||
config = ConfigDict({"reticulum": {}, "interfaces": {}})
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
# SPDX-License-Identifier: 0BSD
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import tempfile
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
import RNS
|
||||
|
||||
from meshchatx.meshchat import ReticulumMeshChat
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_dir():
|
||||
dir_path = tempfile.mkdtemp()
|
||||
yield dir_path
|
||||
shutil.rmtree(dir_path)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_rns_minimal():
|
||||
with (
|
||||
patch("RNS.Reticulum") as mock_rns,
|
||||
patch("RNS.Transport"),
|
||||
patch("LXMF.LXMRouter"),
|
||||
patch("meshchatx.meshchat.get_file_path", return_value="/tmp/mock_path"),
|
||||
):
|
||||
mock_rns_instance = mock_rns.return_value
|
||||
mock_rns_instance.configpath = "/tmp/mock_config"
|
||||
mock_rns_instance.is_connected_to_shared_instance = False
|
||||
mock_rns_instance.transport_enabled.return_value = True
|
||||
|
||||
mock_id = MagicMock(spec=RNS.Identity)
|
||||
mock_id.hash = b"test_hash_32_bytes_long_01234567"
|
||||
mock_id.hexhash = mock_id.hash.hex()
|
||||
mock_id.get_private_key.return_value = b"test_private_key"
|
||||
yield mock_id
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_system_network_interfaces_returns_json(mock_rns_minimal, temp_dir):
|
||||
fake_ifaces = (
|
||||
[
|
||||
{"name": "eth0", "addresses": []},
|
||||
{"name": "lo", "addresses": []},
|
||||
],
|
||||
None,
|
||||
)
|
||||
with (
|
||||
patch("meshchatx.meshchat.generate_ssl_certificate"),
|
||||
patch(
|
||||
"meshchatx.meshchat.list_host_network_interfaces", return_value=fake_ifaces
|
||||
),
|
||||
):
|
||||
app_instance = ReticulumMeshChat(
|
||||
identity=mock_rns_minimal,
|
||||
storage_dir=temp_dir,
|
||||
reticulum_config_dir=temp_dir,
|
||||
)
|
||||
handler = None
|
||||
for route in app_instance.get_routes():
|
||||
if (
|
||||
route.path == "/api/v1/system/network-interfaces"
|
||||
and route.method == "GET"
|
||||
):
|
||||
handler = route.handler
|
||||
break
|
||||
assert handler is not None
|
||||
request = MagicMock()
|
||||
response = await handler(request)
|
||||
data = json.loads(response.body)
|
||||
assert data["interfaces"] == [
|
||||
{"name": "eth0", "addresses": []},
|
||||
{"name": "lo", "addresses": []},
|
||||
]
|
||||
assert data["unavailable_reason"] is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_system_network_interfaces_surfaces_psutil_error(
|
||||
mock_rns_minimal, temp_dir
|
||||
):
|
||||
fake_ifaces = ([], "permission denied")
|
||||
with (
|
||||
patch("meshchatx.meshchat.generate_ssl_certificate"),
|
||||
patch(
|
||||
"meshchatx.meshchat.list_host_network_interfaces", return_value=fake_ifaces
|
||||
),
|
||||
):
|
||||
app_instance = ReticulumMeshChat(
|
||||
identity=mock_rns_minimal,
|
||||
storage_dir=temp_dir,
|
||||
reticulum_config_dir=temp_dir,
|
||||
)
|
||||
handler = None
|
||||
for route in app_instance.get_routes():
|
||||
if (
|
||||
route.path == "/api/v1/system/network-interfaces"
|
||||
and route.method == "GET"
|
||||
):
|
||||
handler = route.handler
|
||||
break
|
||||
assert handler is not None
|
||||
response = await handler(MagicMock())
|
||||
data = json.loads(response.body)
|
||||
assert data["interfaces"] == []
|
||||
assert data["unavailable_reason"] == "permission denied"
|
||||
Reference in New Issue
Block a user