Merge pull request #3473 from Ansuel/upnp-con
authorJo-Philipp Wich <jo@mein.io>
Wed, 15 Jan 2020 17:06:18 +0000 (18:06 +0100)
committerGitHub <noreply@github.com>
Wed, 15 Jan 2020 17:06:18 +0000 (18:06 +0100)
luci-app-upnp: convert to client side implementation

applications/luci-app-upnp/htdocs/luci-static/resources/view/status/include/80_upnp.js [new file with mode: 0644]
applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js [new file with mode: 0644]
applications/luci-app-upnp/luasrc/controller/upnp.lua
applications/luci-app-upnp/luasrc/model/cbi/upnp/upnp.lua [deleted file]
applications/luci-app-upnp/luasrc/view/admin_status/index/upnp.htm [deleted file]
applications/luci-app-upnp/luasrc/view/upnp_status.htm [deleted file]
applications/luci-app-upnp/root/usr/libexec/rpcd/luci.upnp [new file with mode: 0755]
applications/luci-app-upnp/root/usr/share/rpcd/acl.d/luci-app-upnp.json [new file with mode: 0644]

diff --git a/applications/luci-app-upnp/htdocs/luci-static/resources/view/status/include/80_upnp.js b/applications/luci-app-upnp/htdocs/luci-static/resources/view/status/include/80_upnp.js
new file mode 100644 (file)
index 0000000..b1a2531
--- /dev/null
@@ -0,0 +1,72 @@
+'use strict';
+'require rpc';
+'require uci';
+
+var callUpnpGetStatus, callUpnpDeleteRule, handleDelRule;
+
+callUpnpGetStatus = rpc.declare({
+       object: 'luci.upnp',
+       method: 'get_status',
+       expect: {  }
+});
+
+callUpnpDeleteRule = rpc.declare({
+       object: 'luci.upnp',
+       method: 'delete_rule',
+       params: [ 'token' ],
+       expect: { result : "OK" },
+});
+
+handleDelRule = function(num, ev) {
+       L.dom.parent(ev.currentTarget, '.tr').style.opacity = 0.5;
+       ev.currentTarget.classList.add('spinning');
+       ev.currentTarget.disabled = true;
+       ev.currentTarget.blur();
+       callUpnpDeleteRule(num);
+};
+
+return L.Class.extend({
+       title: _('Active UPnP Redirects'),
+
+       load: function() {
+               return Promise.all([
+                       callUpnpGetStatus(),
+               ]);
+       },
+
+       render: function(data) {
+
+               var table = E('div', { 'class': 'table', 'id': 'upnp_status_table' }, [
+                       E('div', { 'class': 'tr table-titles' }, [
+                               E('div', { 'class': 'th' }, _('Protocol')),
+                               E('div', { 'class': 'th' }, _('External Port')),
+                               E('div', { 'class': 'th' }, _('Client Address')),
+                               E('div', { 'class': 'th' }, _('Host')),
+                               E('div', { 'class': 'th' }, _('Client Port')),
+                               E('div', { 'class': 'th' }, _('Description')),
+                               E('div', { 'class': 'th cbi-section-actions' }, '')
+                       ])
+               ]);
+
+               var rules = Array.isArray(data[0].rules) ? data[0].rules : [];
+
+               var rows = rules.map(function(rule) {
+                       return [
+                               rule.proto,
+                               rule.extport,
+                               rule.intaddr,
+                               rule.host_hint || _('Unknown'),
+                               rule.intport,
+                               rule.descr,
+                               E('button', {
+                                       'class': 'btn cbi-button-remove',
+                                       'click': L.bind(handleDelRule, this, rule.num)
+                               }, [ _('Delete') ])
+                       ];
+               });
+
+               cbi_update_table(table, rows, E('em', _('There are no active redirects.')));
+
+               return table;
+       }
+});
diff --git a/applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js b/applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js
new file mode 100644 (file)
index 0000000..b85fd6d
--- /dev/null
@@ -0,0 +1,201 @@
+'use strict';
+'require uci';
+'require rpc';
+'require form';
+
+var callInitAction, callUpnpGetStatus, callUpnpDeleteRule, handleDelRule;
+
+callInitAction = rpc.declare({
+       object: 'luci',
+       method: 'setInitAction',
+       params: [ 'name', 'action' ],
+       expect: { result: false }
+});
+
+callUpnpGetStatus = rpc.declare({
+       object: 'luci.upnp',
+       method: 'get_status',
+       expect: {  }
+});
+
+callUpnpDeleteRule = rpc.declare({
+       object: 'luci.upnp',
+       method: 'delete_rule',
+       params: [ 'token' ],
+       expect: { result : "OK" },
+});
+
+handleDelRule = function(num, ev) {
+       L.dom.parent(ev.currentTarget, '.tr').style.opacity = 0.5;
+       ev.currentTarget.classList.add('spinning');
+       ev.currentTarget.disabled = true;
+       ev.currentTarget.blur();
+       callUpnpDeleteRule(num);
+};
+
+return L.view.extend({
+       load: function() {
+               return Promise.all([
+                       callUpnpGetStatus(),
+                       uci.load('upnpd')
+               ]);
+       },
+
+       poll_status: function(nodes, data) {
+
+               var rules = Array.isArray(data[0].rules) ? data[0].rules : [];
+
+               var rows = rules.map(function(rule) {
+                       return [
+                               rule.proto,
+                               rule.extport,
+                               rule.intaddr,
+                               rule.host_hint || _('Unknown'),
+                               rule.intport,
+                               rule.descr,
+                               E('button', {
+                                       'class': 'btn cbi-button-remove',
+                                       'click': L.bind(handleDelRule, this, rule.num)
+                               }, [ _('Delete') ])
+                       ];
+               });
+
+               cbi_update_table(nodes.querySelector('#upnp_status_table'), rows, E('em', _('There are no active redirects.')));
+
+               return;
+       },
+
+       render: function(data) {
+
+               var m, s, o;
+
+               m = new form.Map('upnpd', _('Universal Plug & Play'),
+                       _('UPnP allows clients in the local network to automatically configure the router.'));
+
+               s = m.section(form.GridSection, '_active_rules');
+
+               s.render = L.bind(function(view, section_id) {
+                       var table = E('div', { 'class': 'table cbi-section-table', 'id': 'upnp_status_table' }, [
+                               E('div', { 'class': 'tr table-titles' }, [
+                                       E('div', { 'class': 'th' }, _('Protocol')),
+                                       E('div', { 'class': 'th' }, _('External Port')),
+                                       E('div', { 'class': 'th' }, _('Client Address')),
+                                       E('div', { 'class': 'th' }, _('Host')),
+                                       E('div', { 'class': 'th' }, _('Client Port')),
+                                       E('div', { 'class': 'th' }, _('Description')),
+                                       E('div', { 'class': 'th cbi-section-actions' }, '')
+                               ])
+                       ]);
+
+                       var rules = Array.isArray(data[0].rules) ? data[0].rules : [];
+
+                       var rows = rules.map(function(rule) {
+                               return [
+                                       rule.proto,
+                                       rule.extport,
+                                       rule.intaddr,
+                                       rule.host_hint || _('Unknown'),
+                                       rule.intport,
+                                       rule.descr,
+                                       E('button', {
+                                               'class': 'btn cbi-button-remove',
+                                               'click': L.bind(handleDelRule, this, rule.num)
+                                       }, [ _('Delete') ])
+                               ];
+                       });
+
+                       cbi_update_table(table, rows, E('em', _('There are no active redirects.')));
+
+                       return E('div', { 'class': 'cbi-section cbi-tblsection' }, [
+                                       E('h3', _('Active UPnP Redirects')), table ]);
+               }, o, this);
+
+               s = m.section(form.NamedSection, 'config', 'upnpd', _('MiniUPnP settings'));
+               s.addremove = false;
+               s.tab('general',  _('General Settings'));
+               s.tab('advanced', _('Advanced Settings'));
+
+               o = s.taboption('general', form.Flag, 'enabled', _('Start UPnP and NAT-PMP service'));
+               o.rmempty  = false;
+
+               s.taboption('general', form.Flag, 'enable_upnp', _('Enable UPnP functionality')).default = '1'
+               s.taboption('general', form.Flag, 'enable_natpmp', _('Enable NAT-PMP functionality')).default = '1'
+
+               s.taboption('general', form.Flag, 'secure_mode', _('Enable secure mode'),
+                       _('Allow adding forwards only to requesting ip addresses')).default = '1'
+
+               s.taboption('general', form.Flag, 'igdv1', _('Enable IGDv1 mode'),
+                       _('Advertise as IGDv1 device instead of IGDv2')).default = '0'
+
+               s.taboption('general', form.Flag, 'log_output', _('Enable additional logging'),
+                       _('Puts extra debugging information into the system log'))
+
+               s.taboption('general', form.Value, 'download', _('Downlink'),
+                       _('Value in KByte/s, informational only')).rmempty = true
+
+               s.taboption('general', form.Value, 'upload', _('Uplink'),
+                       _('Value in KByte/s, informational only')).rmempty = true
+
+               o = s.taboption('general', form.Value, 'port', _('Port'))
+               o.datatype = 'port'
+               o.default  = 5000
+
+               s.taboption('advanced', form.Flag, 'system_uptime', _('Report system instead of daemon uptime')).default = '1'
+
+               s.taboption('advanced', form.Value, 'uuid', _('Device UUID'))
+               s.taboption('advanced', form.Value, 'serial_number', _('Announced serial number'))
+               s.taboption('advanced', form.Value, 'model_number', _('Announced model number'))
+
+               o = s.taboption('advanced', form.Value, 'notify_interval', _('Notify interval'))
+               o.datatype    = 'uinteger'
+               o.placeholder = 30
+
+               o = s.taboption('advanced', form.Value, 'clean_ruleset_threshold', _('Clean rules threshold'))
+               o.datatype    = 'uinteger'
+               o.placeholder = 20
+
+               o = s.taboption('advanced', form.Value, 'clean_ruleset_interval', _('Clean rules interval'))
+               o.datatype    = 'uinteger'
+               o.placeholder = 600
+
+               o = s.taboption('advanced', form.Value, 'presentation_url', _('Presentation URL'))
+               o.placeholder = 'http://192.168.1.1/'
+
+               o = s.taboption('advanced', form.Value, 'upnp_lease_file', _('UPnP lease file'))
+               o.placeholder = '/var/run/miniupnpd.leases'
+
+               s = m.section(form.GridSection, 'perm_rule', _('MiniUPnP ACLs'),
+                       _('ACLs specify which external ports may be redirected to which internal addresses and ports'))
+
+               s.sortable  = true
+               s.anonymous = true
+               s.addremove = true
+
+               s.option(form.Value, 'comment', _('Comment'))
+
+               o = s.option(form.Value, 'ext_ports', _('External ports'))
+               o.datatype    = 'portrange'
+               o.placeholder = '0-65535'
+
+               o = s.option(form.Value, 'int_addr', _('Internal addresses'))
+               o.datatype    = 'ip4addr'
+               o.placeholder = '0.0.0.0/0'
+
+               o = s.option(form.Value, 'int_ports', _('Internal ports'))
+               o.datatype    = 'portrange'
+               o.placeholder = '0-65535'
+
+               o = s.option(form.ListValue, 'action', _('Action'))
+               o.value('allow')
+               o.value('deny')
+
+               return m.render().then(L.bind(function(m, nodes) {
+                       L.Poll.add(L.bind(function() {
+                               return Promise.all([
+                                       callUpnpGetStatus()
+                               ]).then(L.bind(this.poll_status, this, nodes));
+                       }, this), 5);
+                       return nodes;
+               }, this, m));
+       }
+});
index c4762434c5003e6aa1ac6e34bb0b82f125712bdc..4b610f28663f5906405be342a83771fd8cdd33de 100644 (file)
@@ -9,91 +9,5 @@ function index()
                return
        end
 
