// Copyright 2022 Jo-Philipp Wich // Licensed to the public under the Apache License 2.0. 'use strict'; import { access, open, popen } from 'fs'; import { connect } from 'ubus'; import { cursor } from 'uci'; // Establish ubus connection persistently outside of the call handler scope to // prevent premature GC'ing. Can be moved into `get_status` callback once // https://github.com/jow-/ucode/commit/a58fe4709f661b5f28e26701ea8638efccf5aeb6 // is merged. const ubus = connect(); const methods = { get_status: { call: function(req) { const uci = cursor(); const rules = []; const leases = []; const leasefile = open(uci.get('upnpd', 'config', 'upnp_lease_file'), 'r'); if (leasefile) { for (let line = leasefile.read('line'); length(line); line = leasefile.read('line')) { const record = split(line, ':', 6); if (length(record) == 6) { push(leases, { proto: uc(record[0]), extport: +record[1], intaddr: arrtoip(iptoarr(record[2])), intport: +record[3], expiry: +record[4], description: trim(record[5]) }); } } leasefile.close(); } const ipt = popen('iptables --line-numbers -t nat -xnvL MINIUPNPD 2>/dev/null'); if (ipt) { for (let line = ipt.read('line'); length(line); line = ipt.read('line')) { let m = match(line, /^([0-9]+)\s+([a-z]+).+dpt:([0-9]+) to:(\S+):([0-9]+)/); if (m) { push(rules, { num: m[1], proto: uc(m[2]), extport: +m[3], intaddr: arrtoip(iptoarr(m[4])), intport: +m[5], descr: '' }); } } ipt.close(); } const nft = popen('nft --handle list chain inet fw4 upnp_prerouting 2>/dev/null'); if (nft) { for (let line = nft.read('line'), num = 1; length(line); line = nft.read('line')) { let m = match(line, /^\t\tiif ".+" @nh,72,8 (0x6|0x11) th dport ([0-9]+) dnat ip to ([0-9.]+):([0-9]+)/); if (m) { push(rules, { num: `${num}`, proto: (m[1] == '0x6') ? 'TCP' : 'UDP', extport: +m[2], intaddr: arrtoip(iptoarr(m[3])), intport: +m[4], descr: '' }); num++; } } nft.close(); } return ubus.defer('luci-rpc', 'getHostHints', {}, function(rc, host_hints) { for (let rule in rules) { for (let lease in leases) { if (lease.proto == rule.proto && lease.intaddr == rule.intaddr && lease.intport == rule.intport && lease.extport == rule.extport) { rule.descr = lease.description; break; } } for (let mac, hint in host_hints) { if (rule.intaddr in hint.ipaddrs) { rule.host_hint = hint.name; break; } } } req.reply({ rules }); }); } }, delete_rule: { args: { token: 'token' }, call: function(req) { const idx = +req.args?.token; if (idx > 0) { const uci = cursor(); const leasefile = uci.get('upnpd', 'config', 'upnp_lease_file'); if (access(leasefile)) { system(['sed', '-i', '-e', `${idx}d`, leasefile]); system(['/etc/init.d/miniupnpd', 'restart']); } return { result: 'OK' }; } return { result: 'Bad request' }; } } }; return { 'luci.upnp': methods };