diff --git a/luci-mod-network/Makefile b/luci-mod-network/Makefile new file mode 100644 index 000000000..d8fe48ac3 --- /dev/null +++ b/luci-mod-network/Makefile @@ -0,0 +1,18 @@ +# +# Copyright (C) 2008-2014 The LuCI Team +# +# This is free software, licensed under the Apache License, Version 2.0 . +# +# From https://github.com/openwrt/luci/commit/6efaea2ffb46f9909038b85cf12e7acf4467ae2e + +include $(TOPDIR)/rules.mk + +LUCI_TITLE:=LuCI Network Administration +LUCI_DEPENDS:=+luci-base +libiwinfo-lua +rpcd-mod-iwinfo + +PKG_LICENSE:=Apache-2.0 + +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature + diff --git a/luci-mod-network/htdocs/luci-static/resources/view/network/dhcp.js b/luci-mod-network/htdocs/luci-static/resources/view/network/dhcp.js new file mode 100644 index 000000000..6e4f07f27 --- /dev/null +++ b/luci-mod-network/htdocs/luci-static/resources/view/network/dhcp.js @@ -0,0 +1,557 @@ +'use strict'; +'require rpc'; +'require uci'; +'require form'; +'require validation'; + +var callHostHints, callDUIDHints, callDHCPLeases, CBILeaseStatus, CBILease6Status; + +callHostHints = rpc.declare({ + object: 'luci-rpc', + method: 'getHostHints', + expect: { '': {} } +}); + +callDUIDHints = rpc.declare({ + object: 'luci-rpc', + method: 'getDUIDHints', + expect: { '': {} } +}); + +callDHCPLeases = rpc.declare({ + object: 'luci-rpc', + method: 'getDHCPLeases', + expect: { '': {} } +}); + +CBILeaseStatus = form.DummyValue.extend({ + renderWidget: function(section_id, option_id, cfgvalue) { + return E([ + E('h4', _('Active DHCP Leases')), + E('div', { 'id': 'lease_status_table', 'class': 'table' }, [ + E('div', { 'class': 'tr table-titles' }, [ + E('div', { 'class': 'th' }, _('Hostname')), + E('div', { 'class': 'th' }, _('IPv4-Address')), + E('div', { 'class': 'th' }, _('MAC-Address')), + E('div', { 'class': 'th' }, _('Lease time remaining')) + ]), + E('div', { 'class': 'tr placeholder' }, [ + E('div', { 'class': 'td' }, E('em', _('Collecting data...'))) + ]) + ]) + ]); + } +}); + +CBILease6Status = form.DummyValue.extend({ + renderWidget: function(section_id, option_id, cfgvalue) { + return E([ + E('h4', _('Active DHCPv6 Leases')), + E('div', { 'id': 'lease6_status_table', 'class': 'table' }, [ + E('div', { 'class': 'tr table-titles' }, [ + E('div', { 'class': 'th' }, _('Host')), + E('div', { 'class': 'th' }, _('IPv6-Address')), + E('div', { 'class': 'th' }, _('DUID')), + E('div', { 'class': 'th' }, _('Lease time remaining')) + ]), + E('div', { 'class': 'tr placeholder' }, [ + E('div', { 'class': 'td' }, E('em', _('Collecting data...'))) + ]) + ]) + ]); + } +}); + +function validateHostname(sid, s) { + if (s.length > 256) + return _('Expecting: %s').format(_('valid hostname')); + + var labels = s.replace(/^\.+|\.$/g, '').split(/\./); + + for (var i = 0; i < labels.length; i++) + if (!labels[i].match(/^[a-z0-9_](?:[a-z0-9-]{0,61}[a-z0-9])?$/i)) + return _('Expecting: %s').format(_('valid hostname')); + + return true; +} + +function validateAddressList(sid, s) { + if (s == null || s == '') + return true; + + var m = s.match(/^\/(.+)\/$/), + names = m ? m[1].split(/\//) : [ s ]; + + for (var i = 0; i < names.length; i++) { + var res = validateHostname(sid, names[i]); + + if (res !== true) + return res; + } + + return true; +} + +function validateServerSpec(sid, s) { + if (s == null || s == '') + return true; + + var m = s.match(/^\/(.+)\/(.*)$/); + if (!m) + return _('Expecting: %s').format(_('valid hostname')); + + var res = validateAddressList(sid, m[1]); + if (res !== true) + return res; + + if (m[2] == '' || m[2] == '#') + return true; + + // ipaddr%scopeid#srvport@source@interface#srcport + + m = m[2].match(/^([0-9a-f:.]+)(?:%[^#@]+)?(?:#(\d+))?(?:@([0-9a-f:.]+)(?:@[^#]+)?(?:#(\d+))?)?$/); + + if (!m) + return _('Expecting: %s').format(_('valid IP address')); + else if (validation.parseIPv4(m[1]) && m[3] != null && !validation.parseIPv4(m[3])) + return _('Expecting: %s').format(_('valid IPv4 address')); + else if (validation.parseIPv6(m[1]) && m[3] != null && !validation.parseIPv6(m[3])) + return _('Expecting: %s').format(_('valid IPv6 address')); + else if ((m[2] != null && +m[2] > 65535) || (m[4] != null && +m[4] > 65535)) + return _('Expecting: %s').format(_('valid port value')); + + return true; +} + +return L.view.extend({ + load: function() { + return Promise.all([ + callHostHints(), + callDUIDHints() + ]); + }, + + render: function(hosts_duids) { + var has_dhcpv6 = L.hasSystemFeature('dnsmasq', 'dhcpv6') || L.hasSystemFeature('odhcpd'), + hosts = hosts_duids[0], + duids = hosts_duids[1], + m, s, o, ss, so; + + m = new form.Map('dhcp', _('DHCP and DNS'), _('Dnsmasq is a combined DHCP-Server and DNS-Forwarder for NAT firewalls')); + + s = m.section(form.TypedSection, 'dnsmasq', _('Server Settings')); + s.anonymous = true; + s.addremove = false; + + s.tab('general', _('General Settings')); + s.tab('files', _('Resolv and Hosts Files')); + s.tab('tftp', _('TFTP Settings')); + s.tab('advanced', _('Advanced Settings')); + s.tab('leases', _('Static Leases')); + + s.taboption('general', form.Flag, 'domainneeded', + _('Domain required'), + _('Don\'t forward DNS-Requests without DNS-Name')); + + s.taboption('general', form.Flag, 'authoritative', + _('Authoritative'), + _('This is the only DHCP in the local network')); + + + s.taboption('files', form.Flag, 'readethers', + _('Use /etc/ethers'), + _('Read /etc/ethers to configure the DHCP-Server')); + + s.taboption('files', form.Value, 'leasefile', + _('Leasefile'), + _('file where given DHCP-leases will be stored')); + + s.taboption('files', form.Flag, 'noresolv', + _('Ignore resolve file')).optional = true; + + o = s.taboption('files', form.Value, 'resolvfile', + _('Resolve file'), + _('local DNS file')); + + o.depends('noresolv', '0'); + o.placeholder = '/tmp/resolv.conf.d/resolv.conf.auto'; + o.optional = true; + + + s.taboption('files', form.Flag, 'nohosts', + _('Ignore /etc/hosts')).optional = true; + + s.taboption('files', form.DynamicList, 'addnhosts', + _('Additional Hosts files')).optional = true; + + o = s.taboption('advanced', form.Flag, 'quietdhcp', + _('Suppress logging'), + _('Suppress logging of the routine operation of these protocols')); + o.optional = true; + + o = s.taboption('advanced', form.Flag, 'sequential_ip', + _('Allocate IP sequentially'), + _('Allocate IP addresses sequentially, starting from the lowest available address')); + o.optional = true; + + o = s.taboption('advanced', form.Flag, 'boguspriv', + _('Filter private'), + _('Do not forward reverse lookups for local networks')); + o.default = o.enabled; + + s.taboption('advanced', form.Flag, 'filterwin2k', + _('Filter useless'), + _('Do not forward requests that cannot be answered by public name servers')); + + + s.taboption('advanced', form.Flag, 'localise_queries', + _('Localise queries'), + _('Localise hostname depending on the requesting subnet if multiple IPs are available')); + + if (L.hasSystemFeature('dnsmasq', 'dnssec')) { + o = s.taboption('advanced', form.Flag, 'dnssec', + _('DNSSEC')); + o.optional = true; + + o = s.taboption('advanced', form.Flag, 'dnsseccheckunsigned', + _('DNSSEC check unsigned'), + _('Requires upstream supports DNSSEC; verify unsigned domain responses really come from unsigned domains')); + o.default = o.enabled; + o.optional = true; + } + + s.taboption('general', form.Value, 'local', + _('Local server'), + _('Local domain specification. Names matching this domain are never forwarded and are resolved from DHCP or hosts files only')); + + s.taboption('general', form.Value, 'domain', + _('Local domain'), + _('Local domain suffix appended to DHCP names and hosts file entries')); + + s.taboption('advanced', form.Flag, 'expandhosts', + _('Expand hosts'), + _('Add local domain suffix to names served from hosts files')); + + s.taboption('advanced', form.Flag, 'nonegcache', + _('No negative cache'), + _('Do not cache negative replies, e.g. for not existing domains')); + + s.taboption('advanced', form.Value, 'serversfile', + _('Additional servers file'), + _('This file may contain lines like \'server=/domain/1.2.3.4\' or \'server=1.2.3.4\' for domain-specific or full upstream DNS servers.')); + + s.taboption('advanced', form.Flag, 'strictorder', + _('Strict order'), + _('DNS servers will be queried in the order of the resolvfile')).optional = true; + + s.taboption('advanced', form.Flag, 'allservers', + _('All Servers'), + _('Query all available upstream DNS servers')).optional = true; + + o = s.taboption('advanced', form.DynamicList, 'bogusnxdomain', _('Bogus NX Domain Override'), + _('List of hosts that supply bogus NX domain results')); + + o.optional = true; + o.placeholder = '67.215.65.132'; + + + s.taboption('general', form.Flag, 'logqueries', + _('Log queries'), + _('Write received DNS requests to syslog')).optional = true; + + o = s.taboption('general', form.DynamicList, 'server', _('DNS forwardings'), + _('List of DNS servers to forward requests to')); + + o.optional = true; + o.placeholder = '/example.org/10.1.2.3'; + // o.validate = validateServerSpec; + + + o = s.taboption('general', form.Flag, 'rebind_protection', + _('Rebind protection'), + _('Discard upstream RFC1918 responses')); + + o.rmempty = false; + + + o = s.taboption('general', form.Flag, 'rebind_localhost', + _('Allow localhost'), + _('Allow upstream responses in the 127.0.0.0/8 range, e.g. for RBL services')); + + o.depends('rebind_protection', '1'); + + + o = s.taboption('general', form.DynamicList, 'rebind_domain', + _('Domain whitelist'), + _('List of domains to allow RFC1918 responses for')); + o.optional = true; + + o.depends('rebind_protection', '1'); + o.placeholder = 'ihost.netflix.com'; + o.validate = validateAddressList; + + + o = s.taboption('advanced', form.Value, 'port', + _('DNS server port'), + _('Listening port for inbound DNS queries')); + + o.optional = true; + o.datatype = 'port'; + o.placeholder = 53; + + + o = s.taboption('advanced', form.Value, 'queryport', + _('DNS query port'), + _('Fixed source port for outbound DNS queries')); + + o.optional = true; + o.datatype = 'port'; + o.placeholder = _('any'); + + + o = s.taboption('advanced', form.Value, 'dhcpleasemax', + _('Max. DHCP leases'), + _('Maximum allowed number of active DHCP leases')); + + o.optional = true; + o.datatype = 'uinteger'; + o.placeholder = _('unlimited'); + + + o = s.taboption('advanced', form.Value, 'ednspacket_max', + _('Max. EDNS0 packet size'), + _('Maximum allowed size of EDNS.0 UDP packets')); + + o.optional = true; + o.datatype = 'uinteger'; + o.placeholder = 1280; + + + o = s.taboption('advanced', form.Value, 'dnsforwardmax', + _('Max. concurrent queries'), + _('Maximum allowed number of concurrent DNS queries')); + + o.optional = true; + o.datatype = 'uinteger'; + o.placeholder = 150; + + o = s.taboption('advanced', form.Value, 'cachesize', + _('Size of DNS query cache'), + _('Number of cached DNS entries (max is 10000, 0 is no caching)')); + o.optional = true; + o.datatype = 'range(0,10000)'; + o.placeholder = 150; + + s.taboption('tftp', form.Flag, 'enable_tftp', + _('Enable TFTP server')).optional = true; + + o = s.taboption('tftp', form.Value, 'tftp_root', + _('TFTP server root'), + _('Root directory for files served via TFTP')); + + o.optional = true; + o.depends('enable_tftp', '1'); + o.placeholder = '/'; + + + o = s.taboption('tftp', form.Value, 'dhcp_boot', + _('Network boot image'), + _('Filename of the boot image advertised to clients')); + + o.optional = true; + o.depends('enable_tftp', '1'); + o.placeholder = 'pxelinux.0'; + + o = s.taboption('general', form.Flag, 'localservice', + _('Local Service Only'), + _('Limit DNS service to subnets interfaces on which we are serving DNS.')); + o.optional = false; + o.rmempty = false; + + o = s.taboption('general', form.Flag, 'nonwildcard', + _('Non-wildcard'), + _('Bind dynamically to interfaces rather than wildcard address (recommended as linux default)')); + o.default = o.enabled; + o.optional = false; + o.rmempty = true; + + o = s.taboption('general', form.DynamicList, 'interface', + _('Listen Interfaces'), + _('Limit listening to these interfaces, and loopback.')); + o.optional = true; + + o = s.taboption('general', form.DynamicList, 'notinterface', + _('Exclude interfaces'), + _('Prevent listening on these interfaces.')); + o.optional = true; + + o = s.taboption('leases', form.SectionValue, '__leases__', form.GridSection, 'host', null, + _('Static leases are used to assign fixed IP addresses and symbolic hostnames to DHCP clients. They are also required for non-dynamic interface configurations where only hosts with a corresponding lease are served.') + '
' + + _('Use the Add Button to add a new lease entry. The MAC-Address identifies the host, the IPv4-Address specifies the fixed address to use, and the Hostname is assigned as a symbolic name to the requesting host. The optional Lease time can be used to set non-standard host-specific lease time, e.g. 12h, 3d or infinite.')); + + ss = o.subsection; + + ss.addremove = true; + ss.anonymous = true; + + so = ss.option(form.Value, 'name', _('Hostname')); + so.validate = validateHostname; + so.rmempty = true; + so.write = function(section, value) { + uci.set('dhcp', section, 'name', value); + uci.set('dhcp', section, 'dns', '1'); + }; + so.remove = function(section) { + uci.unset('dhcp', section, 'name'); + uci.unset('dhcp', section, 'dns'); + }; + + so = ss.option(form.Value, 'mac', _('MAC-Address')); + so.datatype = 'list(unique(macaddr))'; + so.rmempty = true; + so.cfgvalue = function(section) { + var macs = uci.get('dhcp', section, 'mac'), + result = []; + + if (!Array.isArray(macs)) + macs = (macs != null && macs != '') ? macs.split(/\ss+/) : []; + + for (var i = 0, mac; (mac = macs[i]) != null; i++) + if (/^([0-9a-fA-F]{1,2}):([0-9a-fA-F]{1,2}):([0-9a-fA-F]{1,2}):([0-9a-fA-F]{1,2}):([0-9a-fA-F]{1,2}):([0-9a-fA-F]{1,2})$/.test(mac)) + result.push('%02X:%02X:%02X:%02X:%02X:%02X'.format( + parseInt(RegExp.$1, 16), parseInt(RegExp.$2, 16), + parseInt(RegExp.$3, 16), parseInt(RegExp.$4, 16), + parseInt(RegExp.$5, 16), parseInt(RegExp.$6, 16))); + + return result.length ? result.join(' ') : null; + }; + so.renderWidget = function(section_id, option_index, cfgvalue) { + var node = form.Value.prototype.renderWidget.apply(this, [section_id, option_index, cfgvalue]), + ipopt = this.section.children.filter(function(o) { return o.option == 'ip' })[0]; + + node.addEventListener('cbi-dropdown-change', L.bind(function(ipopt, section_id, ev) { + var mac = ev.detail.value.value; + if (mac == null || mac == '' || !hosts[mac] || !hosts[mac].ipv4) + return; + + var ip = ipopt.formvalue(section_id); + if (ip != null && ip != '') + return; + + var node = ipopt.map.findElement('id', ipopt.cbid(section_id)); + if (node) + L.dom.callClassMethod(node, 'setValue', hosts[mac].ipv4); + }, this, ipopt, section_id)); + + return node; + }; + Object.keys(hosts).forEach(function(mac) { + var hint = hosts[mac].name || hosts[mac].ipv4; + so.value(mac, hint ? '%s (%s)'.format(mac, hint) : mac); + }); + + so = ss.option(form.Value, 'ip', _('IPv4-Address')); + so.datatype = 'or(ip4addr,"ignore")'; + so.validate = function(section, value) { + var mac = this.map.lookupOption('mac', section), + name = this.map.lookupOption('name', section), + m = mac ? mac[0].formvalue(section) : null, + n = name ? name[0].formvalue(section) : null; + + if ((m == null || m == '') && (n == null || n == '')) + return _('One of hostname or mac address must be specified!'); + + return true; + }; + Object.keys(hosts).forEach(function(mac) { + if (hosts[mac].ipv4) { + var hint = hosts[mac].name; + so.value(hosts[mac].ipv4, hint ? '%s (%s)'.format(hosts[mac].ipv4, hint) : hosts[mac].ipv4); + } + }); + + so = ss.option(form.Value, 'gw', _('Gateway')); + so.datatype = 'or(ip4addr,"ignore")'; + so.rmempty = true; + + so = ss.option(form.Value, 'leasetime', _('Lease time')); + so.rmempty = true; + + so = ss.option(form.Value, 'duid', _('DUID')); + so.datatype = 'and(rangelength(20,36),hexstring)'; + Object.keys(duids).forEach(function(duid) { + so.value(duid, '%s (%s)'.format(duid, duids[duid].hostname || duids[duid].macaddr || duids[duid].ip6addr || '?')); + }); + + so = ss.option(form.Value, 'hostid', _('IPv6-Suffix (hex)')); + + o = s.taboption('leases', CBILeaseStatus, '__status__'); + + if (has_dhcpv6) + o = s.taboption('leases', CBILease6Status, '__status6__'); + + return m.render().then(function(mapEl) { + L.Poll.add(function() { + return callDHCPLeases().then(function(leaseinfo) { + var leases = Array.isArray(leaseinfo.dhcp_leases) ? leaseinfo.dhcp_leases : [], + leases6 = Array.isArray(leaseinfo.dhcp6_leases) ? leaseinfo.dhcp6_leases : []; + + cbi_update_table(mapEl.querySelector('#lease_status_table'), + leases.map(function(lease) { + var exp; + + if (lease.expires === false) + exp = E('em', _('unlimited')); + else if (lease.expires <= 0) + exp = E('em', _('expired')); + else + exp = '%t'.format(lease.expires); + + return [ + lease.hostname || '?', + lease.ipaddr, + lease.macaddr, + exp + ]; + }), + E('em', _('There are no active leases'))); + + if (has_dhcpv6) { + cbi_update_table(mapEl.querySelector('#lease6_status_table'), + leases6.map(function(lease) { + var exp; + + if (lease.expires === false) + exp = E('em', _('unlimited')); + else if (lease.expires <= 0) + exp = E('em', _('expired')); + else + exp = '%t'.format(lease.expires); + + var hint = lease.macaddr ? hosts[lease.macaddr] : null, + name = hint ? (hint.name || hint.ipv4 || hint.ipv6) : null, + host = null; + + if (name && lease.hostname && lease.hostname != name && lease.ip6addr != name) + host = '%s (%s)'.format(lease.hostname, name); + else if (lease.hostname) + host = lease.hostname; + else if (name) + host = name; + + return [ + host || '-', + lease.ip6addrs ? lease.ip6addrs.join(' ') : lease.ip6addr, + lease.duid, + exp + ]; + }), + E('em', _('There are no active leases'))); + } + }); + }); + + return mapEl; + }); + } +}); diff --git a/luci-mod-network/htdocs/luci-static/resources/view/network/diagnostics.js b/luci-mod-network/htdocs/luci-static/resources/view/network/diagnostics.js new file mode 100644 index 000000000..24ab84ab9 --- /dev/null +++ b/luci-mod-network/htdocs/luci-static/resources/view/network/diagnostics.js @@ -0,0 +1,137 @@ +'use strict'; +'require fs'; +'require ui'; +'require uci'; + +return L.view.extend({ + handleCommand: function(exec, args) { + var buttons = document.querySelectorAll('.diag-action > .cbi-button'); + + for (var i = 0; i < buttons.length; i++) + buttons[i].setAttribute('disabled', 'true'); + + return fs.exec(exec, args).then(function(res) { + var out = document.querySelector('.command-output'); + out.style.display = ''; + + L.dom.content(out, [ res.stdout || '', res.stderr || '' ]); + }).catch(function(err) { + ui.addNotification(null, E('p', [ err ])) + }).finally(function() { + for (var i = 0; i < buttons.length; i++) + buttons[i].removeAttribute('disabled'); + }); + }, + + handlePing: function(ev, cmd) { + var exec = cmd || 'ping', + addr = ev.currentTarget.parentNode.previousSibling.value, + args = (exec == 'ping') ? [ '-c', '5', '-W', '1', addr ] : [ '-c', '5', addr ]; + + return this.handleCommand(exec, args); + }, + + handleTraceroute: function(ev, cmd) { + var exec = cmd || 'traceroute', + addr = ev.currentTarget.parentNode.previousSibling.value, + args = (exec == 'traceroute') ? [ '-q', '1', '-w', '1', '-n', addr ] : [ '-q', '1', '-w', '2', '-n', addr ]; + + return this.handleCommand(exec, args); + }, + + handleNslookup: function(ev, cmd) { + var addr = ev.currentTarget.parentNode.previousSibling.value; + + return this.handleCommand('nslookup', [ addr ]); + }, + + load: function() { + return Promise.all([ + L.resolveDefault(fs.stat('/bin/ping6'), {}), + L.resolveDefault(fs.stat('/usr/bin/ping6'), {}), + L.resolveDefault(fs.stat('/bin/traceroute6'), {}), + L.resolveDefault(fs.stat('/usr/bin/traceroute6'), {}), + uci.load('luci') + ]); + }, + + render: function(res) { + var has_ping6 = res[0].path || res[1].path, + has_traceroute6 = res[2].path || res[3].path, + dns_host = uci.get('luci', 'diag', 'dns') || 'openwrt.org', + ping_host = uci.get('luci', 'diag', 'ping') || 'openwrt.org', + route_host = uci.get('luci', 'diag', 'route') || 'openwrt.org'; + + return E([], [ + E('h2', {}, [ _('Network Utilities') ]), + E('div', { 'class': 'table' }, [ + E('div', { 'class': 'tr' }, [ + E('div', { 'class': 'td left' }, [ + E('input', { + 'style': 'margin:5px 0', + 'type': 'text', + 'value': ping_host + }), + E('span', { 'class': 'diag-action' }, [ + has_ping6 ? new ui.ComboButton('ping', { + 'ping': '%s %s'.format(_('IPv4'), _('Ping')), + 'ping6': '%s %s'.format(_('IPv6'), _('Ping')), + }, { + 'click': ui.createHandlerFn(this, 'handlePing'), + 'classes': { + 'ping': 'btn cbi-button cbi-button-action', + 'ping6': 'btn cbi-button cbi-button-action' + } + }).render() : E('button', { + 'class': 'cbi-button cbi-button-action', + 'click': ui.createHandlerFn(this, 'handlePing') + }, [ _('Ping') ]) + ]) + ]), + + E('div', { 'class': 'td left' }, [ + E('input', { + 'style': 'margin:5px 0', + 'type': 'text', + 'value': route_host + }), + E('span', { 'class': 'diag-action' }, [ + has_traceroute6 ? new ui.ComboButton('traceroute', { + 'traceroute': '%s %s'.format(_('IPv4'), _('Traceroute')), + 'traceroute6': '%s %s'.format(_('IPv6'), _('Traceroute')), + }, { + 'click': ui.createHandlerFn(this, 'handleTraceroute'), + 'classes': { + 'traceroute': 'btn cbi-button cbi-button-action', + 'traceroute6': 'btn cbi-button cbi-button-action' + } + }).render() : E('button', { + 'class': 'cbi-button cbi-button-action', + 'click': ui.createHandlerFn(this, 'handleTraceroute') + }, [ _('Traceroute') ]) + ]) + ]), + + E('div', { 'class': 'td left' }, [ + E('input', { + 'style': 'margin:5px 0', + 'type': 'text', + 'value': dns_host + }), + E('span', { 'class': 'diag-action' }, [ + E('button', { + 'class': 'cbi-button cbi-button-action', + 'click': ui.createHandlerFn(this, 'handleNslookup') + }, [ _('Nslookup') ]) + ]) + ]) + ]) + ]), + E('pre', { 'class': 'command-output', 'style': 'display:none' }) + ]); + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/luci-mod-network/htdocs/luci-static/resources/view/network/hosts.js b/luci-mod-network/htdocs/luci-static/resources/view/network/hosts.js new file mode 100644 index 000000000..975ac2fec --- /dev/null +++ b/luci-mod-network/htdocs/luci-static/resources/view/network/hosts.js @@ -0,0 +1,42 @@ +'use strict'; +'require rpc'; +'require form'; + +return L.view.extend({ + callHostHints: rpc.declare({ + object: 'luci-rpc', + method: 'getHostHints', + expect: { '': {} } + }), + + load: function() { + return this.callHostHints(); + }, + + render: function(hosts) { + var m, s, o; + + m = new form.Map('dhcp', _('Hostnames')); + + s = m.section(form.GridSection, 'domain', _('Host entries')); + s.addremove = true; + s.anonymous = true; + s.sortable = true; + + o = s.option(form.Value, 'name', _('Hostname')); + o.datatype = 'hostname'; + o.rmempty = true; + + o = s.option(form.Value, 'ip', _('IP address')); + o.datatype = 'ipaddr'; + o.rmempty = true; + L.sortedKeys(hosts, 'ipv4', 'addr').forEach(function(mac) { + o.value(hosts[mac].ipv4, '%s (%s)'.format( + hosts[mac].ipv4, + hosts[mac].name || mac + )); + }); + + return m.render(); + } +}); diff --git a/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js b/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js new file mode 100644 index 000000000..0e6e5a2e2 --- /dev/null +++ b/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js @@ -0,0 +1,1024 @@ +'use strict'; +'require fs'; +'require ui'; +'require uci'; +'require form'; +'require network'; +'require firewall'; +'require tools.widgets as widgets'; + +function count_changes(section_id) { + var changes = ui.changes.changes, n = 0; + + if (!L.isObject(changes)) + return n; + + if (Array.isArray(changes.network)) + for (var i = 0; i < changes.network.length; i++) + n += (changes.network[i][1] == section_id); + + if (Array.isArray(changes.dhcp)) + for (var i = 0; i < changes.dhcp.length; i++) + n += (changes.dhcp[i][1] == section_id); + + return n; +} + +function render_iface(dev, alias) { + var type = dev ? dev.getType() : 'ethernet', + up = dev ? dev.isUp() : false; + + return E('span', { class: 'cbi-tooltip-container' }, [ + E('img', { 'class' : 'middle', 'src': L.resource('icons/%s%s.png').format( + alias ? 'alias' : type, + up ? '' : '_disabled') }), + E('span', { 'class': 'cbi-tooltip ifacebadge large' }, [ + E('img', { 'src': L.resource('icons/%s%s.png').format( + type, up ? '' : '_disabled') }), + L.itemlist(E('span', { 'class': 'left' }), [ + _('Type'), dev ? dev.getTypeI18n() : null, + _('Device'), dev ? dev.getName() : _('Not present'), + _('Connected'), up ? _('yes') : _('no'), + _('MAC'), dev ? dev.getMAC() : null, + _('RX'), dev ? '%.2mB (%d %s)'.format(dev.getRXBytes(), dev.getRXPackets(), _('Pkts.')) : null, + _('TX'), dev ? '%.2mB (%d %s)'.format(dev.getTXBytes(), dev.getTXPackets(), _('Pkts.')) : null + ]) + ]) + ]); +} + +function render_status(node, ifc, with_device) { + var desc = null, c = []; + + if (ifc.isDynamic()) + desc = _('Virtual dynamic interface'); + else if (ifc.isAlias()) + desc = _('Alias Interface'); + else if (!uci.get('network', ifc.getName())) + return L.itemlist(node, [ + null, E('em', _('Interface is marked for deletion')) + ]); + + var i18n = ifc.getI18n(); + if (i18n) + desc = desc ? '%s (%s)'.format(desc, i18n) : i18n; + + var changecount = with_device ? 0 : count_changes(ifc.getName()), + ipaddrs = changecount ? [] : ifc.getIPAddrs(), + ip6addrs = changecount ? [] : ifc.getIP6Addrs(), + errors = ifc.getErrors(), + maindev = ifc.getL3Device() || ifc.getDevice(), + macaddr = maindev ? maindev.getMAC() : null; + + return L.itemlist(node, [ + _('Protocol'), with_device ? null : (desc || '?'), + _('Device'), with_device ? (maindev ? maindev.getShortName() : E('em', _('Not present'))) : null, + _('Uptime'), (!changecount && ifc.isUp()) ? '%t'.format(ifc.getUptime()) : null, + _('MAC'), (!changecount && !ifc.isDynamic() && !ifc.isAlias() && macaddr) ? macaddr : null, + _('RX'), (!changecount && !ifc.isDynamic() && !ifc.isAlias() && maindev) ? '%.2mB (%d %s)'.format(maindev.getRXBytes(), maindev.getRXPackets(), _('Pkts.')) : null, + _('TX'), (!changecount && !ifc.isDynamic() && !ifc.isAlias() && maindev) ? '%.2mB (%d %s)'.format(maindev.getTXBytes(), maindev.getTXPackets(), _('Pkts.')) : null, + _('IPv4'), ipaddrs[0], + _('IPv4'), ipaddrs[1], + _('IPv4'), ipaddrs[2], + _('IPv4'), ipaddrs[3], + _('IPv4'), ipaddrs[4], + _('IPv6'), ip6addrs[0], + _('IPv6'), ip6addrs[1], + _('IPv6'), ip6addrs[2], + _('IPv6'), ip6addrs[3], + _('IPv6'), ip6addrs[4], + _('IPv6'), ip6addrs[5], + _('IPv6'), ip6addrs[6], + _('IPv6'), ip6addrs[7], + _('IPv6'), ip6addrs[8], + _('IPv6'), ip6addrs[9], + _('IPv6-PD'), changecount ? null : ifc.getIP6Prefix(), + _('Information'), with_device ? null : (ifc.get('auto') != '0' ? null : _('Not started on boot')), + _('Error'), errors ? errors[0] : null, + _('Error'), errors ? errors[1] : null, + _('Error'), errors ? errors[2] : null, + _('Error'), errors ? errors[3] : null, + _('Error'), errors ? errors[4] : null, + null, changecount ? E('a', { + href: '#', + click: L.bind(ui.changes.displayChanges, ui.changes) + }, _('Interface has %d pending changes').format(changecount)) : null + ]); +} + +function render_modal_status(node, ifc) { + var dev = ifc ? (ifc.getDevice() || ifc.getL3Device() || ifc.getL3Device()) : null; + + L.dom.content(node, [ + E('img', { + 'src': L.resource('icons/%s%s.png').format(dev ? dev.getType() : 'ethernet', (dev && dev.isUp()) ? '' : '_disabled'), + 'title': dev ? dev.getTypeI18n() : _('Not present') + }), + ifc ? render_status(E('span'), ifc, true) : E('em', _('Interface not present or not connected yet.')) + ]); + + return node; +} + +function render_ifacebox_status(node, ifc) { + var dev = ifc.getL3Device() || ifc.getDevice(), + subdevs = ifc.getDevices(), + c = [ render_iface(dev, ifc.isAlias()) ]; + + if (subdevs && subdevs.length) { + var sifs = [ ' (' ]; + + for (var j = 0; j < subdevs.length; j++) + sifs.push(render_iface(subdevs[j])); + + sifs.push(')'); + + c.push(E('span', {}, sifs)); + } + + c.push(E('br')); + c.push(E('small', {}, ifc.isAlias() ? _('Alias of "%s"').format(ifc.isAlias()) + : (dev ? dev.getName() : E('em', _('Not present'))))); + + L.dom.content(node, c); + + return firewall.getZoneByNetwork(ifc.getName()).then(L.bind(function(zone) { + this.style.backgroundColor = zone ? zone.getColor() : '#EEEEEE'; + this.title = zone ? _('Part of zone %q').format(zone.getName()) : _('No zone assigned'); + }, node.previousElementSibling)); +} + +function iface_updown(up, id, ev, force) { + var row = document.querySelector('.cbi-section-table-row[data-sid="%s"]'.format(id)), + dsc = row.querySelector('[data-name="_ifacestat"] > div'), + btns = row.querySelectorAll('.cbi-section-actions .reconnect, .cbi-section-actions .down'); + + btns[+!up].blur(); + btns[+!up].classList.add('spinning'); + + btns[0].disabled = true; + btns[1].disabled = true; + + if (!up) { + L.resolveDefault(fs.exec_direct('/usr/libexec/luci-peeraddr')).then(function(res) { + var info = null; try { info = JSON.parse(res); } catch(e) {} + + if (L.isObject(info) && + Array.isArray(info.inbound_interfaces) && + info.inbound_interfaces.filter(function(i) { return i == id })[0]) { + + ui.showModal(_('Confirm disconnect'), [ + E('p', _('You appear to be currently connected to the device via the "%h" interface. Do you really want to shut down the interface?').format(id)), + E('div', { 'class': 'right' }, [ + E('button', { + 'class': 'cbi-button cbi-button-neutral', + 'click': function(ev) { + btns[1].classList.remove('spinning'); + btns[1].disabled = false; + btns[0].disabled = false; + + ui.hideModal(); + } + }, _('Cancel')), + ' ', + E('button', { + 'class': 'cbi-button cbi-button-negative important', + 'click': function(ev) { + dsc.setAttribute('disconnect', ''); + L.dom.content(dsc, E('em', _('Interface is shutting down...'))); + + ui.hideModal(); + } + }, _('Disconnect')) + ]) + ]); + } + else { + dsc.setAttribute('disconnect', ''); + L.dom.content(dsc, E('em', _('Interface is shutting down...'))); + } + }); + } + else { + dsc.setAttribute(up ? 'reconnect' : 'disconnect', force ? 'force' : ''); + L.dom.content(dsc, E('em', up ? _('Interface is reconnecting...') : _('Interface is shutting down...'))); + } +} + +function get_netmask(s, use_cfgvalue) { + var readfn = use_cfgvalue ? 'cfgvalue' : 'formvalue', + addropt = s.children.filter(function(o) { return o.option == 'ipaddr'})[0], + addrvals = addropt ? L.toArray(addropt[readfn](s.section)) : [], + maskopt = s.children.filter(function(o) { return o.option == 'netmask'})[0], + maskval = maskopt ? maskopt[readfn](s.section) : null, + firstsubnet = maskval ? addrvals[0] + '/' + maskval : addrvals.filter(function(a) { return a.indexOf('/') > 0 })[0]; + + if (firstsubnet == null) + return null; + + var mask = firstsubnet.split('/')[1]; + + if (!isNaN(mask)) + mask = network.prefixToMask(+mask); + + return mask; +} + +return L.view.extend({ + poll_status: function(map, networks) { + var resolveZone = null; + + for (var i = 0; i < networks.length; i++) { + var ifc = networks[i], + row = map.querySelector('.cbi-section-table-row[data-sid="%s"]'.format(ifc.getName())); + + if (row == null) + continue; + + var dsc = row.querySelector('[data-name="_ifacestat"] > div'), + box = row.querySelector('[data-name="_ifacebox"] .ifacebox-body'), + btn1 = row.querySelector('.cbi-section-actions .reconnect'), + btn2 = row.querySelector('.cbi-section-actions .down'), + stat = document.querySelector('[id="%s-ifc-status"]'.format(ifc.getName())), + resolveZone = render_ifacebox_status(box, ifc), + disabled = ifc ? !ifc.isUp() : true, + dynamic = ifc ? ifc.isDynamic() : false; + + if (dsc.hasAttribute('reconnect')) { + L.dom.content(dsc, E('em', _('Interface is starting...'))); + } + else if (dsc.hasAttribute('disconnect')) { + L.dom.content(dsc, E('em', _('Interface is stopping...'))); + } + else if (ifc.getProtocol() || uci.get('network', ifc.getName()) == null) { + render_status(dsc, ifc, false); + } + else if (!ifc.getProtocol()) { + var e = map.querySelector('[id="cbi-network-%s"] .cbi-button-edit'.format(ifc.getName())); + if (e) e.disabled = true; + + var link = L.url('admin/system/opkg') + '?query=luci-proto'; + L.dom.content(dsc, [ + E('em', _('Unsupported protocol type.')), E('br'), + E('a', { href: link }, _('Install protocol extensions...')) + ]); + } + else { + L.dom.content(dsc, E('em', _('Interface not present or not connected yet.'))); + } + + if (stat) { + var dev = ifc.getDevice(); + L.dom.content(stat, [ + E('img', { + 'src': L.resource('icons/%s%s.png').format(dev ? dev.getType() : 'ethernet', (dev && dev.isUp()) ? '' : '_disabled'), + 'title': dev ? dev.getTypeI18n() : _('Not present') + }), + render_status(E('span'), ifc, true) + ]); + } + + btn1.disabled = btn1.classList.contains('spinning') || btn2.classList.contains('spinning') || dynamic; + btn2.disabled = btn1.classList.contains('spinning') || btn2.classList.contains('spinning') || dynamic || disabled; + } + + return Promise.all([ resolveZone, network.flushCache() ]); + }, + + load: function() { + return Promise.all([ + network.getDSLModemType(), + uci.changes() + ]); + }, + + render: function(data) { + var dslModemType = data[0], + m, s, o; + + m = new form.Map('network'); + m.tabbed = true; + m.chain('dhcp'); + + s = m.section(form.GridSection, 'interface', _('Interfaces')); + s.anonymous = true; + s.addremove = true; + s.addbtntitle = _('Add new interface...'); + + s.load = function() { + return Promise.all([ + network.getNetworks(), + firewall.getZones() + ]).then(L.bind(function(data) { + this.networks = data[0]; + this.zones = data[1]; + }, this)); + }; + + s.tab('general', _('General Settings')); + s.tab('advanced', _('Advanced Settings')); + s.tab('physical', _('Physical Settings')); + s.tab('firewall', _('Firewall Settings')); + s.tab('dhcp', _('DHCP Server')); + + s.cfgsections = function() { + return this.networks.map(function(n) { return n.getName() }) + .filter(function(n) { return n != 'loopback' }); + }; + + s.modaltitle = function(section_id) { + return _('Interfaces') + ' » ' + section_id.toUpperCase(); + }; + + s.renderRowActions = function(section_id) { + var tdEl = this.super('renderRowActions', [ section_id, _('Edit') ]), + net = this.networks.filter(function(n) { return n.getName() == section_id })[0], + disabled = net ? !net.isUp() : true, + dynamic = net ? net.isDynamic() : false; + + L.dom.content(tdEl.lastChild, [ + E('button', { + 'class': 'cbi-button cbi-button-neutral reconnect', + 'click': iface_updown.bind(this, true, section_id), + 'title': _('Reconnect this interface'), + 'disabled': dynamic ? 'disabled' : null + }, _('Restart')), + E('button', { + 'class': 'cbi-button cbi-button-neutral down', + 'click': iface_updown.bind(this, false, section_id), + 'title': _('Shutdown this interface'), + 'disabled': (dynamic || disabled) ? 'disabled' : null + }, _('Stop')), + tdEl.lastChild.firstChild, + tdEl.lastChild.lastChild + ]); + + if (!dynamic && net && !uci.get('network', net.getName())) { + tdEl.lastChild.childNodes[0].disabled = true; + tdEl.lastChild.childNodes[2].disabled = true; + tdEl.lastChild.childNodes[3].disabled = true; + } + + return tdEl; + }; + + s.addModalOptions = function(s) { + var protoval = uci.get('network', s.section, 'proto'), + protoclass = protoval ? network.getProtocol(protoval) : null, + o, ifname_single, ifname_multi, proto_select, proto_switch, type, stp, igmp, ss, so; + + if (!protoval) + return; + + return network.getNetwork(s.section).then(L.bind(function(ifc) { + var protocols = network.getProtocols(); + + protocols.sort(function(a, b) { + return a.getProtocol() > b.getProtocol(); + }); + + o = s.taboption('general', form.DummyValue, '_ifacestat_modal', _('Status')); + o.modalonly = true; + o.cfgvalue = L.bind(function(section_id) { + var net = this.networks.filter(function(n) { return n.getName() == section_id })[0]; + + return render_modal_status(E('div', { + 'id': '%s-ifc-status'.format(section_id), + 'class': 'ifacebadge large' + }), net); + }, this); + o.write = function() {}; + + proto_select = s.taboption('general', form.ListValue, 'proto', _('Protocol')); + proto_select.modalonly = true; + + proto_switch = s.taboption('general', form.Button, '_switch_proto'); + proto_switch.modalonly = true; + proto_switch.title = _('Really switch protocol?'); + proto_switch.inputtitle = _('Switch protocol'); + proto_switch.inputstyle = 'apply'; + proto_switch.onclick = L.bind(function(ev) { + s.map.save() + .then(L.bind(m.load, m)) + .then(L.bind(m.render, m)) + .then(L.bind(this.renderMoreOptionsModal, this, s.section)); + }, this); + + o = s.taboption('general', form.Flag, 'auto', _('Bring up on boot')); + o.modalonly = true; + o.default = o.enabled; + + type = s.taboption('physical', form.Flag, 'type', _('Bridge interfaces'), _('Creates a bridge over specified interface(s)')); + type.modalonly = true; + type.disabled = ''; + type.enabled = 'bridge'; + type.write = type.remove = function(section_id, value) { + var protocol = network.getProtocol(proto_select.formvalue(section_id)), + ifnameopt = this.section.children.filter(function(o) { return o.option == (value ? 'ifname_multi' : 'ifname_single') })[0]; + + if (!protocol.isVirtual() && !this.isActive(section_id)) + return; + + var old_ifnames = [], + devs = ifc.getDevices() || L.toArray(ifc.getDevice()); + + for (var i = 0; i < devs.length; i++) + old_ifnames.push(devs[i].getName()); + + var new_ifnames = L.toArray(ifnameopt.formvalue(section_id)); + + if (!value) + new_ifnames.length = Math.max(new_ifnames.length, 1); + + old_ifnames.sort(); + new_ifnames.sort(); + + for (var i = 0; i < Math.max(old_ifnames.length, new_ifnames.length); i++) { + if (old_ifnames[i] != new_ifnames[i]) { + // backup_ifnames() + for (var j = 0; j < old_ifnames.length; j++) + ifc.deleteDevice(old_ifnames[j]); + + for (var j = 0; j < new_ifnames.length; j++) + ifc.addDevice(new_ifnames[j]); + + break; + } + } + + if (value) + uci.set('network', section_id, 'type', 'bridge'); + else + uci.unset('network', section_id, 'type'); + }; + + stp = s.taboption('physical', form.Flag, 'stp', _('Enable STP'), _('Enables the Spanning Tree Protocol on this bridge')); + + igmp = s.taboption('physical', form.Flag, 'igmp_snooping', _('Enable IGMP snooping'), _('Enables IGMP snooping on this bridge')); + + ifname_single = s.taboption('physical', widgets.DeviceSelect, 'ifname_single', _('Interface')); + ifname_single.nobridges = ifc.isBridge(); + ifname_single.noaliases = false; + ifname_single.optional = false; + ifname_single.network = ifc.getName(); + ifname_single.write = ifname_single.remove = function() {}; + + ifname_multi = s.taboption('physical', widgets.DeviceSelect, 'ifname_multi', _('Interface')); + ifname_multi.nobridges = ifc.isBridge(); + ifname_multi.noaliases = true; + ifname_multi.multiple = true; + ifname_multi.optional = true; + ifname_multi.network = ifc.getName(); + ifname_multi.display_size = 6; + ifname_multi.write = ifname_multi.remove = function() {}; + + ifname_single.cfgvalue = ifname_multi.cfgvalue = function(section_id) { + var devs = ifc.getDevices() || L.toArray(ifc.getDevice()), + ifnames = []; + + for (var i = 0; i < devs.length; i++) + ifnames.push(devs[i].getName()); + + return ifnames; + }; + + if (L.hasSystemFeature('firewall')) { + o = s.taboption('firewall', widgets.ZoneSelect, '_zone', _('Create / Assign firewall-zone'), _('Choose the firewall zone you want to assign to this interface. Select unspecified to remove the interface from the associated zone or fill out the create field to define a new zone and attach the interface to it.')); + o.network = ifc.getName(); + o.optional = true; + + o.cfgvalue = function(section_id) { + return firewall.getZoneByNetwork(ifc.getName()).then(function(zone) { + return (zone != null ? zone.getName() : null); + }); + }; + + o.write = o.remove = function(section_id, value) { + return Promise.all([ + firewall.getZoneByNetwork(ifc.getName()), + (value != null) ? firewall.getZone(value) : null + ]).then(function(data) { + var old_zone = data[0], + new_zone = data[1]; + + if (old_zone == null && new_zone == null && (value == null || value == '')) + return; + + if (old_zone != null && new_zone != null && old_zone.getName() == new_zone.getName()) + return; + + if (old_zone != null) + old_zone.deleteNetwork(ifc.getName()); + + if (new_zone != null) + new_zone.addNetwork(ifc.getName()); + else if (value != null) + return firewall.addZone(value).then(function(new_zone) { + new_zone.addNetwork(ifc.getName()); + }); + }); + }; + } + + for (var i = 0; i < protocols.length; i++) { + proto_select.value(protocols[i].getProtocol(), protocols[i].getI18n()); + + if (protocols[i].getProtocol() != uci.get('network', s.section, 'proto')) + proto_switch.depends('proto', protocols[i].getProtocol()); + + if (!protocols[i].isVirtual()) { + type.depends('proto', protocols[i].getProtocol()); + stp.depends({ type: 'bridge', proto: protocols[i].getProtocol() }); + igmp.depends({ type: 'bridge', proto: protocols[i].getProtocol() }); + ifname_single.depends({ type: '', proto: protocols[i].getProtocol() }); + ifname_multi.depends({ type: 'bridge', proto: protocols[i].getProtocol() }); + } + } + + if (L.hasSystemFeature('dnsmasq') || L.hasSystemFeature('odhcpd')) { + o = s.taboption('dhcp', form.SectionValue, '_dhcp', form.TypedSection, 'dhcp'); + o.depends('proto', 'static'); + + ss = o.subsection; + ss.uciconfig = 'dhcp'; + ss.addremove = false; + ss.anonymous = true; + + ss.tab('general', _('General Setup')); + ss.tab('advanced', _('Advanced Settings')); + ss.tab('ipv6', _('IPv6 Settings')); + + ss.filter = function(section_id) { + return (uci.get('dhcp', section_id, 'interface') == ifc.getName()); + }; + + ss.renderSectionPlaceholder = function() { + return E('div', { 'class': 'cbi-section-create' }, [ + E('p', _('No DHCP Server configured for this interface') + '   '), + E('button', { + 'class': 'cbi-button cbi-button-add', + 'title': _('Setup DHCP Server'), + 'click': ui.createHandlerFn(this, function(section_id, ev) { + this.map.save(function() { + uci.add('dhcp', 'dhcp', section_id); + uci.set('dhcp', section_id, 'interface', section_id); + uci.set('dhcp', section_id, 'start', 100); + uci.set('dhcp', section_id, 'limit', 150); + uci.set('dhcp', section_id, 'leasetime', '12h'); + }); + }, ifc.getName()) + }, _('Setup DHCP Server')) + ]); + }; + + ss.taboption('general', form.Flag, 'ignore', _('Ignore interface'), _('Disable DHCP for this interface.')); + + so = ss.taboption('general', form.Value, 'start', _('Start'), _('Lowest leased address as offset from the network address.')); + so.optional = true; + so.datatype = 'or(uinteger,ip4addr("nomask"))'; + so.default = '100'; + + so = ss.taboption('general', form.Value, 'limit', _('Limit'), _('Maximum number of leased addresses.')); + so.optional = true; + so.datatype = 'uinteger'; + so.default = '150'; + + so = ss.taboption('general', form.Value, 'leasetime', _('Lease time'), _('Expiry time of leased addresses, minimum is 2 minutes (2m).')); + so.optional = true; + so.default = '12h'; + + so = ss.taboption('advanced', form.Flag, 'dynamicdhcp', _('Dynamic DHCP'), _('Dynamically allocate DHCP addresses for clients. If disabled, only clients having static leases will be served.')); + so.default = so.enabled; + + ss.taboption('advanced', form.Flag, 'force', _('Force'), _('Force DHCP on this network even if another server is detected.')); + + // XXX: is this actually useful? + //ss.taboption('advanced', form.Value, 'name', _('Name'), _('Define a name for this network.')); + + so = ss.taboption('advanced', form.Value, 'netmask', _('IPv4-Netmask'), _('Override the netmask sent to clients. Normally it is calculated from the subnet that is served.')); + so.optional = true; + so.datatype = 'ip4addr'; + + so.render = function(option_index, section_id, in_table) { + this.placeholder = get_netmask(s, true); + return form.Value.prototype.render.apply(this, [ option_index, section_id, in_table ]); + }; + + so.validate = function(section_id, value) { + var node = this.map.findElement('id', this.cbid(section_id)); + if (node) + node.querySelector('input').setAttribute('placeholder', get_netmask(s, false)); + return form.Value.prototype.validate.apply(this, [ section_id, value ]); + }; + + ss.taboption('advanced', form.DynamicList, 'dhcp_option', _('DHCP-Options'), _('Define additional DHCP options, for example "6,192.168.2.1,192.168.2.2" which advertises different DNS servers to clients.')); + + for (var i = 0; i < ss.children.length; i++) + if (ss.children[i].option != 'ignore') + ss.children[i].depends('ignore', '0'); + + so = ss.taboption('ipv6', form.ListValue, 'ra', _('Router Advertisement-Service')); + so.value('', _('disabled')); + so.value('server', _('server mode')); + so.value('relay', _('relay mode')); + so.value('hybrid', _('hybrid mode')); + + so = ss.taboption('ipv6', form.ListValue, 'dhcpv6', _('DHCPv6-Service')); + so.value('', _('disabled')); + so.value('server', _('server mode')); + so.value('relay', _('relay mode')); + so.value('hybrid', _('hybrid mode')); + + so = ss.taboption('ipv6', form.ListValue, 'ndp', _('NDP-Proxy')); + so.value('', _('disabled')); + so.value('relay', _('relay mode')); + so.value('hybrid', _('hybrid mode')); + + so = ss.taboption('ipv6', form.Flag , 'master', _('Master'), _('Set this interface as master for the dhcpv6 relay.')); + so.depends('dhcpv6', 'relay'); + so.depends('dhcpv6', 'hybrid'); + + so = ss.taboption('ipv6', form.ListValue, 'ra_management', _('DHCPv6-Mode'), _('Default is stateless + stateful')); + so.value('0', _('stateless')); + so.value('1', _('stateless + stateful')); + so.value('2', _('stateful-only')); + so.depends('dhcpv6', 'server'); + so.depends('dhcpv6', 'hybrid'); + so.default = '1'; + + so = ss.taboption('ipv6', form.Flag, 'ra_default', _('Always announce default router'), _('Announce as default router even if no public prefix is available.')); + so.depends('ra', 'server'); + so.depends('ra', 'hybrid'); + + ss.taboption('ipv6', form.DynamicList, 'dns', _('Announced DNS servers')); + ss.taboption('ipv6', form.DynamicList, 'domain', _('Announced DNS domains')); + } + + ifc.renderFormOptions(s); + + for (var i = 0; i < s.children.length; i++) { + o = s.children[i]; + + switch (o.option) { + case 'proto': + case 'delegate': + case 'auto': + case 'type': + case 'stp': + case 'igmp_snooping': + case 'ifname_single': + case 'ifname_multi': + case '_dhcp': + case '_zone': + case '_switch_proto': + case '_ifacestat_modal': + continue; + + default: + if (o.deps.length) + for (var j = 0; j < o.deps.length; j++) + o.deps[j].proto = protoval; + else + o.depends('proto', protoval); + } + } + }, this)); + }; + + s.handleAdd = function(ev) { + var m2 = new form.Map('network'), + s2 = m2.section(form.NamedSection, '_new_'), + protocols = network.getProtocols(), + proto, name, bridge, ifname_single, ifname_multi; + + protocols.sort(function(a, b) { + return a.getProtocol() > b.getProtocol(); + }); + + s2.render = function() { + return Promise.all([ + {}, + this.renderUCISection('_new_') + ]).then(this.renderContents.bind(this)); + }; + + name = s2.option(form.Value, 'name', _('Name')); + name.rmempty = false; + name.datatype = 'uciname'; + name.placeholder = _('New interface name…'); + name.validate = function(section_id, value) { + if (uci.get('network', value) != null) + return _('The interface name is already used'); + + var pr = network.getProtocol(proto.formvalue(section_id), value), + ifname = pr.isVirtual() ? '%s-%s'.format(pr.getProtocol(), value) : 'br-%s'.format(value); + + if (value.length > 15) + return _('The interface name is too long'); + + return true; + }; + + proto = s2.option(form.ListValue, 'proto', _('Protocol')); + proto.validate = name.validate; + + bridge = s2.option(form.Flag, 'type', _('Bridge interfaces'), _('Creates a bridge over specified interface(s)')); + bridge.modalonly = true; + bridge.disabled = ''; + bridge.enabled = 'bridge'; + + ifname_single = s2.option(widgets.DeviceSelect, 'ifname_single', _('Interface')); + ifname_single.noaliases = false; + ifname_single.optional = false; + + ifname_multi = s2.option(widgets.DeviceSelect, 'ifname_multi', _('Interface')); + ifname_multi.nobridges = true; + ifname_multi.noaliases = true; + ifname_multi.multiple = true; + ifname_multi.optional = true; + ifname_multi.display_size = 6; + + for (var i = 0; i < protocols.length; i++) { + proto.value(protocols[i].getProtocol(), protocols[i].getI18n()); + + if (!protocols[i].isVirtual()) { + bridge.depends({ proto: protocols[i].getProtocol() }); + ifname_single.depends({ type: '', proto: protocols[i].getProtocol() }); + ifname_multi.depends({ type: 'bridge', proto: protocols[i].getProtocol() }); + } + } + + m2.render().then(L.bind(function(nodes) { + ui.showModal(_('Add new interface...'), [ + nodes, + E('div', { 'class': 'right' }, [ + E('button', { + 'class': 'btn', + 'click': ui.hideModal + }, _('Cancel')), ' ', + E('button', { + 'class': 'cbi-button cbi-button-positive important', + 'click': ui.createHandlerFn(this, function(ev) { + var nameval = name.isValid('_new_') ? name.formvalue('_new_') : null, + protoval = proto.isValid('_new_') ? proto.formvalue('_new_') : null, + protoclass = protoval ? network.getProtocol(protoval) : null; + + if (nameval == null || protoval == null || nameval == '' || protoval == '') + return; + + return protoclass.isCreateable(nameval).then(function(checkval) { + if (checkval != null) { + ui.addNotification(null, + E('p', _('New interface for "%s" can not be created: %s').format(protoclass.getI18n(), checkval))); + ui.hideModal(); + return; + } + + return m.save(function() { + var section_id = uci.add('network', 'interface', nameval); + + uci.set('network', section_id, 'proto', protoval); + + if (ifname_single.isActive('_new_')) { + uci.set('network', section_id, 'ifname', ifname_single.formvalue('_new_')); + } + else if (ifname_multi.isActive('_new_')) { + uci.set('network', section_id, 'type', 'bridge'); + uci.set('network', section_id, 'ifname', L.toArray(ifname_multi.formvalue('_new_')).join(' ')); + } + }).then(L.bind(m.children[0].renderMoreOptionsModal, m.children[0], nameval)); + + }); + }) + }, _('Create interface')) + ]) + ], 'cbi-modal'); + + nodes.querySelector('[id="%s"] input[type="text"]'.format(name.cbid('_new_'))).focus(); + }, this)); + }; + + s.handleRemove = function(section_id, ev) { + return network.deleteNetwork(section_id).then(L.bind(function(section_id, ev) { + return form.GridSection.prototype.handleRemove.apply(this, [section_id, ev]); + }, this, section_id, ev)); + }; + + o = s.option(form.DummyValue, '_ifacebox'); + o.modalonly = false; + o.textvalue = function(section_id) { + var net = this.section.networks.filter(function(n) { return n.getName() == section_id })[0], + zone = net ? this.section.zones.filter(function(z) { return !!z.getNetworks().filter(function(n) { return n == section_id })[0] })[0] : null; + + if (!net) + return; + + var node = E('div', { 'class': 'ifacebox' }, [ + E('div', { + 'class': 'ifacebox-head', + 'style': 'background-color:%s'.format(zone ? zone.getColor() : '#EEEEEE'), + 'title': zone ? _('Part of zone %q').format(zone.getName()) : _('No zone assigned') + }, E('strong', net.getName().toUpperCase())), + E('div', { + 'class': 'ifacebox-body', + 'id': '%s-ifc-devices'.format(section_id), + 'data-network': section_id + }, [ + E('img', { + 'src': L.resource('icons/ethernet_disabled.png'), + 'style': 'width:16px; height:16px' + }), + E('br'), E('small', '?') + ]) + ]); + + render_ifacebox_status(node.childNodes[1], net); + + return node; + }; + + o = s.option(form.DummyValue, '_ifacestat'); + o.modalonly = false; + o.textvalue = function(section_id) { + var net = this.section.networks.filter(function(n) { return n.getName() == section_id })[0]; + + if (!net) + return; + + var node = E('div', { 'id': '%s-ifc-description'.format(section_id) }); + + render_status(node, net, false); + + return node; + }; + + o = s.taboption('advanced', form.Flag, 'delegate', _('Use builtin IPv6-management')); + o.modalonly = true; + o.default = o.enabled; + + o = s.taboption('advanced', form.Flag, 'force_link', _('Force link'), _('Set interface properties regardless of the link carrier (If set, carrier sense events do not invoke hotplug handlers).')); + o.modalonly = true; + o.render = function(option_index, section_id, in_table) { + var protoopt = this.section.children.filter(function(o) { return o.option == 'proto' })[0], + protoval = protoopt ? protoopt.cfgvalue(section_id) : null; + + this.default = (protoval == 'static') ? this.enabled : this.disabled; + return this.super('render', [ option_index, section_id, in_table ]); + }; + + + s = m.section(form.TypedSection, 'globals', _('Global network options')); + s.addremove = false; + s.anonymous = true; + + o = s.option(form.Value, 'ula_prefix', _('IPv6 ULA-Prefix')); + o.datatype = 'cidr6'; + + o = s.option(form.Flag, 'packet_steering', _('Packet Steering'), _('Enable packet steering across all CPUs. May help or hinder network speed.')); + o.optional = true; + + + if (dslModemType != null) { + s = m.section(form.TypedSection, 'dsl', _('DSL')); + s.anonymous = true; + + o = s.option(form.ListValue, 'annex', _('Annex')); + o.value('a', _('Annex A + L + M (all)')); + o.value('b', _('Annex B (all)')); + o.value('j', _('Annex J (all)')); + o.value('m', _('Annex M (all)')); + o.value('bdmt', _('Annex B G.992.1')); + o.value('b2', _('Annex B G.992.3')); + o.value('b2p', _('Annex B G.992.5')); + o.value('at1', _('ANSI T1.413')); + o.value('admt', _('Annex A G.992.1')); + o.value('alite', _('Annex A G.992.2')); + o.value('a2', _('Annex A G.992.3')); + o.value('a2p', _('Annex A G.992.5')); + o.value('l', _('Annex L G.992.3 POTS 1')); + o.value('m2', _('Annex M G.992.3')); + o.value('m2p', _('Annex M G.992.5')); + + o = s.option(form.ListValue, 'tone', _('Tone')); + o.value('', _('auto')); + o.value('a', _('A43C + J43 + A43')); + o.value('av', _('A43C + J43 + A43 + V43')); + o.value('b', _('B43 + B43C')); + o.value('bv', _('B43 + B43C + V43')); + + if (dslModemType == 'vdsl') { + o = s.option(form.ListValue, 'xfer_mode', _('Encapsulation mode')); + o.value('', _('auto')); + o.value('atm', _('ATM (Asynchronous Transfer Mode)')); + o.value('ptm', _('PTM/EFM (Packet Transfer Mode)')); + + o = s.option(form.ListValue, 'line_mode', _('DSL line mode')); + o.value('', _('auto')); + o.value('adsl', _('ADSL')); + o.value('vdsl', _('VDSL')); + + o = s.option(form.ListValue, 'ds_snr_offset', _('Downstream SNR offset')); + o.default = '0'; + + for (var i = -100; i <= 100; i += 5) + o.value(i, _('%.1f dB').format(i / 10)); + } + + s.option(form.Value, 'firmware', _('Firmware File')); + } + + + // Show ATM bridge section if we have the capabilities + if (L.hasSystemFeature('br2684ctl')) { + s = m.section(form.TypedSection, 'atm-bridge', _('ATM Bridges'), _('ATM bridges expose encapsulated ethernet in AAL5 connections as virtual Linux network interfaces which can be used in conjunction with DHCP or PPP to dial into the provider network.')); + + s.addremove = true; + s.anonymous = true; + s.addbtntitle = _('Add ATM Bridge'); + + s.handleAdd = function(ev) { + var sections = uci.sections('network', 'atm-bridge'), + max_unit = -1; + + for (var i = 0; i < sections.length; i++) { + var unit = +sections[i].unit; + + if (!isNaN(unit) && unit > max_unit) + max_unit = unit; + } + + return this.map.save(function() { + var sid = uci.add('network', 'atm-bridge'); + + uci.set('network', sid, 'unit', max_unit + 1); + uci.set('network', sid, 'atmdev', 0); + uci.set('network', sid, 'encaps', 'llc'); + uci.set('network', sid, 'payload', 'bridged'); + uci.set('network', sid, 'vci', 35); + uci.set('network', sid, 'vpi', 8); + }); + }; + + s.tab('general', _('General Setup')); + s.tab('advanced', _('Advanced Settings')); + + o = s.taboption('general', form.Value, 'vci', _('ATM Virtual Channel Identifier (VCI)')); + s.taboption('general', form.Value, 'vpi', _('ATM Virtual Path Identifier (VPI)')); + + o = s.taboption('general', form.ListValue, 'encaps', _('Encapsulation mode')); + o.value('llc', _('LLC')); + o.value('vc', _('VC-Mux')); + + s.taboption('advanced', form.Value, 'atmdev', _('ATM device number')); + s.taboption('advanced', form.Value, 'unit', _('Bridge unit number')); + + o = s.taboption('advanced', form.ListValue, 'payload', _('Forwarding mode')); + o.value('bridged', _('bridged')); + o.value('routed', _('routed')); + } + + + return m.render().then(L.bind(function(m, nodes) { + L.Poll.add(L.bind(function() { + var section_ids = m.children[0].cfgsections(), + tasks = []; + + for (var i = 0; i < section_ids.length; i++) { + var row = nodes.querySelector('.cbi-section-table-row[data-sid="%s"]'.format(section_ids[i])), + dsc = row.querySelector('[data-name="_ifacestat"] > div'), + btn1 = row.querySelector('.cbi-section-actions .reconnect'), + btn2 = row.querySelector('.cbi-section-actions .down'); + + if (dsc.getAttribute('reconnect') == '') { + dsc.setAttribute('reconnect', '1'); + tasks.push(fs.exec('/sbin/ifup', [section_ids[i]]).catch(function(e) { + ui.addNotification(null, E('p', e.message)); + })); + } + else if (dsc.getAttribute('disconnect') == '') { + dsc.setAttribute('disconnect', '1'); + tasks.push(fs.exec('/sbin/ifdown', [section_ids[i]]).catch(function(e) { + ui.addNotification(null, E('p', e.message)); + })); + } + else if (dsc.getAttribute('reconnect') == '1') { + dsc.removeAttribute('reconnect'); + btn1.classList.remove('spinning'); + btn1.disabled = false; + } + else if (dsc.getAttribute('disconnect') == '1') { + dsc.removeAttribute('disconnect'); + btn2.classList.remove('spinning'); + btn2.disabled = false; + } + } + + return Promise.all(tasks) + .then(L.bind(network.getNetworks, network)) + .then(L.bind(this.poll_status, this, nodes)); + }, this), 5); + + return nodes; + }, this, m)); + } +}); diff --git a/luci-mod-network/htdocs/luci-static/resources/view/network/routes.js b/luci-mod-network/htdocs/luci-static/resources/view/network/routes.js new file mode 100644 index 000000000..4a002c252 --- /dev/null +++ b/luci-mod-network/htdocs/luci-static/resources/view/network/routes.js @@ -0,0 +1,102 @@ +'use strict'; +'require form'; +'require network'; +'require tools.widgets as widgets'; + +return L.view.extend({ + load: function() { + return network.getDevices(); + }, + + render: function(netdevs) { + var m, s, o; + + m = new form.Map('network', _('Routes'), _('Routes specify over which interface and gateway a certain host or network can be reached.')); + m.tabbed = true; + + for (var i = 4; i <= 6; i += 2) { + s = m.section(form.GridSection, (i == 4) ? 'route' : 'route6', (i == 4) ? _('Static IPv4 Routes') : _('Static IPv6 Routes')); + s.anonymous = true; + s.addremove = true; + s.sortable = true; + + s.tab('general', _('General Settings')); + s.tab('advanced', _('Advanced Settings')); + + o = s.taboption('general', widgets.NetworkSelect, 'interface', _('Interface')); + o.rmempty = false; + o.nocreate = true; + + o = s.taboption('general', form.Value, 'target', _('Target'), (i == 4) ? _('Host-IP or Network') : _('IPv6-Address or Network (CIDR)')); + o.datatype = (i == 4) ? 'ip4addr' : 'ip6addr'; + o.rmempty = false; + + if (i == 4) { + o = s.taboption('general', form.Value, 'netmask', _('IPv4-Netmask'), _('if target is a network')); + o.placeholder = '255.255.255.255'; + o.datatype = 'ip4addr'; + o.rmempty = true; + } + + o = s.taboption('general', form.Value, 'gateway', (i == 4) ? _('IPv4-Gateway') : _('IPv6-Gateway')); + o.datatype = (i == 4) ? 'ip4addr' : 'ip6addr'; + o.rmempty = true; + + o = s.taboption('advanced', form.Value, 'metric', _('Metric')); + o.placeholder = 0; + o.datatype = (i == 4) ? 'range(0,255)' : 'range(0,65535)'; + o.rmempty = true; + o.textvalue = function(section_id) { + return this.cfgvalue(section_id) || 0; + }; + + o = s.taboption('advanced', form.Value, 'mtu', _('MTU')); + o.placeholder = 1500; + o.datatype = 'range(64,9000)'; + o.rmempty = true; + o.modalonly = true; + + o = s.taboption('advanced', form.ListValue, 'type', _('Route type')); + o.value('', 'unicast'); + o.value('local'); + o.value('broadcast'); + o.value('multicast'); + o.value('unreachable'); + o.value('prohibit'); + o.value('blackhole'); + o.value('anycast'); + o.default = ''; + o.rmempty = true; + o.modalonly = true; + + o = s.taboption('advanced', form.Value, 'table', _('Route table')); + o.value('local', 'local (255)'); + o.value('main', 'main (254)'); + o.value('default', 'default (253)'); + o.rmempty = true; + o.modalonly = true; + o.cfgvalue = function(section_id) { + var cfgvalue = this.super('cfgvalue', [section_id]); + return cfgvalue || 'main'; + }; + + o = s.taboption('advanced', form.Value, 'source', _('Source Address')); + o.placeholder = E('em', _('automatic')); + for (var j = 0; j < netdevs.length; j++) { + var addrs = netdevs[j].getIPAddrs(); + for (var k = 0; k < addrs.length; k++) + o.value(addrs[k].split('/')[0]); + } + o.datatype = (i == 4) ? 'ip4addr' : 'ip6addr'; + o.default = ''; + o.rmempty = true; + o.modalonly = true; + + o = s.taboption('advanced', form.Flag, 'onlink', _('On-Link route')); + o.default = o.disabled; + o.rmempty = true; + } + + return m.render(); + } +}); diff --git a/luci-mod-network/htdocs/luci-static/resources/view/network/switch.js b/luci-mod-network/htdocs/luci-static/resources/view/network/switch.js new file mode 100644 index 000000000..63427f147 --- /dev/null +++ b/luci-mod-network/htdocs/luci-static/resources/view/network/switch.js @@ -0,0 +1,372 @@ +'use strict'; +'require ui'; +'require rpc'; +'require uci'; +'require form'; +'require network'; + +function parse_portvalue(section_id) { + var ports = L.toArray(uci.get('network', section_id, 'ports')); + + for (var i = 0; i < ports.length; i++) { + var m = ports[i].match(/^(\d+)([tu]?)/); + + if (m && m[1] == this.option) + return m[2] || 'u'; + } + + return ''; +} + +function validate_portvalue(section_id, value) { + if (value != 'u') + return true; + + var sections = this.section.cfgsections(); + + for (var i = 0; i < sections.length; i++) { + if (sections[i] == section_id) + continue; + + if (this.formvalue(sections[i]) == 'u') + return _('%s is untagged in multiple VLANs!').format(this.title); + } + + return true; +} + +function update_interfaces(old_ifname, new_ifname) { + var interfaces = uci.sections('network', 'interface'); + + for (var i = 0; i < interfaces.length; i++) { + var old_ifnames = L.toArray(interfaces[i].ifname), + new_ifnames = [], + changed = false; + + for (var j = 0; j < old_ifnames.length; j++) { + if (old_ifnames[j] == old_ifname) { + new_ifnames.push(new_ifname); + changed = true; + } + else { + new_ifnames.push(old_ifnames[j]); + } + } + + if (changed) { + uci.set('network', interfaces[i]['.name'], 'ifname', new_ifnames.join(' ')); + + ui.addNotification(null, E('p', _('Interface %q device auto-migrated from %q to %q.') + .replace(/%q/g, '"%s"').format(interfaces[i]['.name'], old_ifname, new_ifname))); + } + } +} + +function render_port_status(node, portstate) { + if (!node) + return null; + + if (!portstate || !portstate.link) + L.dom.content(node, [ + E('img', { src: L.resource('icons/port_down.png') }), + E('br'), + _('no link') + ]); + else + L.dom.content(node, [ + E('img', { src: L.resource('icons/port_up.png') }), + E('br'), + '%d'.format(portstate.speed) + _('baseT'), + E('br'), + portstate.duplex ? _('full-duplex') : _('half-duplex') + ]); + + return node; +} + +function update_port_status(topologies) { + var tasks = []; + + for (var switch_name in topologies) + tasks.push(callSwconfigPortState(switch_name).then(L.bind(function(switch_name, ports) { + for (var i = 0; i < ports.length; i++) { + var node = document.querySelector('[data-switch="%s"][data-port="%d"]'.format(switch_name, ports[i].port)); + render_port_status(node, ports[i]); + } + }, topologies[switch_name], switch_name))); + + return Promise.all(tasks); +} + +var callSwconfigFeatures = rpc.declare({ + object: 'luci', + method: 'getSwconfigFeatures', + params: [ 'switch' ], + expect: { '': {} } +}); + +var callSwconfigPortState = rpc.declare({ + object: 'luci', + method: 'getSwconfigPortState', + params: [ 'switch' ], + expect: { result: [] } +}); + +return L.view.extend({ + load: function() { + return network.getSwitchTopologies().then(function(topologies) { + var tasks = []; + + for (var switch_name in topologies) { + tasks.push(callSwconfigFeatures(switch_name).then(L.bind(function(features) { + this.features = features; + }, topologies[switch_name]))); + tasks.push(callSwconfigPortState(switch_name).then(L.bind(function(ports) { + this.portstate = ports; + }, topologies[switch_name]))); + } + + return Promise.all(tasks).then(function() { return topologies }); + }); + }, + + render: function(topologies) { + var m, s, o; + + m = new form.Map('network', _('Switch'), _('The network ports on this device can be combined to several VLANs in which computers can communicate directly with each other. VLANs are often used to separate different network segments. Often there is by default one Uplink port for a connection to the next greater network like the internet and other ports for a local network.')); + + var switchSections = uci.sections('network', 'switch'); + + for (var i = 0; i < switchSections.length; i++) { + var switchSection = switchSections[i], + sid = switchSection['.name'], + switch_name = switchSection.name || sid, + topology = topologies[switch_name]; + + if (!topology) { + ui.addNotification(null, _('Switch %q has an unknown topology - the VLAN settings might not be accurate.').replace(/%q/, switch_name)); + + topologies[switch_name] = topology = { + features: {}, + netdevs: { + 5: 'eth0' + }, + ports: [ + { num: 0, label: 'Port 1' }, + { num: 1, label: 'Port 2' }, + { num: 2, label: 'Port 3' }, + { num: 3, label: 'Port 4' }, + { num: 4, label: 'Port 5' }, + { num: 5, label: 'CPU (eth0)', device: 'eth0', need_tag: false } + ] + }; + } + + var feat = topology.features, + min_vid = feat.min_vid || 0, + max_vid = feat.max_vid || 16, + num_vlans = feat.num_vlans || 16, + switch_title = _('Switch %q').replace(/%q/, '"%s"'.format(switch_name)), + vlan_title = _('VLANs on %q').replace(/%q/, '"%s"'.format(switch_name)); + + if (feat.switch_title) { + switch_title += ' (%s)'.format(feat.switch_title); + vlan_title += ' (%s)'.format(feat.switch_title); + } + + s = m.section(form.NamedSection, sid, 'switch', switch_title); + s.addremove = false; + + if (feat.vlan_option) + s.option(form.Flag, feat.vlan_option, _('Enable VLAN functionality')); + + if (feat.learning_option) { + o = s.option(form.Flag, feat.learning_option, _('Enable learning and aging')); + o.default = o.enabled; + } + + if (feat.jumbo_option) { + o = s.option(form.Flag, feat.jumbo_option, _('Enable Jumbo Frame passthrough')); + o.enabled = '3'; + o.rmempty = true; + } + + if (feat.mirror_option) { + s.option(form.Flag, 'enable_mirror_rx', _('Enable mirroring of incoming packets')); + s.option(form.Flag, 'enable_mirror_tx', _('Enable mirroring of outgoing packets')); + + var sp = s.option(form.ListValue, 'mirror_source_port', _('Mirror source port')), + mp = s.option(form.ListValue, 'mirror_monitor_port', _('Mirror monitor port')); + + sp.depends('enable_mirror_rx', '1'); + sp.depends('enable_mirror_tx', '1'); + + mp.depends('enable_mirror_rx', '1'); + mp.depends('enable_mirror_tx', '1'); + + for (var j = 0; j < topology.ports.length; j++) { + sp.value(topology.ports[j].num, topology.ports[j].label); + mp.value(topology.ports[j].num, topology.ports[j].label); + } + } + + s = m.section(form.TableSection, 'switch_vlan', vlan_title); + s.anonymous = true; + s.addremove = true; + s.addbtntitle = _('Add VLAN'); + s.topology = topology; + s.device = switch_name; + + s.filter = function(section_id) { + var device = uci.get('network', section_id, 'device'); + return (device == switch_name); + }; + + s.cfgsections = function() { + var sections = form.TableSection.prototype.cfgsections.apply(this); + + return sections.sort(function(a, b) { + var vidA = feat.vid_option ? uci.get('network', a, feat.vid_option) : null, + vidB = feat.vid_option ? uci.get('network', b, feat.vid_option) : null; + + vidA = +(vidA != null ? vidA : uci.get('network', a, 'vlan') || 9999); + vidB = +(vidB != null ? vidB : uci.get('network', b, 'vlan') || 9999); + + return (vidA - vidB); + }); + }; + + s.handleAdd = function(ev) { + var sections = uci.sections('network', 'switch_vlan'), + section_id = uci.add('network', 'switch_vlan'), + max_vlan = 0, + max_vid = 0; + + for (var j = 0; j < sections.length; j++) { + if (sections[j].device != s.device) + continue; + + var vlan = +sections[j].vlan, + vid = feat.vid_option ? +sections[j][feat.vid_option] : null; + + if (vlan > max_vlan) + max_vlan = vlan; + + if (vid > max_vid) + max_vid = vid; + } + + uci.set('network', section_id, 'device', s.device); + uci.set('network', section_id, 'vlan', max_vlan + 1); + + if (feat.vid_option) + uci.set('network', section_id, feat.vid_option, max_vid + 1); + + return this.map.save(null, true); + }; + + var port_opts = []; + + o = s.option(form.Value, feat.vid_option || 'vlan', 'VLAN ID'); + o.rmempty = false; + o.forcewrite = true; + o.vlan_used = {}; + o.datatype = 'range(%u,%u)'.format(min_vid, feat.vid_option ? 4094 : num_vlans - 1); + o.description = _('Port status:'); + + o.validate = function(section_id, value) { + var v = +value, + m = feat.vid_option ? 4094 : num_vlans - 1; + + if (isNaN(v) || v < min_vid || v > m) + return _('Invalid VLAN ID given! Only IDs between %d and %d are allowed.').format(min_vid, m); + + var sections = this.section.cfgsections(); + + for (var i = 0; i < sections.length; i++) { + if (sections[i] == section_id) + continue; + + if (this.formvalue(sections[i]) == v) + return _('Invalid VLAN ID given! Only unique IDs are allowed'); + } + + return true; + }; + + o.write = function(section_id, value) { + var topology = this.section.topology, + values = []; + + for (var i = 0; i < port_opts.length; i++) { + var tagging = port_opts[i].formvalue(section_id), + portspec = Array.isArray(topology.ports) ? topology.ports[i] : null; + + if (tagging == 't') + values.push(port_opts[i].option + tagging); + else if (tagging == 'u') + values.push(port_opts[i].option); + + if (portspec && portspec.device) { + var old_tag = port_opts[i].cfgvalue(section_id), + old_vid = this.cfgvalue(section_id); + + if (old_tag != tagging || old_vid != value) { + var old_ifname = portspec.device + (old_tag != 'u' ? '.' + old_vid : ''), + new_ifname = portspec.device + (tagging != 'u' ? '.' + value : ''); + + if (old_ifname != new_ifname) + update_interfaces(old_ifname, new_ifname); + } + } + } + + if (feat.vlan4k_option) + uci.set('network', sid, feat.vlan4k_option, '1'); + + uci.set('network', section_id, 'ports', values.join(' ')); + + return form.Value.prototype.write.apply(this, [section_id, value]); + }; + + o.cfgvalue = function(section_id) { + var value = feat.vid_option ? uci.get('network', section_id, feat.vid_option) : null; + return (value || uci.get('network', section_id, 'vlan')); + }; + + s.option(form.Value, 'description', _('Description')); + + for (var j = 0; Array.isArray(topology.ports) && j < topology.ports.length; j++) { + var portspec = topology.ports[j], + portstate = Array.isArray(topology.portstate) ? topology.portstate[portspec.num] : null; + + o = s.option(form.ListValue, String(portspec.num), portspec.label); + o.value('', _('off')); + + if (!portspec.need_tag) + o.value('u', _('untagged')); + + o.value('t', _('tagged')); + + o.cfgvalue = parse_portvalue; + o.validate = validate_portvalue; + o.write = function() {}; + + o.description = render_port_status(E('small', { + 'data-switch': switch_name, + 'data-port': portspec.num + }), portstate); + + port_opts.push(o); + } + + port_opts.sort(function(a, b) { + return a.option > b.option; + }); + } + + L.Poll.add(L.bind(update_port_status, m, topologies)); + + return m.render(); + } +}); diff --git a/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js b/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js new file mode 100644 index 000000000..268287134 --- /dev/null +++ b/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js @@ -0,0 +1,2180 @@ +'use strict'; +'require fs'; +'require ui'; +'require rpc'; +'require uci'; +'require form'; +'require network'; +'require firewall'; +'require tools.widgets as widgets'; + +function count_changes(section_id) { + var changes = ui.changes.changes, n = 0; + + if (!L.isObject(changes)) + return n; + + if (Array.isArray(changes.wireless)) + for (var i = 0; i < changes.wireless.length; i++) + n += (changes.wireless[i][1] == section_id); + + return n; +} + +function render_radio_badge(radioDev) { + return E('span', { 'class': 'ifacebadge' }, [ + E('img', { 'src': L.resource('icons/wifi%s.png').format(radioDev.isUp() ? '' : '_disabled') }), + ' ', + radioDev.getName() + ]); +} + +function render_signal_badge(signalPercent, signalValue, noiseValue, wrap) { + var icon, title, value; + + if (signalPercent < 0) + icon = L.resource('icons/signal-none.png'); + else if (signalPercent == 0) + icon = L.resource('icons/signal-0.png'); + else if (signalPercent < 25) + icon = L.resource('icons/signal-0-25.png'); + else if (signalPercent < 50) + icon = L.resource('icons/signal-25-50.png'); + else if (signalPercent < 75) + icon = L.resource('icons/signal-50-75.png'); + else + icon = L.resource('icons/signal-75-100.png'); + + if (signalValue != null && signalValue != 0 && noiseValue != null && noiseValue != 0) { + value = '%d/%d\xa0%s'.format(signalValue, noiseValue, _('dBm')); + title = '%s: %d %s / %s: %d %s / %s %d'.format( + _('Signal'), signalValue, _('dBm'), + _('Noise'), noiseValue, _('dBm'), + _('SNR'), signalValue - noiseValue); + } + else if (signalValue != null && signalValue != 0) { + value = '%d %s'.format(signalValue, _('dBm')); + title = '%s: %d %s'.format(_('Signal'), signalValue, _('dBm')); + } + else if (signalPercent > -1) { + value = '\xa0---\xa0'; + title = _('No signal'); + } + else { + value = E('em', {}, E('small', {}, [ _('disabled') ])); + title = _('Interface is disabled'); + } + + return E('div', { + 'class': wrap ? 'center' : 'ifacebadge', + 'title': title, + 'data-signal': signalValue, + 'data-noise': noiseValue + }, [ + E('img', { 'src': icon }), + E('span', {}, [ + wrap ? E('br') : ' ', + value + ]) + ]); +} + +function render_network_badge(radioNet) { + return render_signal_badge(radioNet.isUp() ? radioNet.getSignalPercent() : -1, radioNet.getSignal(), radioNet.getNoise()); +} + +function render_radio_status(radioDev, wifiNets) { + var name = radioDev.getI18n().replace(/ Wireless Controller .+$/, ''), + node = E('div', [ E('big', {}, E('strong', {}, name)), E('div') ]), + channel, frequency, bitrate; + + for (var i = 0; i < wifiNets.length; i++) { + channel = channel || wifiNets[i].getChannel(); + frequency = frequency || wifiNets[i].getFrequency(); + bitrate = bitrate || wifiNets[i].getBitRate(); + } + + if (radioDev.isUp()) + L.itemlist(node.lastElementChild, [ + _('Channel'), '%s (%s %s)'.format(channel || '?', frequency || '?', _('GHz')), + _('Bitrate'), '%s %s'.format(bitrate || '?', _('Mbit/s')) + ], ' | '); + else + node.lastElementChild.appendChild(E('em', _('Device is not active'))); + + return node; +} + +function render_network_status(radioNet) { + var mode = radioNet.getActiveMode(), + bssid = radioNet.getActiveBSSID(), + channel = radioNet.getChannel(), + disabled = (radioNet.get('disabled') == '1' || uci.get('wireless', radioNet.getWifiDeviceName(), 'disabled') == '1'), + is_assoc = (bssid && bssid != '00:00:00:00:00:00' && channel && mode != 'Unknown' && !disabled), + is_mesh = (radioNet.getMode() == 'mesh'), + changecount = count_changes(radioNet.getName()), + status_text = null; + + if (changecount) + status_text = E('a', { + href: '#', + click: L.bind(ui.changes.displayChanges, ui.changes) + }, _('Interface has %d pending changes').format(changecount)); + else if (!is_assoc) + status_text = E('em', disabled ? _('Wireless is disabled') : _('Wireless is not associated')); + + return L.itemlist(E('div'), [ + is_mesh ? _('Mesh ID') : _('SSID'), (is_mesh ? radioNet.getMeshID() : radioNet.getSSID()) || '?', + _('Mode'), mode, + _('BSSID'), (!changecount && is_assoc) ? bssid : null, + _('Encryption'), (!changecount && is_assoc) ? radioNet.getActiveEncryption() || _('None') : null, + null, status_text + ], [ ' | ', E('br') ]); +} + +function render_modal_status(node, radioNet) { + var mode = radioNet.getActiveMode(), + noise = radioNet.getNoise(), + bssid = radioNet.getActiveBSSID(), + channel = radioNet.getChannel(), + disabled = (radioNet.get('disabled') == '1'), + is_assoc = (bssid && bssid != '00:00:00:00:00:00' && channel && mode != 'Unknown' && !disabled); + + if (node == null) + node = E('span', { 'class': 'ifacebadge large', 'data-network': radioNet.getName() }, [ E('small'), E('span') ]); + + L.dom.content(node.firstElementChild, render_signal_badge(disabled ? -1 : radioNet.getSignalPercent(), radioNet.getSignal(), noise, true)); + + L.itemlist(node.lastElementChild, [ + _('Mode'), mode, + _('SSID'), radioNet.getSSID() || '?', + _('BSSID'), is_assoc ? bssid : null, + _('Encryption'), is_assoc ? radioNet.getActiveEncryption() || _('None') : null, + _('Channel'), is_assoc ? '%d (%.3f %s)'.format(radioNet.getChannel(), radioNet.getFrequency() || 0, _('GHz')) : null, + _('Tx-Power'), is_assoc ? '%d %s'.format(radioNet.getTXPower(), _('dBm')) : null, + _('Signal'), is_assoc ? '%d %s'.format(radioNet.getSignal(), _('dBm')) : null, + _('Noise'), (is_assoc && noise != null) ? '%d %s'.format(noise, _('dBm')) : null, + _('Bitrate'), is_assoc ? '%.1f %s'.format(radioNet.getBitRate() || 0, _('Mbit/s')) : null, + _('Country'), is_assoc ? radioNet.getCountryCode() : null + ], [ ' | ', E('br'), E('br'), E('br'), E('br'), E('br'), ' | ', E('br'), ' | ' ]); + + if (!is_assoc) + L.dom.append(node.lastElementChild, E('em', disabled ? _('Wireless is disabled') : _('Wireless is not associated'))); + + return node; +} + +function format_wifirate(rate) { + var s = '%.1f\xa0%s, %d\xa0%s'.format(rate.rate / 1000, _('Mbit/s'), rate.mhz, _('MHz')), + ht = rate.ht, vht = rate.vht, + mhz = rate.mhz, nss = rate.nss, + mcs = rate.mcs, sgi = rate.short_gi; + + if (ht || vht) { + if (vht) s += ', VHT-MCS\xa0%d'.format(mcs); + if (nss) s += ', VHT-NSS\xa0%d'.format(nss); + if (ht) s += ', MCS\xa0%s'.format(mcs); + if (sgi) s += ', ' + _('Short GI').replace(/ /g, '\xa0'); + } + + return s; +} + +function radio_restart(id, ev) { + var row = document.querySelector('.cbi-section-table-row[data-sid="%s"]'.format(id)), + dsc = row.querySelector('[data-name="_stat"] > div'), + btn = row.querySelector('.cbi-section-actions button'); + + btn.blur(); + btn.classList.add('spinning'); + btn.disabled = true; + + dsc.setAttribute('restart', ''); + L.dom.content(dsc, E('em', _('Device is restarting…'))); +} + +function network_updown(id, map, ev) { + var radio = uci.get('wireless', id, 'device'), + disabled = (uci.get('wireless', id, 'disabled') == '1') || + (uci.get('wireless', radio, 'disabled') == '1'); + + if (disabled) { + uci.unset('wireless', id, 'disabled'); + uci.unset('wireless', radio, 'disabled'); + } + else { + uci.set('wireless', id, 'disabled', '1'); + + var all_networks_disabled = true, + wifi_ifaces = uci.sections('wireless', 'wifi-iface'); + + for (var i = 0; i < wifi_ifaces.length; i++) { + if (wifi_ifaces[i].device == radio && wifi_ifaces[i].disabled != '1') { + all_networks_disabled = false; + break; + } + } + + if (all_networks_disabled) + uci.set('wireless', radio, 'disabled', '1'); + } + + return map.save().then(function() { + ui.changes.apply() + }); +} + +function next_free_sid(offset) { + var sid = 'wifinet' + offset; + + while (uci.get('wireless', sid)) + sid = 'wifinet' + (++offset); + + return sid; +} + +var CBIWifiFrequencyValue = form.Value.extend({ + callFrequencyList: rpc.declare({ + object: 'iwinfo', + method: 'freqlist', + params: [ 'device' ], + expect: { results: [] } + }), + + load: function(section_id) { + return Promise.all([ + network.getWifiDevice(section_id), + this.callFrequencyList(section_id) + ]).then(L.bind(function(data) { + this.channels = { + '11g': L.hasSystemFeature('hostapd', 'acs') ? [ 'auto', 'auto', true ] : [], + '11a': L.hasSystemFeature('hostapd', 'acs') ? [ 'auto', 'auto', true ] : [] + }; + + for (var i = 0; i < data[1].length; i++) + this.channels[(data[1][i].mhz > 2484) ? '11a' : '11g'].push( + data[1][i].channel, + '%d (%d Mhz)'.format(data[1][i].channel, data[1][i].mhz), + !data[1][i].restricted + ); + + var hwmodelist = L.toArray(data[0] ? data[0].getHWModes() : null) + .reduce(function(o, v) { o[v] = true; return o }, {}); + + this.modes = [ + '', 'Legacy', true, + 'n', 'N', hwmodelist.n, + 'ac', 'AC', hwmodelist.ac + ]; + + var htmodelist = L.toArray(data[0] ? data[0].getHTModes() : null) + .reduce(function(o, v) { o[v] = true; return o }, {}); + + this.htmodes = { + '': [ '', '-', true ], + 'n': [ + 'HT20', '20 MHz', htmodelist.HT20, + 'HT40', '40 MHz', htmodelist.HT40 + ], + 'ac': [ + 'VHT20', '20 MHz', htmodelist.VHT20, + 'VHT40', '40 MHz', htmodelist.VHT40, + 'VHT80', '80 MHz', htmodelist.VHT80, + 'VHT160', '160 MHz', htmodelist.VHT160 + ] + }; + + this.bands = { + '': [ + '11g', '2.4 GHz', this.channels['11g'].length > 3, + '11a', '5 GHz', this.channels['11a'].length > 3 + ], + 'n': [ + '11g', '2.4 GHz', this.channels['11g'].length > 3, + '11a', '5 GHz', this.channels['11a'].length > 3 + ], + 'ac': [ + '11a', '5 GHz', true + ] + }; + }, this)); + }, + + setValues: function(sel, vals) { + if (sel.vals) + sel.vals.selected = sel.selectedIndex; + + while (sel.options[0]) + sel.remove(0); + + for (var i = 0; vals && i < vals.length; i += 3) + if (vals[i+2]) + sel.add(E('option', { value: vals[i+0] }, [ vals[i+1] ])); + + if (vals && !isNaN(vals.selected)) + sel.selectedIndex = vals.selected; + + sel.parentNode.style.display = (sel.options.length <= 1) ? 'none' : ''; + sel.vals = vals; + }, + + toggleWifiMode: function(elem) { + this.toggleWifiHTMode(elem); + this.toggleWifiBand(elem); + }, + + toggleWifiHTMode: function(elem) { + var mode = elem.querySelector('.mode'); + var bwdt = elem.querySelector('.htmode'); + + this.setValues(bwdt, this.htmodes[mode.value]); + }, + + toggleWifiBand: function(elem) { + var mode = elem.querySelector('.mode'); + var band = elem.querySelector('.band'); + + this.setValues(band, this.bands[mode.value]); + this.toggleWifiChannel(elem); + }, + + toggleWifiChannel: function(elem) { + var band = elem.querySelector('.band'); + var chan = elem.querySelector('.channel'); + + this.setValues(chan, this.channels[band.value]); + }, + + setInitialValues: function(section_id, elem) { + var mode = elem.querySelector('.mode'), + band = elem.querySelector('.band'), + chan = elem.querySelector('.channel'), + bwdt = elem.querySelector('.htmode'), + htval = uci.get('wireless', section_id, 'htmode'), + hwval = uci.get('wireless', section_id, 'hwmode'), + chval = uci.get('wireless', section_id, 'channel'); + + this.setValues(mode, this.modes); + + if (/VHT20|VHT40|VHT80|VHT160/.test(htval)) + mode.value = 'ac'; + else if (/HT20|HT40/.test(htval)) + mode.value = 'n'; + else + mode.value = ''; + + this.toggleWifiMode(elem); + + if (/a/.test(hwval)) + band.value = '11a'; + else + band.value = '11g'; + + this.toggleWifiBand(elem); + + bwdt.value = htval; + chan.value = chval; + + return elem; + }, + + renderWidget: function(section_id, option_index, cfgvalue) { + var elem = E('div'); + + L.dom.content(elem, [ + E('label', { 'style': 'float:left; margin-right:3px' }, [ + _('Mode'), E('br'), + E('select', { + 'class': 'mode', + 'style': 'width:auto', + 'change': L.bind(this.toggleWifiMode, this, elem) + }) + ]), + E('label', { 'style': 'float:left; margin-right:3px' }, [ + _('Band'), E('br'), + E('select', { + 'class': 'band', + 'style': 'width:auto', + 'change': L.bind(this.toggleWifiBand, this, elem) + }) + ]), + E('label', { 'style': 'float:left; margin-right:3px' }, [ + _('Channel'), E('br'), + E('select', { + 'class': 'channel', + 'style': 'width:auto' + }) + ]), + E('label', { 'style': 'float:left; margin-right:3px' }, [ + _('Width'), E('br'), + E('select', { + 'class': 'htmode', + 'style': 'width:auto' + }) + ]), + E('br', { 'style': 'clear:left' }) + ]); + + return this.setInitialValues(section_id, elem); + }, + + cfgvalue: function(section_id) { + return [ + uci.get('wireless', section_id, 'htmode'), + uci.get('wireless', section_id, 'hwmode'), + uci.get('wireless', section_id, 'channel') + ]; + }, + + formvalue: function(section_id) { + var node = this.map.findElement('data-field', this.cbid(section_id)); + + return [ + node.querySelector('.htmode').value, + node.querySelector('.band').value, + node.querySelector('.channel').value + ]; + }, + + write: function(section_id, value) { + uci.set('wireless', section_id, 'htmode', value[0] || null); + uci.set('wireless', section_id, 'hwmode', value[1]); + uci.set('wireless', section_id, 'channel', value[2]); + } +}); + +var CBIWifiTxPowerValue = form.ListValue.extend({ + callTxPowerList: rpc.declare({ + object: 'iwinfo', + method: 'txpowerlist', + params: [ 'device' ], + expect: { results: [] } + }), + + load: function(section_id) { + return this.callTxPowerList(section_id).then(L.bind(function(pwrlist) { + this.powerval = this.wifiNetwork ? this.wifiNetwork.getTXPower() : null; + this.poweroff = this.wifiNetwork ? this.wifiNetwork.getTXPowerOffset() : null; + + this.value('', _('driver default')); + + for (var i = 0; i < pwrlist.length; i++) + this.value(pwrlist[i].dbm, '%d dBm (%d mW)'.format(pwrlist[i].dbm, pwrlist[i].mw)); + + return form.ListValue.prototype.load.apply(this, [section_id]); + }, this)); + }, + + renderWidget: function(section_id, option_index, cfgvalue) { + var widget = form.ListValue.prototype.renderWidget.apply(this, [section_id, option_index, cfgvalue]); + widget.firstElementChild.style.width = 'auto'; + + L.dom.append(widget, E('span', [ + ' - ', _('Current power'), ': ', + E('span', [ this.powerval != null ? '%d dBm'.format(this.powerval) : E('em', _('unknown')) ]), + this.poweroff ? ' + %d dB offset = %s dBm'.format(this.poweroff, this.powerval != null ? this.powerval + this.poweroff : '?') : '' + ])); + + return widget; + } +}); + +var CBIWifiCountryValue = form.Value.extend({ + callCountryList: rpc.declare({ + object: 'iwinfo', + method: 'countrylist', + params: [ 'device' ], + expect: { results: [] } + }), + + load: function(section_id) { + return this.callCountryList(section_id).then(L.bind(function(countrylist) { + if (Array.isArray(countrylist) && countrylist.length > 0) { + this.value('', _('driver default')); + + for (var i = 0; i < countrylist.length; i++) + this.value(countrylist[i].iso3166, '%s - %s'.format(countrylist[i].iso3166, countrylist[i].country)); + } + + return form.Value.prototype.load.apply(this, [section_id]); + }, this)); + }, + + validate: function(section_id, formvalue) { + if (formvalue != null && formvalue != '' && !/^[A-Z0-9][A-Z0-9]$/.test(formvalue)) + return _('Use ISO/IEC 3166 alpha2 country codes.'); + + return true; + }, + + renderWidget: function(section_id, option_index, cfgvalue) { + var typeClass = (this.keylist && this.keylist.length) ? form.ListValue : form.Value; + return typeClass.prototype.renderWidget.apply(this, [section_id, option_index, cfgvalue]); + } +}); + +return L.view.extend({ + poll_status: function(map, data) { + var rows = map.querySelectorAll('.cbi-section-table-row[data-sid]'); + + for (var i = 0; i < rows.length; i++) { + var section_id = rows[i].getAttribute('data-sid'), + radioDev = data[1].filter(function(d) { return d.getName() == section_id })[0], + radioNet = data[2].filter(function(n) { return n.getName() == section_id })[0], + badge = rows[i].querySelector('[data-name="_badge"] > div'), + stat = rows[i].querySelector('[data-name="_stat"]'), + btns = rows[i].querySelectorAll('.cbi-section-actions button'), + busy = btns[0].classList.contains('spinning') || btns[1].classList.contains('spinning') || btns[2].classList.contains('spinning'); + + if (radioDev) { + L.dom.content(badge, render_radio_badge(radioDev)); + L.dom.content(stat, render_radio_status(radioDev, data[2].filter(function(n) { return n.getWifiDeviceName() == radioDev.getName() }))); + } + else { + L.dom.content(badge, render_network_badge(radioNet)); + L.dom.content(stat, render_network_status(radioNet)); + } + + if (stat.hasAttribute('restart')) + L.dom.content(stat, E('em', _('Device is restarting…'))); + + btns[0].disabled = busy; + btns[1].disabled = busy; + btns[2].disabled = busy; + } + + var table = document.querySelector('#wifi_assoclist_table'), + hosts = data[0], + trows = []; + + for (var i = 0; i < data[3].length; i++) { + var bss = data[3][i], + name = hosts.getHostnameByMACAddr(bss.mac), + ipv4 = hosts.getIPAddrByMACAddr(bss.mac), + ipv6 = hosts.getIP6AddrByMACAddr(bss.mac); + + var hint; + + if (name && ipv4 && ipv6) + hint = '%s (%s, %s)'.format(name, ipv4, ipv6); + else if (name && (ipv4 || ipv6)) + hint = '%s (%s)'.format(name, ipv4 || ipv6); + else + hint = name || ipv4 || ipv6 || '?'; + + var row = [ + E('span', { + 'class': 'ifacebadge', + 'data-ifname': bss.network.getIfname(), + 'data-ssid': bss.network.getSSID() + }, [ + E('img', { + 'src': L.resource('icons/wifi%s.png').format(bss.network.isUp() ? '' : '_disabled'), + 'title': bss.radio.getI18n() + }), + E('span', [ + ' %s '.format(bss.network.getShortName()), + E('small', '(%s)'.format(bss.network.getIfname())) + ]) + ]), + bss.mac, + hint, + render_signal_badge(Math.min((bss.signal + 110) / 70 * 100, 100), bss.signal, bss.noise), + E('span', {}, [ + E('span', format_wifirate(bss.rx)), + E('br'), + E('span', format_wifirate(bss.tx)) + ]) + ]; + + if (bss.network.isClientDisconnectSupported()) { + if (table.firstElementChild.childNodes.length < 6) + table.firstElementChild.appendChild(E('div', { 'class': 'th cbi-section-actions'})); + + row.push(E('button', { + 'class': 'cbi-button cbi-button-remove', + 'click': L.bind(function(net, mac, ev) { + L.dom.parent(ev.currentTarget, '.tr').style.opacity = 0.5; + ev.currentTarget.classList.add('spinning'); + ev.currentTarget.disabled = true; + ev.currentTarget.blur(); + + net.disconnectClient(mac, true, 5, 60000); + }, this, bss.network, bss.mac) + }, [ _('Disconnect') ])); + } + else { + row.push('-'); + } + + trows.push(row); + } + + cbi_update_table(table, trows, E('em', _('No information available'))); + + var stat = document.querySelector('.cbi-modal [data-name="_wifistat_modal"] .ifacebadge.large'); + + if (stat) + render_modal_status(stat, data[2].filter(function(n) { return n.getName() == stat.getAttribute('data-network') })[0]); + + return network.flushCache(); + }, + + load: function() { + return Promise.all([ + uci.changes(), + uci.load('wireless') + ]); + }, + + checkAnonymousSections: function() { + var wifiIfaces = uci.sections('wireless', 'wifi-iface'); + + for (var i = 0; i < wifiIfaces.length; i++) + if (wifiIfaces[i]['.anonymous']) + return true; + + return false; + }, + + callUciRename: rpc.declare({ + object: 'uci', + method: 'rename', + params: [ 'config', 'section', 'name' ] + }), + + render: function() { + if (this.checkAnonymousSections()) + return this.renderMigration(); + else + return this.renderOverview(); + }, + + handleMigration: function(ev) { + var wifiIfaces = uci.sections('wireless', 'wifi-iface'), + id_offset = 0, + tasks = []; + + for (var i = 0; i < wifiIfaces.length; i++) { + if (!wifiIfaces[i]['.anonymous']) + continue; + + var new_name = next_free_sid(id_offset); + + tasks.push(this.callUciRename('wireless', wifiIfaces[i]['.name'], new_name)); + id_offset = +new_name.substring(7) + 1; + } + + return Promise.all(tasks) + .then(L.bind(ui.changes.init, ui.changes)) + .then(L.bind(ui.changes.apply, ui.changes)); + }, + + renderMigration: function() { + ui.showModal(_('Wireless configuration migration'), [ + E('p', _('The existing wireless configuration needs to be changed for LuCI to function properly.')), + E('p', _('Upon pressing "Continue", anonymous "wifi-iface" sections will be assigned with a name in the form wifinet# and the network will be restarted to apply the updated configuration.')), + E('div', { 'class': 'right' }, + E('button', { + 'class': 'btn cbi-button-action important', + 'click': ui.createHandlerFn(this, 'handleMigration') + }, _('Continue'))) + ]); + }, + + renderOverview: function() { + var m, s, o; + + m = new form.Map('wireless'); + m.chain('network'); + m.chain('firewall'); + + s = m.section(form.GridSection, 'wifi-device', _('Wireless Overview')); + s.anonymous = true; + s.addremove = false; + + s.load = function() { + return network.getWifiDevices().then(L.bind(function(radios) { + this.radios = radios.sort(function(a, b) { + return a.getName() > b.getName(); + }); + + var tasks = []; + + for (var i = 0; i < radios.length; i++) + tasks.push(radios[i].getWifiNetworks()); + + return Promise.all(tasks); + }, this)).then(L.bind(function(data) { + this.wifis = []; + + for (var i = 0; i < data.length; i++) + this.wifis.push.apply(this.wifis, data[i]); + }, this)); + }; + + s.cfgsections = function() { + var rv = []; + + for (var i = 0; i < this.radios.length; i++) { + rv.push(this.radios[i].getName()); + + for (var j = 0; j < this.wifis.length; j++) + if (this.wifis[j].getWifiDeviceName() == this.radios[i].getName()) + rv.push(this.wifis[j].getName()); + } + + return rv; + }; + + s.modaltitle = function(section_id) { + var radioNet = this.wifis.filter(function(w) { return w.getName() == section_id})[0]; + return radioNet ? radioNet.getI18n() : _('Edit wireless network'); + }; + + s.lookupRadioOrNetwork = function(section_id) { + var radioDev = this.radios.filter(function(r) { return r.getName() == section_id })[0]; + if (radioDev) + return radioDev; + + var radioNet = this.wifis.filter(function(w) { return w.getName() == section_id })[0]; + if (radioNet) + return radioNet; + + return null; + }; + + s.renderRowActions = function(section_id) { + var inst = this.lookupRadioOrNetwork(section_id), btns; + + if (inst.getWifiNetworks) { + btns = [ + E('button', { + 'class': 'cbi-button cbi-button-neutral', + 'title': _('Restart radio interface'), + 'click': ui.createHandlerFn(this, radio_restart, section_id) + }, _('Restart')), + E('button', { + 'class': 'cbi-button cbi-button-action important', + 'title': _('Find and join network'), + 'click': ui.createHandlerFn(this, 'handleScan', inst) + }, _('Scan')), + E('button', { + 'class': 'cbi-button cbi-button-add', + 'title': _('Provide new network'), + 'click': ui.createHandlerFn(this, 'handleAdd', inst) + }, _('Add')) + ]; + } + else { + var isDisabled = (inst.get('disabled') == '1' || + uci.get('wireless', inst.getWifiDeviceName(), 'disabled') == '1'); + + btns = [ + E('button', { + 'class': 'cbi-button cbi-button-neutral enable-disable', + 'title': isDisabled ? _('Enable this network') : _('Disable this network'), + 'click': ui.createHandlerFn(this, network_updown, section_id, this.map) + }, isDisabled ? _('Enable') : _('Disable')), + E('button', { + 'class': 'cbi-button cbi-button-action important', + 'title': _('Edit this network'), + 'click': ui.createHandlerFn(this, 'renderMoreOptionsModal', section_id) + }, _('Edit')), + E('button', { + 'class': 'cbi-button cbi-button-negative remove', + 'title': _('Delete this network'), + 'click': ui.createHandlerFn(this, 'handleRemove', section_id) + }, _('Remove')) + ]; + } + + return E('div', { 'class': 'td middle cbi-section-actions' }, E('div', btns)); + }; + + s.addModalOptions = function(s) { + return network.getWifiNetwork(s.section).then(function(radioNet) { + var hwtype = uci.get('wireless', radioNet.getWifiDeviceName(), 'type'); + var o, ss; + + o = s.option(form.SectionValue, '_device', form.NamedSection, radioNet.getWifiDeviceName(), 'wifi-device', _('Device Configuration')); + o.modalonly = true; + + ss = o.subsection; + ss.tab('general', _('General Setup')); + ss.tab('advanced', _('Advanced Settings')); + + var isDisabled = (radioNet.get('disabled') == '1' || + uci.get('wireless', radioNet.getWifiDeviceName(), 'disabled') == 1); + + o = ss.taboption('general', form.DummyValue, '_wifistat_modal', _('Status')); + o.cfgvalue = L.bind(function(radioNet) { + return render_modal_status(null, radioNet); + }, this, radioNet); + o.write = function() {}; + + o = ss.taboption('general', form.Button, '_toggle', isDisabled ? _('Wireless network is disabled') : _('Wireless network is enabled')); + o.inputstyle = isDisabled ? 'apply' : 'reset'; + o.inputtitle = isDisabled ? _('Enable') : _('Disable'); + o.onclick = ui.createHandlerFn(s, network_updown, s.section, s.map); + + o = ss.taboption('general', CBIWifiFrequencyValue, '_freq', '
' + _('Operating frequency')); + o.ucisection = s.section; + + if (hwtype == 'mac80211') { + o = ss.taboption('general', CBIWifiTxPowerValue, 'txpower', _('Maximum transmit power'), _('Specifies the maximum transmit power the wireless radio may use. Depending on regulatory requirements and wireless usage, the actual transmit power may be reduced by the driver.')); + o.wifiNetwork = radioNet; + + o = ss.taboption('advanced', CBIWifiCountryValue, 'country', _('Country Code')); + o.wifiNetwork = radioNet; + + o = ss.taboption('advanced', form.Flag, 'legacy_rates', _('Allow legacy 802.11b rates')); + o.default = o.enabled; + + o = ss.taboption('advanced', form.Value, 'distance', _('Distance Optimization'), _('Distance to farthest network member in meters.')); + o.datatype = 'range(0,114750)'; + o.placeholder = 'auto'; + + o = ss.taboption('advanced', form.Value, 'frag', _('Fragmentation Threshold')); + o.datatype = 'min(256)'; + o.placeholder = _('off'); + + o = ss.taboption('advanced', form.Value, 'rts', _('RTS/CTS Threshold')); + o.datatype = 'uinteger'; + o.placeholder = _('off'); + + o = ss.taboption('advanced', form.Flag, 'noscan', _('Force 40MHz mode'), _('Always use 40MHz channels even if the secondary channel overlaps. Using this option does not comply with IEEE 802.11n-2009!')); + o.rmempty = true; + + o = ss.taboption('advanced', form.Value, 'beacon_int', _('Beacon Interval')); + o.datatype = 'range(15,65535)'; + o.placeholder = 100; + o.rmempty = true; + } + + + o = s.option(form.SectionValue, '_device', form.NamedSection, radioNet.getName(), 'wifi-iface', _('Interface Configuration')); + o.modalonly = true; + + ss = o.subsection; + ss.tab('general', _('General Setup')); + ss.tab('encryption', _('Wireless Security')); + ss.tab('macfilter', _('MAC-Filter')); + ss.tab('advanced', _('Advanced Settings')); + + o = ss.taboption('general', form.ListValue, 'mode', _('Mode')); + o.value('ap', _('Access Point')); + o.value('sta', _('Client')); + o.value('adhoc', _('Ad-Hoc')); + + o = ss.taboption('general', form.Value, 'mesh_id', _('Mesh Id')); + o.depends('mode', 'mesh'); + + o = ss.taboption('advanced', form.Flag, 'mesh_fwding', _('Forward mesh peer traffic')); + o.rmempty = false; + o.default = '1'; + o.depends('mode', 'mesh'); + + o = ss.taboption('advanced', form.Value, 'mesh_rssi_threshold', _('RSSI threshold for joining'), _('0 = not using RSSI threshold, 1 = do not change driver default')); + o.rmempty = false; + o.default = '0'; + o.datatype = 'range(-255,1)'; + o.depends('mode', 'mesh'); + + o = ss.taboption('general', form.Value, 'ssid', _('ESSID')); + o.datatype = 'maxlength(32)'; + o.depends('mode', 'ap'); + o.depends('mode', 'sta'); + o.depends('mode', 'adhoc'); + o.depends('mode', 'ahdemo'); + o.depends('mode', 'monitor'); + o.depends('mode', 'ap-wds'); + o.depends('mode', 'sta-wds'); + o.depends('mode', 'wds'); + + o = ss.taboption('general', form.Value, 'bssid', _('BSSID')); + o.datatype = 'macaddr'; + + o = ss.taboption('general', widgets.NetworkSelect, 'network', _('Network'), _('Choose the network(s) you want to attach to this wireless interface or fill out the create field to define a new network.')); + o.rmempty = true; + o.multiple = true; + o.novirtual = true; + o.write = function(section_id, value) { + return network.getDevice(section_id).then(L.bind(function(dev) { + var old_networks = dev.getNetworks().reduce(function(o, v) { o[v.getName()] = v; return o }, {}), + new_networks = {}, + values = L.toArray(value), + tasks = []; + + for (var i = 0; i < values.length; i++) { + new_networks[values[i]] = true; + + if (old_networks[values[i]]) + continue; + + tasks.push(network.getNetwork(values[i]).then(L.bind(function(name, net) { + return net || network.addNetwork(name, { proto: 'none' }); + }, this, values[i])).then(L.bind(function(dev, net) { + if (net) { + if (!net.isEmpty()) + net.set('type', 'bridge'); + net.addDevice(dev); + } + }, this, dev))); + } + + for (var name in old_networks) + if (!new_networks[name]) + tasks.push(network.getNetwork(name).then(L.bind(function(dev, net) { + if (net) + net.deleteDevice(dev); + }, this, dev))); + + return Promise.all(tasks); + }, this)); + }; + + if (hwtype == 'mac80211') { + var mode = ss.children[0], + bssid = ss.children[5], + encr; + + mode.value('mesh', '802.11s'); + mode.value('ahdemo', _('Pseudo Ad-Hoc (ahdemo)')); + mode.value('monitor', _('Monitor')); + + bssid.depends('mode', 'adhoc'); + bssid.depends('mode', 'sta'); + bssid.depends('mode', 'sta-wds'); + + o = ss.taboption('macfilter', form.ListValue, 'macfilter', _('MAC-Address Filter')); + o.depends('mode', 'ap'); + o.depends('mode', 'ap-wds'); + o.value('', _('disable')); + o.value('allow', _('Allow listed only')); + o.value('deny', _('Allow all except listed')); + + o = ss.taboption('macfilter', form.DynamicList, 'maclist', _('MAC-List')); + o.datatype = 'macaddr'; + o.depends('macfilter', 'allow'); + o.depends('macfilter', 'deny'); + o.load = function(section_id) { + return network.getHostHints().then(L.bind(function(hints) { + hints.getMACHints().map(L.bind(function(hint) { + this.value(hint[0], hint[1] ? '%s (%s)'.format(hint[0], hint[1]) : hint[0]); + }, this)); + + return form.DynamicList.prototype.load.apply(this, [section_id]); + }, this)); + }; + + mode.value('ap-wds', '%s (%s)'.format(_('Access Point'), _('WDS'))); + mode.value('sta-wds', '%s (%s)'.format(_('Client'), _('WDS'))); + + mode.write = function(section_id, value) { + switch (value) { + case 'ap-wds': + uci.set('wireless', section_id, 'mode', 'ap'); + uci.set('wireless', section_id, 'wds', '1'); + break; + + case 'sta-wds': + uci.set('wireless', section_id, 'mode', 'sta'); + uci.set('wireless', section_id, 'wds', '1'); + break; + + default: + uci.set('wireless', section_id, 'mode', value); + uci.unset('wireless', section_id, 'wds'); + break; + } + }; + + mode.cfgvalue = function(section_id) { + var mode = uci.get('wireless', section_id, 'mode'), + wds = uci.get('wireless', section_id, 'wds'); + + if (mode == 'ap' && wds) + return 'ap-wds'; + else if (mode == 'sta' && wds) + return 'sta-wds'; + + return mode; + }; + + o = ss.taboption('general', form.Flag, 'hidden', _('Hide ESSID')); + o.depends('mode', 'ap'); + o.depends('mode', 'ap-wds'); + + o = ss.taboption('general', form.Flag, 'wmm', _('WMM Mode')); + o.depends('mode', 'ap'); + o.depends('mode', 'ap-wds'); + o.default = o.enabled; + + o = ss.taboption('advanced', form.Flag, 'isolate', _('Isolate Clients'), _('Prevents client-to-client communication')); + o.depends('mode', 'ap'); + o.depends('mode', 'ap-wds'); + + o = ss.taboption('advanced', form.Value, 'ifname', _('Interface name'), _('Override default interface name')); + o.optional = true; + o.placeholder = radioNet.getIfname(); + if (/^radio\d+\.network/.test(o.placeholder)) + o.placeholder = ''; + + o = ss.taboption('advanced', form.Flag, 'short_preamble', _('Short Preamble')); + o.default = o.enabled; + + o = ss.taboption('advanced', form.Value, 'dtim_period', _('DTIM Interval'), _('Delivery Traffic Indication Message Interval')); + o.optional = true; + o.placeholder = 2; + o.datatype = 'range(1,255)'; + + o = ss.taboption('advanced', form.Value, 'wpa_group_rekey', _('Time interval for rekeying GTK'), _('sec')); + o.optional = true; + o.placeholder = 600; + o.datatype = 'uinteger'; + + o = ss.taboption('advanced', form.Flag , 'skip_inactivity_poll', _('Disable Inactivity Polling')); + o.optional = true; + o.datatype = 'uinteger'; + + o = ss.taboption('advanced', form.Value, 'max_inactivity', _('Station inactivity limit'), _('sec')); + o.optional = true; + o.placeholder = 300; + o.datatype = 'uinteger'; + + o = ss.taboption('advanced', form.Value, 'max_listen_interval', _('Maximum allowed Listen Interval')); + o.optional = true; + o.placeholder = 65535; + o.datatype = 'uinteger'; + + o = ss.taboption('advanced', form.Flag, 'disassoc_low_ack', _('Disassociate On Low Acknowledgement'), _('Allow AP mode to disconnect STAs based on low ACK condition')); + o.default = o.enabled; + } + + + encr = o = ss.taboption('encryption', form.ListValue, 'encryption', _('Encryption')); + o.depends('mode', 'ap'); + o.depends('mode', 'sta'); + o.depends('mode', 'adhoc'); + o.depends('mode', 'ahdemo'); + o.depends('mode', 'ap-wds'); + o.depends('mode', 'sta-wds'); + o.depends('mode', 'mesh'); + + o.cfgvalue = function(section_id) { + var v = String(uci.get('wireless', section_id, 'encryption')); + if (v == 'wep') + return 'wep-open'; + else if (v.match(/\+/)) + return v.replace(/\+.+$/, ''); + return v; + }; + + o.write = function(section_id, value) { + var e = this.section.children.filter(function(o) { return o.option == 'encryption' })[0].formvalue(section_id), + co = this.section.children.filter(function(o) { return o.option == 'cipher' })[0], c = co.formvalue(section_id); + + if (value == 'wpa' || value == 'wpa2') + uci.unset('wireless', section_id, 'key'); + + if (co.isActive(section_id) && e && (c == 'tkip' || c == 'ccmp' || c == 'tkip+ccmp')) + e += '+' + c; + + uci.set('wireless', section_id, 'encryption', e); + }; + + o = ss.taboption('encryption', form.ListValue, 'cipher', _('Cipher')); + o.depends('encryption', 'wpa'); + o.depends('encryption', 'wpa2'); + o.depends('encryption', 'psk'); + o.depends('encryption', 'psk2'); + o.depends('encryption', 'wpa-mixed'); + o.depends('encryption', 'psk-mixed'); + o.value('auto', _('auto')); + o.value('ccmp', _('Force CCMP (AES)')); + o.value('tkip', _('Force TKIP')); + o.value('tkip+ccmp', _('Force TKIP and CCMP (AES)')); + o.write = ss.children.filter(function(o) { return o.option == 'encryption' })[0].write; + + o.cfgvalue = function(section_id) { + var v = String(uci.get('wireless', section_id, 'encryption')); + if (v.match(/\+/)) { + v = v.replace(/^[^+]+\+/, ''); + if (v == 'aes') + v = 'ccmp'; + else if (v == 'tkip+aes' || v == 'aes+tkip' || v == 'ccmp+tkip') + v = 'tkip+ccmp'; + } + return v; + }; + + + var crypto_modes = []; + + if (hwtype == 'mac80211') { + var has_supplicant = L.hasSystemFeature('wpasupplicant'), + has_hostapd = L.hasSystemFeature('hostapd'); + + // Probe EAP support + var has_ap_eap = L.hasSystemFeature('hostapd', 'eap'), + has_sta_eap = L.hasSystemFeature('wpasupplicant', 'eap'); + + // Probe SAE support + var has_ap_sae = L.hasSystemFeature('hostapd', 'sae'), + has_sta_sae = L.hasSystemFeature('wpasupplicant', 'sae'); + + // Probe OWE support + var has_ap_owe = L.hasSystemFeature('hostapd', 'owe'), + has_sta_owe = L.hasSystemFeature('wpasupplicant', 'owe'); + + + if (has_hostapd || has_supplicant) { + crypto_modes.push(['psk2', 'WPA2-PSK', 33]); + crypto_modes.push(['psk-mixed', 'WPA-PSK/WPA2-PSK Mixed Mode', 22]); + crypto_modes.push(['psk', 'WPA-PSK', 21]); + } + else { + encr.description = _('WPA-Encryption requires wpa_supplicant (for client mode) or hostapd (for AP and ad-hoc mode) to be installed.'); + } + + if (has_ap_sae || has_sta_sae) { + crypto_modes.push(['sae', 'WPA3-SAE', 31]); + crypto_modes.push(['sae-mixed', 'WPA2-PSK/WPA3-SAE Mixed Mode', 30]); + } + + if (has_ap_eap || has_sta_eap) { + crypto_modes.push(['wpa2', 'WPA2-EAP', 32]); + crypto_modes.push(['wpa', 'WPA-EAP', 20]); + } + + if (has_ap_owe || has_sta_owe) { + crypto_modes.push(['owe', 'OWE', 1]); + } + + encr.crypto_support = { + 'ap': { + 'wep-open': true, + 'wep-shared': true, + 'psk': has_hostapd || _('Requires hostapd'), + 'psk2': has_hostapd || _('Requires hostapd'), + 'psk-mixed': has_hostapd || _('Requires hostapd'), + 'sae': has_ap_sae || _('Requires hostapd with SAE support'), + 'sae-mixed': has_ap_sae || _('Requires hostapd with SAE support'), + 'wpa': has_ap_eap || _('Requires hostapd with EAP support'), + 'wpa2': has_ap_eap || _('Requires hostapd with EAP support'), + 'owe': has_ap_owe || _('Requires hostapd with OWE support') + }, + 'sta': { + 'wep-open': true, + 'wep-shared': true, + 'psk': has_supplicant || _('Requires wpa-supplicant'), + 'psk2': has_supplicant || _('Requires wpa-supplicant'), + 'psk-mixed': has_supplicant || _('Requires wpa-supplicant'), + 'sae': has_sta_sae || _('Requires wpa-supplicant with SAE support'), + 'sae-mixed': has_sta_sae || _('Requires wpa-supplicant with SAE support'), + 'wpa': has_sta_eap || _('Requires wpa-supplicant with EAP support'), + 'wpa2': has_sta_eap || _('Requires wpa-supplicant with EAP support'), + 'owe': has_sta_owe || _('Requires wpa-supplicant with OWE support') + }, + 'adhoc': { + 'wep-open': true, + 'wep-shared': true, + 'psk': has_supplicant || _('Requires wpa-supplicant'), + 'psk2': has_supplicant || _('Requires wpa-supplicant'), + 'psk-mixed': has_supplicant || _('Requires wpa-supplicant'), + }, + 'mesh': { + 'sae': has_sta_sae || _('Requires wpa-supplicant with SAE support') + }, + 'ahdemo': { + 'wep-open': true, + 'wep-shared': true + }, + 'wds': { + 'wep-open': true, + 'wep-shared': true + } + }; + + encr.crypto_support['ap-wds'] = encr.crypto_support['ap']; + encr.crypto_support['sta-wds'] = encr.crypto_support['sta']; + + encr.validate = function(section_id, value) { + var modeopt = this.section.children.filter(function(o) { return o.option == 'mode' })[0], + modeval = modeopt.formvalue(section_id), + modetitle = modeopt.vallist[modeopt.keylist.indexOf(modeval)], + enctitle = this.vallist[this.keylist.indexOf(value)]; + + if (value == 'none') + return true; + + if (!L.isObject(this.crypto_support[modeval]) || !this.crypto_support[modeval].hasOwnProperty(value)) + return _('The selected %s mode is incompatible with %s encryption').format(modetitle, enctitle); + + return this.crypto_support[modeval][value]; + }; + } + else if (hwtype == 'broadcom') { + crypto_modes.push(['psk2', 'WPA2-PSK', 33]); + crypto_modes.push(['psk+psk2', 'WPA-PSK/WPA2-PSK Mixed Mode', 22]); + crypto_modes.push(['psk', 'WPA-PSK', 21]); + } + + crypto_modes.push(['wep-open', _('WEP Open System'), 11]); + crypto_modes.push(['wep-shared', _('WEP Shared Key'), 10]); + crypto_modes.push(['none', _('No Encryption'), 0]); + + crypto_modes.sort(function(a, b) { return b[2] - a[2] }); + + for (var i = 0; i < crypto_modes.length; i++) { + var security_level = (crypto_modes[i][2] >= 30) ? _('strong security') + : (crypto_modes[i][2] >= 20) ? _('medium security') + : (crypto_modes[i][2] >= 10) ? _('weak security') : _('open network'); + + encr.value(crypto_modes[i][0], '%s (%s)'.format(crypto_modes[i][1], security_level)); + } + + + o = ss.taboption('encryption', form.Value, 'auth_server', _('Radius-Authentication-Server')); + o.depends({ mode: 'ap', encryption: 'wpa' }); + o.depends({ mode: 'ap', encryption: 'wpa2' }); + o.depends({ mode: 'ap-wds', encryption: 'wpa' }); + o.depends({ mode: 'ap-wds', encryption: 'wpa2' }); + o.rmempty = true; + o.datatype = 'host(0)'; + + o = ss.taboption('encryption', form.Value, 'auth_port', _('Radius-Authentication-Port'), _('Default %d').format(1812)); + o.depends({ mode: 'ap', encryption: 'wpa' }); + o.depends({ mode: 'ap', encryption: 'wpa2' }); + o.depends({ mode: 'ap-wds', encryption: 'wpa' }); + o.depends({ mode: 'ap-wds', encryption: 'wpa2' }); + o.rmempty = true; + o.datatype = 'port'; + + o = ss.taboption('encryption', form.Value, 'auth_secret', _('Radius-Authentication-Secret')); + o.depends({ mode: 'ap', encryption: 'wpa' }); + o.depends({ mode: 'ap', encryption: 'wpa2' }); + o.depends({ mode: 'ap-wds', encryption: 'wpa' }); + o.depends({ mode: 'ap-wds', encryption: 'wpa2' }); + o.rmempty = true; + o.password = true; + + o = ss.taboption('encryption', form.Value, 'acct_server', _('Radius-Accounting-Server')); + o.depends({ mode: 'ap', encryption: 'wpa' }); + o.depends({ mode: 'ap', encryption: 'wpa2' }); + o.depends({ mode: 'ap-wds', encryption: 'wpa' }); + o.depends({ mode: 'ap-wds', encryption: 'wpa2' }); + o.rmempty = true; + o.datatype = 'host(0)'; + + o = ss.taboption('encryption', form.Value, 'acct_port', _('Radius-Accounting-Port'), _('Default %d').format(1813)); + o.depends({ mode: 'ap', encryption: 'wpa' }); + o.depends({ mode: 'ap', encryption: 'wpa2' }); + o.depends({ mode: 'ap-wds', encryption: 'wpa' }); + o.depends({ mode: 'ap-wds', encryption: 'wpa2' }); + o.rmempty = true; + o.datatype = 'port'; + + o = ss.taboption('encryption', form.Value, 'acct_secret', _('Radius-Accounting-Secret')); + o.depends({ mode: 'ap', encryption: 'wpa' }); + o.depends({ mode: 'ap', encryption: 'wpa2' }); + o.depends({ mode: 'ap-wds', encryption: 'wpa' }); + o.depends({ mode: 'ap-wds', encryption: 'wpa2' }); + o.rmempty = true; + o.password = true; + + o = ss.taboption('encryption', form.Value, 'dae_client', _('DAE-Client')); + o.depends({ mode: 'ap', encryption: 'wpa' }); + o.depends({ mode: 'ap', encryption: 'wpa2' }); + o.depends({ mode: 'ap-wds', encryption: 'wpa' }); + o.depends({ mode: 'ap-wds', encryption: 'wpa2' }); + o.rmempty = true; + o.datatype = 'host(0)'; + + o = ss.taboption('encryption', form.Value, 'dae_port', _('DAE-Port'), _('Default %d').format(3799)); + o.depends({ mode: 'ap', encryption: 'wpa' }); + o.depends({ mode: 'ap', encryption: 'wpa2' }); + o.depends({ mode: 'ap-wds', encryption: 'wpa' }); + o.depends({ mode: 'ap-wds', encryption: 'wpa2' }); + o.rmempty = true; + o.datatype = 'port'; + + o = ss.taboption('encryption', form.Value, 'dae_secret', _('DAE-Secret')); + o.depends({ mode: 'ap', encryption: 'wpa' }); + o.depends({ mode: 'ap', encryption: 'wpa2' }); + o.depends({ mode: 'ap-wds', encryption: 'wpa' }); + o.depends({ mode: 'ap-wds', encryption: 'wpa2' }); + o.rmempty = true; + o.password = true; + + + o = ss.taboption('encryption', form.Value, '_wpa_key', _('Key')); + o.depends('encryption', 'psk'); + o.depends('encryption', 'psk2'); + o.depends('encryption', 'psk+psk2'); + o.depends('encryption', 'psk-mixed'); + o.depends('encryption', 'sae'); + o.depends('encryption', 'sae-mixed'); + o.datatype = 'wpakey'; + o.rmempty = true; + o.password = true; + + o.cfgvalue = function(section_id) { + var key = uci.get('wireless', section_id, 'key'); + return /^[1234]$/.test(key) ? null : key; + }; + + o.write = function(section_id, value) { + uci.set('wireless', section_id, 'key', value); + uci.unset('wireless', section_id, 'key1'); + uci.unset('wireless', section_id, 'key2'); + uci.unset('wireless', section_id, 'key3'); + uci.unset('wireless', section_id, 'key4'); + }; + + + o = ss.taboption('encryption', form.ListValue, '_wep_key', _('Used Key Slot')); + o.depends('encryption', 'wep-open'); + o.depends('encryption', 'wep-shared'); + o.value('1', _('Key #%d').format(1)); + o.value('2', _('Key #%d').format(2)); + o.value('3', _('Key #%d').format(3)); + o.value('4', _('Key #%d').format(4)); + + o.cfgvalue = function(section_id) { + var slot = +uci.get('wireless', section_id, 'key'); + return (slot >= 1 && slot <= 4) ? String(slot) : ''; + }; + + o.write = function(section_id, value) { + uci.set('wireless', section_id, 'key', value); + }; + + for (var slot = 1; slot <= 4; slot++) { + o = ss.taboption('encryption', form.Value, 'key%d'.format(slot), _('Key #%d').format(slot)); + o.depends('encryption', 'wep-open'); + o.depends('encryption', 'wep-shared'); + o.datatype = 'wepkey'; + o.rmempty = true; + o.password = true; + + o.write = function(section_id, value) { + if (value != null && (value.length == 5 || value.length == 13)) + value = 's:%s'.format(value); + uci.set('wireless', section_id, this.option, value); + }; + } + + + if (hwtype == 'mac80211') { + // Probe 802.11r support (and EAP support as a proxy for Openwrt) + var has_80211r = L.hasSystemFeature('hostapd', '11r') || L.hasSystemFeature('hostapd', 'eap'); + + o = ss.taboption('encryption', form.Flag, 'ieee80211r', _('802.11r Fast Transition'), _('Enables fast roaming among access points that belong to the same Mobility Domain')); + o.depends({ mode: 'ap', encryption: 'wpa' }); + o.depends({ mode: 'ap', encryption: 'wpa2' }); + o.depends({ mode: 'ap-wds', encryption: 'wpa' }); + o.depends({ mode: 'ap-wds', encryption: 'wpa2' }); + if (has_80211r) { + o.depends({ mode: 'ap', encryption: 'psk' }); + o.depends({ mode: 'ap', encryption: 'psk2' }); + o.depends({ mode: 'ap', encryption: 'psk-mixed' }); + o.depends({ mode: 'ap', encryption: 'sae' }); + o.depends({ mode: 'ap', encryption: 'sae-mixed' }); + o.depends({ mode: 'ap-wds', encryption: 'psk' }); + o.depends({ mode: 'ap-wds', encryption: 'psk2' }); + o.depends({ mode: 'ap-wds', encryption: 'psk-mixed' }); + o.depends({ mode: 'ap-wds', encryption: 'sae' }); + o.depends({ mode: 'ap-wds', encryption: 'sae-mixed' }); + } + o.rmempty = true; + + o = ss.taboption('encryption', form.Value, 'nasid', _('NAS ID'), _('Used for two different purposes: RADIUS NAS ID and 802.11r R0KH-ID. Not needed with normal WPA(2)-PSK.')); + o.depends({ mode: 'ap', encryption: 'wpa' }); + o.depends({ mode: 'ap', encryption: 'wpa2' }); + o.depends({ mode: 'ap-wds', encryption: 'wpa' }); + o.depends({ mode: 'ap-wds', encryption: 'wpa2' }); + o.depends({ ieee80211r: '1' }); + o.rmempty = true; + + o = ss.taboption('encryption', form.Value, 'mobility_domain', _('Mobility Domain'), _('4-character hexadecimal ID')); + o.depends({ ieee80211r: '1' }); + o.placeholder = '4f57'; + o.datatype = 'and(hexstring,length(4))'; + o.rmempty = true; + + o = ss.taboption('encryption', form.Value, 'reassociation_deadline', _('Reassociation Deadline'), _('time units (TUs / 1.024 ms) [1000-65535]')); + o.depends({ ieee80211r: '1' }); + o.placeholder = '1000'; + o.datatype = 'range(1000,65535)'; + o.rmempty = true; + + o = ss.taboption('encryption', form.ListValue, 'ft_over_ds', _('FT protocol')); + o.depends({ ieee80211r: '1' }); + o.value('1', _('FT over DS')); + o.value('0', _('FT over the Air')); + o.rmempty = true; + + o = ss.taboption('encryption', form.Flag, 'ft_psk_generate_local', _('Generate PMK locally'), _('When using a PSK, the PMK can be automatically generated. When enabled, the R0/R1 key options below are not applied. Disable this to use the R0 and R1 key options.')); + o.depends({ ieee80211r: '1' }); + o.default = o.enabled; + o.rmempty = false; + + o = ss.taboption('encryption', form.Value, 'r0_key_lifetime', _('R0 Key Lifetime'), _('minutes')); + o.depends({ ieee80211r: '1' }); + o.placeholder = '10000'; + o.datatype = 'uinteger'; + o.rmempty = true; + + o = ss.taboption('encryption', form.Value, 'r1_key_holder', _('R1 Key Holder'), _('6-octet identifier as a hex string - no colons')); + o.depends({ ieee80211r: '1' }); + o.placeholder = '00004f577274'; + o.datatype = 'and(hexstring,length(12))'; + o.rmempty = true; + + o = ss.taboption('encryption', form.Flag, 'pmk_r1_push', _('PMK R1 Push')); + o.depends({ ieee80211r: '1' }); + o.placeholder = '0'; + o.rmempty = true; + + o = ss.taboption('encryption', form.DynamicList, 'r0kh', _('External R0 Key Holder List'), _('List of R0KHs in the same Mobility Domain.
Format: MAC-address,NAS-Identifier,128-bit key as hex string.
This list is used to map R0KH-ID (NAS Identifier) to a destination MAC address when requesting PMK-R1 key from the R0KH that the STA used during the Initial Mobility Domain Association.')); + o.depends({ ieee80211r: '1' }); + o.rmempty = true; + + o = ss.taboption('encryption', form.DynamicList, 'r1kh', _('External R1 Key Holder List'), _ ('List of R1KHs in the same Mobility Domain.
Format: MAC-address,R1KH-ID as 6 octets with colons,128-bit key as hex string.
This list is used to map R1KH-ID to a destination MAC address when sending PMK-R1 key from the R0KH. This is also the list of authorized R1KHs in the MD that can request PMK-R1 keys.')); + o.depends({ ieee80211r: '1' }); + o.rmempty = true; + // End of 802.11r options + + o = ss.taboption('encryption', form.ListValue, 'eap_type', _('EAP-Method')); + o.value('tls', 'TLS'); + o.value('ttls', 'TTLS'); + o.value('peap', 'PEAP'); + o.value('fast', 'FAST'); + o.depends({ mode: 'sta', encryption: 'wpa' }); + o.depends({ mode: 'sta', encryption: 'wpa2' }); + o.depends({ mode: 'sta-wds', encryption: 'wpa' }); + o.depends({ mode: 'sta-wds', encryption: 'wpa2' }); + + o = ss.taboption('encryption', form.Flag, 'ca_cert_usesystem', _('Use system certificates'), _("Validate server certificate using built-in system CA bundle,
requires the \"ca-bundle\" package")); + o.enabled = '1'; + o.disabled = '0'; + o.default = o.disabled; + o.depends({ mode: 'sta', encryption: 'wpa' }); + o.depends({ mode: 'sta', encryption: 'wpa2' }); + o.depends({ mode: 'sta-wds', encryption: 'wpa' }); + o.depends({ mode: 'sta-wds', encryption: 'wpa2' }); + o.validate = function(section_id, value) { + if (value == '1' && !L.hasSystemFeature('cabundle')) { + return _("This option cannot be used because the ca-bundle package is not installed."); + } + return true; + }; + + o = ss.taboption('encryption', form.FileUpload, 'ca_cert', _('Path to CA-Certificate')); + o.depends({ mode: 'sta', encryption: 'wpa', ca_cert_usesystem: '0' }); + o.depends({ mode: 'sta', encryption: 'wpa2', ca_cert_usesystem: '0' }); + o.depends({ mode: 'sta-wds', encryption: 'wpa', ca_cert_usesystem: '0' }); + o.depends({ mode: 'sta-wds', encryption: 'wpa2', ca_cert_usesystem: '0' }); + + o = ss.taboption('encryption', form.Value, 'subject_match', _('Certificate constraint (Subject)'), _("Certificate constraint substring - e.g. /CN=wifi.mycompany.com
See `logread -f` during handshake for actual values")); + o.depends({ mode: 'sta', encryption: 'wpa' }); + o.depends({ mode: 'sta', encryption: 'wpa2' }); + o.depends({ mode: 'sta-wds', encryption: 'wpa' }); + o.depends({ mode: 'sta-wds', encryption: 'wpa2' }); + + o = ss.taboption('encryption', form.DynamicList, 'altsubject_match', _('Certificate constraint (SAN)'), _("Certificate constraint(s) via Subject Alternate Name values
(supported attributes: EMAIL, DNS, URI) - e.g. DNS:wifi.mycompany.com")); + o.depends({ mode: 'sta', encryption: 'wpa' }); + o.depends({ mode: 'sta', encryption: 'wpa2' }); + o.depends({ mode: 'sta-wds', encryption: 'wpa' }); + o.depends({ mode: 'sta-wds', encryption: 'wpa2' }); + + o = ss.taboption('encryption', form.DynamicList, 'domain_match', _('Certificate constraint (Domain)'), _("Certificate constraint(s) against DNS SAN values (if available)
or Subject CN (exact match)")); + o.depends({ mode: 'sta', encryption: 'wpa' }); + o.depends({ mode: 'sta', encryption: 'wpa2' }); + o.depends({ mode: 'sta-wds', encryption: 'wpa' }); + o.depends({ mode: 'sta-wds', encryption: 'wpa2' }); + + o = ss.taboption('encryption', form.DynamicList, 'domain_suffix_match', _('Certificate constraint (Wildcard)'), _("Certificate constraint(s) against DNS SAN values (if available)
or Subject CN (suffix match)")); + o.depends({ mode: 'sta', encryption: 'wpa' }); + o.depends({ mode: 'sta', encryption: 'wpa2' }); + o.depends({ mode: 'sta-wds', encryption: 'wpa' }); + o.depends({ mode: 'sta-wds', encryption: 'wpa2' }); + + o = ss.taboption('encryption', form.FileUpload, 'client_cert', _('Path to Client-Certificate')); + o.depends({ mode: 'sta', eap_type: 'tls', encryption: 'wpa' }); + o.depends({ mode: 'sta', eap_type: 'tls', encryption: 'wpa2' }); + o.depends({ mode: 'sta-wds', eap_type: 'tls', encryption: 'wpa' }); + o.depends({ mode: 'sta-wds', eap_type: 'tls', encryption: 'wpa2' }); + + o = ss.taboption('encryption', form.FileUpload, 'priv_key', _('Path to Private Key')); + o.depends({ mode: 'sta', eap_type: 'tls', encryption: 'wpa2' }); + o.depends({ mode: 'sta', eap_type: 'tls', encryption: 'wpa' }); + o.depends({ mode: 'sta-wds', eap_type: 'tls', encryption: 'wpa2' }); + o.depends({ mode: 'sta-wds', eap_type: 'tls', encryption: 'wpa' }); + + o = ss.taboption('encryption', form.Value, 'priv_key_pwd', _('Password of Private Key')); + o.depends({ mode: 'sta', eap_type: 'tls', encryption: 'wpa2' }); + o.depends({ mode: 'sta', eap_type: 'tls', encryption: 'wpa' }); + o.depends({ mode: 'sta-wds', eap_type: 'tls', encryption: 'wpa2' }); + o.depends({ mode: 'sta-wds', eap_type: 'tls', encryption: 'wpa' }); + o.password = true; + + o = ss.taboption('encryption', form.ListValue, 'auth', _('Authentication')); + o.value('PAP', 'PAP'); + o.value('CHAP', 'CHAP'); + o.value('MSCHAP', 'MSCHAP'); + o.value('MSCHAPV2', 'MSCHAPv2'); + o.value('EAP-GTC'); + o.value('EAP-MD5'); + o.value('EAP-MSCHAPV2'); + o.value('EAP-TLS'); + o.depends({ mode: 'sta', eap_type: 'fast', encryption: 'wpa2' }); + o.depends({ mode: 'sta', eap_type: 'fast', encryption: 'wpa' }); + o.depends({ mode: 'sta', eap_type: 'peap', encryption: 'wpa2' }); + o.depends({ mode: 'sta', eap_type: 'peap', encryption: 'wpa' }); + o.depends({ mode: 'sta', eap_type: 'ttls', encryption: 'wpa2' }); + o.depends({ mode: 'sta', eap_type: 'ttls', encryption: 'wpa' }); + o.depends({ mode: 'sta-wds', eap_type: 'fast', encryption: 'wpa2' }); + o.depends({ mode: 'sta-wds', eap_type: 'fast', encryption: 'wpa' }); + o.depends({ mode: 'sta-wds', eap_type: 'peap', encryption: 'wpa2' }); + o.depends({ mode: 'sta-wds', eap_type: 'peap', encryption: 'wpa' }); + o.depends({ mode: 'sta-wds', eap_type: 'ttls', encryption: 'wpa2' }); + o.depends({ mode: 'sta-wds', eap_type: 'ttls', encryption: 'wpa' }); + + o.validate = function(section_id, value) { + var eo = this.section.children.filter(function(o) { return o.option == 'eap_type' })[0], + ev = eo.formvalue(section_id); + + if (ev != 'ttls' && (value == 'PAP' || value == 'CHAP' || value == 'MSCHAP' || value == 'MSCHAPV2')) + return _('This authentication type is not applicable to the selected EAP method.'); + + return true; + }; + + o = ss.taboption('encryption', form.Flag, 'ca_cert2_usesystem', _('Use system certificates for inner-tunnel'), _("Validate server certificate using built-in system CA bundle,
requires the \"ca-bundle\" package")); + o.enabled = '1'; + o.disabled = '0'; + o.default = o.disabled; + o.depends({ mode: 'sta', auth: 'EAP-TLS', encryption: 'wpa' }); + o.depends({ mode: 'sta', auth: 'EAP-TLS', encryption: 'wpa2' }); + o.depends({ mode: 'sta-wds', auth: 'EAP-TLS', encryption: 'wpa' }); + o.depends({ mode: 'sta-wds', auth: 'EAP-TLS', encryption: 'wpa2' }); + o.validate = function(section_id, value) { + if (value == '1' && !L.hasSystemFeature('cabundle')) { + return _("This option cannot be used because the ca-bundle package is not installed."); + } + return true; + }; + + o = ss.taboption('encryption', form.FileUpload, 'ca_cert2', _('Path to inner CA-Certificate')); + o.depends({ mode: 'sta', auth: 'EAP-TLS', encryption: 'wpa', ca_cert2_usesystem: '0' }); + o.depends({ mode: 'sta', auth: 'EAP-TLS', encryption: 'wpa2', ca_cert2_usesystem: '0' }); + o.depends({ mode: 'sta-wds', auth: 'EAP-TLS', encryption: 'wpa', ca_cert2_usesystem: '0' }); + o.depends({ mode: 'sta-wds', auth: 'EAP-TLS', encryption: 'wpa2', ca_cert2_usesystem: '0' }); + + o = ss.taboption('encryption', form.Value, 'subject_match2', _('Inner certificate constraint (Subject)'), _("Certificate constraint substring - e.g. /CN=wifi.mycompany.com
See `logread -f` during handshake for actual values")); + o.depends({ mode: 'sta', auth: 'EAP-TLS', encryption: 'wpa' }); + o.depends({ mode: 'sta', auth: 'EAP-TLS', encryption: 'wpa2' }); + o.depends({ mode: 'sta-wds', auth: 'EAP-TLS', encryption: 'wpa' }); + o.depends({ mode: 'sta-wds', auth: 'EAP-TLS', encryption: 'wpa2' }); + + o = ss.taboption('encryption', form.DynamicList, 'altsubject_match2', _('Inner certificate constraint (SAN)'), _("Certificate constraint(s) via Subject Alternate Name values
(supported attributes: EMAIL, DNS, URI) - e.g. DNS:wifi.mycompany.com")); + o.depends({ mode: 'sta', auth: 'EAP-TLS', encryption: 'wpa' }); + o.depends({ mode: 'sta', auth: 'EAP-TLS', encryption: 'wpa2' }); + o.depends({ mode: 'sta-wds', auth: 'EAP-TLS', encryption: 'wpa' }); + o.depends({ mode: 'sta-wds', auth: 'EAP-TLS', encryption: 'wpa2' }); + + o = ss.taboption('encryption', form.DynamicList, 'domain_match2', _('Inner certificate constraint (Domain)'), _("Certificate constraint(s) against DNS SAN values (if available)
or Subject CN (exact match)")); + o.depends({ mode: 'sta', auth: 'EAP-TLS', encryption: 'wpa' }); + o.depends({ mode: 'sta', auth: 'EAP-TLS', encryption: 'wpa2' }); + o.depends({ mode: 'sta-wds', auth: 'EAP-TLS', encryption: 'wpa' }); + o.depends({ mode: 'sta-wds', auth: 'EAP-TLS', encryption: 'wpa2' }); + + o = ss.taboption('encryption', form.DynamicList, 'domain_suffix_match2', _('Inner certificate constraint (Wildcard)'), _("Certificate constraint(s) against DNS SAN values (if available)
or Subject CN (suffix match)")); + o.depends({ mode: 'sta', auth: 'EAP-TLS', encryption: 'wpa' }); + o.depends({ mode: 'sta', auth: 'EAP-TLS', encryption: 'wpa2' }); + o.depends({ mode: 'sta-wds', auth: 'EAP-TLS', encryption: 'wpa' }); + o.depends({ mode: 'sta-wds', auth: 'EAP-TLS', encryption: 'wpa2' }); + + o = ss.taboption('encryption', form.FileUpload, 'client_cert2', _('Path to inner Client-Certificate')); + o.depends({ mode: 'sta', auth: 'EAP-TLS', encryption: 'wpa' }); + o.depends({ mode: 'sta', auth: 'EAP-TLS', encryption: 'wpa2' }); + o.depends({ mode: 'sta-wds', auth: 'EAP-TLS', encryption: 'wpa' }); + o.depends({ mode: 'sta-wds', auth: 'EAP-TLS', encryption: 'wpa2' }); + + o = ss.taboption('encryption', form.FileUpload, 'priv_key2', _('Path to inner Private Key')); + o.depends({ mode: 'sta', auth: 'EAP-TLS', encryption: 'wpa' }); + o.depends({ mode: 'sta', auth: 'EAP-TLS', encryption: 'wpa2' }); + o.depends({ mode: 'sta-wds', auth: 'EAP-TLS', encryption: 'wpa' }); + o.depends({ mode: 'sta-wds', auth: 'EAP-TLS', encryption: 'wpa2' }); + + o = ss.taboption('encryption', form.Value, 'priv_key2_pwd', _('Password of inner Private Key')); + o.depends({ mode: 'sta', auth: 'EAP-TLS', encryption: 'wpa' }); + o.depends({ mode: 'sta', auth: 'EAP-TLS', encryption: 'wpa2' }); + o.depends({ mode: 'sta-wds', auth: 'EAP-TLS', encryption: 'wpa' }); + o.depends({ mode: 'sta-wds', auth: 'EAP-TLS', encryption: 'wpa2' }); + o.password = true; + + o = ss.taboption('encryption', form.Value, 'identity', _('Identity')); + o.depends({ mode: 'sta', eap_type: 'fast', encryption: 'wpa2' }); + o.depends({ mode: 'sta', eap_type: 'fast', encryption: 'wpa' }); + o.depends({ mode: 'sta', eap_type: 'peap', encryption: 'wpa2' }); + o.depends({ mode: 'sta', eap_type: 'peap', encryption: 'wpa' }); + o.depends({ mode: 'sta', eap_type: 'ttls', encryption: 'wpa2' }); + o.depends({ mode: 'sta', eap_type: 'ttls', encryption: 'wpa' }); + o.depends({ mode: 'sta-wds', eap_type: 'fast', encryption: 'wpa2' }); + o.depends({ mode: 'sta-wds', eap_type: 'fast', encryption: 'wpa' }); + o.depends({ mode: 'sta-wds', eap_type: 'peap', encryption: 'wpa2' }); + o.depends({ mode: 'sta-wds', eap_type: 'peap', encryption: 'wpa' }); + o.depends({ mode: 'sta-wds', eap_type: 'ttls', encryption: 'wpa2' }); + o.depends({ mode: 'sta-wds', eap_type: 'ttls', encryption: 'wpa' }); + o.depends({ mode: 'sta', eap_type: 'tls', encryption: 'wpa2' }); + o.depends({ mode: 'sta', eap_type: 'tls', encryption: 'wpa' }); + o.depends({ mode: 'sta-wds', eap_type: 'tls', encryption: 'wpa2' }); + o.depends({ mode: 'sta-wds', eap_type: 'tls', encryption: 'wpa' }); + + o = ss.taboption('encryption', form.Value, 'anonymous_identity', _('Anonymous Identity')); + o.depends({ mode: 'sta', eap_type: 'fast', encryption: 'wpa2' }); + o.depends({ mode: 'sta', eap_type: 'fast', encryption: 'wpa' }); + o.depends({ mode: 'sta', eap_type: 'peap', encryption: 'wpa2' }); + o.depends({ mode: 'sta', eap_type: 'peap', encryption: 'wpa' }); + o.depends({ mode: 'sta', eap_type: 'ttls', encryption: 'wpa2' }); + o.depends({ mode: 'sta', eap_type: 'ttls', encryption: 'wpa' }); + o.depends({ mode: 'sta-wds', eap_type: 'fast', encryption: 'wpa2' }); + o.depends({ mode: 'sta-wds', eap_type: 'fast', encryption: 'wpa' }); + o.depends({ mode: 'sta-wds', eap_type: 'peap', encryption: 'wpa2' }); + o.depends({ mode: 'sta-wds', eap_type: 'peap', encryption: 'wpa' }); + o.depends({ mode: 'sta-wds', eap_type: 'ttls', encryption: 'wpa2' }); + o.depends({ mode: 'sta-wds', eap_type: 'ttls', encryption: 'wpa' }); + o.depends({ mode: 'sta', eap_type: 'tls', encryption: 'wpa2' }); + o.depends({ mode: 'sta', eap_type: 'tls', encryption: 'wpa' }); + o.depends({ mode: 'sta-wds', eap_type: 'tls', encryption: 'wpa2' }); + o.depends({ mode: 'sta-wds', eap_type: 'tls', encryption: 'wpa' }); + + o = ss.taboption('encryption', form.Value, 'password', _('Password')); + o.depends({ mode: 'sta', eap_type: 'fast', encryption: 'wpa2' }); + o.depends({ mode: 'sta', eap_type: 'fast', encryption: 'wpa' }); + o.depends({ mode: 'sta', eap_type: 'peap', encryption: 'wpa2' }); + o.depends({ mode: 'sta', eap_type: 'peap', encryption: 'wpa' }); + o.depends({ mode: 'sta', eap_type: 'ttls', encryption: 'wpa2' }); + o.depends({ mode: 'sta', eap_type: 'ttls', encryption: 'wpa' }); + o.depends({ mode: 'sta-wds', eap_type: 'fast', encryption: 'wpa2' }); + o.depends({ mode: 'sta-wds', eap_type: 'fast', encryption: 'wpa' }); + o.depends({ mode: 'sta-wds', eap_type: 'peap', encryption: 'wpa2' }); + o.depends({ mode: 'sta-wds', eap_type: 'peap', encryption: 'wpa' }); + o.depends({ mode: 'sta-wds', eap_type: 'ttls', encryption: 'wpa2' }); + o.depends({ mode: 'sta-wds', eap_type: 'ttls', encryption: 'wpa' }); + o.password = true; + + + if (hwtype == 'mac80211') { + // ieee802.11w options + if (L.hasSystemFeature('hostapd', '11w')) { + o = ss.taboption('encryption', form.ListValue, 'ieee80211w', _('802.11w Management Frame Protection'), _("Requires the 'full' version of wpad/hostapd and support from the wifi driver
(as of Jan 2019: ath9k, ath10k, mwlwifi and mt76)")); + o.value('', _('Disabled')); + o.value('1', _('Optional')); + o.value('2', _('Required')); + o.depends({ mode: 'ap', encryption: 'wpa2' }); + o.depends({ mode: 'ap-wds', encryption: 'wpa2' }); + o.depends({ mode: 'ap', encryption: 'psk2' }); + o.depends({ mode: 'ap', encryption: 'psk-mixed' }); + o.depends({ mode: 'ap', encryption: 'sae' }); + o.depends({ mode: 'ap', encryption: 'sae-mixed' }); + o.depends({ mode: 'ap', encryption: 'owe' }); + o.depends({ mode: 'ap-wds', encryption: 'psk2' }); + o.depends({ mode: 'ap-wds', encryption: 'psk-mixed' }); + o.depends({ mode: 'ap-wds', encryption: 'sae' }); + o.depends({ mode: 'ap-wds', encryption: 'sae-mixed' }); + o.depends({ mode: 'ap-wds', encryption: 'owe' }); + o.depends({ mode: 'sta', encryption: 'wpa2' }); + o.depends({ mode: 'sta-wds', encryption: 'wpa2' }); + o.depends({ mode: 'sta', encryption: 'psk2' }); + o.depends({ mode: 'sta', encryption: 'psk-mixed' }); + o.depends({ mode: 'sta', encryption: 'sae' }); + o.depends({ mode: 'sta', encryption: 'sae-mixed' }); + o.depends({ mode: 'sta', encryption: 'owe' }); + o.depends({ mode: 'sta-wds', encryption: 'psk2' }); + o.depends({ mode: 'sta-wds', encryption: 'psk-mixed' }); + o.depends({ mode: 'sta-wds', encryption: 'sae' }); + o.depends({ mode: 'sta-wds', encryption: 'sae-mixed' }); + o.depends({ mode: 'sta-wds', encryption: 'owe' }); + o.defaults = { + '2': [{ encryption: 'sae' }, { encryption: 'owe' }], + '1': [{ encryption: 'sae-mixed'}], + '': [] + }; + + o = ss.taboption('encryption', form.Value, 'ieee80211w_max_timeout', _('802.11w maximum timeout'), _('802.11w Association SA Query maximum timeout')); + o.depends('ieee80211w', '1'); + o.depends('ieee80211w', '2'); + o.datatype = 'uinteger'; + o.placeholder = '1000'; + o.rmempty = true; + + o = ss.taboption('encryption', form.Value, 'ieee80211w_retry_timeout', _('802.11w retry timeout'), _('802.11w Association SA Query retry timeout')); + o.depends('ieee80211w', '1'); + o.depends('ieee80211w', '2'); + o.datatype = 'uinteger'; + o.placeholder = '201'; + o.rmempty = true; + }; + + o = ss.taboption('encryption', form.Flag, 'wpa_disable_eapol_key_retries', _('Enable key reinstallation (KRACK) countermeasures'), _('Complicates key reinstallation attacks on the client side by disabling retransmission of EAPOL-Key frames that are used to install keys. This workaround might cause interoperability issues and reduced robustness of key negotiation especially in environments with heavy traffic load.')); + o.depends({ mode: 'ap', encryption: 'wpa2' }); + o.depends({ mode: 'ap', encryption: 'psk2' }); + o.depends({ mode: 'ap', encryption: 'psk-mixed' }); + o.depends({ mode: 'ap', encryption: 'sae' }); + o.depends({ mode: 'ap', encryption: 'sae-mixed' }); + o.depends({ mode: 'ap-wds', encryption: 'wpa2' }); + o.depends({ mode: 'ap-wds', encryption: 'psk2' }); + o.depends({ mode: 'ap-wds', encryption: 'psk-mixed' }); + o.depends({ mode: 'ap-wds', encryption: 'sae' }); + o.depends({ mode: 'ap-wds', encryption: 'sae-mixed' }); + + if (L.hasSystemFeature('hostapd', 'cli') && L.hasSystemFeature('wpasupplicant')) { + o = ss.taboption('encryption', form.Flag, 'wps_pushbutton', _('Enable WPS pushbutton, requires WPA(2)-PSK/WPA3-SAE')) + o.enabled = '1'; + o.disabled = '0'; + o.default = o.disabled; + o.depends('encryption', 'psk'); + o.depends('encryption', 'psk2'); + o.depends('encryption', 'psk-mixed'); + o.depends('encryption', 'sae'); + o.depends('encryption', 'sae-mixed'); + } + } + } + }); + }; + + s.handleRemove = function(section_id, ev) { + document.querySelector('.cbi-section-table-row[data-sid="%s"]'.format(section_id)).style.opacity = 0.5; + return form.TypedSection.prototype.handleRemove.apply(this, [section_id, ev]); + }; + + s.handleScan = function(radioDev, ev) { + var table = E('div', { 'class': 'table' }, [ + E('div', { 'class': 'tr table-titles' }, [ + E('div', { 'class': 'th col-2 middle center' }, _('Signal')), + E('div', { 'class': 'th col-4 middle left' }, _('SSID')), + E('div', { 'class': 'th col-2 middle center hide-xs' }, _('Channel')), + E('div', { 'class': 'th col-2 middle left hide-xs' }, _('Mode')), + E('div', { 'class': 'th col-3 middle left hide-xs' }, _('BSSID')), + E('div', { 'class': 'th col-3 middle left' }, _('Encryption')), + E('div', { 'class': 'th cbi-section-actions right' }, ' '), + ]) + ]); + + var stop = E('button', { + 'class': 'btn', + 'click': L.bind(this.handleScanStartStop, this), + 'style': 'display:none', + 'data-state': 'stop' + }, _('Stop refresh')); + + cbi_update_table(table, [], E('em', { class: 'spinning' }, _('Starting wireless scan...'))); + + var md = ui.showModal(_('Join Network: Wireless Scan'), [ + table, + E('div', { 'class': 'right' }, [ + stop, + ' ', + E('button', { + 'class': 'btn', + 'click': L.bind(this.handleScanAbort, this) + }, _('Dismiss')) + ]) + ]); + + md.style.maxWidth = '90%'; + md.style.maxHeight = 'none'; + + this.pollFn = L.bind(this.handleScanRefresh, this, radioDev, {}, table, stop); + + L.Poll.add(this.pollFn); + L.Poll.start(); + }; + + s.handleScanRefresh = function(radioDev, scanCache, table, stop) { + return radioDev.getScanList().then(L.bind(function(results) { + var rows = []; + + for (var i = 0; i < results.length; i++) + scanCache[results[i].bssid] = results[i]; + + for (var k in scanCache) + if (scanCache[k].stale) + results.push(scanCache[k]); + + results.sort(function(a, b) { + var diff = (b.quality - a.quality) || (a.channel - b.channel); + + if (diff) + return diff; + + if (a.ssid < b.ssid) + return -1; + else if (a.ssid > b.ssid) + return 1; + + if (a.bssid < b.bssid) + return -1; + else if (a.bssid > b.bssid) + return 1; + }); + + for (var i = 0; i < results.length; i++) { + var res = results[i], + qv = res.quality || 0, + qm = res.quality_max || 0, + q = (qv > 0 && qm > 0) ? Math.floor((100 / qm) * qv) : 0, + s = res.stale ? 'opacity:0.5' : ''; + + rows.push([ + E('span', { 'style': s }, render_signal_badge(q, res.signal, res.noise)), + E('span', { 'style': s }, (res.ssid != null) ? '%h'.format(res.ssid) : E('em', _('hidden'))), + E('span', { 'style': s }, '%d'.format(res.channel)), + E('span', { 'style': s }, '%h'.format(res.mode)), + E('span', { 'style': s }, '%h'.format(res.bssid)), + E('span', { 'style': s }, '%h'.format(network.formatWifiEncryption(res.encryption))), + E('div', { 'class': 'right' }, E('button', { + 'class': 'cbi-button cbi-button-action important', + 'click': L.bind(this.handleJoin, this, radioDev, res) + }, _('Join Network'))) + ]); + + res.stale = true; + } + + cbi_update_table(table, rows); + + stop.disabled = false; + stop.style.display = ''; + stop.classList.remove('spinning'); + }, this)); + }; + + s.handleScanStartStop = function(ev) { + var btn = ev.currentTarget; + + if (btn.getAttribute('data-state') == 'stop') { + L.Poll.remove(this.pollFn); + btn.firstChild.data = _('Start refresh'); + btn.setAttribute('data-state', 'start'); + } + else { + L.Poll.add(this.pollFn); + btn.firstChild.data = _('Stop refresh'); + btn.setAttribute('data-state', 'stop'); + btn.classList.add('spinning'); + btn.disabled = true; + } + }; + + s.handleScanAbort = function(ev) { + var md = L.dom.parent(ev.target, 'div[aria-modal="true"]'); + if (md) { + md.style.maxWidth = ''; + md.style.maxHeight = ''; + } + + ui.hideModal(); + L.Poll.remove(this.pollFn); + + this.pollFn = null; + }; + + s.handleJoinConfirm = function(radioDev, bss, form, ev) { + var nameopt = L.toArray(form.lookupOption('name', '_new_'))[0], + passopt = L.toArray(form.lookupOption('password', '_new_'))[0], + bssidopt = L.toArray(form.lookupOption('bssid', '_new_'))[0], + zoneopt = L.toArray(form.lookupOption('zone', '_new_'))[0], + replopt = L.toArray(form.lookupOption('replace', '_new_'))[0], + nameval = (nameopt && nameopt.isValid('_new_')) ? nameopt.formvalue('_new_') : null, + passval = (passopt && passopt.isValid('_new_')) ? passopt.formvalue('_new_') : null, + bssidval = (bssidopt && bssidopt.isValid('_new_')) ? bssidopt.formvalue('_new_') : null, + zoneval = zoneopt ? zoneopt.formvalue('_new_') : null, + enc = L.isObject(bss.encryption) ? bss.encryption : null, + is_wep = (enc && Array.isArray(enc.wep)), + is_psk = (enc && Array.isArray(enc.wpa) && L.toArray(enc.authentication).filter(function(a) { return a == 'psk' })), + is_sae = (enc && Array.isArray(enc.wpa) && L.toArray(enc.authentication).filter(function(a) { return a == 'sae' })); + + if (nameval == null || (passopt && passval == null)) + return; + + var section_id = null; + + return this.map.save(function() { + var wifi_sections = uci.sections('wireless', 'wifi-iface'); + + if (replopt.formvalue('_new_') == '1') { + for (var i = 0; i < wifi_sections.length; i++) + if (wifi_sections[i].device == radioDev.getName()) + uci.remove('wireless', wifi_sections[i]['.name']); + } + + if (uci.get('wireless', radioDev.getName(), 'disabled') == '1') { + for (var i = 0; i < wifi_sections.length; i++) + if (wifi_sections[i].device == radioDev.getName()) + uci.set('wireless', wifi_sections[i]['.name'], 'disabled', '1'); + + uci.unset('wireless', radioDev.getName(), 'disabled'); + } + + section_id = next_free_sid(wifi_sections.length); + + uci.add('wireless', 'wifi-iface', section_id); + uci.set('wireless', section_id, 'device', radioDev.getName()); + uci.set('wireless', section_id, 'mode', (bss.mode == 'Ad-Hoc') ? 'adhoc' : 'sta'); + uci.set('wireless', section_id, 'network', nameval); + + if (bss.ssid != null) { + uci.set('wireless', section_id, 'ssid', bss.ssid); + + if (bssidval == '1') + uci.set('wireless', section_id, 'bssid', bss.bssid); + } + else if (bss.bssid != null) { + uci.set('wireless', section_id, 'bssid', bss.bssid); + } + + if (is_sae) { + uci.set('wireless', section_id, 'encryption', 'sae'); + uci.set('wireless', section_id, 'key', passval); + } + else if (is_psk) { + for (var i = enc.wpa.length - 1; i >= 0; i--) { + if (enc.wpa[i] == 2) { + uci.set('wireless', section_id, 'encryption', 'psk2'); + break; + } + else if (enc.wpa[i] == 1) { + uci.set('wireless', section_id, 'encryption', 'psk'); + break; + } + } + + uci.set('wireless', section_id, 'key', passval); + } + else if (is_wep) { + uci.set('wireless', section_id, 'encryption', 'wep-open'); + uci.set('wireless', section_id, 'key', '1'); + uci.set('wireless', section_id, 'key1', passval); + } + else { + uci.set('wireless', section_id, 'encryption', 'none'); + } + + return network.addNetwork(nameval, { proto: 'dhcp' }).then(function(net) { + firewall.deleteNetwork(net.getName()); + + var zonePromise = zoneval + ? firewall.getZone(zoneval).then(function(zone) { return zone || firewall.addZone(zoneval) }) + : Promise.resolve(); + + return zonePromise.then(function(zone) { + if (zone) + zone.addNetwork(net.getName()); + }); + }); + }).then(L.bind(function() { + return this.renderMoreOptionsModal(section_id); + }, this)); + }; + + s.handleJoin = function(radioDev, bss, ev) { + this.handleScanAbort(ev); + + var m2 = new form.Map('wireless'), + s2 = m2.section(form.NamedSection, '_new_'), + enc = L.isObject(bss.encryption) ? bss.encryption : null, + is_wep = (enc && Array.isArray(enc.wep)), + is_psk = (enc && Array.isArray(enc.wpa) && L.toArray(enc.authentication).filter(function(a) { return a == 'psk' || a == 'sae' })), + replace, passphrase, name, bssid, zone; + + var nameUsed = function(name) { + var s = uci.get('network', name); + if (s != null && s['.type'] != 'interface') + return true; + + var net = (s != null) ? network.instantiateNetwork(name) : null; + return (net != null && !net.isEmpty()); + }; + + s2.render = function() { + return Promise.all([ + {}, + this.renderUCISection('_new_') + ]).then(this.renderContents.bind(this)); + }; + + replace = s2.option(form.Flag, 'replace', _('Replace wireless configuration'), _('Check this option to delete the existing networks from this radio.')); + + name = s2.option(form.Value, 'name', _('Name of the new network'), _('The allowed characters are: A-Z, a-z, 0-9 and _')); + name.datatype = 'uciname'; + name.default = 'wwan'; + name.rmempty = false; + name.validate = function(section_id, value) { + if (nameUsed(value)) + return _('The network name is already used'); + + return true; + }; + + for (var i = 2; nameUsed(name.default); i++) + name.default = 'wwan%d'.format(i); + + if (is_wep || is_psk) { + passphrase = s2.option(form.Value, 'password', is_wep ? _('WEP passphrase') : _('WPA passphrase'), _('Specify the secret encryption key here.')); + passphrase.datatype = is_wep ? 'wepkey' : 'wpakey'; + passphrase.password = true; + passphrase.rmempty = false; + } + + if (bss.ssid != null) { + bssid = s2.option(form.Flag, 'bssid', _('Lock to BSSID'), _('Instead of joining any network with a matching SSID, only connect to the BSSID %h.').format(bss.bssid)); + bssid.default = '0'; + } + + zone = s2.option(widgets.ZoneSelect, 'zone', _('Create / Assign firewall-zone'), _('Choose the firewall zone you want to assign to this interface. Select unspecified to remove the interface from the associated zone or fill out the create field to define a new zone and attach the interface to it.')); + zone.default = 'wan'; + + return m2.render().then(L.bind(function(nodes) { + ui.showModal(_('Joining Network: %q').replace(/%q/, '"%h"'.format(bss.ssid)), [ + nodes, + E('div', { 'class': 'right' }, [ + E('button', { + 'class': 'btn', + 'click': ui.hideModal + }, _('Cancel')), ' ', + E('button', { + 'class': 'cbi-button cbi-button-positive important', + 'click': ui.createHandlerFn(this, 'handleJoinConfirm', radioDev, bss, m2) + }, _('Submit')) + ]) + ], 'cbi-modal').querySelector('[id="%s"] input[class][type]'.format((passphrase || name).cbid('_new_'))).focus(); + }, this)); + }; + + s.handleAdd = function(radioDev, ev) { + var section_id = next_free_sid(uci.sections('wireless', 'wifi-iface').length); + + uci.unset('wireless', radioDev.getName(), 'disabled'); + + uci.add('wireless', 'wifi-iface', section_id); + uci.set('wireless', section_id, 'device', radioDev.getName()); + uci.set('wireless', section_id, 'mode', 'ap'); + uci.set('wireless', section_id, 'ssid', 'OpenWrt'); + uci.set('wireless', section_id, 'encryption', 'none'); + + this.addedSection = section_id; + return this.renderMoreOptionsModal(section_id); + }; + + o = s.option(form.DummyValue, '_badge'); + o.modalonly = false; + o.textvalue = function(section_id) { + var inst = this.section.lookupRadioOrNetwork(section_id), + node = E('div', { 'class': 'center' }); + + if (inst.getWifiNetworks) + node.appendChild(render_radio_badge(inst)); + else + node.appendChild(render_network_badge(inst)); + + return node; + }; + + o = s.option(form.DummyValue, '_stat'); + o.modalonly = false; + o.textvalue = function(section_id) { + var inst = this.section.lookupRadioOrNetwork(section_id); + + if (inst.getWifiNetworks) + return render_radio_status(inst, this.section.wifis.filter(function(e) { + return (e.getWifiDeviceName() == inst.getName()); + })); + else + return render_network_status(inst); + }; + + return m.render().then(L.bind(function(m, nodes) { + L.Poll.add(L.bind(function() { + var section_ids = m.children[0].cfgsections(), + tasks = [ network.getHostHints(), network.getWifiDevices() ]; + + for (var i = 0; i < section_ids.length; i++) { + var row = nodes.querySelector('.cbi-section-table-row[data-sid="%s"]'.format(section_ids[i])), + dsc = row.querySelector('[data-name="_stat"] > div'), + btns = row.querySelectorAll('.cbi-section-actions button'); + + if (dsc.getAttribute('restart') == '') { + dsc.setAttribute('restart', '1'); + tasks.push(fs.exec('/sbin/wifi', ['up', section_ids[i]]).catch(function(e) { + ui.addNotification(null, E('p', e.message)); + })); + } + else if (dsc.getAttribute('restart') == '1') { + dsc.removeAttribute('restart'); + btns[0].classList.remove('spinning'); + btns[0].disabled = false; + } + } + + return Promise.all(tasks) + .then(L.bind(function(hosts_radios) { + var tasks = []; + + for (var i = 0; i < hosts_radios[1].length; i++) + tasks.push(hosts_radios[1][i].getWifiNetworks()); + + return Promise.all(tasks).then(function(data) { + hosts_radios[2] = []; + + for (var i = 0; i < data.length; i++) + hosts_radios[2].push.apply(hosts_radios[2], data[i]); + + return hosts_radios; + }); + }, network)) + .then(L.bind(function(hosts_radios_wifis) { + var tasks = []; + + for (var i = 0; i < hosts_radios_wifis[2].length; i++) + tasks.push(hosts_radios_wifis[2][i].getAssocList()); + + return Promise.all(tasks).then(function(data) { + hosts_radios_wifis[3] = []; + + for (var i = 0; i < data.length; i++) { + var wifiNetwork = hosts_radios_wifis[2][i], + radioDev = hosts_radios_wifis[1].filter(function(d) { return d.getName() == wifiNetwork.getWifiDeviceName() })[0]; + + for (var j = 0; j < data[i].length; j++) + hosts_radios_wifis[3].push(Object.assign({ radio: radioDev, network: wifiNetwork }, data[i][j])); + } + + return hosts_radios_wifis; + }); + }, network)) + .then(L.bind(this.poll_status, this, nodes)); + }, this), 5); + + var table = E('div', { 'class': 'table assoclist', 'id': 'wifi_assoclist_table' }, [ + E('div', { 'class': 'tr table-titles' }, [ + E('div', { 'class': 'th nowrap' }, _('Network')), + E('div', { 'class': 'th hide-xs' }, _('MAC-Address')), + E('div', { 'class': 'th' }, _('Host')), + E('div', { 'class': 'th' }, _('Signal / Noise')), + E('div', { 'class': 'th' }, _('RX Rate / TX Rate')) + ]) + ]); + + cbi_update_table(table, [], E('em', { 'class': 'spinning' }, _('Collecting data...'))) + + return E([ nodes, E('h3', _('Associated Stations')), table ]); + }, this, m)); + } +}); diff --git a/luci-mod-network/root/etc/uci-defaults/50_luci-mod-admin-full b/luci-mod-network/root/etc/uci-defaults/50_luci-mod-admin-full new file mode 100755 index 000000000..140c83299 --- /dev/null +++ b/luci-mod-network/root/etc/uci-defaults/50_luci-mod-admin-full @@ -0,0 +1,22 @@ +#!/bin/sh + +if [ "$(uci -q get luci.diag)" != "internal" ]; then + host="" + + if [ -s /etc/os-release ]; then + . /etc/os-release + host="${HOME_URL:-${BUG_URL:-$OPENWRT_DEVICE_MANUFACTURER_URL}}" + host="${host#*://}" + host="${host%%/*}" + fi + + uci -q batch <<-EOF >/dev/null + set luci.diag=internal + set luci.diag.dns='${host:-openwrt.org}' + set luci.diag.ping='${host:-openwrt.org}' + set luci.diag.route='${host:-openwrt.org}' + commit luci + EOF +fi + +exit 0 diff --git a/luci-mod-network/root/usr/libexec/luci-peeraddr b/luci-mod-network/root/usr/libexec/luci-peeraddr new file mode 100755 index 000000000..84a0158fd --- /dev/null +++ b/luci-mod-network/root/usr/libexec/luci-peeraddr @@ -0,0 +1,46 @@ +#!/bin/sh + +NL=" +" + +function ifaces_by_device() { + ubus call network.interface dump 2>/dev/null | \ + jsonfilter -e "@.interface[@.device='$1' || @.l3_device='$1'].interface" +} + +function device_by_addr() { + set -- $(ip route get "$1" ${2:+from "$2"} 2>/dev/null) + echo "$5" +} + +for inbound_device in $(device_by_addr "$REMOTE_ADDR" "$SERVER_ADDR"); do + inbound_devices="$inbound_device" + inbound_interfaces="" + + for iface in $(ifaces_by_device "$inbound_device"); do + inbound_interfaces="${inbound_interfaces:+$inbound_interfaces$NL}$iface" + + for peeraddr in $(uci get "network.$iface.peeraddr"); do + for ipaddr in $(resolveip -t 1 "$peeraddr" 2>/dev/null); do + for peerdev in $(device_by_addr "$ipaddr"); do + for iface in $(ifaces_by_device "$peerdev"); do + inbound_devices="${inbound_devices:+$inbound_devices$NL}$peerdev" + inbound_interfaces="${inbound_interfaces:+$inbound_interfaces$NL}$iface" + done + done + done + done + done +done + +inbound_devices="$(echo "$inbound_devices" | sort -u | sed ':a;N;$!ba;s/\n/", "/g')" +inbound_interfaces="$(echo "$inbound_interfaces" | sort -u | sed ':a;N;$!ba;s/\n/", "/g')" + +cat <