Initial import
[project/ufp.git] / files / usr / share / ufp / plugin_mdns.uc
1 let ubus, global, last_refresh;
2 let rtnl = require("rtnl");
3
4 const homekit_types = {
5 "2": "Bridge",
6 "3": "Fan",
7 "4": "Garage Door Opener",
8 "5": "Lightbulb",
9 "6": "Door Lock",
10 "7": "Outlet",
11 "8": "Switch",
12 "9": "Thermostat",
13 "10": "Sensor",
14 "11": "Security System",
15 "12": "Door",
16 "13": "Window",
17 "14": "Window Covering",
18 "15": "Programmable Switch",
19 "17": "IP Camera",
20 "18": "Video Doorbell",
21 "19": "Air Purifier",
22 "20": "Heater",
23 "21": "Air Conditioner",
24 "22": "Humidifier",
25 "23": "Dehumidifier",
26 "28": "Sprinkler",
27 "29": "Faucet",
28 "30": "Shower System",
29 "31": "Television",
30 "32": "Remote Control",
31 "33": "WiFi Router",
32 "34": "Audio Receiver",
33 "35": "TV Set Top Box",
34 "36": "TV Stick",
35 };
36
37 function get_arp_cache() {
38 let ret = {};
39
40 let cache = rtnl.request(rtnl.const.RTM_GETNEIGH, rtnl.const.NLM_F_DUMP, {});
41 for (let data in cache) {
42 if (!data.lladdr || !data.dst)
43 continue;
44
45 ret[data.dst] = data.lladdr;
46 }
47
48 return ret;
49 }
50
51 function find_arp_entry(arp, addrs)
52 {
53 for (let addr in addrs) {
54 let val = arp[addr];
55 if (val)
56 return val;
57 }
58 return null;
59 }
60
61 function get_host_addr_cache()
62 {
63 let arp = get_arp_cache();
64 let host_cache = ubus.call("umdns", "hosts", { array: true });
65 let host_addr = {};
66 for (let host in host_cache) {
67 let host_data = host_cache[host];
68 host_addr[host] = find_arp_entry(arp, host_data.ipv4) ??
69 find_arp_entry(arp, host_data.ipv6);
70 }
71
72 return host_addr;
73 }
74
75 function convert_txt_array(data) {
76 let txt = {};
77
78 for (let rec in data) {
79 rec = split(rec, "=", 2);
80 if (rec[1])
81 txt[rec[0]] = rec[1];
82 }
83
84 return txt;
85 }
86
87 function handle_apple(txt, name)
88 {
89 let ret = [];
90 let model = txt.model ?? txt.rpMd ?? txt.am;
91 if (model)
92 push(ret, `apple_model|${model}`);
93 if (name)
94 push(ret, `%device_name|mdns_implicit_device_name|${name}`);
95
96 return ret;
97 }
98
99 function handle_homekit(txt)
100 {
101 let ret = [];
102 if (txt.ci) {
103 let type = homekit_types[txt.ci];
104 if (type)
105 push(ret, `%class|homekit_class|${type}`);
106 }
107
108 if (txt.md)
109 push(ret, `%device|mdns_model_string|${txt.md}`);
110
111 return ret;
112 }
113
114 function handle_googlecast_model(txt)
115 {
116 let ret = [];
117 let model = txt.model ?? txt.rpMd ?? txt.md;
118 if (model)
119 push(ret, `%device|mdns_model_string|${model}`);
120 if (txt.fn)
121 push(ret, `%device_name|mdns_device_name|${txt.fn}`);
122 if (txt.rs == 'TV')
123 push(ret, "%class|mdns_tv|TV");
124 return ret;
125 }
126
127 function handle_printer(txt)
128 {
129 let ret = [];
130 let vendor = txt.usb_MFG;
131 let model = txt.usb_MDL ?? txt.ty ?? replace(txt.product, /^\((.*)\)$/, "$1");
132 let weight = (txt.usb_MFG && txt.usb_MDL) ? "mdns_vendor_model_string" : "mdns_model_string";
133 if (vendor)
134 push(ret, `%vendor|${weight}|${vendor}`);
135 if (model)
136 push(ret, `%device|${weight}|${model}`);
137 push(ret, "%class|mdns_printer|Printer");
138
139 return ret;
140 }
141
142 function handle_scanner(txt)
143 {
144 let ret = [];
145 let vendor = txt.mfg;
146 let model = txt.mdl ?? txt.ty;
147 let weight = (txt.mfg && txt.mdl) ? "mdns_vendor_model_string" : "mdns_model_string";
148 if (vendor)
149 push(ret, `%vendor|${weight}|${vendor}`);
150 if (model)
151 push(ret, `%device|${weight}|${model}`);
152 push(ret, "%class|mdns_scanner|Scanner");
153
154 return ret;
155 }
156
157 function handle_hue(txt, name)
158 {
159 let ret = [];
160 push(ret, `%vendor|mdns_service|Philips`);
161 push(ret, `%device|mdns_service|Hue`);
162 if (name)
163 push(ret, `%device_name|mdns_implicit_device_name|${name}`);
164
165 return ret;
166 }
167
168 function handle_fritzbox(txt)
169 {
170 let ret = [];
171 push(ret, `%vendor|mdns_service|AVM`);
172 push(ret, `%device|mdns_service|FRITZ!Box`);
173
174 return ret;
175 }
176
177 const service_handler = {
178 "_airplay._tcp": handle_apple,
179 "_companion-link._tcp": handle_apple,
180 "_raop._tcp": (txt) => handle_apple(txt), // skip name
181 "_googlecast._tcp": handle_googlecast_model,
182 "_ipp._tcp": handle_printer,
183 "_scanner._tcp": handle_scanner,
184 "_hap._tcp": handle_homekit,
185 "_hap._udp": handle_homekit,
186 "_hue._tcp": handle_hue,
187 "_fbox._tcp": handle_fritzbox,
188 "_avmnexus._tcp": handle_fritzbox,
189 };
190
191 function arp_resolve(list)
192 {
193 let host_cache = ubus.call("umdns", "hosts", { array: true });
194 for (let entry in list) {
195 let iface = entry[0];
196 let host = entry[1];
197 if (!host_cache[host])
198 continue;
199 for (let addr in host_cache[host].ipv4)
200 rtnl.request(rtnl.const.RTM_NEWNEIGH,
201 rtnl.const.NLM_F_CREATE | rtnl.const.NLM_F_REPLACE,
202 { dev: iface, state: rtnl.const.NUD_INCOMPLETE,
203 flags: rtnl.const.NTF_USE, family: rtnl.const.AF_INET,
204 dst: addr });
205 }
206 }
207
208 function refresh() {
209 if (last_refresh == time())
210 return;
211
212 let host_cache = get_host_addr_cache();
213 let query_list = [];
214 let arp_pending = [];
215 let mdns_data;
216
217 for (let i = 0; i < 2; i++) {
218 mdns_data = ubus.call("umdns", "browse", { array: true, address: false });
219 for (let service in mdns_data) {
220 if (!service_handler[service])
221 continue;
222 let service_data = mdns_data[service];
223 for (let host in service_data) {
224 let host_entry = service_data[host].host;
225 let iface = service_data[host].iface;
226 if (!iface)
227 continue;
228 if (!host_entry)
229 push(query_list, [ `${host}.${service}.local`, iface ]);
230 else if (!host_cache[host_entry])
231 push(arp_pending, [ iface, host_entry ]);
232 }
233 }
234
235 if (!length(arp_pending) && !length(query_list))
236 break;
237
238 if (length(arp_pending) > 0)
239 arp_resolve(arp_pending);
240
241 if (length(query_list) > 0) {
242 for (let query in query_list)
243 ubus.call("umdns", "query", { question: query[0], interface: query[1] });
244 }
245
246 sleep(1000);
247
248 host_cache = get_host_addr_cache();
249 mdns_data = ubus.call("umdns", "browse", { array: true, address: false });
250 }
251
252 for (let service in mdns_data) {
253 if (!service_handler[service])
254 continue;
255
256 let service_data = mdns_data[service];
257 for (let host in service_data) {
258 let txt = service_data[host].txt;
259 if (!txt)
260 continue;
261
262 let host_entry = service_data[host].host;
263 if (!host_entry)
264 continue;
265
266 let mac = host_cache[host_entry];
267 if (!mac)
268 continue;
269
270 txt = convert_txt_array(txt);
271 let match = service_handler[service](txt, host);
272 for (let info in match)
273 global.device_add_data(mac, info);
274
275 global.device_refresh(mac);
276 }
277 }
278 }
279
280 function init(gl) {
281 global = gl;
282 ubus = gl.ubus;
283
284 global.add_weight({
285 apple_model: 10.0,
286 mdns_device_name: 10.0,
287 mdns_implicit_device_name: 5.0,
288 mdns_vendor_model_string: 10.0,
289 mdns_service: 10.0,
290 mdns_tv: 5.0,
291 mdns_model_string: 5.0,
292 mdns_printer: 5.0,
293 mdns_scanner: 1.0,
294 homekit_class: 2.0,
295 });
296 }
297
298 return { init, refresh };