umdns: add cli module
authorFelix Fietkau <nbd@nbd.name>
Wed, 12 Feb 2025 19:04:10 +0000 (20:04 +0100)
committerFelix Fietkau <nbd@nbd.name>
Thu, 13 Feb 2025 18:00:30 +0000 (19:00 +0100)
This makes it easy to browse mDNS services and hosts on the local network

Signed-off-by: Felix Fietkau <nbd@nbd.name>
package/network/services/umdns/Makefile
package/network/services/umdns/files/mdns.uc [new file with mode: 0644]

index 9a2763d5b2dba324aadcb25207804f3a05dc82e3..60e0eb9ac43ae9eadc98ba103ef1ec6514bbc094 100644 (file)
@@ -37,7 +37,12 @@ define Package/umdns/conffiles
 endef
 
 define Package/umdns/install
-       $(INSTALL_DIR) $(1)/usr/sbin $(1)/etc/init.d $(1)/etc/config
+       $(INSTALL_DIR) \
+               $(1)/usr/sbin \
+               $(1)/etc/init.d \
+               $(1)/etc/config \
+               $(1)/usr/share/ucode/cli/modules
+       $(INSTALL_DATA) ./files/mdns.uc $(1)/usr/share/ucode/cli/modules
        $(INSTALL_BIN) $(PKG_BUILD_DIR)/umdns $(1)/usr/sbin/
        $(INSTALL_BIN) ./files/umdns.init $(1)/etc/init.d/umdns
        $(INSTALL_CONF) ./files/umdns.config $(1)/etc/config/umdns
diff --git a/package/network/services/umdns/files/mdns.uc b/package/network/services/umdns/files/mdns.uc
new file mode 100644 (file)
index 0000000..5b76784
--- /dev/null
@@ -0,0 +1,267 @@
+// 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 = {};