Implement luci.upnp ubus app.
Convert lua page to js client side implementation.
Signed-off-by: Ansuel Smith <ansuelsmth@gmail.com>
--- /dev/null
+'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;
+ }
+});
--- /dev/null
+'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));
+ }
+});
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
+++ /dev/null
--- 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
+++ /dev/null
-<%+upnp_status%>
+++ /dev/null
-<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"> </div>
- </div>
- <div class="tr placeholder">
- <div class="td"><em><%:Collecting data...%></em></div>
- </div>
- </div>
-</div>
--- /dev/null
+#!/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
--- /dev/null
+{
+ "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" ]
+ }
+ }
+}