Merge pull request #3473 from Ansuel/upnp-con

luci-app-upnp: convert to client side implementation
This commit is contained in:
Jo-Philipp Wich 2020-01-15 18:06:18 +01:00 committed by GitHub
commit 30cba86db5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 446 additions and 248 deletions

View file

@ -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;
}
});

View file

@ -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));
}
});

View 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

View file

@ -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

View file

@ -1 +0,0 @@
<%+upnp_status%>

View file

@ -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>

View file

@ -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

View file

@ -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" ]
}
}
}