diff options
author | Jo-Philipp Wich <jo@mein.io> | 2019-09-04 17:26:38 +0200 |
---|---|---|
committer | Jo-Philipp Wich <jo@mein.io> | 2019-09-10 15:28:16 +0200 |
commit | 963b7636b46fd1b9ae47e2317ef491ce1b0ecfd5 (patch) | |
tree | 63c6f7ba20e2b88a50b2cf5656906f344fc036a5 /modules/luci-mod-network/htdocs/luci-static/resources | |
parent | dab0a11b7357e5a609906e43fc5699b73ee11dfb (diff) |
luci-mod-network: switch to client side wifi configuration pages
Rewrite the wireless network management views in client side JS using ubus
rpc calls for the router communication.
Signed-off-by: Jo-Philipp Wich <jo@mein.io>
Diffstat (limited to 'modules/luci-mod-network/htdocs/luci-static/resources')
3 files changed, 1920 insertions, 294 deletions
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..a058b3fe52 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,1942 @@ -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)) ]); +} - ev.target.previousElementSibling.value = '1'; - return true; +function render_network_badge(radioNet) { + return render_signal_badge(radioNet.isUp() ? radioNet.getSignalPercent() : -1, radioNet.getSignal(), radioNet.getNoise()); } -function wifi_restart(ev) { - L.halt(); +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'), + 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'; + } + + return s; +} + +function radio_restart(id, ev) { + var row = document.querySelector('.cbi-section-table-row[data-sid="%s"]'.format(id)), + dsc = row.querySelector('[data-name="_stat"] > div'), + btn = row.querySelector('.cbi-section-actions button'); + + btn.blur(); + btn.classList.add('spinning'); + btn.disabled = true; + + dsc.setAttribute('restart', ''); + L.dom.content(dsc, E('em', _('Device is restarting…'))); +} + +function network_updown(id, map, ev) { + var radio = uci.get('wireless', id, 'device'), + disabled = (uci.get('wireless', id, 'disabled') == '1') || + (uci.get('wireless', radio, 'disabled') == '1'); + + if (disabled) { + uci.unset('wireless', id, 'disabled'); + uci.unset('wireless', radio, 'disabled'); + } + else { + uci.set('wireless', id, 'disabled', '1'); + + var all_networks_disabled = true, + wifi_ifaces = uci.sections('wireless', 'wifi-iface'); + + for (var i = 0; i < wifi_ifaces.length; i++) { + if (wifi_ifaces[i].device == radio && wifi_ifaces[i].disabled != '1') { + all_networks_disabled = false; + break; + } + } + + if (all_networks_disabled) + uci.set('wireless', radio, 'disabled', '1'); + } - findParent(ev.target, '.table').querySelectorAll('[data-disabled="false"]').forEach(function(s) { - L.dom.content(s, E('em', _('Wireless is restarting...'))); + return map.save().then(function() { + L.ui.changes.apply() }); +} + +function next_free_sid(offset) { + var sid = 'wifinet' + offset; - L.post(L.url('admin/network/wireless_reconnect', ev.target.getAttribute('data-radio')), L.run); + while (uci.get('wireless', sid)) + sid = 'wifinet' + (++offset); + + 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'); -document.querySelectorAll('[data-network]').forEach(function(n) { - networks.push(n.getAttribute('data-network')); + this.setValues(band, this.bands[mode.value]); + this.toggleWifiChannel(elem); + }, + + toggleWifiChannel: function(elem) { + var band = elem.querySelector('.band'); + var chan = elem.querySelector('.channel'); + + this.setValues(chan, this.channels[band.value]); + }, + + setInitialValues: function(section_id, elem) { + var mode = elem.querySelector('.mode'), + band = elem.querySelector('.band'), + chan = elem.querySelector('.channel'), + bwdt = elem.querySelector('.htmode'), + htval = uci.get('wireless', section_id, 'htmode'), + hwval = uci.get('wireless', section_id, 'hwmode'), + chval = uci.get('wireless', section_id, 'channel'); + + this.setValues(mode, this.modes); + + if (/VHT20|VHT40|VHT80|VHT160/.test(htval)) + mode.value = 'ac'; + else if (/HT20|HT40/.test(htval)) + mode.value = 'n'; + else + mode.value = ''; + + this.toggleWifiMode(elem); + + if (/a/.test(hwval)) + band.value = '11a'; + else + band.value = '11g'; + + this.toggleWifiBand(elem); + + bwdt.value = htval; + chan.value = chval; + + return elem; + }, + + renderWidget: function(section_id, option_index, cfgvalue) { + var elem = E('div'); + + L.dom.content(elem, [ + E('label', { 'style': 'float:left; margin-right:3px' }, [ + _('Mode'), E('br'), + E('select', { + 'class': 'mode', + 'style': 'width:auto', + 'change': L.bind(this.toggleWifiMode, this, elem) + }) + ]), + E('label', { 'style': 'float:left; margin-right:3px' }, [ + _('Band'), E('br'), + E('select', { + 'class': 'band', + 'style': 'width:auto', + 'change': L.bind(this.toggleWifiBand, this, elem) + }) + ]), + E('label', { 'style': 'float:left; margin-right:3px' }, [ + _('Channel'), E('br'), + E('select', { + 'class': 'channel', + 'style': 'width:auto' + }) + ]), + E('label', { 'style': 'float:left; margin-right:3px' }, [ + _('Width'), E('br'), + E('select', { + 'class': 'htmode', + 'style': 'width:auto' + }) + ]), + E('br', { 'style': 'clear:left' }) + ]); + + return this.setInitialValues(section_id, elem); + }, + + cfgvalue: function(section_id) { + return [ + uci.get('wireless', section_id, 'htmode'), + uci.get('wireless', section_id, 'hwmode'), + uci.get('wireless', section_id, 'channel') + ]; + }, + + formvalue: function(section_id) { + var node = this.map.findElement('data-field', this.cbid(section_id)); + + return [ + node.querySelector('.htmode').value, + node.querySelector('.band').value, + node.querySelector('.channel').value + ]; + }, + + write: function(section_id, value) { + uci.set('wireless', section_id, 'htmode', value[0] || null); + uci.set('wireless', section_id, 'hwmode', value[1]); + uci.set('wireless', section_id, 'channel', value[2]); + } }); -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'; + + 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 : '?') : '' + ])); - 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') ]); + 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; + }, + + 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'); - 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')) - ], ' | '); + 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'); + + 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'); + //nt.mac_hints(function(mac, name) ml:value(mac, '%s (%s)' %{ mac, name }) end); + + 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; + }; + + + encr.value('none', _('No Encryption')); + encr.value('wep-open', _('WEP Open System')); + encr.value('wep-shared', _('WEP Shared Key')); + + 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) { + encr.value('psk', 'WPA-PSK'); + encr.value('psk2', 'WPA2-PSK'); + encr.value('psk-mixed', 'WPA-PSK/WPA2-PSK Mixed Mode'); + } + 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) { + encr.value('sae', 'WPA3-SAE'); + encr.value('sae-mixed', 'WPA2-PSK/WPA3-SAE Mixed Mode'); + } + + if (has_ap_eap || has_sta_eap) { + encr.value('wpa', 'WPA-EAP'); + encr.value('wpa2', 'WPA2-EAP'); + } + + if (has_ap_owe || has_sta_owe) { + encr.value('owe', 'OWE'); + } + + 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') { + encr.value('psk', 'WPA-PSK'); + encr.value('psk2', 'WPA2-PSK'); + encr.value('psk+psk2', 'WPA-PSK/WPA2-PSK Mixed Mode'); + } + + + 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'); + }; + + + 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) ? slot : 1; + }; + + 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.default = ''; + o.value('', _('Disabled (default)')); + 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 = 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 create 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)); } -); +}); |