luci-mod-network: add WPA3-EAP and mixed WPA2/WPA3-EAP settings
[project/luci.git] / modules / luci-mod-network / htdocs / luci-static / resources / view / network / wireless.js
1 'use strict';
2 'require fs';
3 'require ui';
4 'require rpc';
5 'require uci';
6 'require form';
7 'require network';
8 'require firewall';
9 'require tools.widgets as widgets';
10
11 function count_changes(section_id) {
12 var changes = ui.changes.changes, n = 0;
13
14 if (!L.isObject(changes))
15 return n;
16
17 if (Array.isArray(changes.wireless))
18 for (var i = 0; i < changes.wireless.length; i++)
19 n += (changes.wireless[i][1] == section_id);
20
21 return n;
22 }
23
24 function render_radio_badge(radioDev) {
25 return E('span', { 'class': 'ifacebadge' }, [
26 E('img', { 'src': L.resource('icons/wifi%s.png').format(radioDev.isUp() ? '' : '_disabled') }),
27 ' ',
28 radioDev.getName()
29 ]);
30 }
31
32 function render_signal_badge(signalPercent, signalValue, noiseValue, wrap) {
33 var icon, title, value;
34
35 if (signalPercent < 0)
36 icon = L.resource('icons/signal-none.png');
37 else if (signalPercent == 0)
38 icon = L.resource('icons/signal-0.png');
39 else if (signalPercent < 25)
40 icon = L.resource('icons/signal-0-25.png');
41 else if (signalPercent < 50)
42 icon = L.resource('icons/signal-25-50.png');
43 else if (signalPercent < 75)
44 icon = L.resource('icons/signal-50-75.png');
45 else
46 icon = L.resource('icons/signal-75-100.png');
47
48 if (signalValue != null && signalValue != 0 && noiseValue != null && noiseValue != 0) {
49 value = '%d/%d\xa0%s'.format(signalValue, noiseValue, _('dBm'));
50 title = '%s: %d %s / %s: %d %s / %s %d'.format(
51 _('Signal'), signalValue, _('dBm'),
52 _('Noise'), noiseValue, _('dBm'),
53 _('SNR'), signalValue - noiseValue);
54 }
55 else if (signalValue != null && signalValue != 0) {
56 value = '%d %s'.format(signalValue, _('dBm'));
57 title = '%s: %d %s'.format(_('Signal'), signalValue, _('dBm'));
58 }
59 else if (signalPercent > -1) {
60 value = '\xa0---\xa0';
61 title = _('No signal');
62 }
63 else {
64 value = E('em', {}, E('small', {}, [ _('disabled') ]));
65 title = _('Interface is disabled');
66 }
67
68 return E('div', {
69 'class': wrap ? 'center' : 'ifacebadge',
70 'title': title,
71 'data-signal': signalValue,
72 'data-noise': noiseValue
73 }, [
74 E('img', { 'src': icon }),
75 E('span', {}, [
76 wrap ? E('br') : ' ',
77 value
78 ])
79 ]);
80 }
81
82 function render_network_badge(radioNet) {
83 return render_signal_badge(radioNet.isUp() ? radioNet.getSignalPercent() : -1, radioNet.getSignal(), radioNet.getNoise());
84 }
85
86 function render_radio_status(radioDev, wifiNets) {
87 var name = radioDev.getI18n().replace(/ Wireless Controller .+$/, ''),
88 node = E('div', [ E('big', {}, E('strong', {}, name)), E('div') ]),
89 channel, frequency, bitrate;
90
91 for (var i = 0; i < wifiNets.length; i++) {
92 channel = channel || wifiNets[i].getChannel();
93 frequency = frequency || wifiNets[i].getFrequency();
94 bitrate = bitrate || wifiNets[i].getBitRate();
95 }
96
97 if (radioDev.isUp())
98 L.itemlist(node.lastElementChild, [
99 _('Channel'), '%s (%s %s)'.format(channel || '?', frequency || '?', _('GHz')),
100 _('Bitrate'), '%s %s'.format(bitrate || '?', _('Mbit/s'))
101 ], ' | ');
102 else
103 node.lastElementChild.appendChild(E('em', _('Device is not active')));
104
105 return node;
106 }
107
108 function render_network_status(radioNet) {
109 var mode = radioNet.getActiveMode(),
110 bssid = radioNet.getActiveBSSID(),
111 channel = radioNet.getChannel(),
112 disabled = (radioNet.get('disabled') == '1' || uci.get('wireless', radioNet.getWifiDeviceName(), 'disabled') == '1'),
113 is_assoc = (bssid && bssid != '00:00:00:00:00:00' && channel && mode != 'Unknown' && !disabled),
114 is_mesh = (radioNet.getMode() == 'mesh'),
115 changecount = count_changes(radioNet.getName()),
116 status_text = null;
117
118 if (changecount)
119 status_text = E('a', {
120 href: '#',
121 click: L.bind(ui.changes.displayChanges, ui.changes)
122 }, _('Interface has %d pending changes').format(changecount));
123 else if (!is_assoc)
124 status_text = E('em', disabled ? _('Wireless is disabled') : _('Wireless is not associated'));
125
126 return L.itemlist(E('div'), [
127 is_mesh ? _('Mesh ID') : _('SSID'), (is_mesh ? radioNet.getMeshID() : radioNet.getSSID()) || '?',
128 _('Mode'), mode,
129 _('BSSID'), (!changecount && is_assoc) ? bssid : null,
130 _('Encryption'), (!changecount && is_assoc) ? radioNet.getActiveEncryption() || _('None') : null,
131 null, status_text
132 ], [ ' | ', E('br') ]);
133 }
134
135 function render_modal_status(node, radioNet) {
136 var mode = radioNet.getActiveMode(),
137 noise = radioNet.getNoise(),
138 bssid = radioNet.getActiveBSSID(),
139 channel = radioNet.getChannel(),
140 disabled = (radioNet.get('disabled') == '1'),
141 is_assoc = (bssid && bssid != '00:00:00:00:00:00' && channel && mode != 'Unknown' && !disabled);
142
143 if (node == null)
144 node = E('span', { 'class': 'ifacebadge large', 'data-network': radioNet.getName() }, [ E('small'), E('span') ]);
145
146 L.dom.content(node.firstElementChild, render_signal_badge(disabled ? -1 : radioNet.getSignalPercent(), radioNet.getSignal(), noise, true));
147
148 L.itemlist(node.lastElementChild, [
149 _('Mode'), mode,
150 _('SSID'), radioNet.getSSID() || '?',
151 _('BSSID'), is_assoc ? bssid : null,
152 _('Encryption'), is_assoc ? radioNet.getActiveEncryption() || _('None') : null,
153 _('Channel'), is_assoc ? '%d (%.3f %s)'.format(radioNet.getChannel(), radioNet.getFrequency() || 0, _('GHz')) : null,
154 _('Tx-Power'), is_assoc ? '%d %s'.format(radioNet.getTXPower(), _('dBm')) : null,
155 _('Signal'), is_assoc ? '%d %s'.format(radioNet.getSignal(), _('dBm')) : null,
156 _('Noise'), (is_assoc && noise != null) ? '%d %s'.format(noise, _('dBm')) : null,
157 _('Bitrate'), is_assoc ? '%.1f %s'.format(radioNet.getBitRate() || 0, _('Mbit/s')) : null,
158 _('Country'), is_assoc ? radioNet.getCountryCode() : null
159 ], [ ' | ', E('br'), E('br'), E('br'), E('br'), E('br'), ' | ', E('br'), ' | ' ]);
160
161 if (!is_assoc)
162 L.dom.append(node.lastElementChild, E('em', disabled ? _('Wireless is disabled') : _('Wireless is not associated')));
163
164 return node;
165 }
166
167 function format_wifirate(rate) {
168 var s = '%.1f\xa0%s, %d\xa0%s'.format(rate.rate / 1000, _('Mbit/s'), rate.mhz, _('MHz')),
169 ht = rate.ht, vht = rate.vht,
170 mhz = rate.mhz, nss = rate.nss,
171 mcs = rate.mcs, sgi = rate.short_gi;
172
173 if (ht || vht) {
174 if (vht) s += ', VHT-MCS\xa0%d'.format(mcs);
175 if (nss) s += ', VHT-NSS\xa0%d'.format(nss);
176 if (ht) s += ', MCS\xa0%s'.format(mcs);
177 if (sgi) s += ', ' + _('Short GI').replace(/ /g, '\xa0');
178 }
179
180 return s;
181 }
182
183 function radio_restart(id, ev) {
184 var row = document.querySelector('.cbi-section-table-row[data-sid="%s"]'.format(id)),
185 dsc = row.querySelector('[data-name="_stat"] > div'),
186 btn = row.querySelector('.cbi-section-actions button');
187
188 btn.blur();
189 btn.classList.add('spinning');
190 btn.disabled = true;
191
192 dsc.setAttribute('restart', '');
193 L.dom.content(dsc, E('em', _('Device is restarting…')));
194 }
195
196 function network_updown(id, map, ev) {
197 var radio = uci.get('wireless', id, 'device'),
198 disabled = (uci.get('wireless', id, 'disabled') == '1') ||
199 (uci.get('wireless', radio, 'disabled') == '1');
200
201 if (disabled) {
202 uci.unset('wireless', id, 'disabled');
203 uci.unset('wireless', radio, 'disabled');
204 }
205 else {
206 uci.set('wireless', id, 'disabled', '1');
207
208 var all_networks_disabled = true,
209 wifi_ifaces = uci.sections('wireless', 'wifi-iface');
210
211 for (var i = 0; i < wifi_ifaces.length; i++) {
212 if (wifi_ifaces[i].device == radio && wifi_ifaces[i].disabled != '1') {
213 all_networks_disabled = false;
214 break;
215 }
216 }
217
218 if (all_networks_disabled)
219 uci.set('wireless', radio, 'disabled', '1');
220 }
221
222 return map.save().then(function() {
223 ui.changes.apply()
224 });
225 }
226
227 function next_free_sid(offset) {
228 var sid = 'wifinet' + offset;
229
230 while (uci.get('wireless', sid))
231 sid = 'wifinet' + (++offset);
232
233 return sid;
234 }
235
236 function add_dependency_permutations(o, deps) {
237 var res = null;
238
239 for (var key in deps) {
240 if (!deps.hasOwnProperty(key) || !Array.isArray(deps[key]))
241 continue;
242
243 var list = deps[key],
244 tmp = [];
245
246 for (var j = 0; j < list.length; j++) {
247 for (var k = 0; k < (res ? res.length : 1); k++) {
248 var item = (res ? Object.assign({}, res[k]) : {});
249 item[key] = list[j];
250 tmp.push(item);
251 }
252 }
253
254 res = tmp;
255 }
256
257 for (var i = 0; i < (res ? res.length : 0); i++)
258 o.depends(res[i]);
259 }
260
261 var CBIWifiFrequencyValue = form.Value.extend({
262 callFrequencyList: rpc.declare({
263 object: 'iwinfo',
264 method: 'freqlist',
265 params: [ 'device' ],
266 expect: { results: [] }
267 }),
268
269 load: function(section_id) {
270 return Promise.all([
271 network.getWifiDevice(section_id),
272 this.callFrequencyList(section_id)
273 ]).then(L.bind(function(data) {
274 this.channels = {
275 '11g': L.hasSystemFeature('hostapd', 'acs') ? [ 'auto', 'auto', true ] : [],
276 '11a': L.hasSystemFeature('hostapd', 'acs') ? [ 'auto', 'auto', true ] : []
277 };
278
279 for (var i = 0; i < data[1].length; i++)
280 this.channels[(data[1][i].mhz > 2484) ? '11a' : '11g'].push(
281 data[1][i].channel,
282 '%d (%d Mhz)'.format(data[1][i].channel, data[1][i].mhz),
283 !data[1][i].restricted
284 );
285
286 var hwmodelist = L.toArray(data[0] ? data[0].getHWModes() : null)
287 .reduce(function(o, v) { o[v] = true; return o }, {});
288
289 this.modes = [
290 '', 'Legacy', true,
291 'n', 'N', hwmodelist.n,
292 'ac', 'AC', hwmodelist.ac
293 ];
294
295 var htmodelist = L.toArray(data[0] ? data[0].getHTModes() : null)
296 .reduce(function(o, v) { o[v] = true; return o }, {});
297
298 this.htmodes = {
299 '': [ '', '-', true ],
300 'n': [
301 'HT20', '20 MHz', htmodelist.HT20,
302 'HT40', '40 MHz', htmodelist.HT40
303 ],
304 'ac': [
305 'VHT20', '20 MHz', htmodelist.VHT20,
306 'VHT40', '40 MHz', htmodelist.VHT40,
307 'VHT80', '80 MHz', htmodelist.VHT80,
308 'VHT160', '160 MHz', htmodelist.VHT160
309 ]
310 };
311
312 this.bands = {
313 '': [
314 '11g', '2.4 GHz', this.channels['11g'].length > 3,
315 '11a', '5 GHz', this.channels['11a'].length > 3
316 ],
317 'n': [
318 '11g', '2.4 GHz', this.channels['11g'].length > 3,
319 '11a', '5 GHz', this.channels['11a'].length > 3
320 ],
321 'ac': [
322 '11a', '5 GHz', true
323 ]
324 };
325 }, this));
326 },
327
328 setValues: function(sel, vals) {
329 if (sel.vals)
330 sel.vals.selected = sel.selectedIndex;
331
332 while (sel.options[0])
333 sel.remove(0);
334
335 for (var i = 0; vals && i < vals.length; i += 3)
336 if (vals[i+2])
337 sel.add(E('option', { value: vals[i+0] }, [ vals[i+1] ]));
338
339 if (vals && !isNaN(vals.selected))
340 sel.selectedIndex = vals.selected;
341
342 sel.parentNode.style.display = (sel.options.length <= 1) ? 'none' : '';
343 sel.vals = vals;
344 },
345
346 toggleWifiMode: function(elem) {
347 this.toggleWifiHTMode(elem);
348 this.toggleWifiBand(elem);
349 },
350
351 toggleWifiHTMode: function(elem) {
352 var mode = elem.querySelector('.mode');
353 var bwdt = elem.querySelector('.htmode');
354
355 this.setValues(bwdt, this.htmodes[mode.value]);
356 },
357
358 toggleWifiBand: function(elem) {
359 var mode = elem.querySelector('.mode');
360 var band = elem.querySelector('.band');
361
362 this.setValues(band, this.bands[mode.value]);
363 this.toggleWifiChannel(elem);
364 },
365
366 toggleWifiChannel: function(elem) {
367 var band = elem.querySelector('.band');
368 var chan = elem.querySelector('.channel');
369
370 this.setValues(chan, this.channels[band.value]);
371 },
372
373 setInitialValues: function(section_id, elem) {
374 var mode = elem.querySelector('.mode'),
375 band = elem.querySelector('.band'),
376 chan = elem.querySelector('.channel'),
377 bwdt = elem.querySelector('.htmode'),
378 htval = uci.get('wireless', section_id, 'htmode'),
379 hwval = uci.get('wireless', section_id, 'hwmode'),
380 chval = uci.get('wireless', section_id, 'channel');
381
382 this.setValues(mode, this.modes);
383
384 if (/VHT20|VHT40|VHT80|VHT160/.test(htval))
385 mode.value = 'ac';
386 else if (/HT20|HT40/.test(htval))
387 mode.value = 'n';
388 else
389 mode.value = '';
390
391 this.toggleWifiMode(elem);
392
393 if (/a/.test(hwval))
394 band.value = '11a';
395 else
396 band.value = '11g';
397
398 this.toggleWifiBand(elem);
399
400 bwdt.value = htval;
401 chan.value = chval;
402
403 return elem;
404 },
405
406 renderWidget: function(section_id, option_index, cfgvalue) {
407 var elem = E('div');
408
409 L.dom.content(elem, [
410 E('label', { 'style': 'float:left; margin-right:3px' }, [
411 _('Mode'), E('br'),
412 E('select', {
413 'class': 'mode',
414 'style': 'width:auto',
415 'change': L.bind(this.toggleWifiMode, this, elem)
416 })
417 ]),
418 E('label', { 'style': 'float:left; margin-right:3px' }, [
419 _('Band'), E('br'),
420 E('select', {
421 'class': 'band',
422 'style': 'width:auto',
423 'change': L.bind(this.toggleWifiBand, this, elem)
424 })
425 ]),
426 E('label', { 'style': 'float:left; margin-right:3px' }, [
427 _('Channel'), E('br'),
428 E('select', {
429 'class': 'channel',
430 'style': 'width:auto'
431 })
432 ]),
433 E('label', { 'style': 'float:left; margin-right:3px' }, [
434 _('Width'), E('br'),
435 E('select', {
436 'class': 'htmode',
437 'style': 'width:auto'
438 })
439 ]),
440 E('br', { 'style': 'clear:left' })
441 ]);
442
443 return this.setInitialValues(section_id, elem);
444 },
445
446 cfgvalue: function(section_id) {
447 return [
448 uci.get('wireless', section_id, 'htmode'),
449 uci.get('wireless', section_id, 'hwmode'),
450 uci.get('wireless', section_id, 'channel')
451 ];
452 },
453
454 formvalue: function(section_id) {
455 var node = this.map.findElement('data-field', this.cbid(section_id));
456
457 return [
458 node.querySelector('.htmode').value,
459 node.querySelector('.band').value,
460 node.querySelector('.channel').value
461 ];
462 },
463
464 write: function(section_id, value) {
465 uci.set('wireless', section_id, 'htmode', value[0] || null);
466 uci.set('wireless', section_id, 'hwmode', value[1]);
467 uci.set('wireless', section_id, 'channel', value[2]);
468 }
469 });
470
471 var CBIWifiTxPowerValue = form.ListValue.extend({
472 callTxPowerList: rpc.declare({
473 object: 'iwinfo',
474 method: 'txpowerlist',
475 params: [ 'device' ],
476 expect: { results: [] }
477 }),
478
479 load: function(section_id) {
480 return this.callTxPowerList(section_id).then(L.bind(function(pwrlist) {
481 this.powerval = this.wifiNetwork ? this.wifiNetwork.getTXPower() : null;
482 this.poweroff = this.wifiNetwork ? this.wifiNetwork.getTXPowerOffset() : null;
483
484 this.value('', _('driver default'));
485
486 for (var i = 0; i < pwrlist.length; i++)
487 this.value(pwrlist[i].dbm, '%d dBm (%d mW)'.format(pwrlist[i].dbm, pwrlist[i].mw));
488
489 return form.ListValue.prototype.load.apply(this, [section_id]);
490 }, this));
491 },
492
493 renderWidget: function(section_id, option_index, cfgvalue) {
494 var widget = form.ListValue.prototype.renderWidget.apply(this, [section_id, option_index, cfgvalue]);
495 widget.firstElementChild.style.width = 'auto';
496
497 L.dom.append(widget, E('span', [
498 ' - ', _('Current power'), ': ',
499 E('span', [ this.powerval != null ? '%d dBm'.format(this.powerval) : E('em', _('unknown')) ]),
500 this.poweroff ? ' + %d dB offset = %s dBm'.format(this.poweroff, this.powerval != null ? this.powerval + this.poweroff : '?') : ''
501 ]));
502
503 return widget;
504 }
505 });
506
507 var CBIWifiCountryValue = form.Value.extend({
508 callCountryList: rpc.declare({
509 object: 'iwinfo',
510 method: 'countrylist',
511 params: [ 'device' ],
512 expect: { results: [] }
513 }),
514
515 load: function(section_id) {
516 return this.callCountryList(section_id).then(L.bind(function(countrylist) {
517 if (Array.isArray(countrylist) && countrylist.length > 0) {
518 this.value('', _('driver default'));
519
520 for (var i = 0; i < countrylist.length; i++)
521 this.value(countrylist[i].iso3166, '%s - %s'.format(countrylist[i].iso3166, countrylist[i].country));
522 }
523
524 return form.Value.prototype.load.apply(this, [section_id]);
525 }, this));
526 },
527
528 validate: function(section_id, formvalue) {
529 if (formvalue != null && formvalue != '' && !/^[A-Z0-9][A-Z0-9]$/.test(formvalue))
530 return _('Use ISO/IEC 3166 alpha2 country codes.');
531
532 return true;
533 },
534
535 renderWidget: function(section_id, option_index, cfgvalue) {
536 var typeClass = (this.keylist && this.keylist.length) ? form.ListValue : form.Value;
537 return typeClass.prototype.renderWidget.apply(this, [section_id, option_index, cfgvalue]);
538 }
539 });
540
541 return L.view.extend({
542 poll_status: function(map, data) {
543 var rows = map.querySelectorAll('.cbi-section-table-row[data-sid]');
544
545 for (var i = 0; i < rows.length; i++) {
546 var section_id = rows[i].getAttribute('data-sid'),
547 radioDev = data[1].filter(function(d) { return d.getName() == section_id })[0],
548 radioNet = data[2].filter(function(n) { return n.getName() == section_id })[0],
549 badge = rows[i].querySelector('[data-name="_badge"] > div'),
550 stat = rows[i].querySelector('[data-name="_stat"]'),
551 btns = rows[i].querySelectorAll('.cbi-section-actions button'),
552 busy = btns[0].classList.contains('spinning') || btns[1].classList.contains('spinning') || btns[2].classList.contains('spinning');
553
554 if (radioDev) {
555 L.dom.content(badge, render_radio_badge(radioDev));
556 L.dom.content(stat, render_radio_status(radioDev, data[2].filter(function(n) { return n.getWifiDeviceName() == radioDev.getName() })));
557 }
558 else {
559 L.dom.content(badge, render_network_badge(radioNet));
560 L.dom.content(stat, render_network_status(radioNet));
561 }
562
563 if (stat.hasAttribute('restart'))
564 L.dom.content(stat, E('em', _('Device is restarting…')));
565
566 btns[0].disabled = busy;
567 btns[1].disabled = busy;
568 btns[2].disabled = busy;
569 }
570
571 var table = document.querySelector('#wifi_assoclist_table'),
572 hosts = data[0],
573 trows = [];
574
575 for (var i = 0; i < data[3].length; i++) {
576 var bss = data[3][i],
577 name = hosts.getHostnameByMACAddr(bss.mac),
578 ipv4 = hosts.getIPAddrByMACAddr(bss.mac),
579 ipv6 = hosts.getIP6AddrByMACAddr(bss.mac);
580
581 var hint;
582
583 if (name && ipv4 && ipv6)
584 hint = '%s <span class="hide-xs">(%s, %s)</span>'.format(name, ipv4, ipv6);
585 else if (name && (ipv4 || ipv6))
586 hint = '%s <span class="hide-xs">(%s)</span>'.format(name, ipv4 || ipv6);
587 else
588 hint = name || ipv4 || ipv6 || '?';
589
590 var row = [
591 E('span', {
592 'class': 'ifacebadge',
593 'data-ifname': bss.network.getIfname(),
594 'data-ssid': bss.network.getSSID()
595 }, [
596 E('img', {
597 'src': L.resource('icons/wifi%s.png').format(bss.network.isUp() ? '' : '_disabled'),
598 'title': bss.radio.getI18n()
599 }),
600 E('span', [
601 ' %s '.format(bss.network.getShortName()),
602 E('small', '(%s)'.format(bss.network.getIfname()))
603 ])
604 ]),
605 bss.mac,
606 hint,
607 render_signal_badge(Math.min((bss.signal + 110) / 70 * 100, 100), bss.signal, bss.noise),
608 E('span', {}, [
609 E('span', format_wifirate(bss.rx)),
610 E('br'),
611 E('span', format_wifirate(bss.tx))
612 ])
613 ];
614
615 if (bss.network.isClientDisconnectSupported()) {
616 if (table.firstElementChild.childNodes.length < 6)
617 table.firstElementChild.appendChild(E('div', { 'class': 'th cbi-section-actions'}));
618
619 row.push(E('button', {
620 'class': 'cbi-button cbi-button-remove',
621 'click': L.bind(function(net, mac, ev) {
622 L.dom.parent(ev.currentTarget, '.tr').style.opacity = 0.5;
623 ev.currentTarget.classList.add('spinning');
624 ev.currentTarget.disabled = true;
625 ev.currentTarget.blur();
626
627 net.disconnectClient(mac, true, 5, 60000);
628 }, this, bss.network, bss.mac)
629 }, [ _('Disconnect') ]));
630 }
631 else {
632 row.push('-');
633 }
634
635 trows.push(row);
636 }
637
638 cbi_update_table(table, trows, E('em', _('No information available')));
639
640 var stat = document.querySelector('.cbi-modal [data-name="_wifistat_modal"] .ifacebadge.large');
641
642 if (stat)
643 render_modal_status(stat, data[2].filter(function(n) { return n.getName() == stat.getAttribute('data-network') })[0]);
644
645 return network.flushCache();
646 },
647
648 load: function() {
649 return Promise.all([
650 uci.changes(),
651 uci.load('wireless')
652 ]);
653 },
654
655 checkAnonymousSections: function() {
656 var wifiIfaces = uci.sections('wireless', 'wifi-iface');
657
658 for (var i = 0; i < wifiIfaces.length; i++)
659 if (wifiIfaces[i]['.anonymous'])
660 return true;
661
662 return false;
663 },
664
665 callUciRename: rpc.declare({
666 object: 'uci',
667 method: 'rename',
668 params: [ 'config', 'section', 'name' ]
669 }),
670
671 render: function() {
672 if (this.checkAnonymousSections())
673 return this.renderMigration();
674 else
675 return this.renderOverview();
676 },
677
678 handleMigration: function(ev) {
679 var wifiIfaces = uci.sections('wireless', 'wifi-iface'),
680 id_offset = 0,
681 tasks = [];
682
683 for (var i = 0; i < wifiIfaces.length; i++) {
684 if (!wifiIfaces[i]['.anonymous'])
685 continue;
686
687 var new_name = next_free_sid(id_offset);
688
689 tasks.push(this.callUciRename('wireless', wifiIfaces[i]['.name'], new_name));
690 id_offset = +new_name.substring(7) + 1;
691 }
692
693 return Promise.all(tasks)
694 .then(L.bind(ui.changes.init, ui.changes))
695 .then(L.bind(ui.changes.apply, ui.changes));
696 },
697
698 renderMigration: function() {
699 ui.showModal(_('Wireless configuration migration'), [
700 E('p', _('The existing wireless configuration needs to be changed for LuCI to function properly.')),
701 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.')),
702 E('div', { 'class': 'right' },
703 E('button', {
704 'class': 'btn cbi-button-action important',
705 'click': ui.createHandlerFn(this, 'handleMigration')
706 }, _('Continue')))
707 ]);
708 },
709
710 renderOverview: function() {
711 var m, s, o;
712
713 m = new form.Map('wireless');
714 m.chain('network');
715 m.chain('firewall');
716
717 s = m.section(form.GridSection, 'wifi-device', _('Wireless Overview'));
718 s.anonymous = true;
719 s.addremove = false;
720
721 s.load = function() {
722 return network.getWifiDevices().then(L.bind(function(radios) {
723 this.radios = radios.sort(function(a, b) {
724 return a.getName() > b.getName();
725 });
726
727 var tasks = [];
728
729 for (var i = 0; i < radios.length; i++)
730 tasks.push(radios[i].getWifiNetworks());
731
732 return Promise.all(tasks);
733 }, this)).then(L.bind(function(data) {
734 this.wifis = [];
735
736 for (var i = 0; i < data.length; i++)
737 this.wifis.push.apply(this.wifis, data[i]);
738 }, this));
739 };
740
741 s.cfgsections = function() {
742 var rv = [];
743
744 for (var i = 0; i < this.radios.length; i++) {
745 rv.push(this.radios[i].getName());
746
747 for (var j = 0; j < this.wifis.length; j++)
748 if (this.wifis[j].getWifiDeviceName() == this.radios[i].getName())
749 rv.push(this.wifis[j].getName());
750 }
751
752 return rv;
753 };
754
755 s.modaltitle = function(section_id) {
756 var radioNet = this.wifis.filter(function(w) { return w.getName() == section_id})[0];
757 return radioNet ? radioNet.getI18n() : _('Edit wireless network');
758 };
759
760 s.lookupRadioOrNetwork = function(section_id) {
761 var radioDev = this.radios.filter(function(r) { return r.getName() == section_id })[0];
762 if (radioDev)
763 return radioDev;
764
765 var radioNet = this.wifis.filter(function(w) { return w.getName() == section_id })[0];
766 if (radioNet)
767 return radioNet;
768
769 return null;
770 };
771
772 s.renderRowActions = function(section_id) {
773 var inst = this.lookupRadioOrNetwork(section_id), btns;
774
775 if (inst.getWifiNetworks) {
776 btns = [
777 E('button', {
778 'class': 'cbi-button cbi-button-neutral',
779 'title': _('Restart radio interface'),
780 'click': ui.createHandlerFn(this, radio_restart, section_id)
781 }, _('Restart')),
782 E('button', {
783 'class': 'cbi-button cbi-button-action important',
784 'title': _('Find and join network'),
785 'click': ui.createHandlerFn(this, 'handleScan', inst)
786 }, _('Scan')),
787 E('button', {
788 'class': 'cbi-button cbi-button-add',
789 'title': _('Provide new network'),
790 'click': ui.createHandlerFn(this, 'handleAdd', inst)
791 }, _('Add'))
792 ];
793 }
794 else {
795 var isDisabled = (inst.get('disabled') == '1' ||
796 uci.get('wireless', inst.getWifiDeviceName(), 'disabled') == '1');
797
798 btns = [
799 E('button', {
800 'class': 'cbi-button cbi-button-neutral enable-disable',
801 'title': isDisabled ? _('Enable this network') : _('Disable this network'),
802 'click': ui.createHandlerFn(this, network_updown, section_id, this.map)
803 }, isDisabled ? _('Enable') : _('Disable')),
804 E('button', {
805 'class': 'cbi-button cbi-button-action important',
806 'title': _('Edit this network'),
807 'click': ui.createHandlerFn(this, 'renderMoreOptionsModal', section_id)
808 }, _('Edit')),
809 E('button', {
810 'class': 'cbi-button cbi-button-negative remove',
811 'title': _('Delete this network'),
812 'click': ui.createHandlerFn(this, 'handleRemove', section_id)
813 }, _('Remove'))
814 ];
815 }
816
817 return E('div', { 'class': 'td middle cbi-section-actions' }, E('div', btns));
818 };
819
820 s.addModalOptions = function(s) {
821 return network.getWifiNetwork(s.section).then(function(radioNet) {
822 var hwtype = uci.get('wireless', radioNet.getWifiDeviceName(), 'type');
823 var o, ss;
824
825 o = s.option(form.SectionValue, '_device', form.NamedSection, radioNet.getWifiDeviceName(), 'wifi-device', _('Device Configuration'));
826 o.modalonly = true;
827
828 ss = o.subsection;
829 ss.tab('general', _('General Setup'));
830 ss.tab('advanced', _('Advanced Settings'));
831
832 var isDisabled = (radioNet.get('disabled') == '1' ||
833 uci.get('wireless', radioNet.getWifiDeviceName(), 'disabled') == 1);
834
835 o = ss.taboption('general', form.DummyValue, '_wifistat_modal', _('Status'));
836 o.cfgvalue = L.bind(function(radioNet) {
837 return render_modal_status(null, radioNet);
838 }, this, radioNet);
839 o.write = function() {};
840
841 o = ss.taboption('general', form.Button, '_toggle', isDisabled ? _('Wireless network is disabled') : _('Wireless network is enabled'));
842 o.inputstyle = isDisabled ? 'apply' : 'reset';
843 o.inputtitle = isDisabled ? _('Enable') : _('Disable');
844 o.onclick = ui.createHandlerFn(s, network_updown, s.section, s.map);
845
846 o = ss.taboption('general', CBIWifiFrequencyValue, '_freq', '<br />' + _('Operating frequency'));
847 o.ucisection = s.section;
848
849 if (hwtype == 'mac80211') {
850 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.'));
851 o.wifiNetwork = radioNet;
852
853 o = ss.taboption('advanced', CBIWifiCountryValue, 'country', _('Country Code'));
854 o.wifiNetwork = radioNet;
855
856 o = ss.taboption('advanced', form.Flag, 'legacy_rates', _('Allow legacy 802.11b rates'));
857 o.default = o.enabled;
858
859 o = ss.taboption('advanced', form.Value, 'distance', _('Distance Optimization'), _('Distance to farthest network member in meters.'));
860 o.datatype = 'range(0,114750)';
861 o.placeholder = 'auto';
862
863 o = ss.taboption('advanced', form.Value, 'frag', _('Fragmentation Threshold'));
864 o.datatype = 'min(256)';
865 o.placeholder = _('off');
866
867 o = ss.taboption('advanced', form.Value, 'rts', _('RTS/CTS Threshold'));
868 o.datatype = 'uinteger';
869 o.placeholder = _('off');
870
871 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!'));
872 o.rmempty = true;
873
874 o = ss.taboption('advanced', form.Value, 'beacon_int', _('Beacon Interval'));
875 o.datatype = 'range(15,65535)';
876 o.placeholder = 100;
877 o.rmempty = true;
878 }
879
880
881 o = s.option(form.SectionValue, '_device', form.NamedSection, radioNet.getName(), 'wifi-iface', _('Interface Configuration'));
882 o.modalonly = true;
883
884 ss = o.subsection;
885 ss.tab('general', _('General Setup'));
886 ss.tab('encryption', _('Wireless Security'));
887 ss.tab('macfilter', _('MAC-Filter'));
888 ss.tab('advanced', _('Advanced Settings'));
889
890 o = ss.taboption('general', form.ListValue, 'mode', _('Mode'));
891 o.value('ap', _('Access Point'));
892 o.value('sta', _('Client'));
893 o.value('adhoc', _('Ad-Hoc'));
894
895 o = ss.taboption('general', form.Value, 'mesh_id', _('Mesh Id'));
896 o.depends('mode', 'mesh');
897
898 o = ss.taboption('advanced', form.Flag, 'mesh_fwding', _('Forward mesh peer traffic'));
899 o.rmempty = false;
900 o.default = '1';
901 o.depends('mode', 'mesh');
902
903 o = ss.taboption('advanced', form.Value, 'mesh_rssi_threshold', _('RSSI threshold for joining'), _('0 = not using RSSI threshold, 1 = do not change driver default'));
904 o.rmempty = false;
905 o.default = '0';
906 o.datatype = 'range(-255,1)';
907 o.depends('mode', 'mesh');
908
909 o = ss.taboption('general', form.Value, 'ssid', _('<abbr title="Extended Service Set Identifier">ESSID</abbr>'));
910 o.datatype = 'maxlength(32)';
911 o.depends('mode', 'ap');
912 o.depends('mode', 'sta');
913 o.depends('mode', 'adhoc');
914 o.depends('mode', 'ahdemo');
915 o.depends('mode', 'monitor');
916 o.depends('mode', 'ap-wds');
917 o.depends('mode', 'sta-wds');
918 o.depends('mode', 'wds');
919
920 o = ss.taboption('general', form.Value, 'bssid', _('<abbr title="Basic Service Set Identifier">BSSID</abbr>'));
921 o.datatype = 'macaddr';
922
923 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.'));
924 o.rmempty = true;
925 o.multiple = true;
926 o.novirtual = true;
927 o.write = function(section_id, value) {
928 return network.getDevice(section_id).then(L.bind(function(dev) {
929 var old_networks = dev.getNetworks().reduce(function(o, v) { o[v.getName()] = v; return o }, {}),
930 new_networks = {},
931 values = L.toArray(value),
932 tasks = [];
933
934 for (var i = 0; i < values.length; i++) {
935 new_networks[values[i]] = true;
936
937 if (old_networks[values[i]])
938 continue;
939
940 tasks.push(network.getNetwork(values[i]).then(L.bind(function(name, net) {
941 return net || network.addNetwork(name, { proto: 'none' });
942 }, this, values[i])).then(L.bind(function(dev, net) {
943 if (net) {
944 if (!net.isEmpty())
945 net.set('type', 'bridge');
946 net.addDevice(dev);
947 }
948 }, this, dev)));
949 }
950
951 for (var name in old_networks)
952 if (!new_networks[name])
953 tasks.push(network.getNetwork(name).then(L.bind(function(dev, net) {
954 if (net)
955 net.deleteDevice(dev);
956 }, this, dev)));
957
958 return Promise.all(tasks);
959 }, this));
960 };
961
962 if (hwtype == 'mac80211') {
963 var mode = ss.children[0],
964 bssid = ss.children[5],
965 encr;
966
967 mode.value('mesh', '802.11s');
968 mode.value('ahdemo', _('Pseudo Ad-Hoc (ahdemo)'));
969 mode.value('monitor', _('Monitor'));
970
971 bssid.depends('mode', 'adhoc');
972 bssid.depends('mode', 'sta');
973 bssid.depends('mode', 'sta-wds');
974
975 o = ss.taboption('macfilter', form.ListValue, 'macfilter', _('MAC-Address Filter'));
976 o.depends('mode', 'ap');
977 o.depends('mode', 'ap-wds');
978 o.value('', _('disable'));
979 o.value('allow', _('Allow listed only'));
980 o.value('deny', _('Allow all except listed'));
981
982 o = ss.taboption('macfilter', form.DynamicList, 'maclist', _('MAC-List'));
983 o.datatype = 'macaddr';
984 o.depends('macfilter', 'allow');
985 o.depends('macfilter', 'deny');
986 o.load = function(section_id) {
987 return network.getHostHints().then(L.bind(function(hints) {
988 hints.getMACHints().map(L.bind(function(hint) {
989 this.value(hint[0], hint[1] ? '%s (%s)'.format(hint[0], hint[1]) : hint[0]);
990 }, this));
991
992 return form.DynamicList.prototype.load.apply(this, [section_id]);
993 }, this));
994 };
995
996 mode.value('ap-wds', '%s (%s)'.format(_('Access Point'), _('WDS')));
997 mode.value('sta-wds', '%s (%s)'.format(_('Client'), _('WDS')));
998
999 mode.write = function(section_id, value) {
1000 switch (value) {
1001 case 'ap-wds':
1002 uci.set('wireless', section_id, 'mode', 'ap');
1003 uci.set('wireless', section_id, 'wds', '1');
1004 break;
1005
1006 case 'sta-wds':
1007 uci.set('wireless', section_id, 'mode', 'sta');
1008 uci.set('wireless', section_id, 'wds', '1');
1009 break;
1010
1011 default:
1012 uci.set('wireless', section_id, 'mode', value);
1013 uci.unset('wireless', section_id, 'wds');
1014 break;
1015 }
1016 };
1017
1018 mode.cfgvalue = function(section_id) {
1019 var mode = uci.get('wireless', section_id, 'mode'),
1020 wds = uci.get('wireless', section_id, 'wds');
1021
1022 if (mode == 'ap' && wds)
1023 return 'ap-wds';
1024 else if (mode == 'sta' && wds)
1025 return 'sta-wds';
1026
1027 return mode;
1028 };
1029
1030 o = ss.taboption('general', form.Flag, 'hidden', _('Hide <abbr title="Extended Service Set Identifier">ESSID</abbr>'));
1031 o.depends('mode', 'ap');
1032 o.depends('mode', 'ap-wds');
1033
1034 o = ss.taboption('general', form.Flag, 'wmm', _('WMM Mode'));
1035 o.depends('mode', 'ap');
1036 o.depends('mode', 'ap-wds');
1037 o.default = o.enabled;
1038
1039 o = ss.taboption('advanced', form.Flag, 'isolate', _('Isolate Clients'), _('Prevents client-to-client communication'));
1040 o.depends('mode', 'ap');
1041 o.depends('mode', 'ap-wds');
1042
1043 o = ss.taboption('advanced', form.Value, 'ifname', _('Interface name'), _('Override default interface name'));
1044 o.optional = true;
1045 o.placeholder = radioNet.getIfname();
1046 if (/^radio\d+\.network/.test(o.placeholder))
1047 o.placeholder = '';
1048
1049 o = ss.taboption('advanced', form.Flag, 'short_preamble', _('Short Preamble'));
1050 o.default = o.enabled;
1051
1052 o = ss.taboption('advanced', form.Value, 'dtim_period', _('DTIM Interval'), _('Delivery Traffic Indication Message Interval'));
1053 o.optional = true;
1054 o.placeholder = 2;
1055 o.datatype = 'range(1,255)';
1056
1057 o = ss.taboption('advanced', form.Value, 'wpa_group_rekey', _('Time interval for rekeying GTK'), _('sec'));
1058 o.optional = true;
1059 o.placeholder = 600;
1060 o.datatype = 'uinteger';
1061
1062 o = ss.taboption('advanced', form.Flag , 'skip_inactivity_poll', _('Disable Inactivity Polling'));
1063 o.optional = true;
1064 o.datatype = 'uinteger';
1065
1066 o = ss.taboption('advanced', form.Value, 'max_inactivity', _('Station inactivity limit'), _('sec'));
1067 o.optional = true;
1068 o.placeholder = 300;
1069 o.datatype = 'uinteger';
1070
1071 o = ss.taboption('advanced', form.Value, 'max_listen_interval', _('Maximum allowed Listen Interval'));
1072 o.optional = true;
1073 o.placeholder = 65535;
1074 o.datatype = 'uinteger';
1075
1076 o = ss.taboption('advanced', form.Flag, 'disassoc_low_ack', _('Disassociate On Low Acknowledgement'), _('Allow AP mode to disconnect STAs based on low ACK condition'));
1077 o.default = o.enabled;
1078 }
1079
1080
1081 encr = o = ss.taboption('encryption', form.ListValue, 'encryption', _('Encryption'));
1082 o.depends('mode', 'ap');
1083 o.depends('mode', 'sta');
1084 o.depends('mode', 'adhoc');
1085 o.depends('mode', 'ahdemo');
1086 o.depends('mode', 'ap-wds');
1087 o.depends('mode', 'sta-wds');
1088 o.depends('mode', 'mesh');
1089
1090 o.cfgvalue = function(section_id) {
1091 var v = String(uci.get('wireless', section_id, 'encryption'));
1092 if (v == 'wep')
1093 return 'wep-open';
1094 else if (v.match(/\+/))
1095 return v.replace(/\+.+$/, '');
1096 return v;
1097 };
1098
1099 o.write = function(section_id, value) {
1100 var e = this.section.children.filter(function(o) { return o.option == 'encryption' })[0].formvalue(section_id),
1101 co = this.section.children.filter(function(o) { return o.option == 'cipher' })[0], c = co.formvalue(section_id);
1102
1103 if (value == 'wpa' || value == 'wpa2' || value == 'wpa3' || value == 'wpa3-mixed')
1104 uci.unset('wireless', section_id, 'key');
1105
1106 if (co.isActive(section_id) && e && (c == 'tkip' || c == 'ccmp' || c == 'tkip+ccmp'))
1107 e += '+' + c;
1108
1109 uci.set('wireless', section_id, 'encryption', e);
1110 };
1111
1112 o = ss.taboption('encryption', form.ListValue, 'cipher', _('Cipher'));
1113 o.depends('encryption', 'wpa');
1114 o.depends('encryption', 'wpa2');
1115 o.depends('encryption', 'wpa3');
1116 o.depends('encryption', 'wpa3-mixed');
1117 o.depends('encryption', 'psk');
1118 o.depends('encryption', 'psk2');
1119 o.depends('encryption', 'wpa-mixed');
1120 o.depends('encryption', 'psk-mixed');
1121 o.value('auto', _('auto'));
1122 o.value('ccmp', _('Force CCMP (AES)'));
1123 o.value('tkip', _('Force TKIP'));
1124 o.value('tkip+ccmp', _('Force TKIP and CCMP (AES)'));
1125 o.write = ss.children.filter(function(o) { return o.option == 'encryption' })[0].write;
1126
1127 o.cfgvalue = function(section_id) {
1128 var v = String(uci.get('wireless', section_id, 'encryption'));
1129 if (v.match(/\+/)) {
1130 v = v.replace(/^[^+]+\+/, '');
1131 if (v == 'aes')
1132 v = 'ccmp';
1133 else if (v == 'tkip+aes' || v == 'aes+tkip' || v == 'ccmp+tkip')
1134 v = 'tkip+ccmp';
1135 }
1136 return v;
1137 };
1138
1139
1140 var crypto_modes = [];
1141
1142 if (hwtype == 'mac80211') {
1143 var has_supplicant = L.hasSystemFeature('wpasupplicant'),
1144 has_hostapd = L.hasSystemFeature('hostapd');
1145
1146 // Probe EAP support
1147 var has_ap_eap = L.hasSystemFeature('hostapd', 'eap'),
1148 has_sta_eap = L.hasSystemFeature('wpasupplicant', 'eap');
1149
1150 // Probe SAE support
1151 var has_ap_sae = L.hasSystemFeature('hostapd', 'sae'),
1152 has_sta_sae = L.hasSystemFeature('wpasupplicant', 'sae');
1153
1154 // Probe OWE support
1155 var has_ap_owe = L.hasSystemFeature('hostapd', 'owe'),
1156 has_sta_owe = L.hasSystemFeature('wpasupplicant', 'owe');
1157
1158 // Probe Suite-B support
1159 var has_ap_eap192 = L.hasSystemFeature('hostapd', 'suiteb192'),
1160 has_sta_eap192 = L.hasSystemFeature('wpasupplicant', 'suiteb192');
1161
1162
1163 if (has_hostapd || has_supplicant) {
1164 crypto_modes.push(['psk2', 'WPA2-PSK', 35]);
1165 crypto_modes.push(['psk-mixed', 'WPA-PSK/WPA2-PSK Mixed Mode', 22]);
1166 crypto_modes.push(['psk', 'WPA-PSK', 21]);
1167 }
1168 else {
1169 encr.description = _('WPA-Encryption requires wpa_supplicant (for client mode) or hostapd (for AP and ad-hoc mode) to be installed.');
1170 }
1171
1172 if (has_ap_sae || has_sta_sae) {
1173 crypto_modes.push(['sae', 'WPA3-SAE', 31]);
1174 crypto_modes.push(['sae-mixed', 'WPA2-PSK/WPA3-SAE Mixed Mode', 30]);
1175 }
1176
1177 if (has_ap_eap || has_sta_eap) {
1178 if (has_ap_eap192 || has_sta_eap192) {
1179 crypto_modes.push(['wpa3', 'WPA3-EAP', 33]);
1180 crypto_modes.push(['wpa3-mixed', 'WPA2-EAP/WPA3-EAP Mixed Mode', 32]);
1181 }
1182
1183 crypto_modes.push(['wpa2', 'WPA2-EAP', 34]);
1184 crypto_modes.push(['wpa', 'WPA-EAP', 20]);
1185 }
1186
1187 if (has_ap_owe || has_sta_owe) {
1188 crypto_modes.push(['owe', 'OWE', 1]);
1189 }
1190
1191 encr.crypto_support = {
1192 'ap': {
1193 'wep-open': true,
1194 'wep-shared': true,
1195 'psk': has_hostapd || _('Requires hostapd'),
1196 'psk2': has_hostapd || _('Requires hostapd'),
1197 'psk-mixed': has_hostapd || _('Requires hostapd'),
1198 'sae': has_ap_sae || _('Requires hostapd with SAE support'),
1199 'sae-mixed': has_ap_sae || _('Requires hostapd with SAE support'),
1200 'wpa': has_ap_eap || _('Requires hostapd with EAP support'),
1201 'wpa2': has_ap_eap || _('Requires hostapd with EAP support'),
1202 'wpa3': has_ap_eap192 || _('Requires hostapd with EAP Suite-B support'),
1203 'wpa3-mixed': has_ap_eap192 || _('Requires hostapd with EAP Suite-B support'),
1204 'owe': has_ap_owe || _('Requires hostapd with OWE support')
1205 },
1206 'sta': {
1207 'wep-open': true,
1208 'wep-shared': true,
1209 'psk': has_supplicant || _('Requires wpa-supplicant'),
1210 'psk2': has_supplicant || _('Requires wpa-supplicant'),
1211 'psk-mixed': has_supplicant || _('Requires wpa-supplicant'),
1212 'sae': has_sta_sae || _('Requires wpa-supplicant with SAE support'),
1213 'sae-mixed': has_sta_sae || _('Requires wpa-supplicant with SAE support'),
1214 'wpa': has_sta_eap || _('Requires wpa-supplicant with EAP support'),
1215 'wpa2': has_sta_eap || _('Requires wpa-supplicant with EAP support'),
1216 'wpa3': has_sta_eap192 || _('Requires wpa-supplicant with EAP Suite-B support'),
1217 'wpa3-mixed': has_sta_eap192 || _('Requires wpa-supplicant with EAP Suite-B support'),
1218 'owe': has_sta_owe || _('Requires wpa-supplicant with OWE support')
1219 },
1220 'adhoc': {
1221 'wep-open': true,
1222 'wep-shared': true,
1223 'psk': has_supplicant || _('Requires wpa-supplicant'),
1224 'psk2': has_supplicant || _('Requires wpa-supplicant'),
1225 'psk-mixed': has_supplicant || _('Requires wpa-supplicant'),
1226 },
1227 'mesh': {
1228 'sae': has_sta_sae || _('Requires wpa-supplicant with SAE support')
1229 },
1230 'ahdemo': {
1231 'wep-open': true,
1232 'wep-shared': true
1233 },
1234 'wds': {
1235 'wep-open': true,
1236 'wep-shared': true
1237 }
1238 };
1239
1240 encr.crypto_support['ap-wds'] = encr.crypto_support['ap'];
1241 encr.crypto_support['sta-wds'] = encr.crypto_support['sta'];
1242
1243 encr.validate = function(section_id, value) {
1244 var modeopt = this.section.children.filter(function(o) { return o.option == 'mode' })[0],
1245 modeval = modeopt.formvalue(section_id),
1246 modetitle = modeopt.vallist[modeopt.keylist.indexOf(modeval)],
1247 enctitle = this.vallist[this.keylist.indexOf(value)];
1248
1249 if (value == 'none')
1250 return true;
1251
1252 if (!L.isObject(this.crypto_support[modeval]) || !this.crypto_support[modeval].hasOwnProperty(value))
1253 return _('The selected %s mode is incompatible with %s encryption').format(modetitle, enctitle);
1254
1255 return this.crypto_support[modeval][value];
1256 };
1257 }
1258 else if (hwtype == 'broadcom') {
1259 crypto_modes.push(['psk2', 'WPA2-PSK', 33]);
1260 crypto_modes.push(['psk+psk2', 'WPA-PSK/WPA2-PSK Mixed Mode', 22]);
1261 crypto_modes.push(['psk', 'WPA-PSK', 21]);
1262 }
1263
1264 crypto_modes.push(['wep-open', _('WEP Open System'), 11]);
1265 crypto_modes.push(['wep-shared', _('WEP Shared Key'), 10]);
1266 crypto_modes.push(['none', _('No Encryption'), 0]);
1267
1268 crypto_modes.sort(function(a, b) { return b[2] - a[2] });
1269
1270 for (var i = 0; i < crypto_modes.length; i++) {
1271 var security_level = (crypto_modes[i][2] >= 30) ? _('strong security')
1272 : (crypto_modes[i][2] >= 20) ? _('medium security')
1273 : (crypto_modes[i][2] >= 10) ? _('weak security') : _('open network');
1274
1275 encr.value(crypto_modes[i][0], '%s (%s)'.format(crypto_modes[i][1], security_level));
1276 }
1277
1278
1279 o = ss.taboption('encryption', form.Value, 'auth_server', _('Radius-Authentication-Server'));
1280 add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1281 o.rmempty = true;
1282 o.datatype = 'host(0)';
1283
1284 o = ss.taboption('encryption', form.Value, 'auth_port', _('Radius-Authentication-Port'), _('Default %d').format(1812));
1285 add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1286 o.rmempty = true;
1287 o.datatype = 'port';
1288
1289 o = ss.taboption('encryption', form.Value, 'auth_secret', _('Radius-Authentication-Secret'));
1290 add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1291 o.rmempty = true;
1292 o.password = true;
1293
1294 o = ss.taboption('encryption', form.Value, 'acct_server', _('Radius-Accounting-Server'));
1295 add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1296 o.rmempty = true;
1297 o.datatype = 'host(0)';
1298
1299 o = ss.taboption('encryption', form.Value, 'acct_port', _('Radius-Accounting-Port'), _('Default %d').format(1813));
1300 add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1301 o.rmempty = true;
1302 o.datatype = 'port';
1303
1304 o = ss.taboption('encryption', form.Value, 'acct_secret', _('Radius-Accounting-Secret'));
1305 add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1306 o.rmempty = true;
1307 o.password = true;
1308
1309 o = ss.taboption('encryption', form.Value, 'dae_client', _('DAE-Client'));
1310 add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1311 o.rmempty = true;
1312 o.datatype = 'host(0)';
1313
1314 o = ss.taboption('encryption', form.Value, 'dae_port', _('DAE-Port'), _('Default %d').format(3799));
1315 add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1316 o.rmempty = true;
1317 o.datatype = 'port';
1318
1319 o = ss.taboption('encryption', form.Value, 'dae_secret', _('DAE-Secret'));
1320 add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1321 o.rmempty = true;
1322 o.password = true;
1323
1324
1325 o = ss.taboption('encryption', form.Value, '_wpa_key', _('Key'));
1326 o.depends('encryption', 'psk');
1327 o.depends('encryption', 'psk2');
1328 o.depends('encryption', 'psk+psk2');
1329 o.depends('encryption', 'psk-mixed');
1330 o.depends('encryption', 'sae');
1331 o.depends('encryption', 'sae-mixed');
1332 o.datatype = 'wpakey';
1333 o.rmempty = true;
1334 o.password = true;
1335
1336 o.cfgvalue = function(section_id) {
1337 var key = uci.get('wireless', section_id, 'key');
1338 return /^[1234]$/.test(key) ? null : key;
1339 };
1340
1341 o.write = function(section_id, value) {
1342 uci.set('wireless', section_id, 'key', value);
1343 uci.unset('wireless', section_id, 'key1');
1344 uci.unset('wireless', section_id, 'key2');
1345 uci.unset('wireless', section_id, 'key3');
1346 uci.unset('wireless', section_id, 'key4');
1347 };
1348
1349
1350 o = ss.taboption('encryption', form.ListValue, '_wep_key', _('Used Key Slot'));
1351 o.depends('encryption', 'wep-open');
1352 o.depends('encryption', 'wep-shared');
1353 o.value('1', _('Key #%d').format(1));
1354 o.value('2', _('Key #%d').format(2));
1355 o.value('3', _('Key #%d').format(3));
1356 o.value('4', _('Key #%d').format(4));
1357
1358 o.cfgvalue = function(section_id) {
1359 var slot = +uci.get('wireless', section_id, 'key');
1360 return (slot >= 1 && slot <= 4) ? String(slot) : '';
1361 };
1362
1363 o.write = function(section_id, value) {
1364 uci.set('wireless', section_id, 'key', value);
1365 };
1366
1367 for (var slot = 1; slot <= 4; slot++) {
1368 o = ss.taboption('encryption', form.Value, 'key%d'.format(slot), _('Key #%d').format(slot));
1369 o.depends('encryption', 'wep-open');
1370 o.depends('encryption', 'wep-shared');
1371 o.datatype = 'wepkey';
1372 o.rmempty = true;
1373 o.password = true;
1374
1375 o.write = function(section_id, value) {
1376 if (value != null && (value.length == 5 || value.length == 13))
1377 value = 's:%s'.format(value);
1378 uci.set('wireless', section_id, this.option, value);
1379 };
1380 }
1381
1382
1383 if (hwtype == 'mac80211') {
1384 // Probe 802.11r support (and EAP support as a proxy for Openwrt)
1385 var has_80211r = L.hasSystemFeature('hostapd', '11r') || L.hasSystemFeature('hostapd', 'eap');
1386
1387 o = ss.taboption('encryption', form.Flag, 'ieee80211r', _('802.11r Fast Transition'), _('Enables fast roaming among access points that belong to the same Mobility Domain'));
1388 add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1389 if (has_80211r)
1390 add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['psk', 'psk2', 'psk-mixed', 'sae', 'sae-mixed'] });
1391 o.rmempty = true;
1392
1393 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.'));
1394 add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1395 o.depends({ ieee80211r: '1' });
1396 o.rmempty = true;
1397
1398 o = ss.taboption('encryption', form.Value, 'mobility_domain', _('Mobility Domain'), _('4-character hexadecimal ID'));
1399 o.depends({ ieee80211r: '1' });
1400 o.placeholder = '4f57';
1401 o.datatype = 'and(hexstring,length(4))';
1402 o.rmempty = true;
1403
1404 o = ss.taboption('encryption', form.Value, 'reassociation_deadline', _('Reassociation Deadline'), _('time units (TUs / 1.024 ms) [1000-65535]'));
1405 o.depends({ ieee80211r: '1' });
1406 o.placeholder = '1000';
1407 o.datatype = 'range(1000,65535)';
1408 o.rmempty = true;
1409
1410 o = ss.taboption('encryption', form.ListValue, 'ft_over_ds', _('FT protocol'));
1411 o.depends({ ieee80211r: '1' });
1412 o.value('1', _('FT over DS'));
1413 o.value('0', _('FT over the Air'));
1414 o.rmempty = true;
1415
1416 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.'));
1417 o.depends({ ieee80211r: '1' });
1418 o.default = o.enabled;
1419 o.rmempty = false;
1420
1421 o = ss.taboption('encryption', form.Value, 'r0_key_lifetime', _('R0 Key Lifetime'), _('minutes'));
1422 o.depends({ ieee80211r: '1' });
1423 o.placeholder = '10000';
1424 o.datatype = 'uinteger';
1425 o.rmempty = true;
1426
1427 o = ss.taboption('encryption', form.Value, 'r1_key_holder', _('R1 Key Holder'), _('6-octet identifier as a hex string - no colons'));
1428 o.depends({ ieee80211r: '1' });
1429 o.placeholder = '00004f577274';
1430 o.datatype = 'and(hexstring,length(12))';
1431 o.rmempty = true;
1432
1433 o = ss.taboption('encryption', form.Flag, 'pmk_r1_push', _('PMK R1 Push'));
1434 o.depends({ ieee80211r: '1' });
1435 o.placeholder = '0';
1436 o.rmempty = true;
1437
1438 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.'));
1439 o.depends({ ieee80211r: '1' });
1440 o.rmempty = true;
1441
1442 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.'));
1443 o.depends({ ieee80211r: '1' });
1444 o.rmempty = true;
1445 // End of 802.11r options
1446
1447 o = ss.taboption('encryption', form.ListValue, 'eap_type', _('EAP-Method'));
1448 o.value('tls', 'TLS');
1449 o.value('ttls', 'TTLS');
1450 o.value('peap', 'PEAP');
1451 o.value('fast', 'FAST');
1452 add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1453
1454 o = ss.taboption('encryption', form.Flag, 'ca_cert_usesystem', _('Use system certificates'), _("Validate server certificate using built-in system CA bundle,<br />requires the \"ca-bundle\" package"));
1455 o.enabled = '1';
1456 o.disabled = '0';
1457 o.default = o.disabled;
1458 add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1459 o.validate = function(section_id, value) {
1460 if (value == '1' && !L.hasSystemFeature('cabundle')) {
1461 return _("This option cannot be used because the ca-bundle package is not installed.");
1462 }
1463 return true;
1464 };
1465
1466 o = ss.taboption('encryption', form.FileUpload, 'ca_cert', _('Path to CA-Certificate'));
1467 add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'], ca_cert_usesystem: ['0'] });
1468
1469 o = ss.taboption('encryption', form.Value, 'subject_match', _('Certificate constraint (Subject)'), _("Certificate constraint substring - e.g. /CN=wifi.mycompany.com<br />See `logread -f` during handshake for actual values"));
1470 add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1471
1472 o = ss.taboption('encryption', form.DynamicList, 'altsubject_match', _('Certificate constraint (SAN)'), _("Certificate constraint(s) via Subject Alternate Name values<br />(supported attributes: EMAIL, DNS, URI) - e.g. DNS:wifi.mycompany.com"));
1473 add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1474
1475 o = ss.taboption('encryption', form.DynamicList, 'domain_match', _('Certificate constraint (Domain)'), _("Certificate constraint(s) against DNS SAN values (if available)<br />or Subject CN (exact match)"));
1476 add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1477
1478 o = ss.taboption('encryption', form.DynamicList, 'domain_suffix_match', _('Certificate constraint (Wildcard)'), _("Certificate constraint(s) against DNS SAN values (if available)<br />or Subject CN (suffix match)"));
1479 add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1480
1481 o = ss.taboption('encryption', form.FileUpload, 'client_cert', _('Path to Client-Certificate'));
1482 add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'], eap_type: ['tls'] });
1483
1484 o = ss.taboption('encryption', form.FileUpload, 'priv_key', _('Path to Private Key'));
1485 add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'], eap_type: ['tls'] });
1486
1487 o = ss.taboption('encryption', form.Value, 'priv_key_pwd', _('Password of Private Key'));
1488 add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'], eap_type: ['tls'] });
1489 o.password = true;
1490
1491 o = ss.taboption('encryption', form.ListValue, 'auth', _('Authentication'));
1492 o.value('PAP', 'PAP');
1493 o.value('CHAP', 'CHAP');
1494 o.value('MSCHAP', 'MSCHAP');
1495 o.value('MSCHAPV2', 'MSCHAPv2');
1496 o.value('EAP-GTC', 'EAP-GTC');
1497 o.value('EAP-MD5', 'EAP-MD5');
1498 o.value('EAP-MSCHAPV2', 'EAP-MSCHAPv2');
1499 o.value('EAP-TLS', 'EAP-TLS');
1500 add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'], eap_type: ['fast', 'peap', 'ttls'] });
1501
1502 o.validate = function(section_id, value) {
1503 var eo = this.section.children.filter(function(o) { return o.option == 'eap_type' })[0],
1504 ev = eo.formvalue(section_id);
1505
1506 if (ev != 'ttls' && (value == 'PAP' || value == 'CHAP' || value == 'MSCHAP' || value == 'MSCHAPV2'))
1507 return _('This authentication type is not applicable to the selected EAP method.');
1508
1509 return true;
1510 };
1511
1512 o = ss.taboption('encryption', form.Flag, 'ca_cert2_usesystem', _('Use system certificates for inner-tunnel'), _("Validate server certificate using built-in system CA bundle,<br />requires the \"ca-bundle\" package"));
1513 o.enabled = '1';
1514 o.disabled = '0';
1515 o.default = o.disabled;
1516 add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'], auth: ['EAP-TLS'] });
1517 o.validate = function(section_id, value) {
1518 if (value == '1' && !L.hasSystemFeature('cabundle')) {
1519 return _("This option cannot be used because the ca-bundle package is not installed.");
1520 }
1521 return true;
1522 };
1523
1524 o = ss.taboption('encryption', form.FileUpload, 'ca_cert2', _('Path to inner CA-Certificate'));
1525 add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'], auth: ['EAP-TLS'], ca_cert2_usesystem: ['0'] });
1526
1527 o = ss.taboption('encryption', form.Value, 'subject_match2', _('Inner certificate constraint (Subject)'), _("Certificate constraint substring - e.g. /CN=wifi.mycompany.com<br />See `logread -f` during handshake for actual values"));
1528 add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'], auth: ['EAP-TLS'] });
1529
1530 o = ss.taboption('encryption', form.DynamicList, 'altsubject_match2', _('Inner certificate constraint (SAN)'), _("Certificate constraint(s) via Subject Alternate Name values<br />(supported attributes: EMAIL, DNS, URI) - e.g. DNS:wifi.mycompany.com"));
1531 add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'], auth: ['EAP-TLS'] });
1532
1533 o = ss.taboption('encryption', form.DynamicList, 'domain_match2', _('Inner certificate constraint (Domain)'), _("Certificate constraint(s) against DNS SAN values (if available)<br />or Subject CN (exact match)"));
1534 add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'], auth: ['EAP-TLS'] });
1535
1536 o = ss.taboption('encryption', form.DynamicList, 'domain_suffix_match2', _('Inner certificate constraint (Wildcard)'), _("Certificate constraint(s) against DNS SAN values (if available)<br />or Subject CN (suffix match)"));
1537 add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'], auth: ['EAP-TLS'] });
1538
1539 o = ss.taboption('encryption', form.FileUpload, 'client_cert2', _('Path to inner Client-Certificate'));
1540 add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'], auth: ['EAP-TLS'] });
1541
1542 o = ss.taboption('encryption', form.FileUpload, 'priv_key2', _('Path to inner Private Key'));
1543 add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'], auth: ['EAP-TLS'] });
1544
1545 o = ss.taboption('encryption', form.Value, 'priv_key2_pwd', _('Password of inner Private Key'));
1546 add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'], auth: ['EAP-TLS'] });
1547 o.password = true;
1548
1549 o = ss.taboption('encryption', form.Value, 'identity', _('Identity'));
1550 add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'], eap_type: ['fast', 'peap', 'tls', 'ttls'] });
1551
1552 o = ss.taboption('encryption', form.Value, 'anonymous_identity', _('Anonymous Identity'));
1553 add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'], eap_type: ['fast', 'peap', 'tls', 'ttls'] });
1554
1555 o = ss.taboption('encryption', form.Value, 'password', _('Password'));
1556 add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'], eap_type: ['fast', 'peap', 'ttls'] });
1557 o.password = true;
1558
1559
1560 if (hwtype == 'mac80211') {
1561 // ieee802.11w options
1562 if (L.hasSystemFeature('hostapd', '11w')) {
1563 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)"));
1564 o.value('', _('Disabled'));
1565 o.value('1', _('Optional'));
1566 o.value('2', _('Required'));
1567 add_dependency_permutations(o, { mode: ['ap', 'ap-wds', 'sta', 'sta-wds'], encryption: ['owe', 'psk2', 'psk-mixed', 'sae', 'sae-mixed', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1568
1569 o.defaults = {
1570 '2': [{ encryption: 'sae' }, { encryption: 'owe' }, { encryption: 'wpa3' }, { encryption: 'wpa3-mixed' }],
1571 '1': [{ encryption: 'sae-mixed'}],
1572 '': []
1573 };
1574
1575 o = ss.taboption('encryption', form.Value, 'ieee80211w_max_timeout', _('802.11w maximum timeout'), _('802.11w Association SA Query maximum timeout'));
1576 o.depends('ieee80211w', '1');
1577 o.depends('ieee80211w', '2');
1578 o.datatype = 'uinteger';
1579 o.placeholder = '1000';
1580 o.rmempty = true;
1581
1582 o = ss.taboption('encryption', form.Value, 'ieee80211w_retry_timeout', _('802.11w retry timeout'), _('802.11w Association SA Query retry timeout'));
1583 o.depends('ieee80211w', '1');
1584 o.depends('ieee80211w', '2');
1585 o.datatype = 'uinteger';
1586 o.placeholder = '201';
1587 o.rmempty = true;
1588 };
1589
1590 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.'));
1591 add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['psk2', 'psk-mixed', 'sae', 'sae-mixed', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1592
1593 if (L.hasSystemFeature('hostapd', 'cli') && L.hasSystemFeature('wpasupplicant')) {
1594 o = ss.taboption('encryption', form.Flag, 'wps_pushbutton', _('Enable WPS pushbutton, requires WPA(2)-PSK/WPA3-SAE'))
1595 o.enabled = '1';
1596 o.disabled = '0';
1597 o.default = o.disabled;
1598 o.depends('encryption', 'psk');
1599 o.depends('encryption', 'psk2');
1600 o.depends('encryption', 'psk-mixed');
1601 o.depends('encryption', 'sae');
1602 o.depends('encryption', 'sae-mixed');
1603 }
1604 }
1605 }
1606 });
1607 };
1608
1609 s.handleRemove = function(section_id, ev) {
1610 document.querySelector('.cbi-section-table-row[data-sid="%s"]'.format(section_id)).style.opacity = 0.5;
1611 return form.TypedSection.prototype.handleRemove.apply(this, [section_id, ev]);
1612 };
1613
1614 s.handleScan = function(radioDev, ev) {
1615 var table = E('div', { 'class': 'table' }, [
1616 E('div', { 'class': 'tr table-titles' }, [
1617 E('div', { 'class': 'th col-2 middle center' }, _('Signal')),
1618 E('div', { 'class': 'th col-4 middle left' }, _('SSID')),
1619 E('div', { 'class': 'th col-2 middle center hide-xs' }, _('Channel')),
1620 E('div', { 'class': 'th col-2 middle left hide-xs' }, _('Mode')),
1621 E('div', { 'class': 'th col-3 middle left hide-xs' }, _('BSSID')),
1622 E('div', { 'class': 'th col-3 middle left' }, _('Encryption')),
1623 E('div', { 'class': 'th cbi-section-actions right' }, ' '),
1624 ])
1625 ]);
1626
1627 var stop = E('button', {
1628 'class': 'btn',
1629 'click': L.bind(this.handleScanStartStop, this),
1630 'style': 'display:none',
1631 'data-state': 'stop'
1632 }, _('Stop refresh'));
1633
1634 cbi_update_table(table, [], E('em', { class: 'spinning' }, _('Starting wireless scan...')));
1635
1636 var md = ui.showModal(_('Join Network: Wireless Scan'), [
1637 table,
1638 E('div', { 'class': 'right' }, [
1639 stop,
1640 ' ',
1641 E('button', {
1642 'class': 'btn',
1643 'click': L.bind(this.handleScanAbort, this)
1644 }, _('Dismiss'))
1645 ])
1646 ]);
1647
1648 md.style.maxWidth = '90%';
1649 md.style.maxHeight = 'none';
1650
1651 this.pollFn = L.bind(this.handleScanRefresh, this, radioDev, {}, table, stop);
1652
1653 L.Poll.add(this.pollFn);
1654 L.Poll.start();
1655 };
1656
1657 s.handleScanRefresh = function(radioDev, scanCache, table, stop) {
1658 return radioDev.getScanList().then(L.bind(function(results) {
1659 var rows = [];
1660
1661 for (var i = 0; i < results.length; i++)
1662 scanCache[results[i].bssid] = results[i];
1663
1664 for (var k in scanCache)
1665 if (scanCache[k].stale)
1666 results.push(scanCache[k]);
1667
1668 results.sort(function(a, b) {
1669 var diff = (b.quality - a.quality) || (a.channel - b.channel);
1670
1671 if (diff)
1672 return diff;
1673
1674 if (a.ssid < b.ssid)
1675 return -1;
1676 else if (a.ssid > b.ssid)
1677 return 1;
1678
1679 if (a.bssid < b.bssid)
1680 return -1;
1681 else if (a.bssid > b.bssid)
1682 return 1;
1683 });
1684
1685 for (var i = 0; i < results.length; i++) {
1686 var res = results[i],
1687 qv = res.quality || 0,
1688 qm = res.quality_max || 0,
1689 q = (qv > 0 && qm > 0) ? Math.floor((100 / qm) * qv) : 0,
1690 s = res.stale ? 'opacity:0.5' : '';
1691
1692 rows.push([
1693 E('span', { 'style': s }, render_signal_badge(q, res.signal, res.noise)),
1694 E('span', { 'style': s }, (res.ssid != null) ? '%h'.format(res.ssid) : E('em', _('hidden'))),
1695 E('span', { 'style': s }, '%d'.format(res.channel)),
1696 E('span', { 'style': s }, '%h'.format(res.mode)),
1697 E('span', { 'style': s }, '%h'.format(res.bssid)),
1698 E('span', { 'style': s }, '%h'.format(network.formatWifiEncryption(res.encryption))),
1699 E('div', { 'class': 'right' }, E('button', {
1700 'class': 'cbi-button cbi-button-action important',
1701 'click': L.bind(this.handleJoin, this, radioDev, res)
1702 }, _('Join Network')))
1703 ]);
1704
1705 res.stale = true;
1706 }
1707
1708 cbi_update_table(table, rows);
1709
1710 stop.disabled = false;
1711 stop.style.display = '';
1712 stop.classList.remove('spinning');
1713 }, this));
1714 };
1715
1716 s.handleScanStartStop = function(ev) {
1717 var btn = ev.currentTarget;
1718
1719 if (btn.getAttribute('data-state') == 'stop') {
1720 L.Poll.remove(this.pollFn);
1721 btn.firstChild.data = _('Start refresh');
1722 btn.setAttribute('data-state', 'start');
1723 }
1724 else {
1725 L.Poll.add(this.pollFn);
1726 btn.firstChild.data = _('Stop refresh');
1727 btn.setAttribute('data-state', 'stop');
1728 btn.classList.add('spinning');
1729 btn.disabled = true;
1730 }
1731 };
1732
1733 s.handleScanAbort = function(ev) {
1734 var md = L.dom.parent(ev.target, 'div[aria-modal="true"]');
1735 if (md) {
1736 md.style.maxWidth = '';
1737 md.style.maxHeight = '';
1738 }
1739
1740 ui.hideModal();
1741 L.Poll.remove(this.pollFn);
1742
1743 this.pollFn = null;
1744 };
1745
1746 s.handleJoinConfirm = function(radioDev, bss, form, ev) {
1747 var nameopt = L.toArray(form.lookupOption('name', '_new_'))[0],
1748 passopt = L.toArray(form.lookupOption('password', '_new_'))[0],
1749 bssidopt = L.toArray(form.lookupOption('bssid', '_new_'))[0],
1750 zoneopt = L.toArray(form.lookupOption('zone', '_new_'))[0],
1751 replopt = L.toArray(form.lookupOption('replace', '_new_'))[0],
1752 nameval = (nameopt && nameopt.isValid('_new_')) ? nameopt.formvalue('_new_') : null,
1753 passval = (passopt && passopt.isValid('_new_')) ? passopt.formvalue('_new_') : null,
1754 bssidval = (bssidopt && bssidopt.isValid('_new_')) ? bssidopt.formvalue('_new_') : null,
1755 zoneval = zoneopt ? zoneopt.formvalue('_new_') : null,
1756 enc = L.isObject(bss.encryption) ? bss.encryption : null,
1757 is_wep = (enc && Array.isArray(enc.wep)),
1758 is_psk = (enc && Array.isArray(enc.wpa) && L.toArray(enc.authentication).filter(function(a) { return a == 'psk' })),
1759 is_sae = (enc && Array.isArray(enc.wpa) && L.toArray(enc.authentication).filter(function(a) { return a == 'sae' }));
1760
1761 if (nameval == null || (passopt && passval == null))
1762 return;
1763
1764 var section_id = null;
1765
1766 return this.map.save(function() {
1767 var wifi_sections = uci.sections('wireless', 'wifi-iface');
1768
1769 if (replopt.formvalue('_new_') == '1') {
1770 for (var i = 0; i < wifi_sections.length; i++)
1771 if (wifi_sections[i].device == radioDev.getName())
1772 uci.remove('wireless', wifi_sections[i]['.name']);
1773 }
1774
1775 if (uci.get('wireless', radioDev.getName(), 'disabled') == '1') {
1776 for (var i = 0; i < wifi_sections.length; i++)
1777 if (wifi_sections[i].device == radioDev.getName())
1778 uci.set('wireless', wifi_sections[i]['.name'], 'disabled', '1');
1779
1780 uci.unset('wireless', radioDev.getName(), 'disabled');
1781 }
1782
1783 section_id = next_free_sid(wifi_sections.length);
1784
1785 uci.add('wireless', 'wifi-iface', section_id);
1786 uci.set('wireless', section_id, 'device', radioDev.getName());
1787 uci.set('wireless', section_id, 'mode', (bss.mode == 'Ad-Hoc') ? 'adhoc' : 'sta');
1788 uci.set('wireless', section_id, 'network', nameval);
1789
1790 if (bss.ssid != null) {
1791 uci.set('wireless', section_id, 'ssid', bss.ssid);
1792
1793 if (bssidval == '1')
1794 uci.set('wireless', section_id, 'bssid', bss.bssid);
1795 }
1796 else if (bss.bssid != null) {
1797 uci.set('wireless', section_id, 'bssid', bss.bssid);
1798 }
1799
1800 if (is_sae) {
1801 uci.set('wireless', section_id, 'encryption', 'sae');
1802 uci.set('wireless', section_id, 'key', passval);
1803 }
1804 else if (is_psk) {
1805 for (var i = enc.wpa.length - 1; i >= 0; i--) {
1806 if (enc.wpa[i] == 2) {
1807 uci.set('wireless', section_id, 'encryption', 'psk2');
1808 break;
1809 }
1810 else if (enc.wpa[i] == 1) {
1811 uci.set('wireless', section_id, 'encryption', 'psk');
1812 break;
1813 }
1814 }
1815
1816 uci.set('wireless', section_id, 'key', passval);
1817 }
1818 else if (is_wep) {
1819 uci.set('wireless', section_id, 'encryption', 'wep-open');
1820 uci.set('wireless', section_id, 'key', '1');
1821 uci.set('wireless', section_id, 'key1', passval);
1822 }
1823 else {
1824 uci.set('wireless', section_id, 'encryption', 'none');
1825 }
1826
1827 return network.addNetwork(nameval, { proto: 'dhcp' }).then(function(net) {
1828 firewall.deleteNetwork(net.getName());
1829
1830 var zonePromise = zoneval
1831 ? firewall.getZone(zoneval).then(function(zone) { return zone || firewall.addZone(zoneval) })
1832 : Promise.resolve();
1833
1834 return zonePromise.then(function(zone) {
1835 if (zone)
1836 zone.addNetwork(net.getName());
1837 });
1838 });
1839 }).then(L.bind(function() {
1840 return this.renderMoreOptionsModal(section_id);
1841 }, this));
1842 };
1843
1844 s.handleJoin = function(radioDev, bss, ev) {
1845 this.handleScanAbort(ev);
1846
1847 var m2 = new form.Map('wireless'),
1848 s2 = m2.section(form.NamedSection, '_new_'),
1849 enc = L.isObject(bss.encryption) ? bss.encryption : null,
1850 is_wep = (enc && Array.isArray(enc.wep)),
1851 is_psk = (enc && Array.isArray(enc.wpa) && L.toArray(enc.authentication).filter(function(a) { return a == 'psk' || a == 'sae' })),
1852 replace, passphrase, name, bssid, zone;
1853
1854 var nameUsed = function(name) {
1855 var s = uci.get('network', name);
1856 if (s != null && s['.type'] != 'interface')
1857 return true;
1858
1859 var net = (s != null) ? network.instantiateNetwork(name) : null;
1860 return (net != null && !net.isEmpty());
1861 };
1862
1863 s2.render = function() {
1864 return Promise.all([
1865 {},
1866 this.renderUCISection('_new_')
1867 ]).then(this.renderContents.bind(this));
1868 };
1869
1870 replace = s2.option(form.Flag, 'replace', _('Replace wireless configuration'), _('Check this option to delete the existing networks from this radio.'));
1871
1872 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>'));
1873 name.datatype = 'uciname';
1874 name.default = 'wwan';
1875 name.rmempty = false;
1876 name.validate = function(section_id, value) {
1877 if (nameUsed(value))
1878 return _('The network name is already used');
1879
1880 return true;
1881 };
1882
1883 for (var i = 2; nameUsed(name.default); i++)
1884 name.default = 'wwan%d'.format(i);
1885
1886 if (is_wep || is_psk) {
1887 passphrase = s2.option(form.Value, 'password', is_wep ? _('WEP passphrase') : _('WPA passphrase'), _('Specify the secret encryption key here.'));
1888 passphrase.datatype = is_wep ? 'wepkey' : 'wpakey';
1889 passphrase.password = true;
1890 passphrase.rmempty = false;
1891 }
1892
1893 if (bss.ssid != null) {
1894 bssid = s2.option(form.Flag, 'bssid', _('Lock to BSSID'), _('Instead of joining any network with a matching SSID, only connect to the BSSID <code>%h</code>.').format(bss.bssid));
1895 bssid.default = '0';
1896 }
1897
1898 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.'));
1899 zone.default = 'wan';
1900
1901 return m2.render().then(L.bind(function(nodes) {
1902 ui.showModal(_('Joining Network: %q').replace(/%q/, '"%h"'.format(bss.ssid)), [
1903 nodes,
1904 E('div', { 'class': 'right' }, [
1905 E('button', {
1906 'class': 'btn',
1907 'click': ui.hideModal
1908 }, _('Cancel')), ' ',
1909 E('button', {
1910 'class': 'cbi-button cbi-button-positive important',
1911 'click': ui.createHandlerFn(this, 'handleJoinConfirm', radioDev, bss, m2)
1912 }, _('Submit'))
1913 ])
1914 ], 'cbi-modal').querySelector('[id="%s"] input[class][type]'.format((passphrase || name).cbid('_new_'))).focus();
1915 }, this));
1916 };
1917
1918 s.handleAdd = function(radioDev, ev) {
1919 var section_id = next_free_sid(uci.sections('wireless', 'wifi-iface').length);
1920
1921 uci.unset('wireless', radioDev.getName(), 'disabled');
1922
1923 uci.add('wireless', 'wifi-iface', section_id);
1924 uci.set('wireless', section_id, 'device', radioDev.getName());
1925 uci.set('wireless', section_id, 'mode', 'ap');
1926 uci.set('wireless', section_id, 'ssid', 'OpenWrt');
1927 uci.set('wireless', section_id, 'encryption', 'none');
1928
1929 this.addedSection = section_id;
1930 return this.renderMoreOptionsModal(section_id);
1931 };
1932
1933 o = s.option(form.DummyValue, '_badge');
1934 o.modalonly = false;
1935 o.textvalue = function(section_id) {
1936 var inst = this.section.lookupRadioOrNetwork(section_id),
1937 node = E('div', { 'class': 'center' });
1938
1939 if (inst.getWifiNetworks)
1940 node.appendChild(render_radio_badge(inst));
1941 else
1942 node.appendChild(render_network_badge(inst));
1943
1944 return node;
1945 };
1946
1947 o = s.option(form.DummyValue, '_stat');
1948 o.modalonly = false;
1949 o.textvalue = function(section_id) {
1950 var inst = this.section.lookupRadioOrNetwork(section_id);
1951
1952 if (inst.getWifiNetworks)
1953 return render_radio_status(inst, this.section.wifis.filter(function(e) {
1954 return (e.getWifiDeviceName() == inst.getName());
1955 }));
1956 else
1957 return render_network_status(inst);
1958 };
1959
1960 return m.render().then(L.bind(function(m, nodes) {
1961 L.Poll.add(L.bind(function() {
1962 var section_ids = m.children[0].cfgsections(),
1963 tasks = [ network.getHostHints(), network.getWifiDevices() ];
1964
1965 for (var i = 0; i < section_ids.length; i++) {
1966 var row = nodes.querySelector('.cbi-section-table-row[data-sid="%s"]'.format(section_ids[i])),
1967 dsc = row.querySelector('[data-name="_stat"] > div'),
1968 btns = row.querySelectorAll('.cbi-section-actions button');
1969
1970 if (dsc.getAttribute('restart') == '') {
1971 dsc.setAttribute('restart', '1');
1972 tasks.push(fs.exec('/sbin/wifi', ['up', section_ids[i]]).catch(function(e) {
1973 ui.addNotification(null, E('p', e.message));
1974 }));
1975 }
1976 else if (dsc.getAttribute('restart') == '1') {
1977 dsc.removeAttribute('restart');
1978 btns[0].classList.remove('spinning');
1979 btns[0].disabled = false;
1980 }
1981 }
1982
1983 return Promise.all(tasks)
1984 .then(L.bind(function(hosts_radios) {
1985 var tasks = [];
1986
1987 for (var i = 0; i < hosts_radios[1].length; i++)
1988 tasks.push(hosts_radios[1][i].getWifiNetworks());
1989
1990 return Promise.all(tasks).then(function(data) {
1991 hosts_radios[2] = [];
1992
1993 for (var i = 0; i < data.length; i++)
1994 hosts_radios[2].push.apply(hosts_radios[2], data[i]);
1995
1996 return hosts_radios;
1997 });
1998 }, network))
1999 .then(L.bind(function(hosts_radios_wifis) {
2000 var tasks = [];
2001
2002 for (var i = 0; i < hosts_radios_wifis[2].length; i++)
2003 tasks.push(hosts_radios_wifis[2][i].getAssocList());
2004
2005 return Promise.all(tasks).then(function(data) {
2006 hosts_radios_wifis[3] = [];
2007
2008 for (var i = 0; i < data.length; i++) {
2009 var wifiNetwork = hosts_radios_wifis[2][i],
2010 radioDev = hosts_radios_wifis[1].filter(function(d) { return d.getName() == wifiNetwork.getWifiDeviceName() })[0];
2011
2012 for (var j = 0; j < data[i].length; j++)
2013 hosts_radios_wifis[3].push(Object.assign({ radio: radioDev, network: wifiNetwork }, data[i][j]));
2014 }
2015
2016 return hosts_radios_wifis;
2017 });
2018 }, network))
2019 .then(L.bind(this.poll_status, this, nodes));
2020 }, this), 5);
2021
2022 var table = E('div', { 'class': 'table assoclist', 'id': 'wifi_assoclist_table' }, [
2023 E('div', { 'class': 'tr table-titles' }, [
2024 E('div', { 'class': 'th nowrap' }, _('Network')),
2025 E('div', { 'class': 'th hide-xs' }, _('MAC-Address')),
2026 E('div', { 'class': 'th' }, _('Host')),
2027 E('div', { 'class': 'th' }, _('Signal / Noise')),
2028 E('div', { 'class': 'th' }, _('RX Rate / TX Rate'))
2029 ])
2030 ]);
2031
2032 cbi_update_table(table, [], E('em', { 'class': 'spinning' }, _('Collecting data...')))
2033
2034 return E([ nodes, E('h3', _('Associated Stations')), table ]);
2035 }, this, m));
2036 }
2037 });