luci-app-upnp: convert rpcd backend script to ucode
authorJo-Philipp Wich <[email protected]>
Wed, 21 Sep 2022 21:38:46 +0000 (23:38 +0200)
committerJo-Philipp Wich <[email protected]>
Mon, 24 Oct 2022 23:03:37 +0000 (01:03 +0200)
Utilize the rpcd ucode plugin to reimplement the upnp backend ubus actions
in ucode, simplifying the implementation and roughly halving the processing
time for the `get_status` call.

Signed-off-by: Jo-Philipp Wich <[email protected]>
applications/luci-app-upnp/Makefile
applications/luci-app-upnp/root/usr/libexec/rpcd/luci.upnp [deleted file]
applications/luci-app-upnp/root/usr/share/rpcd/ucode/luci.upnp [new file with mode: 0644]

index 324e3e2aa42910e4dd96bb5b76a148923954c25b..84a4d485765865c11769ddedf3b4b38e1d13d65d 100644 (file)
@@ -7,7 +7,7 @@
 include $(TOPDIR)/rules.mk
 
 LUCI_TITLE:=Universal Plug & Play configuration module
-LUCI_DEPENDS:=+miniupnpd
+LUCI_DEPENDS:=+miniupnpd +rpcd-mod-ucode
 
 include ../../luci.mk
 
