Initial import
[project/ufp.git] / files / usr / sbin / ufpd
1 #!/usr/bin/env ucode
2 'use strict';
3 import * as uloop from "uloop";
4 import * as libubus from "ubus";
5 import { readfile, glob, basename } from "fs";
6 let uht = require("uht");
7 push(REQUIRE_SEARCH_PATH, "/usr/share/ufp/*.uc");
8
9 uloop.init();
10 let ubus = libubus.connect();
11 let fingerprints = {};
12 let fingerprint_ht;
13 let devices = {};
14 let gc_timer;
15 let weight = {
16 "mac-oui": 3.0,
17 };
18
19 function get_weight(type) {
20 let w = weight[type];
21 if (w)
22 return w;
23 type = split(type, "-");
24 if (length(type) < 2)
25 return null;
26 pop(type);
27 type = join("-", type);
28
29 return weight[type];
30 }
31
32
33 function match_fingerprint(key)
34 {
35 let fp, user_fp;
36
37 if (fingerprint_ht)
38 fp = fingerprint_ht.get(null, key);
39 user_fp = fingerprints[key];
40
41 if (fp && user_fp) {
42 fp = slice(fp);
43 for (entry in user_fp)
44 push(fp, entry);
45 }
46
47 return fp ?? user_fp;
48 }
49
50 let global = {
51 uloop: uloop,
52 ubus: ubus,
53 weight: weight,
54 devices: devices,
55 fingerprints: fingerprints,
56 plugins: [],
57
58 load_fingerprint_json: function(file) {
59 let data = json(readfile(file));
60 fingerprints = data;
61 },
62
63 get_weight: get_weight,
64
65 add_weight: function(data) {
66 for (let entry in data)
67 weight[entry] = data[entry];
68 },
69
70 device_refresh: function(mac) {
71 mac = lc(mac);
72 let dev = devices[mac];
73 if (!dev)
74 return;
75
76 dev.timestamp = time();
77 },
78
79 device_add_data: function(mac, line) {
80 mac = lc(mac);
81 let dev = devices[mac];
82 if (!dev) {
83 dev = devices[mac] = {
84 data: {},
85 meta: {},
86 timestamp: time()
87 };
88 let oui = "mac-oui-" + join("", slice(split(mac, ":"), 0, 3));
89 dev.data[oui] = `${oui}|1`;
90 }
91
92 if (substr(line, 0, 1) == "%") {
93 line = substr(line, 1);
94 let meta = split(line, "|", 3);
95 if (!meta[2])
96 return;
97
98 dev.meta[meta[0]] ??= {};
99 if (!get_weight(meta[1]))
100 return;
101
102 dev.meta[meta[0]][meta[1]] = meta[2];
103 return;
104 }
105
106 let fp = split(line, "|", 2);
107 if (!fp[1])
108 return;
109
110 dev.data[fp[0]] = line;
111 }
112 };
113
114 function load_plugins()
115 {
116 let plugins = glob("/usr/share/ufp/plugin_*.uc");
117 for (let name in plugins) {
118 name = substr(basename(name), 0, -3);
119 try {
120 let plugin = require(name);
121 plugin.init(global);
122 push(global.plugins, plugin);
123 } catch (e) {
124 warn(`Failed to load plugin ${name}: ${e}\n${e.stacktrace[0].context}\n`);
125 }
126 }
127 }
128
129 function refresh_plugins()
130 {
131 for (let plugin in global.plugins) {
132 if (!plugin.refresh)
133 continue;
134
135 try {
136 plugin.refresh();
137 } catch (e) {
138 warn(`Failed to refresh plugin: ${e}\n${e.stacktrace[0].context}\n`);
139 }
140 }
141 }
142
143 function device_gc()
144 {
145 gc_timer.set(60 * 60 * 1000);
146 let timeout = time() - 60 * 60 * 24;
147
148 for (let mac in devices) {
149 if (devices[mac].timestamp < timeout)
150 delete devices[mac];
151 }
152 }
153
154 // returns: { "<meta>": { "<val>": [ <weight>, [ <fingerprints> ] ] } }
155 function __device_match_list(mac)
156 {
157 let dev = devices[mac];
158 if (!dev || !length(dev))
159 return null;
160
161 let ret = {};
162 let data = dev.data;
163 let match_devs = [];
164
165 for (let fp in data) {
166 let match = match_fingerprint(data[fp]);
167 if (!match)
168 continue;
169
170 for (let match_cur in match)
171 push(match_devs, [ match_cur, global.get_weight(fp), fp ]);
172 }
173
174 for (let meta in dev.meta) {
175 let meta_cur = dev.meta[meta];
176 for (let type in meta_cur) {
177 let match = {};
178 match[meta] = meta_cur[type];
179 push(match_devs, [ match, global.get_weight(type), type ]);
180 }
181 }
182
183 for (let i = 0; i < length(match_devs); i++) {
184 let match = match_devs[i];
185 let match_data = match[0];
186 let match_weight = match[1];
187 let match_fp = [ match[2] ];
188 let meta_entry = {};
189
190 for (let j = 0; j < length(match_devs); j++) {
191 if (j == i)
192 continue;
193
194 let cur = match_devs[j];
195 let cur_data = cur[0];
196 for (let key in cur_data) {
197 if (lc(match_data[key]) == lc(cur_data[key])) {
198 match_weight += cur[1];
199 push(match_fp, cur[2]);
200 break;
201 }
202 }
203 }
204
205 for (let key in match_data) {
206 let val = match_data[key];
207 ret[key] ??= {};
208 let ret_key = ret[key];
209
210 ret_key[val] ??= [ 0.0, {} ];
211 let ret_val = ret_key[val];
212
213 ret_val[0] += match_weight;
214 for (let fp in match_fp)
215 ret_val[1][fp]++;
216 }
217 }
218
219 for (let key in ret) {
220 let ret_key = ret[key];
221 for (let val in ret_key) {
222 let ret_val = ret_key[val];
223 ret_val[1] = keys(ret_val[1]);
224 }
225 }
226
227 return ret;
228 }
229
230 function device_match_list(mac)
231 {
232 let match = __device_match_list(mac);
233
234 for (let meta in match) {
235 let match_meta = match[meta];
236 let meta_list = keys(match_meta);
237 sort(meta_list, (a, b) => match_meta[b][0] - match_meta[a][0]);
238 match[meta] = map(meta_list, (key) => [ key, match_meta[key][0], match_meta[key][1] ]);
239 }
240
241 return match;
242 }
243
244 global.ubus_object = {
245 load_fingerprints: {
246 args: {
247 file: "",
248 },
249 call: function(req) {
250 let file = req.args.file;
251 if (!file)
252 return libubus.STATUS_INVALID_ARGUMENT;
253
254 try {
255 global.load_fingerprint_json(file);
256 } catch (e) {
257 warn(`Exception in ubus function: ${e}\n${e.stacktrace[0].context}, file=${file}\n`);
258 return libubus.STATUS_INVALID_ARGUMENT;
259 }
260
261 return 0;
262 }
263 },
264
265 get_data: {
266 args: {
267 macaddr: "",
268 },
269 call: function(req) {
270 let mac = req.args.macaddr;
271
272 refresh_plugins();
273
274 if (!mac)
275 return devices;
276
277 let dev = devices[mac];
278 if (!dev)
279 return libubus.STATUS_NOT_FOUND;
280
281 return dev;
282 }
283 },
284
285 add_data: {
286 args: {
287 macaddr: "",
288 data: []
289 },
290 call: function(req) {
291 let mac = req.args.macaddr;
292 let data = req.args.data;
293 if (!mac || !data)
294 return libubus.STATUS_INVALID_ARGUMENT;
295
296 for (let line in data)
297 global.device_add_data(mac, line);
298
299 return 0;
300 }
301 },
302
303 fingerprint: {
304 args: {
305 macaddr: "",
306 weight: false
307 },
308 call: function(req) {
309 refresh_plugins();
310
311 let mac_list = req.args.macaddr ? [ req.args.macaddr ] : keys(devices);
312 let ret = {};
313
314 for (let mac in mac_list) {
315 let match_list = device_match_list(mac);
316 if (!match_list)
317 return libubus.STATUS_NOT_FOUND;
318
319 let cur_ret = { };
320 if (req.args.weight)
321 cur_ret.weight = {};
322 ret[mac] = cur_ret;
323
324 for (let meta in match_list) {
325 let match_meta = match_list[meta];
326
327 if (length(match_meta) < 1)
328 continue;
329
330 match_meta = match_meta[0];
331
332 cur_ret[meta] = match_meta[0];
333 if (req.args.weight)
334 cur_ret.weight[meta] = match_meta[1];
335 }
336 }
337
338 return req.args.macaddr ? ret[req.args.macaddr] : ret;
339 }
340 },
341
342 list: {
343 args: {
344 macaddr: ""
345 },
346 call: function(req) {
347 refresh_plugins();
348
349 let mac_list = req.args.macaddr ? [ req.args.macaddr ] : keys(devices);
350 let ret = {};
351
352 for (let mac in mac_list) {
353 let match_list = device_match_list(mac);
354 if (!match_list)
355 return libubus.STATUS_NOT_FOUND;
356
357 let cur_ret = {};
358 ret[mac] = cur_ret;
359
360 for (let meta in match_list)
361 cur_ret[meta] = match_list[meta];
362 }
363
364 return req.args.macaddr ? ret[req.args.macaddr] : ret;
365 }
366 },
367 };
368
369 try {
370 fingerprint_ht = uht.open("/usr/share/ufp/devices.bin");
371 } catch (e) {
372 warn(`Failed to load fingerprints: ${e}\n${e.stacktrace[0].context}\n`);
373 }
374 load_plugins();
375 ubus.publish("fingerprint", global.ubus_object);
376 gc_timer = uloop.timer(1000, device_gc);
377 uloop.run();