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