#!/usr/bin/env ucode 'use strict'; import * as uloop from "uloop"; import * as libubus from "ubus"; import * as unetmsg from "unetmsg.client"; import { readfile, glob, basename } from "fs"; let uht = require("uht"); push(REQUIRE_SEARCH_PATH, "/usr/share/ufp/*.uc"); uloop.init(); let ubus = libubus.connect(); let unet = unetmsg.open(ubus); let fingerprints = {}; let fingerprint_ht = []; let devices = {}; let gc_timer; let weight = { "mac-oui": 3.0, }; function get_weight(type) { let w = weight[type]; if (w) return w; type = split(type, "-"); if (length(type) < 2) return null; pop(type); type = join("-", type); return weight[type]; } function match_fingerprint(key) { let fp = []; for (let ht in fingerprint_ht) { let cur_fp = ht.get(null, key); if (!cur_fp) continue; push(fp, ...cur_fp); } let user_fp = fingerprints[key]; if (user_fp) push(fp, ...user_fp); return fp; } unet.publish("ufp", (req) => { let data = req.args; switch (data.type) { case "get_data": let mac = data.macaddr; if (mac) return { data: devices[mac] }; return { data: devices }; } }); unet.subscribe("ufp"); function dev_timestamp_cmp(a, b) { return a[1].timestamp - b[1].timestamp; } function network_devices(local) { if (local) return devices; let device_lists = [ devices ]; unet.request("ufp", "get_data", {}, (msg) => { push(device_lists, msg.data); }); let cur_devices = []; for (let list in device_lists) for (let mac, dev in list) push(cur_devices, [ mac, dev ]); let ret = {}; sort(cur_devices, dev_timestamp_cmp); for (let entry in cur_devices) { let mac = entry[0]; let data = entry[1]; if (!ret[mac]) { ret[mac] = data; continue; } let new_data = { ...data }; new_data.data = { ...ret[mac].data, ...data.data }; new_data.meta = { ...ret[mac].meta, ...data.meta }; ret[mac] = new_data; } return ret; } let global = { uloop: uloop, ubus: ubus, weight: weight, devices: devices, fingerprints: fingerprints, plugins: [], load_fingerprint_json: function(file) { let data = json(readfile(file)); fingerprints = data; }, get_weight: get_weight, add_weight: function(data) { for (let entry in data) weight[entry] = data[entry]; }, device_refresh: function(mac) { mac = lc(mac); let dev = devices[mac]; if (!dev) return; dev.timestamp = time(); }, device_add_data: function(mac, line) { mac = lc(mac); let dev = devices[mac]; if (!dev) { dev = devices[mac] = { data: {}, meta: {}, timestamp: time() }; let oui = "mac-oui-" + join("", slice(split(mac, ":"), 0, 3)); dev.data[oui] = `${oui}|1`; } if (substr(line, 0, 1) == "%") { line = substr(line, 1); let meta = split(line, "|", 3); if (!meta[2]) return; dev.meta[meta[0]] ??= {}; if (!get_weight(meta[1])) return; dev.meta[meta[0]][meta[1]] = meta[2]; return; } let fp = split(line, "|", 2); if (!fp[1]) return; dev.data[fp[0]] = line; } }; function load_plugins() { let plugins = glob("/usr/share/ufp/plugin_*.uc"); for (let name in plugins) { name = substr(basename(name), 0, -3); try { let plugin = require(name); plugin.init(global); push(global.plugins, plugin); } catch (e) { warn(`Failed to load plugin ${name}: ${e}\n${e.stacktrace[0].context}\n`); } } } function refresh_plugins() { for (let plugin in global.plugins) { if (!plugin.refresh) continue; try { plugin.refresh(); } catch (e) { warn(`Failed to refresh plugin: ${e}\n${e.stacktrace[0].context}\n`); } } } function device_gc() { gc_timer.set(60 * 60 * 1000); let timeout = time() - 60 * 60 * 24; for (let mac in devices) { if (devices[mac].timestamp < timeout) delete devices[mac]; } } // returns: { "": { "": [ , [ ] ] } } function __device_match_list(mac, devices) { let dev = devices[mac]; if (!dev || !length(dev)) return null; let ret = {}; let data = dev.data; let match_devs = []; for (let fp in data) { let match = match_fingerprint(data[fp]); for (let match_cur in match) push(match_devs, [ match_cur, global.get_weight(fp), fp ]); } for (let meta in dev.meta) { let meta_cur = dev.meta[meta]; for (let type in meta_cur) { let match = {}; match[meta] = meta_cur[type]; push(match_devs, [ match, global.get_weight(type), type ]); } } for (let i = 0; i < length(match_devs); i++) { let match = match_devs[i]; let match_data = match[0]; let match_weight = match[1]; let match_fp = [ match[2] ]; let meta_entry = {}; for (let j = 0; j < length(match_devs); j++) { if (j == i) continue; let cur = match_devs[j]; let cur_data = cur[0]; let data_match; for (let key in cur_data) { data_match ??= true; if (lc(match_data[key]) != lc(cur_data[key])) { data_match = false; break; } } if (data_match) { match_weight += cur[1]; push(match_fp, cur[2]); } } for (let key in match_data) { let val = match_data[key]; ret[key] ??= {}; let ret_key = ret[key]; ret_key[val] ??= [ 0.0, {} ]; let ret_val = ret_key[val]; ret_val[0] += match_weight; for (let fp in match_fp) ret_val[1][fp]++; } } for (let key in ret) { let ret_key = ret[key]; for (let val in ret_key) { let ret_val = ret_key[val]; ret_val[1] = keys(ret_val[1]); } } return ret; } function device_match_list(mac, devices) { let match = __device_match_list(mac, devices); for (let meta in match) { let match_meta = match[meta]; let meta_list = keys(match_meta); sort(meta_list, (a, b) => match_meta[b][0] - match_meta[a][0]); match[meta] = map(meta_list, (key) => [ key, match_meta[key][0], match_meta[key][1] ]); } return match; } global.ubus_object = { load_fingerprints: { args: { file: "", }, call: function(req) { let file = req.args.file; if (!file) return libubus.STATUS_INVALID_ARGUMENT; try { global.load_fingerprint_json(file); } catch (e) { warn(`Exception in ubus function: ${e}\n${e.stacktrace[0].context}, file=${file}\n`); return libubus.STATUS_INVALID_ARGUMENT; } return 0; } }, get_data: { args: { macaddr: "", local: false }, call: function(req) { let mac = req.args.macaddr; refresh_plugins(); let cur_devices = network_devices(req.args.local); if (!mac) return cur_devices; let dev = cur_devices[mac]; if (!dev) return libubus.STATUS_NOT_FOUND; return dev; } }, add_data: { args: { macaddr: "", data: [], local: false }, call: function(req) { let mac = req.args.macaddr; let data = req.args.data; if (!mac || !data) return libubus.STATUS_INVALID_ARGUMENT; for (let line in data) global.device_add_data(mac, line); return 0; } }, fingerprint: { args: { macaddr: "", ubus_rpc_session: "", weight: false, local: false }, call: function(req) { refresh_plugins(); let cur_devices = network_devices(req.args.local); let mac_list = req.args.macaddr ? [ req.args.macaddr ] : keys(cur_devices); let ret = {}; for (let mac in mac_list) { let match_list = device_match_list(mac, cur_devices); if (!match_list) return libubus.STATUS_NOT_FOUND; let cur_ret = { }; if (req.args.weight) cur_ret.weight = {}; ret[mac] = cur_ret; for (let meta in match_list) { let match_meta = match_list[meta]; if (length(match_meta) < 1) continue; match_meta = match_meta[0]; cur_ret[meta] = match_meta[0]; if (req.args.weight) cur_ret.weight[meta] = match_meta[1]; } } return req.args.macaddr ? ret[req.args.macaddr] : ret; } }, list: { args: { macaddr: "", local: false }, call: function(req) { refresh_plugins(); let cur_devices = network_devices(req.args.local); let mac_list = req.args.macaddr ? [ req.args.macaddr ] : keys(cur_devices); let ret = {}; for (let mac in mac_list) { let match_list = device_match_list(mac, cur_devices); if (!match_list) return libubus.STATUS_NOT_FOUND; let cur_ret = {}; ret[mac] = cur_ret; for (let meta in match_list) cur_ret[meta] = match_list[meta]; } return req.args.macaddr ? ret[req.args.macaddr] : ret; } }, }; for (let f in [ "/usr/share/ufp/devices.bin", ...glob("/usr/share/ufp/db/*.bin") ]) { let ht; try { ht = uht.open(f); } catch (e) { warn(`Failed to load fingerprints: ${e}\n${e.stacktrace[0].context}\n`); } if (!ht) continue; push(fingerprint_ht, ht); } load_plugins(); ubus.publish("fingerprint", global.ubus_object); gc_timer = uloop.timer(1000, device_gc); uloop.run();