-       local page
-
-       page = entry({"admin", "services", "upnp"}, cbi("upnp/upnp"), _("UPnP"))
-       page.dependent = true
-
-       entry({"admin", "services", "upnp", "status"}, call("act_status")).leaf = true
-       entry({"admin", "services", "upnp", "delete"}, post("act_delete")).leaf = true
-end
-
-function act_status()
-       local uci = luci.model.uci.cursor()
-       local lease_file = uci:get("upnpd", "config", "upnp_lease_file")
-       
-       local ipv4_hints = luci.sys.net.ipv4_hints()
-
-       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")
-               local fwd = { }
-               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
-                                       num     = tonumber(num)
-                                       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
-
-                                       fwd[#fwd+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()
-
-               luci.http.prepare_content("application/json")
-               luci.http.write_json(fwd)
-       end
-end
-
-function act_delete(num)
-       local idx = tonumber(num)
-       local uci = luci.model.uci.cursor()
-
-       if idx and idx > 0 then
-               luci.sys.call("iptables -t filter -D MINIUPNPD %d 2>/dev/null" % idx)
-               luci.sys.call("iptables -t nat -D MINIUPNPD %d 2>/dev/null" % idx)
-
-               local lease_file = uci:get("upnpd", "config", "upnp_lease_file")
-               if lease_file and nixio.fs.access(lease_file) then
-                       luci.sys.call("sed -i -e '%dd' %s" %{ idx, luci.util.shellquote(lease_file) })
-               end
-
-               luci.http.status(200, "OK")
-               return
-       end
-
-       luci.http.status(400, "Bad request")
+       entry({"admin", "services", "upnp"}, view("upnp/upnp"), _("UPnP"))
 end
diff --git a/applications/luci-app-upnp/luasrc/model/cbi/upnp/upnp.lua b/applications/luci-app-upnp/luasrc/model/cbi/upnp/upnp.lua
deleted file mode 100644 (file)
index a202392..0000000
+++ /dev/null
@@ -1,106 +0,0 @@
--- Copyright 2008 Steven Barth <steven@midlink.org>
--- Copyright 2008-2011 Jo-Philipp Wich <jow@openwrt.org>
--- Licensed to the public under the Apache License 2.0.
-
-m = Map("upnpd", luci.util.pcdata(translate("Universal Plug & Play")),
-       translate("UPnP allows clients in the local network to automatically configure the router."))
-
-m:section(SimpleSection).template  = "upnp_status"
-
-s = m:section(NamedSection, "config", "upnpd", translate("MiniUPnP settings"))
-s.addremove = false
-s:tab("general",  translate("General Settings"))
-s:tab("advanced", translate("Advanced Settings"))
-
-e = s:taboption("general", Flag, "enabled", translate("Start UPnP and NAT-PMP service"))
-e.rmempty  = false
-
---function e.cfgvalue(self, section)
---     return luci.sys.init.enabled("miniupnpd") and self.enabled or self.disabled
---end
-
-function e.write(self, section, value)
-       if value == "1" then
-               luci.sys.call("/etc/init.d/miniupnpd start >/dev/null")
-       else
-               luci.sys.call("/etc/init.d/miniupnpd stop >/dev/null")
-       end
-
-       return Flag.write(self, section, value)
-end
-
-s:taboption("general", Flag, "enable_upnp", translate("Enable UPnP functionality")).default = "1"
-s:taboption("general", Flag, "enable_natpmp", translate("Enable NAT-PMP functionality")).default = "1"
-
-s:taboption("general", Flag, "secure_mode", translate("Enable secure mode"),
-       translate("Allow adding forwards only to requesting ip addresses")).default = "1"
-
-s:taboption("general", Flag, "igdv1", translate("Enable IGDv1 mode"),
-       translate("Advertise as IGDv1 device instead of IGDv2")).default = "0"
-
-s:taboption("general", Flag, "log_output", translate("Enable additional logging"),
-       translate("Puts extra debugging information into the system log"))
-
-s:taboption("general", Value, "download", translate("Downlink"),
-       translate("Value in KByte/s, informational only")).rmempty = true
-
-s:taboption("general", Value, "upload", translate("Uplink"),
-       translate("Value in KByte/s, informational only")).rmempty = true
-
-port = s:taboption("general", Value, "port", translate("Port"))
-port.datatype = "port"
-port.default  = 5000
-
-
-s:taboption("advanced", Flag, "system_uptime", translate("Report system instead of daemon uptime")).default = "1"
-
-s:taboption("advanced", Value, "uuid", translate("Device UUID"))
-s:taboption("advanced", Value, "serial_number", translate("Announced serial number"))
-s:taboption("advanced", Value, "model_number", translate("Announced model number"))
-
-ni = s:taboption("advanced", Value, "notify_interval", translate("Notify interval"))
-ni.datatype    = "uinteger"
-ni.placeholder = 30
-
-ct = s:taboption("advanced", Value, "clean_ruleset_threshold", translate("Clean rules threshold"))
-ct.datatype    = "uinteger"
-ct.placeholder = 20
-
-ci = s:taboption("advanced", Value, "clean_ruleset_interval", translate("Clean rules interval"))
-ci.datatype    = "uinteger"
-ci.placeholder = 600
-
-pu = s:taboption("advanced", Value, "presentation_url", translate("Presentation URL"))
-pu.placeholder = "http://192.168.1.1/"
-
-lf = s:taboption("advanced", Value, "upnp_lease_file", translate("UPnP lease file"))
-lf.placeholder = "/var/run/miniupnpd.leases"
-
-
-s2 = m:section(TypedSection, "perm_rule", translate("MiniUPnP ACLs"),
-       translate("ACLs specify which external ports may be redirected to which internal addresses and ports"))
-
-s2.template  = "cbi/tblsection"
-s2.sortable  = true
-s2.anonymous = true
-s2.addremove = true
-
-s2:option(Value, "comment", translate("Comment"))
-
-ep = s2:option(Value, "ext_ports", translate("External ports"))
-ep.datatype    = "portrange"
-ep.placeholder = "0-65535"
-
-ia = s2:option(Value, "int_addr", translate("Internal addresses"))
-ia.datatype    = "ip4addr"
-ia.placeholder = "0.0.0.0/0"
-
-ip = s2:option(Value, "int_ports", translate("Internal ports"))
-ip.datatype    = "portrange"
-ip.placeholder = "0-65535"
-
-ac = s2:option(ListValue, "action", translate("Action"))
-ac:value("allow")
-ac:value("deny")
-
-return m
diff --git a/applications/luci-app-upnp/luasrc/view/admin_status/index/upnp.htm b/applications/luci-app-upnp/luasrc/view/admin_status/index/upnp.htm
deleted file mode 100644 (file)
index d0c2e2e..0000000
+++ /dev/null
@@ -1 +0,0 @@
-<%+upnp_status%>
diff --git a/applications/luci-app-upnp/luasrc/view/upnp_status.htm b/applications/luci-app-upnp/luasrc/view/upnp_status.htm
deleted file mode 100644 (file)
index 7238a56..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-<script type="text/javascript">//<![CDATA[
-       function upnp_delete_fwd(idx) {
-               (new XHR()).post('<%=url('admin/services/upnp/delete')%>/' + idx, { token: '<%=token%>' },
-                       function(x)
-                       {
-                               var tb = document.getElementById('upnp_status_table');
-                               if (tb && (idx + 1 < tb.childNodes.length))
-                                       tb.removeChild(tb.childNodes[idx + 1]);
-                       }
-               );
-       }
-
-       XHR.poll(-1, '<%=url('admin/services/upnp/status')%>', null,
-               function(x, st)
-               {
-                       var tb = document.getElementById('upnp_status_table');
-                       if (st && tb)
-                       {
-                               var rows = [];
-
-                               for (var i = 0; i < st.length; i++)
-                                       rows.push([
-                                               st[i].proto,
-                                               st[i].extport,
-                                               st[i].intaddr,
-                                               st[i].host_hint || "<%:Unknown%>",
-                                               st[i].intport,
-                                               st[i].descr,
-                                               E('<div><input class="cbi-button cbi-button-remove" type="button" value="<%:Delete%>" onclick="upnp_delete_fwd(%d)" /></div>'.format(st[i].num))
-                                       ]);
-
-                               cbi_update_table(tb, rows, '<em><%:There are no active redirects.%></em>');
-                       }
-               }
-       );
-//]]></script>
-
-<div class="cbi-section">
-       <h3><%:Active UPnP Redirects%></h3>
-       <div class="table" id="upnp_status_table">
-               <div class="tr table-titles">
-                       <div class="th"><%:Protocol%></div>
-                       <div class="th"><%:External Port%></div>
-                       <div class="th"><%:Client Address%></div>
-                       <div class="th"><%:Host%></div>
-                       <div class="th"><%:Client Port%></div>
-                       <div class="th"><%:Description%></div>
-                       <div class="th cbi-section-actions">&#160;</div>
-               </div>
-               <div class="tr placeholder">
-                       <div class="td"><em><%:Collecting data...%></em></div>
-               </div>
-       </div>
-</div>
diff --git a/applications/luci-app-upnp/root/usr/libexec/rpcd/luci.upnp b/applications/luci-app-upnp/root/usr/libexec/rpcd/luci.upnp
new file mode 100755 (executable)
index 0000000..a122360
--- /dev/null
@@ -0,0 +1,155 @@
+#!/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
+
+                       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()
+
+                               sys.call("iptables -t filter -D MINIUPNPD %d 2>/dev/null" % idx)
+                               sys.call("iptables -t nat -D MINIUPNPD %d 2>/dev/null" % idx)
+
+                               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) })
+                               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
\ No newline at end of file
diff --git a/applications/luci-app-upnp/root/usr/share/rpcd/acl.d/luci-app-upnp.json b/applications/luci-app-upnp/root/usr/share/rpcd/acl.d/luci-app-upnp.json
new file mode 100644 (file)
index 0000000..b01ffb2
--- /dev/null
@@ -0,0 +1,17 @@
+{
+       "luci-app-ddns": {
+               "description": "Grant access to upnp procedures",
+               "read": {
+                       "ubus": {
+                               "luci.upnp": [ "get_status" ],
+                               "luci": [ "setInitAction" ]
+                       }
+               },
+               "write": {
+                       "ubus": {
+                               "luci.upnp": [ "delete_rule" ]
+                       },
+                       "uci": [ "upnpd" ]
+               }
+       }
+}