Felix Fietkau 562ced3d8f
Some checks are pending
Build all core packages / Build all core packages for selected target (push) Waiting to run
umdns: add cli module
This makes it easy to browse mDNS services and hosts on the local network

Signed-off-by: Felix Fietkau <nbd@nbd.name>
2025-02-13 19:00:30 +01:00

268 lines
5.3 KiB
Ucode

// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright (C) 2025 Felix Fietkau <nbd@nbd.name>
'use strict';
function mdns_data(model)
{
return model.ubus.call("umdns", "browse", { array: true, address: false });
}
function refresh_timer(model)
{
model.ubus.call("umdns", "update");
model.mdns.refresh_count++;
if (model.mdns.refresh_count < 3)
model.mdns.timer.set(500);
if (model.mdns.refresh_count < 2)
return;
let data = mdns_data(model);
for (let service_name, service_data in data) {
for (let host_name, host_data in service_data) {
let interface = host_data.iface;
if (!interface)
continue;
if (host_data.host)
continue;
let question = host_name + "." + service_name + ".local";
model.ubus.call("umdns", "query", { question, interface });
}
}
}
function refresh_start(model)
{
model.mdns.refresh_count = 0;
model.mdns.timer ??= model.uloop.timer(500, () => refresh_timer(model));
}
function strip_local(name)
{
if (substr(name, -6) == ".local")
name = substr(name, 0, -6);
return name;
}
function get_hosts(model)
{
let data = model.ubus.call("umdns", "hosts", { array: true });
let ret = {};
for (let name, val in data) {
name = strip_local(name);
ret[lc(name)] = val;
}
return ret;
}
function get_host_names(model)
{
let data = model.ubus.call("umdns", "hosts", { array: true });
let ret = {};
for (let name, val in data) {
name = strip_local(name);
ret[lc(name)] = name;
}
return ret;
}
function get_host_servicenames(model)
{
let hosts = {};
let data = mdns_data(model);
for (let service_name, service_data in data) {
for (let host_name, host_data in service_data) {
let name = strip_local(host_data.host);
hosts[lc(name)] ??= [];
if (index(hosts[lc(name)], host_name) < 0)
push(hosts[lc(name)], host_name);
}
}
return hosts;
}
function get_service_hosts(model, name)
{
name = lc(name);
let data = mdns_data(model);
for (let cur_name, hosts in data)
if (lc(cur_name) == name)
return hosts;
}
function get_host_services(model)
{
let hosts = {};
let data = mdns_data(model);
for (let service_name, service_data in data) {
for (let host_name, host_data in service_data) {
host_data.name = host_name;
host_name = lc(strip_local(host_data.host));
hosts[host_name] ??= {};
hosts[host_name][service_name] = host_data;
}
}
return hosts;
}
function host_info(host)
{
let ret = {};
if (host.ipv4)
ret.IPv4 = host.ipv4;
if (host.ipv6)
ret.IPv6 = host.ipv6;
return ret;
}
const host_arg = {
name: "host",
help: "host name",
type: "enum",
ignore_case: true,
value: () => values(get_host_names(model)),
};
const service_arg = {
name: "service",
help: "service name",
type: "enum",
ignore_case: true,
value: () => keys(mdns_data(model)),
};
function add_field(ret, name, val)
{
if (val == null)
return;
if (type(ret) == "array")
push(ret, [ name, val ]);
else
ret[name] = val;
}
function service_info(data)
{
let info = [];
add_field(info, "Name", data.name);
add_field(info, "Interface", data.iface);
add_field(info, "Port", data.port);
add_field(info, "Text", data.txt);
return info;
}
const MDNS = {
refresh: {
help: "Refresh service list by sending queries",
call: function(ctx, argv, named) {
refresh_start(model);
return ctx.ok("Querying hosts");
}
},
service: {
help: "Show service info",
args: [ service_arg ],
call: function (ctx, argv, named) {
let name = argv[0];
if (name != null) {
let data = get_service_hosts(model, name);
if (!data)
return ctx.not_found("Service not found: %s", name);
let ret = {};
for (let name, host in data) {
if (!host.host)
continue;
let host_name = strip_local(host.host);
host.name = name;
ret["Host " + host_name] = service_info(host);
}
return ctx.multi_table("Service " + name, ret);
}
let data = mdns_data(model);
let services = {};
for (let service_name, service_data in data) {
let hosts = [];
for (let name, host in service_data)
if (host.host)
push(hosts, `${name}(${strip_local(host.host)})`);
if (length(hosts))
services[service_name] = sort(hosts);
}
return ctx.table("Services", services);
}
},
host: {
help: "Host information",
args: [ host_arg ],
call: function (ctx, argv, named) {
let hosts_svc = get_host_services(model);
let hosts = get_hosts(model);
let host = argv[0];
if (host == null) {
let host_names = get_host_names(model);
let ret = {};
for (let lc_name, name in host_names) {
let data = hosts[lc_name];
if (!data)
continue;
let title = "Host " + name;
ret[title] = host_info(data);
let svc = hosts_svc[lc_name];
if (svc)
ret[title].services = keys(svc);
}
return ctx.multi_table("Hosts", ret);
}
let lc_host = lc(host);
let data = hosts[lc_host];
if (!data)
return ctx.not_found("Host not found: " + host);
let ret = {};
ret.Info = host_info(data);
for (let service_name, sdata in hosts_svc[lc_host])
ret["Service " + service_name] = service_info(sdata);
return ctx.multi_table("Host " + host, ret);
}
},
};
const Root = {
mdns: {
help: "Browse mdns hosts",
select_node: "MDNS",
select: function(ctx, argv) {
try {
refresh_start(model);
} catch (e) {
ctx.model.exception(e);
}
return true;
},
}
};
model.add_nodes({ Root, MDNS });
model.mdns = {};