diff options
Diffstat (limited to 'modules/luci-mod-network')
20 files changed, 3389 insertions, 3516 deletions
diff --git a/modules/luci-mod-network/Makefile b/modules/luci-mod-network/Makefile index 5958765da7..bf5627d64d 100644 --- a/modules/luci-mod-network/Makefile +++ b/modules/luci-mod-network/Makefile @@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk LUCI_TITLE:=LuCI Network Administration -LUCI_DEPENDS:=+luci-base +libiwinfo-lua +LUCI_DEPENDS:=+luci-base +libiwinfo-lua +rpcd-mod-iwinfo PKG_LICENSE:=Apache-2.0 diff --git a/modules/luci-mod-network/htdocs/luci-static/resources/view/network/dhcp.js b/modules/luci-mod-network/htdocs/luci-static/resources/view/network/dhcp.js index 1e9c402e0c..3acb6d1a16 100644 --- a/modules/luci-mod-network/htdocs/luci-static/resources/view/network/dhcp.js +++ b/modules/luci-mod-network/htdocs/luci-static/resources/view/network/dhcp.js @@ -345,8 +345,29 @@ return L.view.extend({ 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) { - so.value(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', _('<abbr title="Internet Protocol Version 4">IPv4</abbr>-Address')); @@ -363,8 +384,10 @@ return L.view.extend({ return true; }; Object.keys(hosts).forEach(function(mac) { - if (hosts[mac].ipv4) - so.value(hosts[mac].ipv4); + 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, 'leasetime', _('Lease time')); diff --git a/modules/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js b/modules/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js new file mode 100644 index 0000000000..624718dd84 --- /dev/null +++ b/modules/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js @@ -0,0 +1,982 @@ +'use strict'; +'require uci'; +'require form'; +'require network'; +'require firewall'; +'require tools.widgets as widgets'; + +function count_changes(section_id) { + var changes = L.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(L.ui.changes.displayChanges, L.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; + + 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 <abbr title="Spanning Tree Protocol">STP</abbr>'), _('Enables the Spanning Tree Protocol on this bridge')); + + igmp = s.taboption('physical', form.Flag, 'igmp_snooping', _('Enable <abbr title="Internet Group Management Protocol">IGMP</abbr> 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 <em>unspecified</em> to remove the interface from the associated zone or fill out the <em>create</em> 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': L.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 <abbr title="Dynamic Host Configuration Protocol">DHCP</abbr> 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 (<code>2m</code>).')); + so.optional = true; + so.default = '12h'; + + so = ss.taboption('advanced', form.Flag, 'dynamicdhcp', _('Dynamic <abbr title="Dynamic Host Configuration Protocol">DHCP</abbr>'), _('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', _('<abbr title="Internet Protocol Version 4">IPv4</abbr>-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 "<code>6,192.168.2.1,192.168.2.2</code>" 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.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) { + L.ui.showModal(_('Add new interface...'), [ + nodes, + E('div', { 'class': 'right' }, [ + E('button', { + 'class': 'btn', + 'click': L.ui.hideModal + }, _('Cancel')), ' ', + E('button', { + 'class': 'cbi-button cbi-button-positive important', + 'click': L.ui.createHandlerFn(this, function(ev) { + var nameval = name.isValid('_new_') ? name.formvalue('_new_') : null, + protoval = proto.isValid('_new_') ? proto.formvalue('_new_') : null; + + if (nameval == null || protoval == null || nameval == '' || protoval == '') + 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)); + }; + + 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'; + + + 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(L.Request.post( + L.url('admin/network/iface_reconnect', section_ids[i]), + 'token=' + L.env.token, + { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } } + ).catch(function() {})); + } + else if (dsc.getAttribute('disconnect') == '' || dsc.getAttribute('disconnect') == 'force') { + var force = dsc.getAttribute('disconnect'); + dsc.setAttribute('disconnect', '1'); + tasks.push(L.Request.post( + L.url('admin/network/iface_down', section_ids[i], force), + 'token=' + L.env.token, + { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } } + ).then(L.bind(function(ifname, res) { + if (res.status == 409) { + L.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(ifname)), + E('div', { 'class': 'right' }, [ + E('button', { + 'class': 'cbi-button cbi-button-neutral', + 'click': L.ui.hideModal + }, _('Cancel')), + ' ', + E('button', { + 'class': 'cbi-button cbi-button-negative important', + 'click': function(ev) { + iface_updown(false, ifname, ev, true); + L.ui.hideModal(); + } + }, _('Disconnect')) + ]) + ]); + } + }, this, section_ids[i]), function() {})); + } + 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/modules/luci-mod-network/htdocs/luci-static/resources/view/network/switch.js b/modules/luci-mod-network/htdocs/luci-static/resources/view/network/switch.js new file mode 100644 index 0000000000..b281bb1808 --- /dev/null +++ b/modules/luci-mod-network/htdocs/luci-static/resources/view/network/switch.js @@ -0,0 +1,369 @@ +'use strict'; +'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(' ')); + + L.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.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 <abbr title=\"Virtual Local Area Network\">VLAN</abbr>s in which computers can communicate directly with each other. <abbr title=\"Virtual Local Area Network\">VLAN</abbr>s 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) { + L.ui.addNotification(null, _('Switch %q has an unknown topology - the VLAN settings might not be accurate.').replace(/%q/, 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')); + }; + + 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/modules/luci-mod-network/htdocs/luci-static/resources/view/network/wifi_join.js b/modules/luci-mod-network/htdocs/luci-static/resources/view/network/wifi_join.js deleted file mode 100644 index f30e47ec7c..0000000000 --- a/modules/luci-mod-network/htdocs/luci-static/resources/view/network/wifi_join.js +++ /dev/null @@ -1,164 +0,0 @@ -var poll = null; - -function format_signal(bss) { - var qval = bss.quality || 0, - qmax = bss.quality_max || 100, - scale = 100 / qmax * qval, - range = 'none'; - - if (!bss.bssid || bss.bssid == '00:00:00:00:00:00') - range = 'none'; - else if (scale < 15) - range = '0'; - else if (scale < 35) - range = '0-25'; - else if (scale < 55) - range = '25-50'; - else if (scale < 75) - range = '50-75'; - else - range = '75-100'; - - return E('span', { - class: 'ifacebadge', - title: '%s: %d%s / %s: %d/%d'.format(_('Signal'), bss.signal, _('dB'), _('Quality'), qval, qmax) - }, [ - E('img', { src: L.resource('icons/signal-%s.png').format(range) }), - ' %d%%'.format(scale) - ]); -} - -function format_encryption(bss) { - var enc = bss.encryption || { } - - if (enc.wep === true) - return 'WEP'; - else if (enc.wpa > 0) - return E('abbr', { - title: 'Pairwise: %h / Group: %h'.format( - enc.pair_ciphers.join(', '), - enc.group_ciphers.join(', ')) - }, - '%h - %h'.format( - (enc.wpa === 3) ? _('mixed WPA/WPA2') : (enc.wpa === 2 ? 'WPA2' : 'WPA'), - enc.auth_suites.join(', '))); - else - return E('em', enc.enabled ? _('unknown') : _('open')); -} - -function format_actions(dev, type, bss) { - var enc = bss.encryption || { }, - input = [ - E('input', { type: 'submit', class: 'cbi-button cbi-button-action important', value: _('Join Network') }), - E('input', { type: 'hidden', name: 'token', value: L.env.token }), - E('input', { type: 'hidden', name: 'device', value: dev }), - E('input', { type: 'hidden', name: 'join', value: bss.ssid }), - E('input', { type: 'hidden', name: 'mode', value: bss.mode }), - E('input', { type: 'hidden', name: 'bssid', value: bss.bssid }), - E('input', { type: 'hidden', name: 'channel', value: bss.channel }), - E('input', { type: 'hidden', name: 'clbridge', value: type === 'wl' ? 1 : 0 }), - E('input', { type: 'hidden', name: 'wep', value: enc.wep ? 1 : 0 }) - ]; - - if (enc.wpa) { - input.push(E('input', { type: 'hidden', name: 'wpa_version', value: enc.wpa })); - - enc.auth_suites.forEach(function(s) { - input.push(E('input', { type: 'hidden', name: 'wpa_suites', value: s })); - }); - - enc.group_ciphers.forEach(function(s) { - input.push(E('input', { type: 'hidden', name: 'wpa_group', value: s })); - }); - - enc.pair_ciphers.forEach(function(s) { - input.push(E('input', { type: 'hidden', name: 'wpa_pairwise', value: s })); - }); - } - - return E('form', { - class: 'inline', - method: 'post', - action: L.url('admin/network/wireless_join') - }, input); -} - -function fade(bss, content) { - if (bss.stale) - return E('span', { style: 'opacity:0.5' }, content); - else - return content; -} - -function flush() { - L.stop(poll); - L.halt(); - - scan(); -} - -function scan() { - var tbl = document.querySelector('[data-wifi-scan]'), - dev = tbl.getAttribute('data-wifi-scan'), - type = tbl.getAttribute('data-wifi-type'); - - cbi_update_table(tbl, [], E('em', { class: 'spinning' }, _('Starting wireless scan...'))); - - L.post(L.url('admin/network/wireless_scan_trigger', dev), null, function(s) { - if (s.status !== 204) { - cbi_update_table(tbl, [], E('em', _('Scan request failed'))); - return; - } - - var count = 0; - - poll = L.poll(3, L.url('admin/network/wireless_scan_results', dev), null, function(s, results) { - - if (Array.isArray(results)) { - var bss = []; - - 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; - }).forEach(function(res) { - bss.push([ - fade(res, format_signal(res)), - fade(res, res.ssid ? '%h'.format(res.ssid) : E('em', {}, _('hidden'))), - fade(res, res.channel), - fade(res, res.mode), - fade(res, res.bssid), - fade(res, format_encryption(res)), - format_actions(dev, type, res) - ]); - }); - - cbi_update_table(tbl, bss, E('em', {}, _('No networks in range'))); - } - else { - cbi_update_table(tbl, [], E('em', { class: 'spinning' }, _('No scan results available yet...'))); - } - - - if (count++ >= 3) { - count = 0; - L.post(L.url('admin/network/wireless_scan_trigger', dev, 1), null, function() {}); - } - }); - - L.run(); - }); -} - -document.addEventListener('DOMContentLoaded', scan); diff --git a/modules/luci-mod-network/htdocs/luci-static/resources/view/network/wifi_status.js b/modules/luci-mod-network/htdocs/luci-static/resources/view/network/wifi_status.js deleted file mode 100644 index 108a141f88..0000000000 --- a/modules/luci-mod-network/htdocs/luci-static/resources/view/network/wifi_status.js +++ /dev/null @@ -1,59 +0,0 @@ -requestAnimationFrame(function() { - document.querySelectorAll('[data-wifi-status]').forEach(function(container) { - var ifname = container.getAttribute('data-wifi-status'), - small = container.querySelector('small'), - info = container.querySelector('span'); - - L.poll(5, L.url('admin/network/wireless_status', ifname), null, function(xhr, iws) { - var iw = Array.isArray(iws) ? iws[0] : null; - if (!iw) - return; - - var is_assoc = (iw.bssid && iw.bssid != '00:00:00:00:00:00' && iw.channel && !iw.disabled); - var p = iw.quality; - var q = iw.disabled ? -1 : p; - - var icon; - if (q < 0) - icon = L.resource('icons/signal-none.png'); - else if (q == 0) - icon = L.resource('icons/signal-0.png'); - else if (q < 25) - icon = L.resource('icons/signal-0-25.png'); - else if (q < 50) - icon = L.resource('icons/signal-25-50.png'); - else if (q < 75) - icon = L.resource('icons/signal-50-75.png'); - else - icon = L.resource('icons/signal-75-100.png'); - - L.dom.content(small, [ - E('img', { - src: icon, - title: '%s: %d %s / %s: %d %s'.format( - _('Signal'), iw.signal, _('dBm'), - _('Noise'), iw.noise, _('dBm')) - }), - '\u00a0', E('br'), '%d%%\u00a0'.format(p) - ]); - - L.itemlist(info, [ - _('Mode'), iw.mode, - _('SSID'), iw.ssid || '?', - _('BSSID'), is_assoc ? iw.bssid : null, - _('Encryption'), is_assoc ? iw.encryption || _('None') : null, - _('Channel'), is_assoc ? '%d (%.3f %s)'.format(iw.channel, iw.frequency || 0, _('GHz')) : null, - _('Tx-Power'), is_assoc ? '%d %s'.format(iw.txpower, _('dBm')) : null, - _('Signal'), is_assoc ? '%d %s'.format(iw.signal, _('dBm')) : null, - _('Noise'), is_assoc ? '%d %s'.format(iw.noise, _('dBm')) : null, - _('Bitrate'), is_assoc ? '%.1f %s'.format(iw.bitrate || 0, _('Mbit/s')) : null, - _('Country'), is_assoc ? iw.country : null - ], [ ' | ', E('br'), E('br'), E('br'), E('br'), E('br'), ' | ', E('br'), ' | ' ]); - - if (!is_assoc) - L.dom.append(info, E('em', iw.disabled ? _('Wireless is disabled') : _('Wireless is not associated'))); - }); - - L.run(); - }); -}); diff --git a/modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js b/modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js index 57e6bbb04c..9e75fe9013 100644 --- a/modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js +++ b/modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js @@ -1,93 +1,1970 @@ -function wifi_delete(ev) { - if (!confirm(_('Really delete this wireless network? The deletion cannot be undone! You might lose access to this device if you are connected via this network.'))) { - ev.preventDefault(); - return false; +'use strict'; +'require rpc'; +'require uci'; +'require form'; +'require network'; +'require firewall'; +'require tools.widgets as widgets'; + +function count_changes(section_id) { + var changes = L.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; + + 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) { + title = '%s %d %s'.format(_('Signal'), signalValue, _('dBm')); + + if (noiseValue != null && noiseValue != 0) + title += ' / %s: %d %s'.format(_('Noise'), noiseValue, _('dBm')); + } + else { + title = _('No signal'); + } + + return E('div', { 'class': wrap ? 'center' : 'ifacebadge', 'title': title }, + [ E('img', { 'src': icon }), wrap ? E('br') : ' ', '%d%%'.format(Math.max(signalPercent, 0)) ]); +} + +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), + changecount = count_changes(radioNet.getName()), + status_text = null; + + if (changecount) + status_text = E('a', { + href: '#', + click: L.bind(L.ui.changes.displayChanges, L.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'), [ + _('SSID'), 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 Mbit/s, %dMHz'.format(rate.rate / 1000, rate.mhz); + + if (rate.ht || rate.vht) { + if (rate.vht) s += ', VHT-MCS %d'.format(rate.mcs); + if (rate.nss) s += ', VHT-NSS %d'.format(rate.nss); + if (rate.ht) s += ', MCS %s'.format(rate.mcs); + if (rate.short_gi) s += ', Short GI'; } - ev.target.previousElementSibling.value = '1'; - return true; + 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 wifi_restart(ev) { - L.halt(); +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'); - findParent(ev.target, '.table').querySelectorAll('[data-disabled="false"]').forEach(function(s) { - L.dom.content(s, E('em', _('Wireless is restarting...'))); + 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() { + L.ui.changes.apply() }); +} + +function next_free_sid(offset) { + var sid = 'wifinet' + offset; + + while (uci.get('wireless', sid)) + sid = 'wifinet' + (++offset); - L.post(L.url('admin/network/wireless_reconnect', ev.target.getAttribute('data-radio')), L.run); + return sid; } -var networks = [ ]; +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; Array.isArray(data[1]) && 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 (!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); + }, -document.querySelectorAll('[data-network]').forEach(function(n) { - networks.push(n.getAttribute('data-network')); + 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]); + } }); -L.poll(5, L.url('admin/network/wireless_status', networks.join(',')), null, - function(x, st) { - if (st) { - var rowstyle = 1; - var radiostate = { }; +var CBIWifiTxPowerValue = form.ListValue.extend({ + callTxPowerList: rpc.declare({ + object: 'iwinfo', + method: 'txpowerlist', + params: [ 'device' ], + expect: { results: [] } + }), - st.forEach(function(s) { - var r = radiostate[s.device.device] || (radiostate[s.device.device] = {}); + 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; - s.is_assoc = (s.bssid && s.bssid != '00:00:00:00:00:00' && s.channel && s.mode != 'Unknown' && !s.disabled); + this.value('', _('driver default')); - r.up = r.up || s.is_assoc; - r.channel = r.channel || s.channel; - r.bitrate = r.bitrate || s.bitrate; - r.frequency = r.frequency || s.frequency; - }); + 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'; - for (var i = 0; i < st.length; i++) { - var iw = st[i], - sig = document.getElementById(iw.id + '-iw-signal'), - info = document.getElementById(iw.id + '-iw-status'), - disabled = (info && info.getAttribute('data-disabled') === 'true'); - - var p = iw.quality; - var q = disabled ? -1 : p; - - var icon; - if (q < 0) - icon = L.resource('icons/signal-none.png'); - else if (q == 0) - icon = L.resource('icons/signal-0.png'); - else if (q < 25) - icon = L.resource('icons/signal-0-25.png'); - else if (q < 50) - icon = L.resource('icons/signal-25-50.png'); - else if (q < 75) - icon = L.resource('icons/signal-50-75.png'); - else - icon = L.resource('icons/signal-75-100.png'); - - L.dom.content(sig, E('span', { - class: 'ifacebadge', - title: '%s %d %s / %s: %d %s'.format(_('Signal'), iw.signal, _('dBm'), _('Noise'), iw.noise, _('dBm')) - }, [ E('img', { src: icon }), ' %d%%'.format(p) ])); - - L.itemlist(info, [ - _('SSID'), iw.ssid || '?', - _('Mode'), iw.mode, - _('BSSID'), iw.is_assoc ? iw.bssid : null, - _('Encryption'), iw.is_assoc ? iw.encryption || _('None') : null, - null, iw.is_assoc ? null : E('em', disabled ? _('Wireless is disabled') : _('Wireless is not associated')) - ], [ ' | ', E('br') ]); + 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)); } - for (var dev in radiostate) { - var img = document.getElementById(dev + '-iw-upstate'); - if (img) img.src = L.resource('icons/wifi' + (radiostate[dev].up ? '' : '_disabled') + '.png'); + 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; + }, - var stat = document.getElementById(dev + '-iw-devinfo'); - L.itemlist(stat, [ - _('Channel'), '%s (%s %s)'.format(radiostate[dev].channel || '?', radiostate[dev].frequency || '?', _('GHz')), - _('Bitrate'), '%s %s'.format(radiostate[dev].bitrate || '?', _('Mbit/s')) - ], ' | '); + renderWidget: function(section_id, option_index, cfgvalue) { + var typeClass = 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); + + trows.push([ + E('span', { 'class': 'ifacebadge' }, [ + E('img', { + 'src': L.resource('icons/wifi%s.png').format(bss.network.isUp() ? '' : '_disabled'), + 'title': bss.radio.getI18n() + }), + ' %s '.format(bss.network.getShortName()), + E('small', '(%s)'.format(bss.network.getIfname())) + ]), + bss.mac, + name ? '%s (%s)'.format(name, ipv4 || ipv6 || '?') : ipv4 || ipv6 || '?', + 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)) + ]) + ]); } + + cbi_update_table('#wifi_assoclist_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(L.ui.changes.init, L.ui.changes)) + .then(L.bind(L.ui.changes.apply, L.ui.changes)); + }, + + renderMigration: function() { + L.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 <em>wifinet#</em> and the network will be restarted to apply the updated configuration.')), + E('div', { 'class': 'right' }, + E('button', { + 'class': 'btn cbi-button-action important', + 'click': L.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': L.ui.createHandlerFn(this, radio_restart, section_id) + }, _('Restart')), + E('button', { + 'class': 'cbi-button cbi-button-action important', + 'title': _('Find and join network'), + 'click': L.ui.createHandlerFn(this, 'handleScan', inst) + }, _('Scan')), + E('button', { + 'class': 'cbi-button cbi-button-add', + 'title': _('Provide new network'), + 'click': L.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': L.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': L.ui.createHandlerFn(this, 'renderMoreOptionsModal', section_id) + }, _('Edit')), + E('button', { + 'class': 'cbi-button cbi-button-negative remove', + 'title': _('Delete this network'), + 'click': L.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'); + + 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 = L.ui.createHandlerFn(s, network_updown, s.section, s.map); + + o = ss.taboption('general', CBIWifiFrequencyValue, '_freq', '<br />' + _('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', _('<abbr title="Extended Service Set Identifier">ESSID</abbr>')); + 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', _('<abbr title="Basic Service Set Identifier">BSSID</abbr>')); + 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 <em>create</em> 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 <abbr title="Extended Service Set Identifier">ESSID</abbr>')); + 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. <br />Format: MAC-address,NAS-Identifier,128-bit key as hex string. <br />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. <br />Format: MAC-address,R1KH-ID as 6 octets with colons,128-bit key as hex string. <br />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.FileUpload, 'ca_cert', _('Path to CA-Certificate')); + 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.FileUpload, 'ca_cert2', _('Path to inner CA-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, '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 <br />(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')) + o.enabled = '1'; + o.disabled = '0'; + o.default = o.disabled; + o.depends('encryption', 'psk'); + o.depends('encryption', 'psk2'); + o.depends('encryption', 'psk-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' }, ' '), + ]) + ]); + + cbi_update_table(table, [], E('em', { class: 'spinning' }, _('Starting wireless scan...'))); + + var md = L.ui.showModal(_('Join Network: Wireless Scan'), [ + table, + E('div', { 'class': 'right' }, + 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); + + L.Poll.add(this.pollFn); + L.Poll.start(); + }; + + s.handleScanRefresh = function(radioDev, scanCache, table) { + 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 }, '%h'.format(res.ssid)), + 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); + }, this)); + }; + + s.handleScanAbort = function(ev) { + var md = L.dom.parent(ev.target, 'div[aria-modal="true"]'); + if (md) { + md.style.maxWidth = ''; + md.style.maxHeight = ''; + } + + L.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], + 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, + 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) && Array.isArray(enc.authentication) && enc.authentication[0] == 'psk'); + + if (nameval == null || (passopt && passval == null)) + return; + + var section_id = null; + + return this.map.save(function() { + if (replopt.formvalue('_new_') == '1') { + var sections = uci.sections('wireless', 'wifi-iface'); + + for (var i = 0; i < sections.length; i++) + if (sections[i].device == radioDev.getName()) + uci.remove('wireless', sections[i]['.name']); + } + + section_id = next_free_sid(uci.sections('wifi-iface').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); + else if (bss.bssid != null) + uci.set('wireless', section_id, 'bssid', bss.bssid); + + 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); + } + + var zonePromise = zoneval + ? firewall.getZone(zoneval).then(function(zone) { return zone || firewall.addZone(zoneval) }) + : Promise.resolve(); + + return zonePromise.then(function(zone) { + return network.addNetwork(nameval, { proto: 'dhcp' }).then(function(net) { + firewall.deleteNetwork(net.getName()); + + 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) && Array.isArray(enc.authentication) && enc.authentication[0] == 'psk'), + replace, passphrase, name, zone; + + 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: <code>A-Z</code>, <code>a-z</code>, <code>0-9</code> and <code>_</code>')); + name.datatype = 'uciname'; + name.default = 'wwan'; + name.rmempty = false; + name.validate = function(section_id, value) { + if (uci.get('network', value)) + return _('The network name is already used'); + + return true; + }; + + for (var i = 2; uci.get('network', 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; + } + + zone = s2.option(widgets.ZoneSelect, 'zone', _('Create / Assign firewall-zone'), _('Choose the firewall zone you want to assign to this interface. Select <em>unspecified</em> to remove the interface from the associated zone or fill out the <em>create</em> field to define a new zone and attach the interface to it.')); + zone.default = 'wan'; + + return m2.render().then(L.bind(function(nodes) { + L.ui.showModal(_('Joining Network: %q').replace(/%q/, '"%h"'.format(bss.ssid)), [ + nodes, + E('div', { 'class': 'right' }, [ + E('button', { + 'class': 'btn', + 'click': L.ui.hideModal + }, _('Cancel')), ' ', + E('button', { + 'class': 'cbi-button cbi-button-positive important', + 'click': L.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(L.Request.post( + L.url('admin/network/wireless_reconnect', section_ids[i]), + 'token=' + L.env.token, + { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } } + ).catch(function() {})); + } + 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', '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 nowrap' }, _('Host')), + E('div', { 'class': 'th nowrap' }, _('Signal / Noise')), + E('div', { 'class': 'th nowrap' }, _('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/modules/luci-mod-network/luasrc/controller/admin/network.lua b/modules/luci-mod-network/luasrc/controller/admin/network.lua index b20607e2e9..f8623be93e 100644 --- a/modules/luci-mod-network/luasrc/controller/admin/network.lua +++ b/modules/luci-mod-network/luasrc/controller/admin/network.lua @@ -18,13 +18,7 @@ function index() end) if has_switch then - page = node("admin", "network", "vlan") - page.target = cbi("admin_network/vlan") - page.title = _("Switch") - page.order = 20 - - page = entry({"admin", "network", "switch_status"}, call("switch_status"), nil) - page.leaf = true + entry({"admin", "network", "switch"}, view("network/switch"), _("Switch"), 20) end @@ -37,69 +31,30 @@ function index() end) if has_wifi then - page = entry({"admin", "network", "wireless_join"}, post("wifi_join"), nil) - page.leaf = true - - page = entry({"admin", "network", "wireless_add"}, post("wifi_add"), nil) - page.leaf = true - page = entry({"admin", "network", "wireless_status"}, call("wifi_status"), nil) page.leaf = true page = entry({"admin", "network", "wireless_reconnect"}, post("wifi_reconnect"), nil) page.leaf = true - page = entry({"admin", "network", "wireless_scan_trigger"}, post("wifi_scan_trigger"), nil) + page = entry({"admin", "network", "wireless"}, view("network/wireless"), _('Wireless'), 15) page.leaf = true - - page = entry({"admin", "network", "wireless_scan_results"}, call("wifi_scan_results"), nil) - page.leaf = true - - page = entry({"admin", "network", "wireless"}, arcombine(cbi("admin_network/wifi_overview"), cbi("admin_network/wifi")), _("Wireless"), 15) - page.leaf = true - page.subindex = true - - if page.inreq then - local wdev - local net = require "luci.model.network".init(uci) - for _, wdev in ipairs(net:get_wifidevs()) do - local wnet - for _, wnet in ipairs(wdev:get_wifinets()) do - entry( - {"admin", "network", "wireless", wnet:id()}, - alias("admin", "network", "wireless"), - wdev:name() .. ": " .. wnet:shortname() - ) - end - end - end end - page = entry({"admin", "network", "iface_add"}, form("admin_network/iface_add"), nil) - page.leaf = true - page = entry({"admin", "network", "iface_status"}, call("iface_status"), nil) page.leaf = true page = entry({"admin", "network", "iface_reconnect"}, post("iface_reconnect"), nil) page.leaf = true - page = entry({"admin", "network", "network"}, arcombine(cbi("admin_network/network"), cbi("admin_network/ifaces")), _("Interfaces"), 10) + page = entry({"admin", "network", "iface_down"}, post("iface_down"), nil) + page.leaf = true + + page = entry({"admin", "network", "network"}, view("network/interfaces"), _("Interfaces"), 10) page.leaf = true page.subindex = true - if page.inreq then - uci:foreach("network", "interface", - function (section) - local ifc = section[".name"] - if ifc ~= "loopback" then - entry({"admin", "network", "network", ifc}, - true, ifc:upper()) - end - end) - end - if nixio.fs.access("/etc/config/dhcp") then page = node("admin", "network", "dhcp") @@ -140,50 +95,6 @@ function index() -- end end -function wifi_join() - local tpl = require "luci.template" - local http = require "luci.http" - local dev = http.formvalue("device") - local ssid = http.formvalue("join") - - if dev and ssid then - local cancel = (http.formvalue("cancel") or http.formvalue("cbi.cancel")) - if not cancel then - local cbi = require "luci.cbi" - local map = luci.cbi.load("admin_network/wifi_add")[1] - - if map:parse() ~= cbi.FORM_DONE then - tpl.render("header") - map:render() - tpl.render("footer") - end - - return - end - end - - tpl.render("admin_network/wifi_join") -end - -function wifi_add() - local dev = luci.http.formvalue("device") - local ntm = require "luci.model.network".init() - - dev = dev and ntm:get_wifidev(dev) - - if dev then - local net = dev:add_wifinet({ - mode = "ap", - ssid = "OpenWrt", - encryption = "none", - disabled = 1 - }) - - ntm:save("wireless") - luci.http.redirect(net:adminlink()) - end -end - function iface_status(ifaces) local netm = require "luci.model.network".init() local rv = { } @@ -268,6 +179,62 @@ function iface_reconnect(iface) luci.http.status(404, "No such interface") end +local function addr2dev(addr, src) + local ip = require "luci.ip" + local route = ip.route(addr, src) + if not src and route and route.src then + route = ip.route(addr, route.src:string()) + end + return route and route.dev +end + +function iface_down(iface, force) + local netmd = require "luci.model.network".init() + local peer = luci.http.getenv("REMOTE_ADDR") + local serv = luci.http.getenv("SERVER_ADDR") + + if force ~= "force" and serv and peer then + local dev = addr2dev(peer, serv) + if dev then + local nets = netmd:get_networks() + local outnet = nil + local _, net, ai + + for _, net in ipairs(nets) do + if net:contains_interface(dev) then + outnet = net + break + end + end + + if outnet:name() == iface then + luci.http.status(409, "Is inbound interface") + return + end + + local peeraddr = outnet:get("peeraddr") + for _, ai in ipairs(peeraddr and nixio.getaddrinfo(peeraddr) or {}) do + local peerdev = addr2dev(ai.address) + for _, net in ipairs(peerdev and nets or {}) do + if net:contains_interface(peerdev) and net:name() == iface then + luci.http.status(409, "Is inbound interface") + return + end + end + end + end + end + + if netmd:get_network(iface) then + luci.sys.call("env -i /sbin/ifdown %s >/dev/null 2>/dev/null" + % luci.util.shellquote(iface)) + luci.http.status(200, "Shut down") + return + end + + luci.http.status(404, "No such interface") +end + function wifi_status(devs) local s = require "luci.tools.status" local rv = { } @@ -289,7 +256,7 @@ function wifi_status(devs) end function wifi_reconnect(radio) - local rc = luci.sys.call("env -i /sbin/wifi up %s" % luci.util.shellquote(radio)) + local rc = luci.sys.call("env -i /sbin/wifi up %s >/dev/null" % luci.util.shellquote(radio)) if rc == 0 then luci.http.status(200, "Reconnected") @@ -298,87 +265,6 @@ function wifi_reconnect(radio) end end -local function _wifi_get_scan_results(cache_key) - local results = luci.util.ubus("session", "get", { - ubus_rpc_session = luci.model.uci:get_session_id(), - keys = { cache_key } - }) - - if type(results) == "table" and - type(results.values) == "table" and - type(results.values[cache_key]) == "table" - then - return results.values[cache_key] - end - - return nil -end - -function wifi_scan_trigger(radio, update) - local iw = radio and luci.sys.wifi.getiwinfo(radio) - - if not iw then - luci.http.status(404, "No such radio device") - return - end - - luci.http.status(204, "Scan scheduled") - - if nixio.fork() == 0 then - io.stderr:close() - io.stdout:close() - - local _, bss - local data, bssids = { }, { } - local cache_key = "scan_%s" % radio - - luci.util.ubus("session", "set", { - ubus_rpc_session = luci.model.uci:get_session_id(), - values = { [cache_key] = nil } - }) - - for _, bss in ipairs(iw.scanlist or { }) do - data[_] = bss - bssids[bss.bssid] = bss - end - - if update then - local cached = _wifi_get_scan_results(cache_key) - if cached then - for _, bss in ipairs(cached) do - if not bssids[bss.bssid] then - bss.stale = true - data[#data + 1] = bss - end - end - end - end - - luci.util.ubus("session", "set", { - ubus_rpc_session = luci.model.uci:get_session_id(), - values = { [cache_key] = data } - }) - end -end - -function wifi_scan_results(radio) - local results = radio and _wifi_get_scan_results("scan_%s" % radio) - - if results then - luci.http.prepare_content("application/json") - luci.http.write_json(results) - else - luci.http.status(404, "No wireless scan results") - end -end - -function switch_status(switches) - local s = require "luci.tools.status" - - luci.http.prepare_content("application/json") - luci.http.write_json(s.switch_status(switches)) -end - function diag_command(cmd, addr) if addr and addr:match("^[a-zA-Z0-9%-%.:_]+$") then luci.http.prepare_content("text/plain") diff --git a/modules/luci-mod-network/luasrc/model/cbi/admin_network/iface_add.lua b/modules/luci-mod-network/luasrc/model/cbi/admin_network/iface_add.lua deleted file mode 100644 index ca66e9f365..0000000000 --- a/modules/luci-mod-network/luasrc/model/cbi/admin_network/iface_add.lua +++ /dev/null @@ -1,101 +0,0 @@ --- Copyright 2009-2010 Jo-Philipp Wich <jow@openwrt.org> --- Licensed to the public under the Apache License 2.0. - -local nw = require "luci.model.network".init() -local fw = require "luci.model.firewall".init() -local utl = require "luci.util" -local uci = require "luci.model.uci".cursor() - -m = SimpleForm("network", translate("Create Interface")) -m.redirect = luci.dispatcher.build_url("admin/network/network") -m.reset = false - -function m.on_cancel() - luci.http.redirect(luci.dispatcher.build_url("admin/network/network")) -end - -newnet = m:field(Value, "_netname", translate("Name of the new interface"), - translate("The allowed characters are: <code>A-Z</code>, <code>a-z</code>, " .. - "<code>0-9</code> and <code>_</code>" - )) - -newnet:depends("_attach", "") -newnet.default = arg[1] and "net_" .. arg[1]:gsub("[^%w_]+", "_") -newnet.datatype = "and(uciname,maxlength(15))" - -advice = m:field(DummyValue, "d1", translate("Note: interface name length"), - translate("Maximum length of the name is 15 characters including " .. - "the automatic protocol/bridge prefix (br-, 6in4-, pppoe- etc.)" - )) - -newproto = m:field(ListValue, "_netproto", translate("Protocol of the new interface")) - -netbridge = m:field(Flag, "_bridge", translate("Create a bridge over multiple interfaces")) - - -sifname = m:field(Value, "_ifname", translate("Cover the following interface")) - -sifname.widget = "radio" -sifname.template = "cbi/network_ifacelist" -sifname.nobridges = true - - -mifname = m:field(Value, "_ifnames", translate("Cover the following interfaces")) - -mifname.widget = "checkbox" -mifname.template = "cbi/network_ifacelist" -mifname.nobridges = true - - -local _, p -for _, p in ipairs(nw:get_protocols()) do - if p:is_installed() then - newproto:value(p:proto(), p:get_i18n()) - if not p:is_virtual() then netbridge:depends("_netproto", p:proto()) end - if not p:is_floating() then - sifname:depends({ _bridge = "", _netproto = p:proto()}) - mifname:depends({ _bridge = "1", _netproto = p:proto()}) - end - end -end - -function newproto.validate(self, value, section) - local name = newnet:formvalue(section) - if not name or #name == 0 then - newnet:add_error(section, translate("No network name specified")) - elseif m:get(name) then - newnet:add_error(section, translate("The given network name is not unique")) - end - - local proto = nw:get_protocol(value) - if proto and not proto:is_floating() then - local br = (netbridge:formvalue(section) == "1") - local ifn = br and mifname:formvalue(section) or sifname:formvalue(section) - for ifn in utl.imatch(ifn) do - return value - end - return nil, translate("The selected protocol needs a device assigned") - end - return value -end - -function newproto.write(self, section, value) - local name = newnet:formvalue(section) - if name and #name > 0 then - local br = (netbridge:formvalue(section) == "1") and "bridge" or nil - local net = nw:add_network(name, { proto = value, type = br }) - if net then - local ifn - for ifn in utl.imatch( - br and mifname:formvalue(section) or sifname:formvalue(section) - ) do - net:add_interface(ifn) - end - nw:save("network") - nw:save("wireless") - end - luci.http.redirect(luci.dispatcher.build_url("admin/network/network", name)) - end -end - -return m diff --git a/modules/luci-mod-network/luasrc/model/cbi/admin_network/ifaces.lua b/modules/luci-mod-network/luasrc/model/cbi/admin_network/ifaces.lua deleted file mode 100644 index de7b8676e8..0000000000 --- a/modules/luci-mod-network/luasrc/model/cbi/admin_network/ifaces.lua +++ /dev/null @@ -1,563 +0,0 @@ --- Copyright 2008 Steven Barth <steven@midlink.org> --- Copyright 2008-2011 Jo-Philipp Wich <jow@openwrt.org> --- Licensed to the public under the Apache License 2.0. - -local fs = require "nixio.fs" -local ut = require "luci.util" -local pt = require "luci.tools.proto" -local nw = require "luci.model.network" -local fw = require "luci.model.firewall" - -arg[1] = arg[1] or "" - -local has_dnsmasq = fs.access("/etc/config/dhcp") -local has_firewall = fs.access("/etc/config/firewall") - -m = Map("network", translate("Interfaces") .. " - " .. arg[1]:upper(), translate("On this page you can configure the network interfaces. You can bridge several interfaces by ticking the \"bridge interfaces\" field and enter the names of several network interfaces separated by spaces. You can also use <abbr title=\"Virtual Local Area Network\">VLAN</abbr> notation <samp>INTERFACE.VLANNR</samp> (<abbr title=\"for example\">e.g.</abbr>: <samp>eth0.1</samp>).")) -m.redirect = luci.dispatcher.build_url("admin", "network", "network") -m:chain("wireless") -m:chain("luci") - -if has_firewall then - m:chain("firewall") -end - -nw.init(m.uci) -fw.init(m.uci) - - -local net = nw:get_network(arg[1]) - -local function set_ifstate(name, option, value) - local found = false - - m.uci:foreach("luci", "ifstate", function (s) - if s.interface == name then - m.uci:set("luci", s[".name"], option, value) - found = true - return false - end - end) - - if not found then - local sid = m.uci:add("luci", "ifstate") - m.uci:set("luci", sid, "interface", name) - m.uci:set("luci", sid, option, value) - end - - m.uci:save("luci") -end - -local function get_ifstate(name, option) - local val - - m.uci:foreach("luci", "ifstate", function (s) - if s.interface == name then - val = s[option] - return false - end - end) - - return val -end - -local function backup_ifnames(is_bridge) - if not net:is_floating() and not get_ifstate(net:name(), "ifname") then - local ifcs = net:get_interfaces() or { net:get_interface() } - if ifcs then - local _, ifn - local ifns = { } - for _, ifn in ipairs(ifcs) do - local wif = ifn:get_wifinet() - ifns[#ifns+1] = wif and wif:id() or ifn:name() - end - if #ifns > 0 then - set_ifstate(net:name(), "ifname", table.concat(ifns, " ")) - set_ifstate(net:name(), "bridge", tostring(net:is_bridge())) - end - end - end -end - - --- redirect to overview page if network does not exist anymore (e.g. after a revert) -if not net then - luci.http.redirect(luci.dispatcher.build_url("admin/network/network")) - return -end - --- protocol switch was requested, rebuild interface config and reload page -if m:formvalue("cbid.network.%s._switch" % net:name()) then - -- get new protocol - local ptype = m:formvalue("cbid.network.%s.proto" % net:name()) or "-" - local proto = nw:get_protocol(ptype, net:name()) - if proto then - -- backup default - backup_ifnames() - - -- if current proto is not floating and target proto is not floating, - -- then attempt to retain the ifnames - --error(net:proto() .. " > " .. proto:proto()) - if not net:is_floating() and not proto:is_floating() then - -- if old proto is a bridge and new proto not, then clip the - -- interface list to the first ifname only - if net:is_bridge() and proto:is_virtual() then - local _, ifn - local first = true - for _, ifn in ipairs(net:get_interfaces() or { net:get_interface() }) do - if first then - first = false - else - net:del_interface(ifn) - end - end - m:del(net:name(), "type") - end - - -- if the current proto is floating, the target proto not floating, - -- then attempt to restore ifnames from backup - elseif net:is_floating() and not proto:is_floating() then - -- if we have backup data, then re-add all orphaned interfaces - -- from it and restore the bridge choice - local br = (get_ifstate(net:name(), "bridge") == "true") - local ifn - local ifns = { } - for ifn in ut.imatch(get_ifstate(net:name(), "ifname")) do - ifn = nw:get_interface(ifn) - if ifn and not ifn:get_network() then - proto:add_interface(ifn) - if not br then - break - end - end - end - if br then - m:set(net:name(), "type", "bridge") - end - - -- in all other cases clear the ifnames - else - local _, ifc - for _, ifc in ipairs(net:get_interfaces() or { net:get_interface() }) do - net:del_interface(ifc) - end - m:del(net:name(), "type") - end - - -- clear options - local k, v - for k, v in pairs(m:get(net:name())) do - if k:sub(1,1) ~= "." and - k ~= "type" and - k ~= "ifname" - then - m:del(net:name(), k) - end - end - - -- set proto - m:set(net:name(), "proto", proto:proto()) - m.uci:save("network") - m.uci:save("wireless") - - -- reload page - luci.http.redirect(luci.dispatcher.build_url("admin/network/network", arg[1])) - return - end -end - --- dhcp setup was requested, create section and reload page -if m:formvalue("cbid.dhcp._enable._enable") then - m.uci:section("dhcp", "dhcp", arg[1], { - interface = arg[1], - start = "100", - limit = "150", - leasetime = "12h" - }) - - m.uci:save("dhcp") - luci.http.redirect(luci.dispatcher.build_url("admin/network/network", arg[1])) - return -end - -local ifc = net:get_interface() - -s = m:section(NamedSection, arg[1], "interface", translate("Common Configuration")) -s.addremove = false - -s:tab("general", translate("General Setup")) -s:tab("advanced", translate("Advanced Settings")) -s:tab("physical", translate("Physical Settings")) - -if has_firewall then - s:tab("firewall", translate("Firewall Settings")) -end - - -st = s:taboption("general", DummyValue, "__status", translate("Status")) - -local function set_status() - -- if current network is empty, print a warning - if not net:is_floating() and net:is_empty() then - st.template = "cbi/dvalue" - st.network = nil - st.value = translate("There is no device assigned yet, please attach a network device in the \"Physical Settings\" tab") - else - st.template = "admin_network/iface_status" - st.network = arg[1] - st.value = nil - end -end - -m.on_init = set_status -m.on_after_save = set_status - - -p = s:taboption("general", ListValue, "proto", translate("Protocol")) -p.default = net:proto() - - -if not net:is_installed() then - p_install = s:taboption("general", Button, "_install") - p_install.title = translate("Protocol support is not installed") - p_install.inputtitle = translate("Install package %q" % net:opkg_package()) - p_install.inputstyle = "apply" - p_install:depends("proto", net:proto()) - - function p_install.write() - return luci.http.redirect( - luci.dispatcher.build_url("admin/system/opkg") .. - "?query=%s" % net:opkg_package() - ) - end -end - - -p_switch = s:taboption("general", Button, "_switch") -p_switch.title = translate("Really switch protocol?") -p_switch.inputtitle = translate("Switch protocol") -p_switch.inputstyle = "apply" - -local _, pr -for _, pr in ipairs(nw:get_protocols()) do - p:value(pr:proto(), pr:get_i18n()) - if pr:proto() ~= net:proto() then - p_switch:depends("proto", pr:proto()) - end -end - - -auto = s:taboption("general", Flag, "auto", translate("Bring up on boot")) -auto.default = (net:proto() == "none") and auto.disabled or auto.enabled - -delegate = s:taboption("advanced", Flag, "delegate", translate("Use builtin IPv6-management")) -delegate.default = delegate.enabled - -force_link = s:taboption("advanced", Flag, "force_link", - translate("Force link"), - translate("Set interface properties regardless of the link carrier (If set, carrier sense events do not invoke hotplug handlers).")) - -force_link.default = (net:proto() == "static") and force_link.enabled or force_link.disabled - - -if not net:is_virtual() then - br = s:taboption("physical", Flag, "type", translate("Bridge interfaces"), translate("creates a bridge over specified interface(s)")) - br.enabled = "bridge" - br.rmempty = true - br:depends("proto", "static") - br:depends("proto", "dhcp") - br:depends("proto", "none") - - stp = s:taboption("physical", Flag, "stp", translate("Enable <abbr title=\"Spanning Tree Protocol\">STP</abbr>"), - translate("Enables the Spanning Tree Protocol on this bridge")) - stp:depends("type", "bridge") - stp.rmempty = true - - igmp = s:taboption("physical", Flag, "igmp_snooping", translate("Enable <abbr title=\"Internet Group Management Protocol\">IGMP</abbr> snooping"), - translate("Enables IGMP snooping on this bridge")) - igmp:depends("type", "bridge") - igmp.rmempty = true -end - - -if not net:is_floating() then - ifname_single = s:taboption("physical", Value, "ifname_single", translate("Interface")) - ifname_single.template = "cbi/network_ifacelist" - ifname_single.widget = "radio" - ifname_single.nobridges = net:is_bridge() - ifname_single.noaliases = false - ifname_single.rmempty = false - ifname_single.network = arg[1] - ifname_single:depends("type", "") - - function ifname_single.cfgvalue(self, s) - -- let the template figure out the related ifaces through the network model - return nil - end - - function ifname_single.write(self, s, val) - local _, i - local new_ifs = { } - local old_ifs = { } - - local alias = net:is_alias() - - if alias then - old_ifs[1] = '@' .. alias - else - for _, i in ipairs(net:get_interfaces() or { net:get_interface() }) do - old_ifs[#old_ifs+1] = i:name() - end - end - - for i in ut.imatch(val) do - new_ifs[#new_ifs+1] = i - - -- if this is not a bridge, only assign first interface - if self.option == "ifname_single" then - break - end - end - - table.sort(old_ifs) - table.sort(new_ifs) - - for i = 1, math.max(#old_ifs, #new_ifs) do - if old_ifs[i] ~= new_ifs[i] then - backup_ifnames() - for i = 1, #old_ifs do - net:del_interface(old_ifs[i]) - end - for i = 1, #new_ifs do - net:add_interface(new_ifs[i]) - end - break - end - end - end -end - - -if not net:is_virtual() then - ifname_multi = s:taboption("physical", Value, "ifname_multi", translate("Interface")) - ifname_multi.template = "cbi/network_ifacelist" - ifname_multi.nobridges = net:is_bridge() - ifname_multi.noaliases = true - ifname_multi.rmempty = false - ifname_multi.network = arg[1] - ifname_multi.widget = "checkbox" - ifname_multi:depends("type", "bridge") - ifname_multi.cfgvalue = ifname_single.cfgvalue - ifname_multi.write = ifname_single.write -end - - -if has_firewall then - fwzone = s:taboption("firewall", Value, "_fwzone", - translate("Create / Assign firewall-zone"), - translate("Choose the firewall zone you want to assign to this interface. Select <em>unspecified</em> to remove the interface from the associated zone or fill out the <em>create</em> field to define a new zone and attach the interface to it.")) - - fwzone.template = "cbi/firewall_zonelist" - fwzone.network = arg[1] - - function fwzone.cfgvalue(self, section) - self.iface = section - local z = fw:get_zone_by_network(section) - return z and z:name() - end - - function fwzone.write(self, section, value) - local zone = fw:get_zone(value) or fw:add_zone(value) - if zone then - fw:del_network(section) - zone:add_network(section) - end - end - - function fwzone.remove(self, section) - fw:del_network(section) - end -end - - -function p.write() end -function p.remove() end -function p.validate(self, value, section) - if value == net:proto() then - if not net:is_floating() and net:is_empty() then - local ifn = ((br and (br:formvalue(section) == "bridge")) - and ifname_multi:formvalue(section) - or ifname_single:formvalue(section)) - - for ifn in ut.imatch(ifn) do - return value - end - return nil, translate("The selected protocol needs a device assigned") - end - end - return value -end - - -local form, ferr = loadfile( - ut.libpath() .. "/model/cbi/admin_network/proto_%s.lua" % net:proto() -) - -if not form then - s:taboption("general", DummyValue, "_error", - translate("Missing protocol extension for proto %q" % net:proto()) - ).value = ferr -else - setfenv(form, getfenv(1))(m, s, net) -end - - -local _, field -for _, field in ipairs(s.children) do - if field ~= st and field ~= p and field ~= p_install and field ~= p_switch then - if next(field.deps) then - local _, dep - for _, dep in ipairs(field.deps) do - dep.proto = net:proto() - end - else - field:depends("proto", net:proto()) - end - end -end - - --- --- Display DNS settings if dnsmasq is available --- - -if has_dnsmasq and net:proto() == "static" then - m2 = Map("dhcp", "", "") - - local has_section = false - - m2.uci:foreach("dhcp", "dhcp", function(s) - if s.interface == arg[1] then - has_section = true - return false - end - end) - - if not has_section and has_dnsmasq then - - s = m2:section(TypedSection, "dhcp", translate("DHCP Server")) - s.anonymous = true - s.cfgsections = function() return { "_enable" } end - - x = s:option(Button, "_enable") - x.title = translate("No DHCP Server configured for this interface") - x.inputtitle = translate("Setup DHCP Server") - x.inputstyle = "apply" - - elseif has_section then - - s = m2:section(TypedSection, "dhcp", translate("DHCP Server")) - s.addremove = false - s.anonymous = true - s:tab("general", translate("General Setup")) - s:tab("advanced", translate("Advanced Settings")) - s:tab("ipv6", translate("IPv6 Settings")) - - function s.filter(self, section) - return m2.uci:get("dhcp", section, "interface") == arg[1] - end - - local ignore = s:taboption("general", Flag, "ignore", - translate("Ignore interface"), - translate("Disable <abbr title=\"Dynamic Host Configuration Protocol\">DHCP</abbr> for " .. - "this interface.")) - - local start = s:taboption("general", Value, "start", translate("Start"), - translate("Lowest leased address as offset from the network address.")) - start.optional = true - start.datatype = "or(uinteger,ip4addr)" - start.default = "100" - - local limit = s:taboption("general", Value, "limit", translate("Limit"), - translate("Maximum number of leased addresses.")) - limit.optional = true - limit.datatype = "uinteger" - limit.default = "150" - - local ltime = s:taboption("general", Value, "leasetime", translate("Lease time"), - translate("Expiry time of leased addresses, minimum is 2 minutes (<code>2m</code>).")) - ltime.rmempty = true - ltime.default = "12h" - - local dd = s:taboption("advanced", Flag, "dynamicdhcp", - translate("Dynamic <abbr title=\"Dynamic Host Configuration Protocol\">DHCP</abbr>"), - translate("Dynamically allocate DHCP addresses for clients. If disabled, only " .. - "clients having static leases will be served.")) - dd.default = dd.enabled - - s:taboption("advanced", Flag, "force", translate("Force"), - translate("Force DHCP on this network even if another server is detected.")) - - -- XXX: is this actually useful? - --s:taboption("advanced", Value, "name", translate("Name"), - -- translate("Define a name for this network.")) - - mask = s:taboption("advanced", Value, "netmask", - translate("<abbr title=\"Internet Protocol Version 4\">IPv4</abbr>-Netmask"), - translate("Override the netmask sent to clients. Normally it is calculated " .. - "from the subnet that is served.")) - - mask.optional = true - mask.datatype = "ip4addr" - - s:taboption("advanced", DynamicList, "dhcp_option", translate("DHCP-Options"), - translate("Define additional DHCP options, for example \"<code>6,192.168.2.1," .. - "192.168.2.2</code>\" which advertises different DNS servers to clients.")) - - for i, n in ipairs(s.children) do - if n ~= ignore then - n:depends("ignore", "") - end - end - - o = s:taboption("ipv6", ListValue, "ra", translate("Router Advertisement-Service")) - o:value("", translate("disabled")) - o:value("server", translate("server mode")) - o:value("relay", translate("relay mode")) - o:value("hybrid", translate("hybrid mode")) - - o = s:taboption("ipv6", ListValue, "dhcpv6", translate("DHCPv6-Service")) - o:value("", translate("disabled")) - o:value("server", translate("server mode")) - o:value("relay", translate("relay mode")) - o:value("hybrid", translate("hybrid mode")) - - o = s:taboption("ipv6", ListValue, "ndp", translate("NDP-Proxy")) - o:value("", translate("disabled")) - o:value("relay", translate("relay mode")) - o:value("hybrid", translate("hybrid mode")) - - o = s:taboption("ipv6", ListValue, "ra_management", translate("DHCPv6-Mode"), - translate("Default is stateless + stateful")) - o:value("0", translate("stateless")) - o:value("1", translate("stateless + stateful")) - o:value("2", translate("stateful-only")) - o:depends("dhcpv6", "server") - o:depends("dhcpv6", "hybrid") - o.default = "1" - - o = s:taboption("ipv6", Flag, "ra_default", translate("Always announce default router"), - translate("Announce as default router even if no public prefix is available.")) - o:depends("ra", "server") - o:depends("ra", "hybrid") - - s:taboption("ipv6", DynamicList, "dns", translate("Announced DNS servers")) - s:taboption("ipv6", DynamicList, "domain", translate("Announced DNS domains")) - - else - m2 = nil - end -end - - -return m, m2 diff --git a/modules/luci-mod-network/luasrc/model/cbi/admin_network/network.lua b/modules/luci-mod-network/luasrc/model/cbi/admin_network/network.lua deleted file mode 100644 index b98086dea6..0000000000 --- a/modules/luci-mod-network/luasrc/model/cbi/admin_network/network.lua +++ /dev/null @@ -1,202 +0,0 @@ --- Copyright 2008 Steven Barth <steven@midlink.org> --- Copyright 2008 Jo-Philipp Wich <jow@openwrt.org> --- Licensed to the public under the Apache License 2.0. - -local fs = require "nixio.fs" -local tpl = require "luci.template" -local ntm = require "luci.model.network".init() -local fwm = require "luci.model.firewall".init() -local json = require "luci.jsonc" - -m = Map("network", translate("Interfaces")) -m:chain("wireless") -m:chain("firewall") -m:chain("dhcp") -m.pageaction = false - - -local _, net -local ifaces, netlist = { }, { } - -for _, net in ipairs(ntm:get_networks()) do - if net:name() ~= "loopback" then - local zn = net:zonename() - local z = zn and fwm:get_zone(zn) or fwm:get_zone_by_network(net:name()) - - local w = 1 - if net:is_alias() then - w = 2 - elseif net:is_dynamic() then - w = 3 - end - - ifaces[#ifaces+1] = net:name() - netlist[#netlist+1] = { - net:name(), z and z:name() or "-", z, net, w - } - end -end - -table.sort(netlist, - function(a, b) - if a[2] ~= b[2] then - return a[2] < b[2] - elseif a[5] ~= b[5] then - return a[5] < b[5] - else - return a[1] < b[1] - end - end) - -s = m:section(TypedSection, "interface", translate("Interface Overview")) -s.template = "admin_network/iface_overview" -s.netlist = netlist - -function s.cfgsections(self) - local _, net, sl = nil, nil, { } - - for _, net in ipairs(netlist) do - sl[#sl+1] = net[1] - end - - return sl -end - -o = s:option(Value, "__disable__") - -function o.write(self, sid, value) - if value ~= "1" then - m:set(sid, "auto", "") - else - m:set(sid, "auto", "0") - end -end - -o.remove = o.write - -o = s:option(Value, "__delete__") - -function o.write(self, sid, value) - ntm:del_network(sid) -end - - -if fs.access("/etc/init.d/dsl_control") then - local ok, boarddata = pcall(json.parse, fs.readfile("/etc/board.json")) - local modemtype = (ok == true) - and (type(boarddata) == "table") - and (type(boarddata.dsl) == "table") - and (type(boarddata.dsl.modem) == "table") - and boarddata.dsl.modem.type - - dsl = m:section(TypedSection, "dsl", translate("DSL")) - dsl.anonymous = true - - annex = dsl:option(ListValue, "annex", translate("Annex")) - annex:value("a", translate("Annex A + L + M (all)")) - annex:value("b", translate("Annex B (all)")) - annex:value("j", translate("Annex J (all)")) - annex:value("m", translate("Annex M (all)")) - annex:value("bdmt", translate("Annex B G.992.1")) - annex:value("b2", translate("Annex B G.992.3")) - annex:value("b2p", translate("Annex B G.992.5")) - annex:value("at1", translate("ANSI T1.413")) - annex:value("admt", translate("Annex A G.992.1")) - annex:value("alite", translate("Annex A G.992.2")) - annex:value("a2", translate("Annex A G.992.3")) - annex:value("a2p", translate("Annex A G.992.5")) - annex:value("l", translate("Annex L G.992.3 POTS 1")) - annex:value("m2", translate("Annex M G.992.3")) - annex:value("m2p", translate("Annex M G.992.5")) - - tone = dsl:option(ListValue, "tone", translate("Tone")) - tone:value("", translate("auto")) - tone:value("a", translate("A43C + J43 + A43")) - tone:value("av", translate("A43C + J43 + A43 + V43")) - tone:value("b", translate("B43 + B43C")) - tone:value("bv", translate("B43 + B43C + V43")) - - if modemtype == "vdsl" then - xfer_mode = dsl:option(ListValue, "xfer_mode", translate("Encapsulation mode")) - xfer_mode:value("", translate("auto")) - xfer_mode:value("atm", translate("ATM (Asynchronous Transfer Mode)")) - xfer_mode:value("ptm", translate("PTM/EFM (Packet Transfer Mode)")) - - line_mode = dsl:option(ListValue, "line_mode", translate("DSL line mode")) - line_mode:value("", translate("auto")) - line_mode:value("adsl", translate("ADSL")) - line_mode:value("vdsl", translate("VDSL")) - - ds_snr = dsl:option(ListValue, "ds_snr_offset", translate("Downstream SNR offset")) - ds_snr.default = "0" - for i = -100, 100, 5 do - ds_snr:value(i, translatef("%.1f dB", i / 10)) - end - end - - firmware = dsl:option(Value, "firmware", translate("Firmware File")) - - m.pageaction = true -end - --- Show ATM bridge section if we have the capabilities -if fs.access("/usr/sbin/br2684ctl") then - atm = m:section(TypedSection, "atm-bridge", translate("ATM Bridges"), - translate("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.")) - - atm.addremove = true - atm.anonymous = true - - atm.create = function(self, section) - local sid = TypedSection.create(self, section) - local max_unit = -1 - - m.uci:foreach("network", "atm-bridge", - function(s) - local u = tonumber(s.unit) - if u ~= nil and u > max_unit then - max_unit = u - end - end) - - m.uci:set("network", sid, "unit", max_unit + 1) - m.uci:set("network", sid, "atmdev", 0) - m.uci:set("network", sid, "encaps", "llc") - m.uci:set("network", sid, "payload", "bridged") - m.uci:set("network", sid, "vci", 35) - m.uci:set("network", sid, "vpi", 8) - - return sid - end - - atm:tab("general", translate("General Setup")) - atm:tab("advanced", translate("Advanced Settings")) - - vci = atm:taboption("general", Value, "vci", translate("ATM Virtual Channel Identifier (VCI)")) - vpi = atm:taboption("general", Value, "vpi", translate("ATM Virtual Path Identifier (VPI)")) - encaps = atm:taboption("general", ListValue, "encaps", translate("Encapsulation mode")) - encaps:value("llc", translate("LLC")) - encaps:value("vc", translate("VC-Mux")) - - atmdev = atm:taboption("advanced", Value, "atmdev", translate("ATM device number")) - unit = atm:taboption("advanced", Value, "unit", translate("Bridge unit number")) - payload = atm:taboption("advanced", ListValue, "payload", translate("Forwarding mode")) - payload:value("bridged", translate("bridged")) - payload:value("routed", translate("routed")) - m.pageaction = true -end - -local network = require "luci.model.network" -if network:has_ipv6() then - local s = m:section(NamedSection, "globals", "globals", translate("Global network options")) - local o = s:option(Value, "ula_prefix", translate("IPv6 ULA-Prefix")) - o.datatype = "ip6addr" - o.rmempty = true - m.pageaction = true -end - - -return m diff --git a/modules/luci-mod-network/luasrc/model/cbi/admin_network/vlan.lua b/modules/luci-mod-network/luasrc/model/cbi/admin_network/vlan.lua deleted file mode 100644 index edeb193ef7..0000000000 --- a/modules/luci-mod-network/luasrc/model/cbi/admin_network/vlan.lua +++ /dev/null @@ -1,389 +0,0 @@ --- Copyright 2008 Steven Barth <steven@midlink.org> --- Copyright 2010-2011 Jo-Philipp Wich <jow@openwrt.org> --- Licensed to the public under the Apache License 2.0. - -m = Map("network", translate("Switch"), translate("The network ports on this device can be combined to several <abbr title=\"Virtual Local Area Network\">VLAN</abbr>s in which computers can communicate directly with each other. <abbr title=\"Virtual Local Area Network\">VLAN</abbr>s 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.")) - -local fs = require "nixio.fs" -local ut = require "luci.util" -local nw = require "luci.model.network" -local switches = { } - -nw.init(m.uci) - -local topologies = nw:get_switch_topologies() or {} - -local update_interfaces = function(old_ifname, new_ifname) - local info = { } - - m.uci:foreach("network", "interface", function(section) - local old_ifnames = section.ifname - local new_ifnames = { } - local cur_ifname - local changed = false - for cur_ifname in luci.util.imatch(old_ifnames) do - if cur_ifname == old_ifname then - new_ifnames[#new_ifnames+1] = new_ifname - changed = true - else - new_ifnames[#new_ifnames+1] = cur_ifname - end - end - if changed then - m.uci:set("network", section[".name"], "ifname", table.concat(new_ifnames, " ")) - - info[#info+1] = translatef("Interface %q device auto-migrated from %q to %q.", - section[".name"], old_ifname, new_ifname) - end - end) - - if #info > 0 then - m.message = (m.message and m.message .. "\n" or "") .. table.concat(info, "\n") - end -end - -local vlan_already_created - -m.uci:foreach("network", "switch", - function(x) - local sid = x['.name'] - local switch_name = x.name or sid - local has_vlan = nil - local has_learn = nil - local has_vlan4k = nil - local has_jumbo3 = nil - local has_mirror = nil - local min_vid = 0 - local max_vid = 16 - local num_vlans = 16 - - local switch_title - local enable_vlan4k = false - - local topo = topologies[switch_name] - - if not topo then - m.message = translatef("Switch %q has an unknown topology - the VLAN settings might not be accurate.", switch_name) - topo = { - 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)", tagged = false } - } - } - end - - -- Parse some common switch properties from swconfig help output. - local swc = io.popen("swconfig dev %s help 2>/dev/null" % ut.shellquote(switch_name)) - if swc then - - local is_port_attr = false - local is_vlan_attr = false - - while true do - local line = swc:read("*l") - if not line then break end - - if line:match("^%s+%-%-vlan") then - is_vlan_attr = true - - elseif line:match("^%s+%-%-port") then - is_vlan_attr = false - is_port_attr = true - - elseif line:match("cpu @") then - switch_title = line:match("^switch%d: %w+%((.-)%)") - num_vlans = tonumber(line:match("vlans: (%d+)")) or 16 - min_vid = 1 - - elseif line:match(": pvid") or line:match(": tag") or line:match(": vid") then - if is_vlan_attr then has_vlan4k = line:match(": (%w+)") end - - elseif line:match(": enable_vlan4k") then - enable_vlan4k = true - - elseif line:match(": enable_vlan") then - has_vlan = "enable_vlan" - - elseif line:match(": enable_learning") then - has_learn = "enable_learning" - - elseif line:match(": enable_mirror_rx") then - has_mirror = "enable_mirror_rx" - - elseif line:match(": max_length") then - has_jumbo3 = "max_length" - end - end - - swc:close() - end - - - -- Switch properties - s = m:section(NamedSection, x['.name'], "switch", - switch_title and translatef("Switch %q (%s)", switch_name, switch_title) - or translatef("Switch %q", switch_name)) - - s.addremove = false - - if has_vlan then - s:option(Flag, has_vlan, translate("Enable VLAN functionality")) - end - - if has_learn then - x = s:option(Flag, has_learn, translate("Enable learning and aging")) - x.default = x.enabled - end - - if has_jumbo3 then - x = s:option(Flag, has_jumbo3, translate("Enable Jumbo Frame passthrough")) - x.enabled = "3" - x.rmempty = true - end - - -- Does this switch support port mirroring? - if has_mirror then - s:option(Flag, "enable_mirror_rx", translate("Enable mirroring of incoming packets")) - s:option(Flag, "enable_mirror_tx", translate("Enable mirroring of outgoing packets")) - - local sp = s:option(ListValue, "mirror_source_port", translate("Mirror source port")) - local mp = s:option(ListValue, "mirror_monitor_port", translate("Mirror monitor port")) - - sp:depends("enable_mirror_tx", "1") - sp:depends("enable_mirror_rx", "1") - - mp:depends("enable_mirror_tx", "1") - mp:depends("enable_mirror_rx", "1") - - local _, pt - for _, pt in ipairs(topo.ports) do - sp:value(pt.num, pt.label) - mp:value(pt.num, pt.label) - end - end - - -- VLAN table - s = m:section(TypedSection, "switch_vlan", - switch_title and translatef("VLANs on %q (%s)", switch_name, switch_title) - or translatef("VLANs on %q", switch_name)) - - s.template = "cbi/tblsection" - s.addremove = true - s.anonymous = true - - -- Filter by switch - s.filter = function(self, section) - local device = m:get(section, "device") - return (device and device == switch_name) - end - - -- Override cfgsections callback to enforce row ordering by vlan id. - s.cfgsections = function(self) - local osections = TypedSection.cfgsections(self) - local sections = { } - local section - - for _, section in luci.util.spairs( - osections, - function(a, b) - return (tonumber(m:get(osections[a], has_vlan4k or "vlan")) or 9999) - < (tonumber(m:get(osections[b], has_vlan4k or "vlan")) or 9999) - end - ) do - sections[#sections+1] = section - end - - return sections - end - - -- When creating a new vlan, preset it with the highest found vid + 1. - s.create = function(self, section, origin) - -- VLAN has already been created for another switch - if vlan_already_created then - return - - -- VLAN add button was pressed in an empty VLAN section so only - -- accept the create event if our switch is without existing VLANs - elseif origin == "" then - local is_empty_switch = true - - m.uci:foreach("network", "switch_vlan", - function(s) - if s.device == switch_name then - is_empty_switch = false - return false - end - end) - - if not is_empty_switch then - return - end - - -- VLAN was created for another switch - elseif m:get(origin, "device") ~= switch_name then - return - end - - local sid = TypedSection.create(self, section) - - local max_nr = 0 - local max_id = 0 - - m.uci:foreach("network", "switch_vlan", - function(s) - if s.device == switch_name then - local nr = tonumber(s.vlan) - local id = has_vlan4k and tonumber(s[has_vlan4k]) - if nr ~= nil and nr > max_nr then max_nr = nr end - if id ~= nil and id > max_id then max_id = id end - end - end) - - m:set(sid, "device", switch_name) - m:set(sid, "vlan", max_nr + 1) - - if has_vlan4k then - m:set(sid, has_vlan4k, max_id + 1) - end - - vlan_already_created = true - - return sid - end - - - local port_opts = { } - local untagged = { } - - -- Parse current tagging state from the "ports" option. - local portvalue = function(self, section) - local pt - for pt in (m:get(section, "ports") or ""):gmatch("%w+") do - local pc, tu = pt:match("^(%d+)([tu]*)") - if pc == self.option then return (#tu > 0) and tu or "u" end - end - return "" - end - - -- Validate port tagging. Ensure that a port is only untagged once, - -- bail out if not. - local portvalidate = function(self, value, section) - -- ensure that the ports appears untagged only once - if value == "u" then - if not untagged[self.option] then - untagged[self.option] = true - else - return nil, - translatef("%s is untagged in multiple VLANs!", self.title) - end - end - return value - end - - - local vid = s:option(Value, has_vlan4k or "vlan", "VLAN ID") - local mx_vid = has_vlan4k and 4094 or (num_vlans - 1) - - vid.rmempty = false - vid.forcewrite = true - vid.vlan_used = { } - vid.datatype = "and(uinteger,range("..min_vid..","..mx_vid.."))" - - -- Validate user provided VLAN ID, make sure its within the bounds - -- allowed by the switch. - vid.validate = function(self, value, section) - local v = tonumber(value) - local m = has_vlan4k and 4094 or (num_vlans - 1) - if v ~= nil and v >= min_vid and v <= m then - if not self.vlan_used[v] then - self.vlan_used[v] = true - return value - else - return nil, - translatef("Invalid VLAN ID given! Only unique IDs are allowed") - end - else - return nil, - translatef("Invalid VLAN ID given! Only IDs between %d and %d are allowed.", min_vid, m) - end - end - - -- When writing the "vid" or "vlan" option, serialize the port states - -- as well and write them as "ports" option to uci. - vid.write = function(self, section, new_vid) - local o - local p = { } - for _, o in ipairs(port_opts) do - local new_tag = o:formvalue(section) - if new_tag == "t" then - p[#p+1] = o.option .. new_tag - elseif new_tag == "u" then - p[#p+1] = o.option - end - - if o.info and o.info.device then - local old_tag = o:cfgvalue(section) - local old_vid = self:cfgvalue(section) - if old_tag ~= new_tag or old_vid ~= new_vid then - local old_ifname = (old_tag == "u") and o.info.device - or "%s.%s" %{ o.info.device, old_vid } - - local new_ifname = (new_tag == "u") and o.info.device - or "%s.%s" %{ o.info.device, new_vid } - - if old_ifname ~= new_ifname then - update_interfaces(old_ifname, new_ifname) - end - end - end - end - - if enable_vlan4k then - m:set(sid, "enable_vlan4k", "1") - end - - m:set(section, "ports", table.concat(p, " ")) - return Value.write(self, section, new_vid) - end - - -- Fallback to "vlan" option if "vid" option is supported but unset. - vid.cfgvalue = function(self, section) - return m:get(section, has_vlan4k or "vlan") - or m:get(section, "vlan") - end - - local _, pt - for _, pt in ipairs(topo.ports) do - local po = s:option(ListValue, tostring(pt.num), pt.label) - - po:value("", translate("off")) - - if not pt.tagged then - po:value("u", translate("untagged")) - end - - po:value("t", translate("tagged")) - - po.cfgvalue = portvalue - po.validate = portvalidate - po.write = function() end - po.info = pt - - port_opts[#port_opts+1] = po - end - - table.sort(port_opts, function(a, b) return a.option < b.option end) - switches[#switches+1] = switch_name - end -) - --- Switch status template -s = m:section(SimpleSection) -s.template = "admin_network/switch_status" -s.switches = switches - -return m diff --git a/modules/luci-mod-network/luasrc/model/cbi/admin_network/wifi.lua b/modules/luci-mod-network/luasrc/model/cbi/admin_network/wifi.lua deleted file mode 100644 index 2e6c026bb2..0000000000 --- a/modules/luci-mod-network/luasrc/model/cbi/admin_network/wifi.lua +++ /dev/null @@ -1,1216 +0,0 @@ --- Copyright 2008 Steven Barth <steven@midlink.org> --- Licensed to the public under the Apache License 2.0. - -local wa = require "luci.tools.webadmin" -local nw = require "luci.model.network" -local ut = require "luci.util" -local nt = require "luci.sys".net -local fs = require "nixio.fs" - -local acct_port, acct_secret, acct_server, anonymous_identity, ant1, ant2, - auth, auth_port, auth_secret, auth_server, bssid, cacert, cacert2, - cc, ch, cipher, clientcert, clientcert2, ea, eaptype, en, encr, - ft_protocol, ft_psk_generate_local, hidden, htmode, identity, - ieee80211r, ieee80211w, ifname, isolate, key_retries, - legacyrates, max_timeout, meshfwd, meshid, ml, mobility_domain, mode, - mp, nasid, network, password, pmk_r1_push, privkey, privkey2, privkeypwd, - privkeypwd2, r0_key_lifetime, r0kh, r1_key_holder, r1kh, - reassociation_deadline, retry_timeout, ssid, st, tp, wepkey, wepslot, - wmm, wpakey, wps, disassoc_low_ack, short_preamble, beacon_int, dtim_period, - wparekey, inactivitypool, maxinactivity, listeninterval, - dae_client, dae_port, dae_port - - -arg[1] = arg[1] or "" - -m = Map("wireless", "", - translate("The <em>Device Configuration</em> section covers physical settings of the radio " .. - "hardware such as channel, transmit power or antenna selection which are shared among all " .. - "defined wireless networks (if the radio hardware is multi-SSID capable). Per network settings " .. - "like encryption or operation mode are grouped in the <em>Interface Configuration</em>.")) - -m:chain("network") -m:chain("firewall") -m.redirect = luci.dispatcher.build_url("admin/network/wireless") - -nw.init(m.uci) - -local wnet = nw:get_wifinet(arg[1]) -local wdev = wnet and wnet:get_device() - --- redirect to overview page if network does not exist anymore (e.g. after a revert) -if not wnet or not wdev then - luci.http.redirect(luci.dispatcher.build_url("admin/network/wireless")) - return -end - -local function txpower_list(iw) - local list = iw.txpwrlist or { } - local off = tonumber(iw.txpower_offset) or 0 - local new = { } - local prev = -1 - local _, val - for _, val in ipairs(list) do - local dbm = val.dbm + off - local mw = math.floor(10 ^ (dbm / 10)) - if mw ~= prev then - prev = mw - new[#new+1] = { - display_dbm = dbm, - display_mw = mw, - driver_dbm = val.dbm, - driver_mw = val.mw - } - end - end - return new -end - -local function txpower_current(pwr, list) - pwr = tonumber(pwr) - if pwr ~= nil then - local _, item - for _, item in ipairs(list) do - if item.driver_dbm >= pwr then - return item.driver_dbm - end - end - end - return pwr or "" -end - -local iw = luci.sys.wifi.getiwinfo(arg[1]) -local hw_modes = iw.hwmodelist or { } -local tx_power_list = txpower_list(iw) -local tx_power_cur = txpower_current(wdev:get("txpower"), tx_power_list) - --- wireless toggle was requested, commit and reload page -function m.parse(map) - local new_cc = m:formvalue("cbid.wireless.%s.country" % wdev:name()) - local old_cc = m:get(wdev:name(), "country") - - if m:formvalue("cbid.wireless.%s.__toggle" % wdev:name()) then - if wdev:get("disabled") == "1" or wnet:get("disabled") == "1" then - wnet:set("disabled", nil) - else - wnet:set("disabled", "1") - end - wdev:set("disabled", nil) - m.apply_needed = true - m.redirect = nil - end - - Map.parse(map) - - if m:get(wdev:name(), "type") == "mac80211" and new_cc and new_cc ~= old_cc then - luci.sys.call("iw reg set %s" % ut.shellquote(new_cc)) - - local old_ch = tonumber(m:formvalue("cbid.wireless.%s._mode_freq.channel" % wdev:name()) or "") - if old_ch then - local _, c, new_ch - for _, c in ipairs(iw.freqlist) do - if c.channel > old_ch or (old_ch <= 14 and c.channel > 14) then - break - end - new_ch = c.channel - end - if new_ch ~= old_ch then - wdev:set("channel", new_ch) - m.message = translatef("Channel %d is not available in the %s regulatory domain and has been auto-adjusted to %d.", - old_ch, new_cc, new_ch) - end - end - end - - if wdev:get("disabled") == "1" or wnet:get("disabled") == "1" then - en.title = translate("Wireless network is disabled") - en.inputtitle = translate("Enable") - en.inputstyle = "apply" - else - en.title = translate("Wireless network is enabled") - en.inputtitle = translate("Disable") - en.inputstyle = "reset" - end -end - -m.title = luci.util.pcdata(wnet:get_i18n()) - -s = m:section(NamedSection, wdev:name(), "wifi-device", translate("Device Configuration")) -s.addremove = false - -s:tab("general", translate("General Setup")) -s:tab("macfilter", translate("MAC-Filter")) -s:tab("advanced", translate("Advanced Settings")) - -st = s:taboption("general", DummyValue, "__status", translate("Status")) -st.template = "admin_network/wifi_status" -st.ifname = arg[1] - -en = s:taboption("general", Button, "__toggle") - -local hwtype = wdev:get("type") - --- NanoFoo -local nsantenna = wdev:get("antenna") - --- Check whether there are client interfaces on the same radio, --- if yes, lock the channel choice as these stations will dicatate the freq -local found_sta = nil -local _, net -if wnet:mode() ~= "sta" then - for _, net in ipairs(wdev:get_wifinets()) do - if net:mode() == "sta" and net:get("disabled") ~= "1" then - if not found_sta then - found_sta = {} - found_sta.channel = net:channel() - found_sta.names = {} - end - found_sta.names[#found_sta.names+1] = net:shortname() - end - end -end - -if found_sta then - ch = s:taboption("general", DummyValue, "choice", translate("Channel")) - ch.value = translatef("Locked to channel %s used by: %s", - found_sta.channel or "(auto)", table.concat(found_sta.names, ", ")) -else - ch = s:taboption("general", Value, "_mode_freq", '<br />'..translate("Operating frequency")) - ch.iwinfo = iw - ch.hostapd_acs = (os.execute("hostapd -vacs >/dev/null 2>/dev/null") == 0) - ch.template = "cbi/wireless_modefreq" - - function ch.cfgvalue(self, section) - return { - m:get(section, "hwmode") or "", - m:get(section, "channel") or "auto", - m:get(section, "htmode") or "" - } - end - - function ch.formvalue(self, section) - return { - m:formvalue(self:cbid(section) .. ".band") or (hw_modes.g and "11g" or "11a"), - m:formvalue(self:cbid(section) .. ".channel") or "auto", - m:formvalue(self:cbid(section) .. ".htmode") or "" - } - end - - function ch.write(self, section, value) - m:set(section, "hwmode", value[1]) - m:set(section, "channel", value[2]) - m:set(section, "htmode", value[3]) - end -end - -------------------- MAC80211 Device ------------------ - -if hwtype == "mac80211" then - if #tx_power_list > 0 then - tp = s:taboption("general", ListValue, - "txpower", translate("Transmit Power"), "dBm") - tp.rmempty = true - tp.default = tx_power_cur - function tp.cfgvalue(...) - return txpower_current(Value.cfgvalue(...), tx_power_list) - end - - tp:value("", translate("auto")) - for _, p in ipairs(tx_power_list) do - tp:value(p.driver_dbm, "%i dBm (%i mW)" - %{ p.display_dbm, p.display_mw }) - end - end - - local cl = iw and iw.countrylist - if cl and #cl > 0 then - cc = s:taboption("advanced", ListValue, "country", translate("Country Code"), translate("Use ISO/IEC 3166 alpha2 country codes.")) - cc.default = tostring(iw and iw.country or "00") - for _, c in ipairs(cl) do - cc:value(c.alpha2, "%s - %s" %{ c.alpha2, c.name }) - end - else - s:taboption("advanced", Value, "country", translate("Country Code"), translate("Use ISO/IEC 3166 alpha2 country codes.")) - end - - legacyrates = s:taboption("advanced", Flag, "legacy_rates", translate("Allow legacy 802.11b rates")) - legacyrates.rmempty = false - legacyrates.default = "1" - - s:taboption("advanced", Value, "distance", translate("Distance Optimization"), - translate("Distance to farthest network member in meters.")) - - -- external antenna profiles - local eal = iw and iw.extant - if eal and #eal > 0 then - ea = s:taboption("advanced", ListValue, "extant", translate("Antenna Configuration")) - for _, eap in ipairs(eal) do - ea:value(eap.id, "%s (%s)" %{ eap.name, eap.description }) - if eap.selected then - ea.default = eap.id - end - end - end - - s:taboption("advanced", Value, "frag", translate("Fragmentation Threshold")) - s:taboption("advanced", Value, "rts", translate("RTS/CTS Threshold")) - - s:taboption("advanced", Flag, "noscan", translate("Force 40MHz mode"), - translate("Always use 40MHz channels even if the secondary channel overlaps. Using this option does not comply with IEEE 802.11n-2009!")).optional = true - - beacon_int = s:taboption("advanced", Value, "beacon_int", translate("Beacon Interval")) - beacon_int.optional = true - beacon_int.placeholder = 100 - beacon_int.datatype = "range(15,65535)" -end - - -------------------- Broadcom Device ------------------ - -if hwtype == "broadcom" then - tp = s:taboption("general", - (#tx_power_list > 0) and ListValue or Value, - "txpower", translate("Transmit Power"), "dBm") - - tp.rmempty = true - tp.default = tx_power_cur - - function tp.cfgvalue(...) - return txpower_current(Value.cfgvalue(...), tx_power_list) - end - - tp:value("", translate("auto")) - for _, p in ipairs(tx_power_list) do - tp:value(p.driver_dbm, "%i dBm (%i mW)" - %{ p.display_dbm, p.display_mw }) - end - - mode = s:taboption("advanced", ListValue, "hwmode", translate("Band")) - if hw_modes.b then - mode:value("11b", "2.4GHz (802.11b)") - if hw_modes.g then - mode:value("11bg", "2.4GHz (802.11b+g)") - end - end - if hw_modes.g then - mode:value("11g", "2.4GHz (802.11g)") - mode:value("11gst", "2.4GHz (802.11g + Turbo)") - mode:value("11lrs", "2.4GHz (802.11g Limited Rate Support)") - end - if hw_modes.a then mode:value("11a", "5GHz (802.11a)") end - if hw_modes.n then - if hw_modes.g then - mode:value("11ng", "2.4GHz (802.11g+n)") - mode:value("11n", "2.4GHz (802.11n)") - end - if hw_modes.a then - mode:value("11na", "5GHz (802.11a+n)") - mode:value("11n", "5GHz (802.11n)") - end - htmode = s:taboption("advanced", ListValue, "htmode", translate("HT mode (802.11n)")) - htmode:depends("hwmode", "11ng") - htmode:depends("hwmode", "11na") - htmode:depends("hwmode", "11n") - htmode:value("HT20", "20MHz") - htmode:value("HT40", "40MHz") - end - - ant1 = s:taboption("advanced", ListValue, "txantenna", translate("Transmitter Antenna")) - ant1.widget = "radio" - ant1:depends("diversity", "") - ant1:value("3", translate("auto")) - ant1:value("0", translate("Antenna 1")) - ant1:value("1", translate("Antenna 2")) - - ant2 = s:taboption("advanced", ListValue, "rxantenna", translate("Receiver Antenna")) - ant2.widget = "radio" - ant2:depends("diversity", "") - ant2:value("3", translate("auto")) - ant2:value("0", translate("Antenna 1")) - ant2:value("1", translate("Antenna 2")) - - s:taboption("advanced", Flag, "frameburst", translate("Frame Bursting")) - - s:taboption("advanced", Value, "distance", translate("Distance Optimization")) - --s:option(Value, "slottime", translate("Slot time")) - - s:taboption("advanced", Value, "country", translate("Country Code")) - s:taboption("advanced", Value, "maxassoc", translate("Connection Limit")) -end - - ---------------------- HostAP Device --------------------- - -if hwtype == "prism2" then - s:taboption("advanced", Value, "txpower", translate("Transmit Power"), "att units").rmempty = true - - s:taboption("advanced", Flag, "diversity", translate("Diversity")).rmempty = false - - s:taboption("advanced", Value, "txantenna", translate("Transmitter Antenna")) - s:taboption("advanced", Value, "rxantenna", translate("Receiver Antenna")) -end - - ------------------------ Interface ----------------------- - -s = m:section(NamedSection, wnet.sid, "wifi-iface", translate("Interface Configuration")) -s.addremove = false -s.anonymous = true -s.defaults.device = wdev:name() - -s:tab("general", translate("General Setup")) -s:tab("encryption", translate("Wireless Security")) -s:tab("macfilter", translate("MAC-Filter")) -s:tab("advanced", translate("Advanced Settings")) - -mode = s:taboption("general", ListValue, "mode", translate("Mode")) -mode.override_values = true -mode:value("ap", translate("Access Point")) -mode:value("sta", translate("Client")) -mode:value("adhoc", translate("Ad-Hoc")) - -meshid = s:taboption("general", Value, "mesh_id", translate("Mesh Id")) -meshid:depends({mode="mesh"}) - -meshfwd = s:taboption("advanced", Flag, "mesh_fwding", translate("Forward mesh peer traffic")) -meshfwd.rmempty = false -meshfwd.default = "1" -meshfwd:depends({mode="mesh"}) - -mesh_rssi_th = s:taboption("advanced", Value, "mesh_rssi_threshold", - translate("RSSI threshold for joining"), - translate("0 = not using RSSI threshold, 1 = do not change driver default")) -mesh_rssi_th.rmempty = false -mesh_rssi_th.default = "0" -mesh_rssi_th.datatype = "range(-255,1)" -mesh_rssi_th:depends({mode="mesh"}) - -ssid = s:taboption("general", Value, "ssid", translate("<abbr title=\"Extended Service Set Identifier\">ESSID</abbr>")) -ssid.datatype = "maxlength(32)" -ssid:depends({mode="ap"}) -ssid:depends({mode="sta"}) -ssid:depends({mode="adhoc"}) -ssid:depends({mode="ahdemo"}) -ssid:depends({mode="monitor"}) -ssid:depends({mode="ap-wds"}) -ssid:depends({mode="sta-wds"}) -ssid:depends({mode="wds"}) - -bssid = s:taboption("general", Value, "bssid", translate("<abbr title=\"Basic Service Set Identifier\">BSSID</abbr>")) -bssid.datatype = "macaddr" - -network = s:taboption("general", Value, "network", translate("Network"), - translate("Choose the network(s) you want to attach to this wireless interface or " .. - "fill out the <em>create</em> field to define a new network.")) - -network.rmempty = true -network.template = "cbi/network_netlist" -network.widget = "checkbox" -network.novirtual = true - -function network.write(self, section, value) - local i = nw:get_interface(section) - if i then - local _, net, old, new = nil, nil, {}, {} - - for _, net in ipairs(i:get_networks()) do - old[net:name()] = true - end - - for net in ut.imatch(value) do - new[net] = true - if not old[net] then - local n = nw:get_network(net) or nw:add_network(net, { proto = "none" }) - if n then - if not n:is_empty() then - n:set("type", "bridge") - end - n:add_interface(i) - end - end - end - - for net, _ in pairs(old) do - if not new[net] then - local n = nw:get_network(net) - if n then - n:del_interface(i) - end - end - end - end -end - --------------------- MAC80211 Interface ---------------------- - -if hwtype == "mac80211" then - if fs.access("/usr/sbin/iw") then - mode:value("mesh", "802.11s") - end - - mode:value("ahdemo", translate("Pseudo Ad-Hoc (ahdemo)")) - mode:value("monitor", translate("Monitor")) - bssid:depends({mode="adhoc"}) - bssid:depends({mode="sta"}) - bssid:depends({mode="sta-wds"}) - - mp = s:taboption("macfilter", ListValue, "macfilter", translate("MAC-Address Filter")) - mp:depends({mode="ap"}) - mp:depends({mode="ap-wds"}) - mp:value("", translate("disable")) - mp:value("allow", translate("Allow listed only")) - mp:value("deny", translate("Allow all except listed")) - - ml = s:taboption("macfilter", DynamicList, "maclist", translate("MAC-List")) - ml.datatype = "macaddr" - ml:depends({macfilter="allow"}) - ml:depends({macfilter="deny"}) - nt.mac_hints(function(mac, name) ml:value(mac, "%s (%s)" %{ mac, name }) end) - - mode:value("ap-wds", "%s (%s)" % {translate("Access Point"), translate("WDS")}) - mode:value("sta-wds", "%s (%s)" % {translate("Client"), translate("WDS")}) - - function mode.write(self, section, value) - if value == "ap-wds" then - ListValue.write(self, section, "ap") - m.uci:set("wireless", section, "wds", 1) - elseif value == "sta-wds" then - ListValue.write(self, section, "sta") - m.uci:set("wireless", section, "wds", 1) - else - ListValue.write(self, section, value) - m.uci:delete("wireless", section, "wds") - end - end - - function mode.cfgvalue(self, section) - local mode = ListValue.cfgvalue(self, section) - local wds = m.uci:get("wireless", section, "wds") == "1" - - if mode == "ap" and wds then - return "ap-wds" - elseif mode == "sta" and wds then - return "sta-wds" - else - return mode - end - end - - hidden = s:taboption("general", Flag, "hidden", translate("Hide <abbr title=\"Extended Service Set Identifier\">ESSID</abbr>")) - hidden:depends({mode="ap"}) - hidden:depends({mode="ap-wds"}) - - wmm = s:taboption("general", Flag, "wmm", translate("WMM Mode")) - wmm:depends({mode="ap"}) - wmm:depends({mode="ap-wds"}) - wmm.default = wmm.enabled - - isolate = s:taboption("advanced", Flag, "isolate", translate("Isolate Clients"), - translate("Prevents client-to-client communication")) - isolate:depends({mode="ap"}) - isolate:depends({mode="ap-wds"}) - - ifname = s:taboption("advanced", Value, "ifname", translate("Interface name"), translate("Override default interface name")) - ifname.optional = true - - short_preamble = s:taboption("advanced", Flag, "short_preamble", translate("Short Preamble")) - short_preamble.default = short_preamble.enabled - - dtim_period = s:taboption("advanced", Value, "dtim_period", translate("DTIM Interval"), translate("Delivery Traffic Indication Message Interval")) - dtim_period.optional = true - dtim_period.placeholder = 2 - dtim_period.datatype = "range(1,255)" - - - wparekey = s:taboption("advanced", Value, "wpa_group_rekey", translate("Time interval for rekeying GTK"), translate("sec")) - wparekey.optional = true - wparekey.placeholder = 600 - wparekey.datatype = "uinteger" - - inactivitypool = s:taboption("advanced", Flag , "skip_inactivity_poll", translate("Disable Inactivity Polling")) - inactivitypool.optional = true - inactivitypool.datatype = "uinteger" - - maxinactivity = s:taboption("advanced", Value, "max_inactivity", translate("Station inactivity limit"), translate("sec")) - maxinactivity.optional = true - maxinactivity.placeholder = 300 - maxinactivity.datatype = "uinteger" - - listeninterval = s:taboption("advanced", Value, "max_listen_interval", translate("Maximum allowed Listen Interval")) - listeninterval.optional = true - listeninterval.placeholder = 65535 - listeninterval.datatype = "uinteger" - - disassoc_low_ack = s:taboption("advanced", Flag, "disassoc_low_ack", translate("Disassociate On Low Acknowledgement"), - translate("Allow AP mode to disconnect STAs based on low ACK condition")) - disassoc_low_ack.default = disassoc_low_ack.enabled -end - - --------------------- Broadcom Interface ---------------------- - -if hwtype == "broadcom" then - mode:value("wds", translate("WDS")) - mode:value("monitor", translate("Monitor")) - - hidden = s:taboption("general", Flag, "hidden", translate("Hide <abbr title=\"Extended Service Set Identifier\">ESSID</abbr>")) - hidden:depends({mode="ap"}) - hidden:depends({mode="adhoc"}) - hidden:depends({mode="wds"}) - - isolate = s:taboption("advanced", Flag, "isolate", translate("Separate Clients"), - translate("Prevents client-to-client communication")) - isolate:depends({mode="ap"}) - - s:taboption("advanced", Flag, "doth", "802.11h") - s:taboption("advanced", Flag, "wmm", translate("WMM Mode")) - - bssid:depends({mode="wds"}) - bssid:depends({mode="adhoc"}) -end - - ------------------------ HostAP Interface --------------------- - -if hwtype == "prism2" then - mode:value("wds", translate("WDS")) - mode:value("monitor", translate("Monitor")) - - hidden = s:taboption("general", Flag, "hidden", translate("Hide <abbr title=\"Extended Service Set Identifier\">ESSID</abbr>")) - hidden:depends({mode="ap"}) - hidden:depends({mode="adhoc"}) - hidden:depends({mode="wds"}) - - bssid:depends({mode="sta"}) - - mp = s:taboption("macfilter", ListValue, "macpolicy", translate("MAC-Address Filter")) - mp:value("", translate("disable")) - mp:value("allow", translate("Allow listed only")) - mp:value("deny", translate("Allow all except listed")) - ml = s:taboption("macfilter", DynamicList, "maclist", translate("MAC-List")) - ml:depends({macpolicy="allow"}) - ml:depends({macpolicy="deny"}) - nt.mac_hints(function(mac, name) ml:value(mac, "%s (%s)" %{ mac, name }) end) - - s:taboption("advanced", Value, "rate", translate("Transmission Rate")) - s:taboption("advanced", Value, "frag", translate("Fragmentation Threshold")) - s:taboption("advanced", Value, "rts", translate("RTS/CTS Threshold")) -end - - -------------------- WiFI-Encryption ------------------- - -encr = s:taboption("encryption", ListValue, "encryption", translate("Encryption")) -encr.override_values = true -encr.override_depends = true -encr:depends({mode="ap"}) -encr:depends({mode="sta"}) -encr:depends({mode="adhoc"}) -encr:depends({mode="ahdemo"}) -encr:depends({mode="ap-wds"}) -encr:depends({mode="sta-wds"}) -encr:depends({mode="mesh"}) - -cipher = s:taboption("encryption", ListValue, "cipher", translate("Cipher")) -cipher:depends({encryption="wpa"}) -cipher:depends({encryption="wpa2"}) -cipher:depends({encryption="psk"}) -cipher:depends({encryption="psk2"}) -cipher:depends({encryption="wpa-mixed"}) -cipher:depends({encryption="psk-mixed"}) -cipher:value("auto", translate("auto")) -cipher:value("ccmp", translate("Force CCMP (AES)")) -cipher:value("tkip", translate("Force TKIP")) -cipher:value("tkip+ccmp", translate("Force TKIP and CCMP (AES)")) - -function encr.cfgvalue(self, section) - local v = tostring(ListValue.cfgvalue(self, section)) - if v == "wep" then - return "wep-open" - elseif v and v:match("%+") then - return (v:gsub("%+.+$", "")) - end - return v -end - -function encr.write(self, section, value) - local e = tostring(encr:formvalue(section)) - local c = tostring(cipher:formvalue(section)) - if value == "wpa" or value == "wpa2" then - self.map.uci:delete("wireless", section, "key") - end - if e and (c == "tkip" or c == "ccmp" or c == "tkip+ccmp") then - e = e .. "+" .. c - end - self.map:set(section, "encryption", e) -end - -function cipher.cfgvalue(self, section) - local v = tostring(ListValue.cfgvalue(encr, section)) - if v and v:match("%+") then - v = v:gsub("^[^%+]+%+", "") - if v == "aes" then v = "ccmp" - elseif v == "tkip+aes" then v = "tkip+ccmp" - elseif v == "aes+tkip" then v = "tkip+ccmp" - elseif v == "ccmp+tkip" then v = "tkip+ccmp" - end - end - return v -end - -function cipher.write(self, section) - return encr:write(section) -end - - -encr:value("none", "No Encryption") -encr:value("wep-open", translate("WEP Open System"), {mode="ap"}, {mode="sta"}, {mode="ap-wds"}, {mode="sta-wds"}, {mode="adhoc"}, {mode="ahdemo"}, {mode="wds"}) -encr:value("wep-shared", translate("WEP Shared Key"), {mode="ap"}, {mode="sta"}, {mode="ap-wds"}, {mode="sta-wds"}, {mode="adhoc"}, {mode="ahdemo"}, {mode="wds"}) - -if hwtype == "mac80211" or hwtype == "prism2" then - local supplicant = fs.access("/usr/sbin/wpa_supplicant") - local hostapd = fs.access("/usr/sbin/hostapd") - - -- Probe EAP support - local has_ap_eap = (os.execute("hostapd -veap >/dev/null 2>/dev/null") == 0) - local has_sta_eap = (os.execute("wpa_supplicant -veap >/dev/null 2>/dev/null") == 0) - - -- Probe SAE support - local has_ap_sae = (os.execute("hostapd -vsae >/dev/null 2>/dev/null") == 0) - local has_sta_sae = (os.execute("wpa_supplicant -vsae >/dev/null 2>/dev/null") == 0) - - -- Probe OWE support - local has_ap_owe = (os.execute("hostapd -vowe >/dev/null 2>/dev/null") == 0) - local has_sta_owe = (os.execute("wpa_supplicant -vowe >/dev/null 2>/dev/null") == 0) - - if hostapd and supplicant then - encr:value("psk", "WPA-PSK", {mode="ap"}, {mode="sta"}, {mode="ap-wds"}, {mode="sta-wds"}, {mode="adhoc"}) - encr:value("psk2", "WPA2-PSK", {mode="ap"}, {mode="sta"}, {mode="ap-wds"}, {mode="sta-wds"}, {mode="adhoc"}) - encr:value("psk-mixed", "WPA-PSK/WPA2-PSK Mixed Mode", {mode="ap"}, {mode="sta"}, {mode="ap-wds"}, {mode="sta-wds"}, {mode="adhoc"}) - if has_ap_sae and has_sta_sae then - encr:value("sae", "WPA3-SAE", {mode="ap"}, {mode="sta"}, {mode="ap-wds"}, {mode="sta-wds"}, {mode="adhoc"}, {mode="mesh"}) - encr:value("sae-mixed", "WPA2-PSK/WPA3-SAE Mixed Mode", {mode="ap"}, {mode="sta"}, {mode="ap-wds"}, {mode="sta-wds"}, {mode="adhoc"}) - end - if has_ap_eap and has_sta_eap then - encr:value("wpa", "WPA-EAP", {mode="ap"}, {mode="sta"}, {mode="ap-wds"}, {mode="sta-wds"}) - encr:value("wpa2", "WPA2-EAP", {mode="ap"}, {mode="sta"}, {mode="ap-wds"}, {mode="sta-wds"}) - end - if has_ap_owe and has_sta_owe then - encr:value("owe", "OWE", {mode="ap"}, {mode="sta"}, {mode="ap-wds"}, {mode="sta-wds"}, {mode="adhoc"}) - end - elseif hostapd and not supplicant then - encr:value("psk", "WPA-PSK", {mode="ap"}, {mode="ap-wds"}) - encr:value("psk2", "WPA2-PSK", {mode="ap"}, {mode="ap-wds"}) - encr:value("psk-mixed", "WPA-PSK/WPA2-PSK Mixed Mode", {mode="ap"}, {mode="ap-wds"}) - if has_ap_sae then - encr:value("sae", "WPA3-SAE", {mode="ap"}, {mode="ap-wds"}) - encr:value("sae-mixed", "WPA2-PSK/WPA3-SAE Mixed Mode", {mode="ap"}, {mode="ap-wds"}) - end - if has_ap_eap then - encr:value("wpa", "WPA-EAP", {mode="ap"}, {mode="ap-wds"}) - encr:value("wpa2", "WPA2-EAP", {mode="ap"}, {mode="ap-wds"}) - end - if has_ap_owe then - encr:value("owe", "OWE", {mode="ap"}, {mode="ap-wds"}) - end - encr.description = translate( - "WPA-Encryption requires wpa_supplicant (for client mode) or hostapd (for AP " .. - "and ad-hoc mode) to be installed." - ) - elseif not hostapd and supplicant then - encr:value("psk", "WPA-PSK", {mode="sta"}, {mode="sta-wds"}, {mode="adhoc"}) - encr:value("psk2", "WPA2-PSK", {mode="sta"}, {mode="sta-wds"}, {mode="adhoc"}) - encr:value("psk-mixed", "WPA-PSK/WPA2-PSK Mixed Mode", {mode="sta"}, {mode="sta-wds"}, {mode="adhoc"}) - if has_sta_sae then - encr:value("sae", "WPA3-SAE", {mode="sta"}, {mode="sta-wds"}, {mode="mesh"}) - encr:value("sae-mixed", "WPA2-PSK/WPA3-SAE Mixed Mode", {mode="sta"}, {mode="sta-wds"}) - end - if has_sta_eap then - encr:value("wpa", "WPA-EAP", {mode="sta"}, {mode="sta-wds"}) - encr:value("wpa2", "WPA2-EAP", {mode="sta"}, {mode="sta-wds"}) - end - if has_sta_owe then - encr:value("owe", "OWE", {mode="sta"}, {mode="sta-wds"}) - end - encr.description = translate( - "WPA-Encryption requires wpa_supplicant (for client mode) or hostapd (for AP " .. - "and ad-hoc mode) to be installed." - ) - else - encr.description = translate( - "WPA-Encryption requires wpa_supplicant (for client mode) or hostapd (for AP " .. - "and ad-hoc mode) to be installed." - ) - end -elseif hwtype == "broadcom" then - encr:value("psk", "WPA-PSK") - encr:value("psk2", "WPA2-PSK") - encr:value("psk+psk2", "WPA-PSK/WPA2-PSK Mixed Mode") -end - -auth_server = s:taboption("encryption", Value, "auth_server", translate("Radius-Authentication-Server")) -auth_server:depends({mode="ap", encryption="wpa"}) -auth_server:depends({mode="ap", encryption="wpa2"}) -auth_server:depends({mode="ap-wds", encryption="wpa"}) -auth_server:depends({mode="ap-wds", encryption="wpa2"}) -auth_server.rmempty = true -auth_server.datatype = "host(0)" - -auth_port = s:taboption("encryption", Value, "auth_port", translate("Radius-Authentication-Port"), translatef("Default %d", 1812)) -auth_port:depends({mode="ap", encryption="wpa"}) -auth_port:depends({mode="ap", encryption="wpa2"}) -auth_port:depends({mode="ap-wds", encryption="wpa"}) -auth_port:depends({mode="ap-wds", encryption="wpa2"}) -auth_port.rmempty = true -auth_port.datatype = "port" - -auth_secret = s:taboption("encryption", Value, "auth_secret", translate("Radius-Authentication-Secret")) -auth_secret:depends({mode="ap", encryption="wpa"}) -auth_secret:depends({mode="ap", encryption="wpa2"}) -auth_secret:depends({mode="ap-wds", encryption="wpa"}) -auth_secret:depends({mode="ap-wds", encryption="wpa2"}) -auth_secret.rmempty = true -auth_secret.password = true - -acct_server = s:taboption("encryption", Value, "acct_server", translate("Radius-Accounting-Server")) -acct_server:depends({mode="ap", encryption="wpa"}) -acct_server:depends({mode="ap", encryption="wpa2"}) -acct_server:depends({mode="ap-wds", encryption="wpa"}) -acct_server:depends({mode="ap-wds", encryption="wpa2"}) -acct_server.rmempty = true -acct_server.datatype = "host(0)" - -acct_port = s:taboption("encryption", Value, "acct_port", translate("Radius-Accounting-Port"), translatef("Default %d", 1813)) -acct_port:depends({mode="ap", encryption="wpa"}) -acct_port:depends({mode="ap", encryption="wpa2"}) -acct_port:depends({mode="ap-wds", encryption="wpa"}) -acct_port:depends({mode="ap-wds", encryption="wpa2"}) -acct_port.rmempty = true -acct_port.datatype = "port" - -acct_secret = s:taboption("encryption", Value, "acct_secret", translate("Radius-Accounting-Secret")) -acct_secret:depends({mode="ap", encryption="wpa"}) -acct_secret:depends({mode="ap", encryption="wpa2"}) -acct_secret:depends({mode="ap-wds", encryption="wpa"}) -acct_secret:depends({mode="ap-wds", encryption="wpa2"}) -acct_secret.rmempty = true -acct_secret.password = true - -dae_client = s:taboption("encryption", Value, "dae_client", translate("DAE-Client")) -dae_client:depends({mode="ap", encryption="wpa"}) -dae_client:depends({mode="ap", encryption="wpa2"}) -dae_client:depends({mode="ap-wds", encryption="wpa"}) -dae_client:depends({mode="ap-wds", encryption="wpa2"}) -dae_client.rmempty = true -dae_client.datatype = "host(0)" - -dae_port = s:taboption("encryption", Value, "dae_port", translate("DAE-Port"), translatef("Default %d", 3799)) -dae_port:depends({mode="ap", encryption="wpa"}) -dae_port:depends({mode="ap", encryption="wpa2"}) -dae_port:depends({mode="ap-wds", encryption="wpa"}) -dae_port:depends({mode="ap-wds", encryption="wpa2"}) -dae_port.rmempty = true -dae_port.datatype = "port" - -dae_secret = s:taboption("encryption", Value, "dae_secret", translate("DAE-Secret")) -dae_secret:depends({mode="ap", encryption="wpa"}) -dae_secret:depends({mode="ap", encryption="wpa2"}) -dae_secret:depends({mode="ap-wds", encryption="wpa"}) -dae_secret:depends({mode="ap-wds", encryption="wpa2"}) -dae_secret.rmempty = true -dae_secret.password = true - -wpakey = s:taboption("encryption", Value, "_wpa_key", translate("Key")) -wpakey:depends("encryption", "psk") -wpakey:depends("encryption", "psk2") -wpakey:depends("encryption", "psk+psk2") -wpakey:depends("encryption", "psk-mixed") -wpakey:depends("encryption", "sae") -wpakey:depends("encryption", "sae-mixed") -wpakey.datatype = "wpakey" -wpakey.rmempty = true -wpakey.password = true - -wpakey.cfgvalue = function(self, section, value) - local key = m.uci:get("wireless", section, "key") - if key == "1" or key == "2" or key == "3" or key == "4" then - return nil - end - return key -end - -wpakey.write = function(self, section, value) - self.map.uci:set("wireless", section, "key", value) - self.map.uci:delete("wireless", section, "key1") -end - - -wepslot = s:taboption("encryption", ListValue, "_wep_key", translate("Used Key Slot")) -wepslot:depends("encryption", "wep-open") -wepslot:depends("encryption", "wep-shared") -wepslot:value("1", translatef("Key #%d", 1)) -wepslot:value("2", translatef("Key #%d", 2)) -wepslot:value("3", translatef("Key #%d", 3)) -wepslot:value("4", translatef("Key #%d", 4)) - -wepslot.cfgvalue = function(self, section) - local slot = tonumber(m.uci:get("wireless", section, "key")) - if not slot or slot < 1 or slot > 4 then - return 1 - end - return slot -end - -wepslot.write = function(self, section, value) - self.map.uci:set("wireless", section, "key", value) -end - -local slot -for slot=1,4 do - wepkey = s:taboption("encryption", Value, "key" .. slot, translatef("Key #%d", slot)) - wepkey:depends("encryption", "wep-open") - wepkey:depends("encryption", "wep-shared") - wepkey.datatype = "wepkey" - wepkey.rmempty = true - wepkey.password = true - - function wepkey.write(self, section, value) - if value and (#value == 5 or #value == 13) then - value = "s:" .. value - end - return Value.write(self, section, value) - end -end - -if hwtype == "mac80211" or hwtype == "prism2" then - - -- Probe 802.11r support (and EAP support as a proxy for Openwrt) - local has_80211r = (os.execute("hostapd -v11r 2>/dev/null || hostapd -veap 2>/dev/null") == 0) - - ieee80211r = s:taboption("encryption", Flag, "ieee80211r", - translate("802.11r Fast Transition"), - translate("Enables fast roaming among access points that belong " .. - "to the same Mobility Domain")) - ieee80211r:depends({mode="ap", encryption="wpa"}) - ieee80211r:depends({mode="ap", encryption="wpa2"}) - ieee80211r:depends({mode="ap-wds", encryption="wpa"}) - ieee80211r:depends({mode="ap-wds", encryption="wpa2"}) - if has_80211r then - ieee80211r:depends({mode="ap", encryption="psk"}) - ieee80211r:depends({mode="ap", encryption="psk2"}) - ieee80211r:depends({mode="ap", encryption="psk-mixed"}) - ieee80211r:depends({mode="ap", encryption="sae"}) - ieee80211r:depends({mode="ap", encryption="sae-mixed"}) - ieee80211r:depends({mode="ap-wds", encryption="psk"}) - ieee80211r:depends({mode="ap-wds", encryption="psk2"}) - ieee80211r:depends({mode="ap-wds", encryption="psk-mixed"}) - ieee80211r:depends({mode="ap-wds", encryption="sae"}) - ieee80211r:depends({mode="ap-wds", encryption="sae-mixed"}) - end - ieee80211r.rmempty = true - - nasid = s:taboption("encryption", Value, "nasid", translate("NAS ID"), - translate("Used for two different purposes: RADIUS NAS ID and " .. - "802.11r R0KH-ID. Not needed with normal WPA(2)-PSK.")) - nasid:depends({mode="ap", encryption="wpa"}) - nasid:depends({mode="ap", encryption="wpa2"}) - nasid:depends({mode="ap-wds", encryption="wpa"}) - nasid:depends({mode="ap-wds", encryption="wpa2"}) - nasid:depends({ieee80211r="1"}) - nasid.rmempty = true - - mobility_domain = s:taboption("encryption", Value, "mobility_domain", - translate("Mobility Domain"), - translate("4-character hexadecimal ID")) - mobility_domain:depends({ieee80211r="1"}) - mobility_domain.placeholder = "4f57" - mobility_domain.datatype = "and(hexstring,rangelength(4,4))" - mobility_domain.rmempty = true - - reassociation_deadline = s:taboption("encryption", Value, "reassociation_deadline", - translate("Reassociation Deadline"), - translate("time units (TUs / 1.024 ms) [1000-65535]")) - reassociation_deadline:depends({ieee80211r="1"}) - reassociation_deadline.placeholder = "1000" - reassociation_deadline.datatype = "range(1000,65535)" - reassociation_deadline.rmempty = true - - ft_protocol = s:taboption("encryption", ListValue, "ft_over_ds", translate("FT protocol")) - ft_protocol:depends({ieee80211r="1"}) - ft_protocol:value("1", translatef("FT over DS")) - ft_protocol:value("0", translatef("FT over the Air")) - ft_protocol.rmempty = true - - ft_psk_generate_local = s:taboption("encryption", Flag, "ft_psk_generate_local", - translate("Generate PMK locally"), - translate("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.")) - ft_psk_generate_local:depends({ieee80211r="1"}) - ft_psk_generate_local.default = ft_psk_generate_local.enabled - ft_psk_generate_local.rmempty = false - - r0_key_lifetime = s:taboption("encryption", Value, "r0_key_lifetime", - translate("R0 Key Lifetime"), translate("minutes")) - r0_key_lifetime:depends({ieee80211r="1"}) - r0_key_lifetime.placeholder = "10000" - r0_key_lifetime.datatype = "uinteger" - r0_key_lifetime.rmempty = true - - r1_key_holder = s:taboption("encryption", Value, "r1_key_holder", - translate("R1 Key Holder"), - translate("6-octet identifier as a hex string - no colons")) - r1_key_holder:depends({ieee80211r="1"}) - r1_key_holder.placeholder = "00004f577274" - r1_key_holder.datatype = "and(hexstring,rangelength(12,12))" - r1_key_holder.rmempty = true - - pmk_r1_push = s:taboption("encryption", Flag, "pmk_r1_push", translate("PMK R1 Push")) - pmk_r1_push:depends({ieee80211r="1"}) - pmk_r1_push.placeholder = "0" - pmk_r1_push.rmempty = true - - r0kh = s:taboption("encryption", DynamicList, "r0kh", translate("External R0 Key Holder List"), - translate("List of R0KHs in the same Mobility Domain. " .. - "<br />Format: MAC-address,NAS-Identifier,128-bit key as hex string. " .. - "<br />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.")) - r0kh:depends({ieee80211r="1"}) - r0kh.rmempty = true - - r1kh = s:taboption("encryption", DynamicList, "r1kh", translate("External R1 Key Holder List"), - translate ("List of R1KHs in the same Mobility Domain. ".. - "<br />Format: MAC-address,R1KH-ID as 6 octets with colons,128-bit key as hex string. ".. - "<br />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.")) - r1kh:depends({ieee80211r="1"}) - r1kh.rmempty = true - -- End of 802.11r options - - eaptype = s:taboption("encryption", ListValue, "eap_type", translate("EAP-Method")) - eaptype:value("tls", "TLS") - eaptype:value("ttls", "TTLS") - eaptype:value("peap", "PEAP") - eaptype:value("fast", "FAST") - eaptype:depends({mode="sta", encryption="wpa"}) - eaptype:depends({mode="sta", encryption="wpa2"}) - eaptype:depends({mode="sta-wds", encryption="wpa"}) - eaptype:depends({mode="sta-wds", encryption="wpa2"}) - - cacert = s:taboption("encryption", FileUpload, "ca_cert", translate("Path to CA-Certificate")) - cacert:depends({mode="sta", encryption="wpa"}) - cacert:depends({mode="sta", encryption="wpa2"}) - cacert:depends({mode="sta-wds", encryption="wpa"}) - cacert:depends({mode="sta-wds", encryption="wpa2"}) - cacert.rmempty = true - - clientcert = s:taboption("encryption", FileUpload, "client_cert", translate("Path to Client-Certificate")) - clientcert:depends({mode="sta", eap_type="tls", encryption="wpa"}) - clientcert:depends({mode="sta", eap_type="tls", encryption="wpa2"}) - clientcert:depends({mode="sta-wds", eap_type="tls", encryption="wpa"}) - clientcert:depends({mode="sta-wds", eap_type="tls", encryption="wpa2"}) - - privkey = s:taboption("encryption", FileUpload, "priv_key", translate("Path to Private Key")) - privkey:depends({mode="sta", eap_type="tls", encryption="wpa2"}) - privkey:depends({mode="sta", eap_type="tls", encryption="wpa"}) - privkey:depends({mode="sta-wds", eap_type="tls", encryption="wpa2"}) - privkey:depends({mode="sta-wds", eap_type="tls", encryption="wpa"}) - - privkeypwd = s:taboption("encryption", Value, "priv_key_pwd", translate("Password of Private Key")) - privkeypwd:depends({mode="sta", eap_type="tls", encryption="wpa2"}) - privkeypwd:depends({mode="sta", eap_type="tls", encryption="wpa"}) - privkeypwd:depends({mode="sta-wds", eap_type="tls", encryption="wpa2"}) - privkeypwd:depends({mode="sta-wds", eap_type="tls", encryption="wpa"}) - privkeypwd.rmempty = true - privkeypwd.password = true - - auth = s:taboption("encryption", ListValue, "auth", translate("Authentication")) - auth:value("PAP", "PAP", {eap_type="ttls"}) - auth:value("CHAP", "CHAP", {eap_type="ttls"}) - auth:value("MSCHAP", "MSCHAP", {eap_type="ttls"}) - auth:value("MSCHAPV2", "MSCHAPv2", {eap_type="ttls"}) - auth:value("EAP-GTC") - auth:value("EAP-MD5") - auth:value("EAP-MSCHAPV2") - auth:value("EAP-TLS") - auth:depends({mode="sta", eap_type="fast", encryption="wpa2"}) - auth:depends({mode="sta", eap_type="fast", encryption="wpa"}) - auth:depends({mode="sta", eap_type="peap", encryption="wpa2"}) - auth:depends({mode="sta", eap_type="peap", encryption="wpa"}) - auth:depends({mode="sta", eap_type="ttls", encryption="wpa2"}) - auth:depends({mode="sta", eap_type="ttls", encryption="wpa"}) - auth:depends({mode="sta-wds", eap_type="fast", encryption="wpa2"}) - auth:depends({mode="sta-wds", eap_type="fast", encryption="wpa"}) - auth:depends({mode="sta-wds", eap_type="peap", encryption="wpa2"}) - auth:depends({mode="sta-wds", eap_type="peap", encryption="wpa"}) - auth:depends({mode="sta-wds", eap_type="ttls", encryption="wpa2"}) - auth:depends({mode="sta-wds", eap_type="ttls", encryption="wpa"}) - - cacert2 = s:taboption("encryption", FileUpload, "ca_cert2", translate("Path to inner CA-Certificate")) - cacert2:depends({mode="sta", auth="EAP-TLS", encryption="wpa"}) - cacert2:depends({mode="sta", auth="EAP-TLS", encryption="wpa2"}) - cacert2:depends({mode="sta-wds", auth="EAP-TLS", encryption="wpa"}) - cacert2:depends({mode="sta-wds", auth="EAP-TLS", encryption="wpa2"}) - - clientcert2 = s:taboption("encryption", FileUpload, "client_cert2", translate("Path to inner Client-Certificate")) - clientcert2:depends({mode="sta", auth="EAP-TLS", encryption="wpa"}) - clientcert2:depends({mode="sta", auth="EAP-TLS", encryption="wpa2"}) - clientcert2:depends({mode="sta-wds", auth="EAP-TLS", encryption="wpa"}) - clientcert2:depends({mode="sta-wds", auth="EAP-TLS", encryption="wpa2"}) - - privkey2 = s:taboption("encryption", FileUpload, "priv_key2", translate("Path to inner Private Key")) - privkey2:depends({mode="sta", auth="EAP-TLS", encryption="wpa"}) - privkey2:depends({mode="sta", auth="EAP-TLS", encryption="wpa2"}) - privkey2:depends({mode="sta-wds", auth="EAP-TLS", encryption="wpa"}) - privkey2:depends({mode="sta-wds", auth="EAP-TLS", encryption="wpa2"}) - - privkeypwd2 = s:taboption("encryption", Value, "priv_key2_pwd", translate("Password of inner Private Key")) - privkeypwd2:depends({mode="sta", auth="EAP-TLS", encryption="wpa"}) - privkeypwd2:depends({mode="sta", auth="EAP-TLS", encryption="wpa2"}) - privkeypwd2:depends({mode="sta-wds", auth="EAP-TLS", encryption="wpa"}) - privkeypwd2:depends({mode="sta-wds", auth="EAP-TLS", encryption="wpa2"}) - privkeypwd2.rmempty = true - privkeypwd2.password = true - - identity = s:taboption("encryption", Value, "identity", translate("Identity")) - identity:depends({mode="sta", eap_type="fast", encryption="wpa2"}) - identity:depends({mode="sta", eap_type="fast", encryption="wpa"}) - identity:depends({mode="sta", eap_type="peap", encryption="wpa2"}) - identity:depends({mode="sta", eap_type="peap", encryption="wpa"}) - identity:depends({mode="sta", eap_type="ttls", encryption="wpa2"}) - identity:depends({mode="sta", eap_type="ttls", encryption="wpa"}) - identity:depends({mode="sta-wds", eap_type="fast", encryption="wpa2"}) - identity:depends({mode="sta-wds", eap_type="fast", encryption="wpa"}) - identity:depends({mode="sta-wds", eap_type="peap", encryption="wpa2"}) - identity:depends({mode="sta-wds", eap_type="peap", encryption="wpa"}) - identity:depends({mode="sta-wds", eap_type="ttls", encryption="wpa2"}) - identity:depends({mode="sta-wds", eap_type="ttls", encryption="wpa"}) - identity:depends({mode="sta", eap_type="tls", encryption="wpa2"}) - identity:depends({mode="sta", eap_type="tls", encryption="wpa"}) - identity:depends({mode="sta-wds", eap_type="tls", encryption="wpa2"}) - identity:depends({mode="sta-wds", eap_type="tls", encryption="wpa"}) - - anonymous_identity = s:taboption("encryption", Value, "anonymous_identity", translate("Anonymous Identity")) - anonymous_identity:depends({mode="sta", eap_type="fast", encryption="wpa2"}) - anonymous_identity:depends({mode="sta", eap_type="fast", encryption="wpa"}) - anonymous_identity:depends({mode="sta", eap_type="peap", encryption="wpa2"}) - anonymous_identity:depends({mode="sta", eap_type="peap", encryption="wpa"}) - anonymous_identity:depends({mode="sta", eap_type="ttls", encryption="wpa2"}) - anonymous_identity:depends({mode="sta", eap_type="ttls", encryption="wpa"}) - anonymous_identity:depends({mode="sta-wds", eap_type="fast", encryption="wpa2"}) - anonymous_identity:depends({mode="sta-wds", eap_type="fast", encryption="wpa"}) - anonymous_identity:depends({mode="sta-wds", eap_type="peap", encryption="wpa2"}) - anonymous_identity:depends({mode="sta-wds", eap_type="peap", encryption="wpa"}) - anonymous_identity:depends({mode="sta-wds", eap_type="ttls", encryption="wpa2"}) - anonymous_identity:depends({mode="sta-wds", eap_type="ttls", encryption="wpa"}) - anonymous_identity:depends({mode="sta", eap_type="tls", encryption="wpa2"}) - anonymous_identity:depends({mode="sta", eap_type="tls", encryption="wpa"}) - anonymous_identity:depends({mode="sta-wds", eap_type="tls", encryption="wpa2"}) - anonymous_identity:depends({mode="sta-wds", eap_type="tls", encryption="wpa"}) - - password = s:taboption("encryption", Value, "password", translate("Password")) - password:depends({mode="sta", eap_type="fast", encryption="wpa2"}) - password:depends({mode="sta", eap_type="fast", encryption="wpa"}) - password:depends({mode="sta", eap_type="peap", encryption="wpa2"}) - password:depends({mode="sta", eap_type="peap", encryption="wpa"}) - password:depends({mode="sta", eap_type="ttls", encryption="wpa2"}) - password:depends({mode="sta", eap_type="ttls", encryption="wpa"}) - password:depends({mode="sta-wds", eap_type="fast", encryption="wpa2"}) - password:depends({mode="sta-wds", eap_type="fast", encryption="wpa"}) - password:depends({mode="sta-wds", eap_type="peap", encryption="wpa2"}) - password:depends({mode="sta-wds", eap_type="peap", encryption="wpa"}) - password:depends({mode="sta-wds", eap_type="ttls", encryption="wpa2"}) - password:depends({mode="sta-wds", eap_type="ttls", encryption="wpa"}) - password.rmempty = true - password.password = true -end - --- ieee802.11w options -if hwtype == "mac80211" then - local has_80211w = (os.execute("hostapd -v11w 2>/dev/null || hostapd -veap 2>/dev/null") == 0) - if has_80211w then - ieee80211w = s:taboption("encryption", ListValue, "ieee80211w", - translate("802.11w Management Frame Protection"), - translate("Requires the 'full' version of wpad/hostapd " .. - "and support from the wifi driver <br />(as of Jan 2019: " .. - "ath9k, ath10k, mwlwifi and mt76)")) - ieee80211w.default = "" - ieee80211w.rmempty = true - ieee80211w:value("", translate("Disabled (default)")) - ieee80211w:value("1", translate("Optional")) - ieee80211w:value("2", translate("Required")) - ieee80211w:depends({mode="ap", encryption="wpa2"}) - ieee80211w:depends({mode="ap-wds", encryption="wpa2"}) - ieee80211w:depends({mode="ap", encryption="psk2"}) - ieee80211w:depends({mode="ap", encryption="psk-mixed"}) - ieee80211w:depends({mode="ap", encryption="sae"}) - ieee80211w:depends({mode="ap", encryption="sae-mixed"}) - ieee80211w:depends({mode="ap", encryption="owe"}) - ieee80211w:depends({mode="ap-wds", encryption="psk2"}) - ieee80211w:depends({mode="ap-wds", encryption="psk-mixed"}) - ieee80211w:depends({mode="ap-wds", encryption="sae"}) - ieee80211w:depends({mode="ap-wds", encryption="sae-mixed"}) - ieee80211w:depends({mode="ap-wds", encryption="owe"}) - ieee80211w:depends({mode="sta", encryption="wpa2"}) - ieee80211w:depends({mode="sta-wds", encryption="wpa2"}) - ieee80211w:depends({mode="sta", encryption="psk2"}) - ieee80211w:depends({mode="sta", encryption="psk-mixed"}) - ieee80211w:depends({mode="sta", encryption="sae"}) - ieee80211w:depends({mode="sta", encryption="sae-mixed"}) - ieee80211w:depends({mode="sta", encryption="owe"}) - ieee80211w:depends({mode="sta-wds", encryption="psk2"}) - ieee80211w:depends({mode="sta-wds", encryption="psk-mixed"}) - ieee80211w:depends({mode="sta-wds", encryption="sae"}) - ieee80211w:depends({mode="sta-wds", encryption="sae-mixed"}) - ieee80211w:depends({mode="sta-wds", encryption="owe"}) - - max_timeout = s:taboption("encryption", Value, "ieee80211w_max_timeout", - translate("802.11w maximum timeout"), - translate("802.11w Association SA Query maximum timeout")) - max_timeout:depends({ieee80211w="1"}) - max_timeout:depends({ieee80211w="2"}) - max_timeout.datatype = "uinteger" - max_timeout.placeholder = "1000" - max_timeout.rmempty = true - - retry_timeout = s:taboption("encryption", Value, "ieee80211w_retry_timeout", - translate("802.11w retry timeout"), - translate("802.11w Association SA Query retry timeout")) - retry_timeout:depends({ieee80211w="1"}) - retry_timeout:depends({ieee80211w="2"}) - retry_timeout.datatype = "uinteger" - retry_timeout.placeholder = "201" - retry_timeout.rmempty = true - end - - key_retries = s:taboption("encryption", Flag, "wpa_disable_eapol_key_retries", - translate("Enable key reinstallation (KRACK) countermeasures"), - translate("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.")) - - key_retries:depends({mode="ap", encryption="wpa2"}) - key_retries:depends({mode="ap", encryption="psk2"}) - key_retries:depends({mode="ap", encryption="psk-mixed"}) - key_retries:depends({mode="ap", encryption="sae"}) - key_retries:depends({mode="ap", encryption="sae-mixed"}) - key_retries:depends({mode="ap-wds", encryption="wpa2"}) - key_retries:depends({mode="ap-wds", encryption="psk2"}) - key_retries:depends({mode="ap-wds", encryption="psk-mixed"}) - key_retries:depends({mode="ap-wds", encryption="sae"}) - key_retries:depends({mode="ap-wds", encryption="sae-mixed"}) -end - -if hwtype == "mac80211" or hwtype == "prism2" then - local wpasupplicant = fs.access("/usr/sbin/wpa_supplicant") - local hostcli = fs.access("/usr/sbin/hostapd_cli") - if hostcli and wpasupplicant then - wps = s:taboption("encryption", Flag, "wps_pushbutton", translate("Enable WPS pushbutton, requires WPA(2)-PSK")) - wps.enabled = "1" - wps.disabled = "0" - wps.rmempty = false - wps:depends("encryption", "psk") - wps:depends("encryption", "psk2") - wps:depends("encryption", "psk-mixed") - end -end - -return m diff --git a/modules/luci-mod-network/luasrc/model/cbi/admin_network/wifi_add.lua b/modules/luci-mod-network/luasrc/model/cbi/admin_network/wifi_add.lua deleted file mode 100644 index e8a3058826..0000000000 --- a/modules/luci-mod-network/luasrc/model/cbi/admin_network/wifi_add.lua +++ /dev/null @@ -1,168 +0,0 @@ --- Copyright 2009 Jo-Philipp Wich <jow@openwrt.org> --- Licensed to the public under the Apache License 2.0. - -local fs = require "nixio.fs" -local nw = require "luci.model.network" -local fw = require "luci.model.firewall" -local uci = require "luci.model.uci".cursor() -local http = require "luci.http" - -local iw = luci.sys.wifi.getiwinfo(http.formvalue("device")) - -local has_firewall = fs.access("/etc/config/firewall") - -if not iw then - luci.http.redirect(luci.dispatcher.build_url("admin/network/wireless")) - return -end - -m = SimpleForm("network", translatef("Joining Network: %q", http.formvalue("join"))) -m.cancel = translate("Back to scan results") -m.reset = false - -function m.on_cancel() - local dev = http.formvalue("device") - http.redirect(luci.dispatcher.build_url( - dev and "admin/network/wireless_join?device=" .. dev - or "admin/network/wireless" - )) -end - -nw.init(uci) -fw.init(uci) - -m.hidden = { - device = http.formvalue("device"), - join = http.formvalue("join"), - channel = http.formvalue("channel"), - mode = http.formvalue("mode"), - bssid = http.formvalue("bssid"), - wep = http.formvalue("wep"), - wpa_suites = http.formvalue("wpa_suites"), - wpa_version = http.formvalue("wpa_version") -} - -if iw and iw.mbssid_support then - replace = m:field(Flag, "replace", translate("Replace wireless configuration"), - translate("Check this option to delete the existing networks from this radio.")) - - function replace.cfgvalue() return "0" end -else - replace = m:field(DummyValue, "replace", translate("Replace wireless configuration")) - replace.default = translate("The hardware is not multi-SSID capable and the existing " .. - "configuration will be replaced if you proceed.") - - function replace.formvalue() return "1" end -end - -if http.formvalue("wep") == "1" then - key = m:field(Value, "key", translate("WEP passphrase"), - translate("Specify the secret encryption key here.")) - - key.password = true - key.datatype = "wepkey" - -elseif (tonumber(m.hidden.wpa_version) or 0) > 0 and - (m.hidden.wpa_suites == "PSK" or m.hidden.wpa_suites == "PSK2") -then - key = m:field(Value, "key", translate("WPA passphrase"), - translate("Specify the secret encryption key here.")) - - key.password = true - key.datatype = "wpakey" - --m.hidden.wpa_suite = (tonumber(http.formvalue("wpa_version")) or 0) >= 2 and "psk2" or "psk" -end - -newnet = m:field(Value, "_netname_new", translate("Name of the new network"), - translate("The allowed characters are: <code>A-Z</code>, <code>a-z</code>, " .. - "<code>0-9</code> and <code>_</code>" - )) - -newnet.default = m.hidden.mode == "Ad-Hoc" and "mesh" or "wwan" -newnet.datatype = "uciname" - -if has_firewall then - fwzone = m:field(Value, "_fwzone", - translate("Create / Assign firewall-zone"), - translate("Choose the firewall zone you want to assign to this interface. Select <em>unspecified</em> to remove the interface from the associated zone or fill out the <em>create</em> field to define a new zone and attach the interface to it.")) - - fwzone.template = "cbi/firewall_zonelist" - fwzone.default = m.hidden.mode == "Ad-Hoc" and "mesh" or "wan" -end - -function newnet.parse(self, section) - local net, zone - - if has_firewall then - local value = fwzone:formvalue(section) - if value and #value > 0 then - zone = fw:get_zone(value) or fw:add_zone(value) - end - end - - local wdev = nw:get_wifidev(m.hidden.device) - - wdev:set("disabled", false) - wdev:set("channel", m.hidden.channel) - - if replace:formvalue(section) then - local n - for _, n in ipairs(wdev:get_wifinets()) do - wdev:del_wifinet(n) - end - end - - local wconf = { - device = m.hidden.device, - ssid = m.hidden.join, - mode = (m.hidden.mode == "Ad-Hoc" and "adhoc" or "sta") - } - - if m.hidden.wep == "1" then - wconf.encryption = "wep-open" - wconf.key = "1" - wconf.key1 = key and key:formvalue(section) or "" - elseif (tonumber(m.hidden.wpa_version) or 0) > 0 then - wconf.encryption = (tonumber(m.hidden.wpa_version) or 0) >= 2 and "psk2" or "psk" - wconf.key = key and key:formvalue(section) or "" - else - wconf.encryption = "none" - end - - if wconf.mode == "adhoc" or wconf.mode == "sta" then - wconf.bssid = m.hidden.bssid - end - - local value = self:formvalue(section) - net = nw:add_network(value, { proto = "dhcp" }) - - if not net then - self.error = { [section] = "missing" } - else - wconf.network = net:name() - - local wnet = wdev:add_wifinet(wconf) - if wnet then - if zone then - fw:del_network(net:name()) - zone:add_network(net:name()) - end - - uci:save("wireless") - uci:save("network") - uci:save("firewall") - - luci.http.redirect(wnet:adminlink()) - end - end -end - -if has_firewall then - function fwzone.cfgvalue(self, section) - self.iface = section - local z = fw:get_zone_by_network(section) - return z and z:name() - end -end - -return m diff --git a/modules/luci-mod-network/luasrc/model/cbi/admin_network/wifi_overview.lua b/modules/luci-mod-network/luasrc/model/cbi/admin_network/wifi_overview.lua deleted file mode 100644 index 54720d6889..0000000000 --- a/modules/luci-mod-network/luasrc/model/cbi/admin_network/wifi_overview.lua +++ /dev/null @@ -1,153 +0,0 @@ --- Copyright 2018 Jo-Philipp Wich <jo@mein.io> --- Licensed to the public under the Apache License 2.0. - -local fs = require "nixio.fs" -local utl = require "luci.util" -local tpl = require "luci.template" -local ntm = require "luci.model.network" - -local has_iwinfo = pcall(require, "iwinfo") - -function guess_wifi_hw(dev) - local bands = "" - local ifname = dev:name() - local name, idx = ifname:match("^([a-z]+)(%d+)") - idx = tonumber(idx) - - if has_iwinfo then - local bl = dev.iwinfo.hwmodelist - if bl and next(bl) then - if bl.a then bands = bands .. "a" end - if bl.b then bands = bands .. "b" end - if bl.g then bands = bands .. "g" end - if bl.n then bands = bands .. "n" end - if bl.ac then bands = bands .. "ac" end - end - - local hw = dev.iwinfo.hardware_name - if hw then - return "%s 802.11%s" %{ hw, bands } - end - end - - -- wl.o - if name == "wl" then - local name = translatef("Broadcom 802.11%s Wireless Controller", bands) - local nm = 0 - - local fd = nixio.open("/proc/bus/pci/devices", "r") - if fd then - local ln - for ln in fd:linesource() do - if ln:match("wl$") then - if nm == idx then - local version = ln:match("^%S+%s+%S%S%S%S([0-9a-f]+)") - name = translatef( - "Broadcom BCM%04x 802.11 Wireless Controller", - tonumber(version, 16) - ) - - break - else - nm = nm + 1 - end - end - end - fd:close() - end - - return name - - -- dunno yet - else - return translatef("Generic 802.11%s Wireless Controller", bands) - end -end - - -m = Map("wireless", translate("Wireless Overview")) -m:chain("network") -m.pageaction = false - -if not has_iwinfo then - s = m:section(NamedSection, "__warning__") - - function s.render(self) - tpl.render_string([[ - <div class="alert-message warning"> - <h4><%:Package libiwinfo required!%></h4> - <p><%_The <em>libiwinfo-lua</em> package is not installed. You must install this component for working wireless configuration!%></p> - </div> - ]]) - end -end - -local _, dev, net -for _, dev in ipairs(ntm:get_wifidevs()) do - s = m:section(TypedSection) - s.template = "admin_network/wifi_overview" - s.wnets = dev:get_wifinets() - s.dev = dev - s.hw = guess_wifi_hw(dev) - - function s.cfgsections(self) - local _, net, sl = nil, nil, { } - for _, net in ipairs(self.wnets) do - sl[#sl+1] = net:name() - self.wnets[net:name()] = net - end - return sl - end - - o = s:option(Value, "__disable__") - - function o.cfgvalue(self, sid) - local wnet = self.section.wnets[sid] - local wdev = wnet:get_device() - - return ((wnet and wnet:get("disabled") == "1") or - (wdev and wdev:get("disabled") == "1")) and "1" or "0" - end - - function o.write(self, sid, value) - local wnet = self.section.wnets[sid] - local wdev = wnet:get_device() - - if value ~= "1" then - wnet:set("disabled", nil) - wdev:set("disabled", nil) - else - wnet:set("disabled", "1") - end - end - - o.remove = o.write - - - o = s:option(Value, "__delete__") - - function o.write(self, sid, value) - local wnet = self.section.wnets[sid] - local nets = wnet:get_networks() - - ntm:del_wifinet(wnet:id()) - - local _, net - for _, net in ipairs(nets) do - if net:is_empty() then - ntm:del_network(net:name()) - end - end - end -end - -s = m:section(NamedSection, "__assoclist__") - -function s.render(self, sid) - tpl.render_string([[ - <h2><%:Associated Stations%></h2> - <%+wifi_assoclist%> - ]]) -end - -return m diff --git a/modules/luci-mod-network/luasrc/view/admin_network/iface_overview.htm b/modules/luci-mod-network/luasrc/view/admin_network/iface_overview.htm deleted file mode 100644 index 9d4afd2b27..0000000000 --- a/modules/luci-mod-network/luasrc/view/admin_network/iface_overview.htm +++ /dev/null @@ -1,53 +0,0 @@ -<div class="cbi-section-node"> - <div class="table"> - <% - for i, net in ipairs(self.netlist) do - local z = net[3] - local c = z and z:get_color() or "#EEEEEE" - local t = z and translate("Part of zone %q") % z:name() or translate("No zone assigned") - local disabled = (net[4]:get("auto") == "0") - local dynamic = net[4]:is_dynamic() - %> - <div class="tr cbi-rowstyle-<%=i % 2 + 1%>"> - <div class="td col-3 center middle"> - <div class="ifacebox"> - <div class="ifacebox-head" style="background-color:<%=c%>" title="<%=pcdata(t)%>"> - <strong><%=net[1]:upper()%></strong> - </div> - <div class="ifacebox-body" id="<%=net[1]%>-ifc-devices" data-network="<%=net[1]%>"> - <img src="<%=resource%>/icons/ethernet_disabled.png" style="width:16px; height:16px" /><br /> - <small>?</small> - </div> - </div> - </div> - <div class="td col-5 left middle" id="<%=net[1]%>-ifc-description"> - <em><%:Collecting data...%></em> - </div> - <div class="td cbi-section-actions"> - <div> - <input type="button" class="cbi-button cbi-button-neutral" onclick="iface_reconnect('<%=net[1]%>')" title="<%:Reconnect this interface%>" value="<%:Restart%>"<%=ifattr(disabled or dynamic, "disabled", "disabled")%> /> - - <% if disabled then %> - <input type="hidden" name="cbid.network.<%=net[1]%>.__disable__" value="1" /> - <input type="submit" name="cbi.apply" class="cbi-button cbi-button-neutral" onclick="this.previousElementSibling.value='0'" title="<%:Reconnect this interface%>" value="<%:Connect%>"<%=ifattr(dynamic, "disabled", "disabled")%> /> - <% else %> - <input type="hidden" name="cbid.network.<%=net[1]%>.__disable__" value="0" /> - <input type="submit" name="cbi.apply" class="cbi-button cbi-button-neutral" onclick="this.previousElementSibling.value='1'" title="<%:Shutdown this interface%>" value="<%:Stop%>"<%=ifattr(dynamic, "disabled", "disabled")%> /> - <% end %> - - <input type="button" class="cbi-button cbi-button-action important" onclick="location.href='<%=url("admin/network/network", net[1])%>'" title="<%:Edit this interface%>" value="<%:Edit%>" id="<%=net[1]%>-ifc-edit"<%=ifattr(dynamic, "disabled", "disabled")%> /> - - <input type="hidden" name="cbid.network.<%=net[1]%>.__delete__" value="" /> - <input type="submit" name="cbi.apply" class="cbi-button cbi-button-negative" onclick="iface_delete(event)" value="<%:Delete%>"<%=ifattr(dynamic, "disabled", "disabled")%> /> - </div> - </div> - </div> - <% end %> - </div> -</div> - -<div class="cbi-section-create"> - <input type="button" class="cbi-button cbi-button-add" value="<%:Add new interface...%>" onclick="location.href='<%=url("admin/network/iface_add")%>'" /> -</div> - -<script type="text/javascript" src="<%=resource%>/view/network/network.js"></script> diff --git a/modules/luci-mod-network/luasrc/view/admin_network/switch_status.htm b/modules/luci-mod-network/luasrc/view/admin_network/switch_status.htm deleted file mode 100644 index 6e741b419a..0000000000 --- a/modules/luci-mod-network/luasrc/view/admin_network/switch_status.htm +++ /dev/null @@ -1,62 +0,0 @@ -<script type="text/javascript">//<![CDATA[ - var switches = [ '<%=table.concat(self.switches, "', '")%>' ], - tables = document.querySelectorAll('.cbi-section-table'); - - function add_status_row(table) { - var first_row = table.querySelector('.cbi-section-table-row'); - if (first_row.classList.contains('port-status')) - return first_row; - - var status_row = first_row.parentNode.insertBefore( - E('div', { 'class': first_row.className }), first_row); - - first_row.querySelectorAll('.td').forEach(function(td) { - status_row.appendChild(td.cloneNode(false)); - status_row.lastElementChild.removeAttribute('data-title'); - }); - - status_row.firstElementChild.innerHTML = '<%:Port status:%>'; - status_row.classList.add('port-status') ; - - return status_row; - } - - XHR.poll(-1, '<%=url('admin/network/switch_status')%>/' + switches.join(','), null, - function(x, st) - { - for (var i = 0; i < switches.length; i++) - { - var ports = st[switches[i]]; - var tr = add_status_row(tables[i]); - - if (tr && ports && ports.length) - { - for (var j = 0; j < ports.length; j++) - { - var th = tr.querySelector('[data-name="%d"]'.format(j)); - - if (!th) - continue; - - if (ports[j].link) - { - th.innerHTML = String.format( - '<small><img src="<%=resource%>/icons/port_up.png" />' + - '<br />%d<%:baseT%><br />%s</small>', - ports[j].speed, ports[j].duplex - ? '<%:full-duplex%>' : '<%:half-duplex%>' - ); - } - else - { - th.innerHTML = String.format( - '<small><img src="<%=resource%>/icons/port_down.png" />' + - '<br /><%:no link%></small>' - ); - } - } - } - } - } - ); -//]]></script> diff --git a/modules/luci-mod-network/luasrc/view/admin_network/wifi_join.htm b/modules/luci-mod-network/luasrc/view/admin_network/wifi_join.htm deleted file mode 100644 index 5a61ba099c..0000000000 --- a/modules/luci-mod-network/luasrc/view/admin_network/wifi_join.htm +++ /dev/null @@ -1,59 +0,0 @@ -<%# - Copyright 2009-2015 Jo-Philipp Wich <jow@openwrt.org> - Licensed to the public under the Apache License 2.0. --%> - -<%- - - local sys = require "luci.sys" - local utl = require "luci.util" - - local dev = luci.http.formvalue("device") - local iw = luci.sys.wifi.getiwinfo(dev) - - if not iw then - luci.http.redirect(luci.dispatcher.build_url("admin/network/wireless")) - return - end --%> - -<%+header%> - -<h2 name="content"><%:Join Network: Wireless Scan%></h2> - -<div class="cbi-map"> - <div class="cbi-section"> - <div class="table"<%=attr("data-wifi-scan", dev) .. attr("data-wifi-type", iw.type)%>> - <div class="tr table-titles"> - <div class="th col-2 middle center"><%:Signal%></div> - <div class="th col-4 middle left"><%:SSID%></div> - <div class="th col-2 middle center hide-xs"><%:Channel%></div> - <div class="th col-2 middle left hide-xs"><%:Mode%></div> - <div class="th col-3 middle left hide-xs"><%:BSSID%></div> - <div class="th col-3 middle left"><%:Encryption%></div> - <div class="th cbi-section-actions"> </div> - </div> - - <div class="tr placeholder"> - <div class="td"> - <img src="<%=resource%>/icons/loading.gif" class="middle" /> - <em><%:Collecting data...%></em> - </div> - </div> - </div> - </div> -</div> -<div class="cbi-page-actions right"> - <form class="inline" action="<%=url("admin/network/wireless")%>" method="get"> - <input class="cbi-button cbi-button-neutral" type="submit" value="<%:Back to overview%>" /> - </form> - <form class="inline" action="<%=url('admin/network/wireless_join')%>" method="post"> - <input type="hidden" name="token" value="<%=token%>" /> - <input type="hidden" name="device" value="<%=utl.pcdata(dev)%>" /> - <input type="button" class="cbi-button cbi-button-action" value="<%:Repeat scan%>" onclick="flush()" /> - </form> -</div> - -<script type="text/javascript" src="<%=resource%>/view/network/wifi_join.js"></script> - -<%+footer%> diff --git a/modules/luci-mod-network/luasrc/view/admin_network/wifi_overview.htm b/modules/luci-mod-network/luasrc/view/admin_network/wifi_overview.htm deleted file mode 100644 index 89bb404fd8..0000000000 --- a/modules/luci-mod-network/luasrc/view/admin_network/wifi_overview.htm +++ /dev/null @@ -1,61 +0,0 @@ -<div class="cbi-section-node"> - <div class="table"> - <!-- physical device --> - <div class="tr cbi-rowstyle-2"> - <div class="td col-2 center middle"> - <span class="ifacebadge"><img src="<%=resource%>/icons/wifi_disabled.png" id="<%=self.dev:name()%>-iw-upstate" /> <%=self.dev:name()%></span> - </div> - <div class="td col-7 left middle"> - <big><strong><%=self.hw%></strong></big><br /> - <span id="<%=self.dev:name()%>-iw-devinfo"></span> - </div> - <div class="td middle cbi-section-actions"> - <div> - <input type="button" class="cbi-button cbi-button-neutral" title="<%:Restart radio interface%>" value="<%:Restart%>" data-radio="<%=self.dev:name()%>" onclick="wifi_restart(event)" /> - <input type="button" class="cbi-button cbi-button-action important" title="<%:Find and join network%>" value="<%:Scan%>" onclick="cbi_submit(this, 'device', '<%=self.dev:name()%>', '<%=url('admin/network/wireless_join')%>')" /> - <input type="button" class="cbi-button cbi-button-add" title="<%:Provide new network%>" value="<%:Add%>" onclick="cbi_submit(this, 'device', '<%=self.dev:name()%>', '<%=url('admin/network/wireless_add')%>')" /> - </div> - </div> - </div> - <!-- /physical device --> - - <!-- network list --> - <% if #self.wnets > 0 then %> - <% for i, net in ipairs(self.wnets) do local disabled = (self.dev:get("disabled") == "1" or net:get("disabled") == "1") %> - <div class="tr cbi-rowstyle-<%=1 + ((i-1) % 2)%>"> - <div class="td col-2 center middle" id="<%=net:id()%>-iw-signal"> - <span class="ifacebadge" title="<%:Not associated%>"><img src="<%=resource%>/icons/signal-<%= disabled and "none" or "0" %>.png" /> 0%</span> - </div> - <div class="td col-7 left middle" id="<%=net:id()%>-iw-status" data-network="<%=net:id()%>" data-disabled="<%= disabled and "true" or "false" %>"> - <em><%= disabled and translate("Wireless is disabled") or translate("Collecting data...") %></em> - </div> - <div class="td middle cbi-section-actions"> - <div> - <% if disabled then %> - <input name="cbid.wireless.<%=net:name()%>.__disable__" type="hidden" value="1" /> - <input name="cbi.apply" type="submit" class="cbi-button cbi-button-neutral" title="<%:Enable this network%>" value="<%:Enable%>" onclick="this.previousElementSibling.value='0'" /> - <% else %> - <input name="cbid.wireless.<%=net:name()%>.__disable__" type="hidden" value="0" /> - <input name="cbi.apply" type="submit" class="cbi-button cbi-button-neutral" title="<%:Disable this network%>" value="<%:Disable%>" onclick="this.previousElementSibling.value='1'" /> - <% end %> - - <input type="button" class="cbi-button cbi-button-action important" onclick="location.href='<%=net:adminlink()%>'" title="<%:Edit this network%>" value="<%:Edit%>" /> - - <input name="cbid.wireless.<%=net:name()%>.__delete__" type="hidden" value="" /> - <input name="cbi.apply" type="submit" class="cbi-button cbi-button-negative" title="<%:Delete this network%>" value="<%:Remove%>" onclick="wifi_delete(event)" /> - </div> - </div> - </div> - <% end %> - <% else %> - <div class="tr placeholder"> - <div class="td"> - <em><%:No network configured on this device%></em> - </div> - </div> - <% end %> - <!-- /network list --> - </div> -</div> - -<script type="text/javascript" src="<%=resource%>/view/network/wireless.js"></script> diff --git a/modules/luci-mod-network/luasrc/view/admin_network/wifi_status.htm b/modules/luci-mod-network/luasrc/view/admin_network/wifi_status.htm deleted file mode 100644 index 93ae2f51fb..0000000000 --- a/modules/luci-mod-network/luasrc/view/admin_network/wifi_status.htm +++ /dev/null @@ -1,14 +0,0 @@ -<%+cbi/valueheader%> - -<span class="ifacebadge large"<%=attr("data-wifi-status", self.ifname)%>> - <small> - <img src="<%=resource%>/icons/signal-none.png" title="<%:Not associated%>" />  - </small> - <span> - <em class="spinning"><%:Collecting data...%></em> - </span> -</span> - -<script type="text/javascript" src="<%=resource%>/view/network/wifi_status.js"></script> - -<%+cbi/valuefooter%> |