diff --git a/applications/luci-app-upnp/root/usr/libexec/rpcd/luci.upnp b/applications/luci-app-upnp/root/usr/libexec/rpcd/luci.upnp
deleted file mode 100755 (executable)
index 37768f9..0000000
+++ /dev/null
@@ -1,205 +0,0 @@
-#!/usr/bin/env lua
-
-local json = require "luci.jsonc"
-local UCI = require "luci.model.uci"
-local fs   = require "nixio.fs"
-local sys  = require "luci.sys"
-
-local methods = {
-       get_status = {
-               call = function()
-                       local uci = UCI.cursor()
-                       local lease_file = uci:get("upnpd", "config", "upnp_lease_file")
-
-                       local ipv4_hints = sys.net.ipv4_hints()
-                       local rule = { }
-
-                       local ipt = io.popen("iptables --line-numbers -t nat -xnvL MINIUPNPD 2>/dev/null")
-                       if ipt then
-                               local upnpf = lease_file and io.open(lease_file, "r")
-                               while true do
-                                       local ln = ipt:read("*l")
-                                       if not ln then
-                                               break
-                                       elseif ln:match("^%d+") then
-                                               local num, proto, extport, intaddr, intport =
-                                                       ln:match("^(%d+).-([a-z]+).-dpt:(%d+) to:(%S-):(%d+)")
-                                               local descr = ""
-
-                                               if num and proto and extport and intaddr and intport then
-                                                       extport = tonumber(extport)
-                                                       intport = tonumber(intport)
-
-                                                       if upnpf then
-                                                               local uln = upnpf:read("*l")
-                                                               if uln then descr = uln:match(string.format("^%s:%d:%s:%d:%%d*:(.*)$", proto:upper(), extport, intaddr, intport)) end
-                                                               if not descr then descr = "" end
-                                                       end
-
-                                                       local host_hint, _, e
-
-                                                       for _,e in pairs(ipv4_hints) do
-                                                               if e[1] == intaddr then
-                                                                       host_hint = e[2]
-                                                                       break
-                                                               end
-                                                       end
-
-                                                       rule[#rule+1] = {
-                                                               num = num,
-                                                               proto   = proto:upper(),
-                                                               extport = extport,
-                                                               intaddr = intaddr,
-                                                               host_hint = host_hint,
-                                                               intport = intport,
-                                                               descr = descr
-                                                       }
-                                               end
-                                       end
-                               end
-
-                               if upnpf then upnpf:close() end
-                               ipt:close()
-                       end
-
-                       local nft = io.popen("nft --handle list chain inet fw4 upnp_prerouting")
-                       if nft then
-                               local num = 1
-                               local upnpf = lease_file and io.open(lease_file, "r")
-                               while true do
-                                       local ln = nft:read("*l")
-                                       if not ln then
-                                               break
-                                       elseif ln:match("iif ") then
-                                               local proto, extport, intaddr, intport =
-                                                       ln:match('^\t\tiif ".-" @nh,72,8 (0x[0-9a-f]+) th dport ([0-9]+) dnat ip to ([0-9%.]+):([0-9]+)')
-                                               local descr = ""
-
-                                               if (proto == "0x6" or proto == "0x11") and extport and intaddr and intport then
-                                                       proto = (proto == "0x6") and "TCP" or "UDP"
-                                                       extport = tonumber(extport)
-                                                       intport = tonumber(intport)
-
-                                                       if upnpf then
-                                                               local uln = upnpf:read("*l")
-                                                               if uln then descr = uln:match(string.format("^%s:%d:%s:%d:%%d*:(.*)$", proto, extport, intaddr, intport)) end
-                                                               if not descr then descr = "" end
-                                                       end
-
-                                                       local host_hint, _, e
-
-                                                       for _,e in pairs(ipv4_hints) do
-                                                               if e[1] == intaddr then
-                                                                       host_hint = e[2]
-                                                                       break
-                                                               end
-                                                       end
-
-                                                       rule[#rule+1] = {
-                                                               num = tostring(num),
-                                                               proto   = proto,
-                                                               extport = extport,
-                                                               intaddr = intaddr,
-                                                               host_hint = host_hint,
-                                                               intport = intport,
-                                                               descr = descr
-                                                       }
-
-                                                       num = num + 1
-                                               end
-                                       end
-                               end
-
-                               if upnpf then upnpf:close() end
-                               nft:close()
-                       end
-
-                       return { rules = rule }
-               end
-       },
-       delete_rule = {
-               args = { token = "token" },
-               call = function(args)
-                       local util = require "luci.util"
-                       local idx = args and tonumber(args.token)
-                       local res = {}
-
-                       if idx and idx > 0 then
-                               local uci = UCI.cursor()
-
-                               local lease_file = uci:get("upnpd", "config", "upnp_lease_file")
-                               if lease_file and fs.access(lease_file) then
-                                       sys.call("sed -i -e '%dd' %s" %{ idx, util.shellquote(lease_file) })
-                                       sys.call("/etc/init.d/miniupnpd restart")
-                               end
-
-                               uci.unload()
-
-                               return { result = "OK" }
-                       end
-
-                       return { result = "Bad request" }
-               end
-       }
-}
-
-local function parseInput()
-       local parse = json.new()
-       local done, err
-
-       while true do
-               local chunk = io.read(4096)
-               if not chunk then
-                       break
-               elseif not done and not err then
-                       done, err = parse:parse(chunk)
-               end
-       end
-
-       if not done then
-               print(json.stringify({ error = err or "Incomplete input" }))
-               os.exit(1)
-       end
-
-       return parse:get()
-end
-
-local function validateArgs(func, uargs)
-       local method = methods[func]
-       if not method then
-               print(json.stringify({ error = "Method not found" }))
-               os.exit(1)
-       end
-
-       if type(uargs) ~= "table" then
-               print(json.stringify({ error = "Invalid arguments" }))
-               os.exit(1)
-       end
-
-       uargs.ubus_rpc_session = nil
-
-       local k, v
-       local margs = method.args or {}
-       for k, v in pairs(uargs) do
-               if margs[k] == nil or
-                  (v ~= nil and type(v) ~= type(margs[k]))
-               then
-                       print(json.stringify({ error = "Invalid arguments" }))
-                       os.exit(1)
-               end
-       end
-
-       return method
-end
-
-if arg[1] == "list" then
-       local _, method, rv = nil, nil, {}
-       for _, method in pairs(methods) do rv[_] = method.args or {} end
-       print((json.stringify(rv):gsub(":%[%]", ":{}")))
-elseif arg[1] == "call" then
-       local args = parseInput()
-       local method = validateArgs(arg[2], args)
-       local result, code = method.call(args)
-       print((json.stringify(result):gsub("^%[%]$", "{}")))
-       os.exit(code or 0)
-end
diff --git a/applications/luci-app-upnp/root/usr/share/rpcd/ucode/luci.upnp b/applications/luci-app-upnp/root/usr/share/rpcd/ucode/luci.upnp
new file mode 100644 (file)
index 0000000..9ee47f2
--- /dev/null
@@ -0,0 +1,139 @@
+// Copyright 2022 Jo-Philipp Wich <[email protected]>
+// 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 };
+
+