luci-mod-network: wireless.js cell_density support
[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('th', { '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('td', { '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.ListValue, 'cell_density', _('Coverage cell density'), _('Configures data rates based on the coverage cell density. Normal configures basic rates to 6, 12, 24 Mbps if legacy 802.11b rates are not used else to 5.5, 11 Mbps. High configures basic rates to 12, 24 Mbps if legacy 802.11b rates are not used else to the 11 Mbps rate. Very High configures 24 Mbps as the basic rate. Supported rates lower than the minimum basic rate are not offered.'));
893 o.value('0', _('Disabled'));
894 o.value('1', _('Normal'));
895 o.value('2', _('High'));
896 o.value('3', _('Very High'));
897
898 o = ss.taboption('advanced', form.Flag, 'legacy_rates', _('Allow legacy 802.11b rates'));
899 o.default = o.enabled;
900
901 o = ss.taboption('advanced', form.Value, 'distance', _('Distance Optimization'), _('Distance to farthest network member in meters.'));
902 o.datatype = 'or(range(0,114750),"auto")';
903 o.placeholder = 'auto';
904
905 o = ss.taboption('advanced', form.Value, 'frag', _('Fragmentation Threshold'));
906 o.datatype = 'min(256)';
907 o.placeholder = _('off');
908
909 o = ss.taboption('advanced', form.Value, 'rts', _('RTS/CTS Threshold'));
910 o.datatype = 'uinteger';
911 o.placeholder = _('off');
912
913 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!'));
914 o.rmempty = true;
915
916 o = ss.taboption('advanced', form.Value, 'beacon_int', _('Beacon Interval'));
917 o.datatype = 'range(15,65535)';
918 o.placeholder = 100;
919 o.rmempty = true;
920 }
921
922
923 o = s.option(form.SectionValue, '_device', form.NamedSection, radioNet.getName(), 'wifi-iface', _('Interface Configuration'));
924 o.modalonly = true;
925
926 ss = o.subsection;
927 ss.tab('general', _('General Setup'));
928 ss.tab('encryption', _('Wireless Security'));
929 ss.tab('macfilter', _('MAC-Filter'));
930 ss.tab('advanced', _('Advanced Settings'));
931
932 o = ss.taboption('general', form.ListValue, 'mode', _('Mode'));
933 o.value('ap', _('Access Point'));
934 o.value('sta', _('Client'));
935 o.value('adhoc', _('Ad-Hoc'));
936
937 o = ss.taboption('general', form.Value, 'mesh_id', _('Mesh Id'));
938 o.depends('mode', 'mesh');
939
940 o = ss.taboption('advanced', form.Flag, 'mesh_fwding', _('Forward mesh peer traffic'));
941 o.rmempty = false;
942 o.default = '1';
943 o.depends('mode', 'mesh');
944
945 o = ss.taboption('advanced', form.Value, 'mesh_rssi_threshold', _('RSSI threshold for joining'), _('0 = not using RSSI threshold, 1 = do not change driver default'));
946 o.rmempty = false;
947 o.default = '0';
948 o.datatype = 'range(-255,1)';
949 o.depends('mode', 'mesh');
950
951 o = ss.taboption('general', form.Value, 'ssid', _('<abbr title="Extended Service Set Identifier">ESSID</abbr>'));
952 o.datatype = 'maxlength(32)';
953 o.depends('mode', 'ap');
954 o.depends('mode', 'sta');
955 o.depends('mode', 'adhoc');
956 o.depends('mode', 'ahdemo');
957 o.depends('mode', 'monitor');
958 o.depends('mode', 'ap-wds');
959 o.depends('mode', 'sta-wds');
960 o.depends('mode', 'wds');
961
962 o = ss.taboption('general', form.Value, 'bssid', _('<abbr title="Basic Service Set Identifier">BSSID</abbr>'));
963 o.datatype = 'macaddr';
964
965 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.'));
966 o.rmempty = true;
967 o.multiple = true;
968 o.novirtual = true;
969 o.write = function(section_id, value) {
970 return network.getDevice(section_id).then(L.bind(function(dev) {
971 var old_networks = dev.getNetworks().reduce(function(o, v) { o[v.getName()] = v; return o }, {}),
972 new_networks = {},
973 values = L.toArray(value),
974 tasks = [];
975
976 for (var i = 0; i < values.length; i++) {
977 new_networks[values[i]] = true;
978
979 if (old_networks[values[i]])
980 continue;
981
982 tasks.push(network.getNetwork(values[i]).then(L.bind(function(name, net) {
983 return net || network.addNetwork(name, { proto: 'none' });
984 }, this, values[i])).then(L.bind(function(dev, net) {
985 if (net) {
986 if (!net.isEmpty())
987 net.set('type', 'bridge');
988 net.addDevice(dev);
989 }
990 }, this, dev)));
991 }
992
993 for (var name in old_networks)
994 if (!new_networks[name])
995 tasks.push(network.getNetwork(name).then(L.bind(function(dev, net) {
996 if (net)
997 net.deleteDevice(dev);
998 }, this, dev)));
999
1000 return Promise.all(tasks);
1001 }, this));
1002 };
1003
1004 if (hwtype == 'mac80211') {
1005 var mode = ss.children[0],
1006 bssid = ss.children[5],
1007 encr;
1008
1009 mode.value('mesh', '802.11s');
1010 mode.value('ahdemo', _('Pseudo Ad-Hoc (ahdemo)'));
1011 mode.value('monitor', _('Monitor'));
1012
1013 bssid.depends('mode', 'adhoc');
1014 bssid.depends('mode', 'sta');
1015 bssid.depends('mode', 'sta-wds');
1016
1017 o = ss.taboption('macfilter', form.ListValue, 'macfilter', _('MAC-Address Filter'));
1018 o.depends('mode', 'ap');
1019 o.depends('mode', 'ap-wds');
1020 o.value('', _('disable'));
1021 o.value('allow', _('Allow listed only'));
1022 o.value('deny', _('Allow all except listed'));
1023
1024 o = ss.taboption('macfilter', form.DynamicList, 'maclist', _('MAC-List'));
1025 o.datatype = 'macaddr';
1026 o.depends('macfilter', 'allow');
1027 o.depends('macfilter', 'deny');
1028 o.load = function(section_id) {
1029 return network.getHostHints().then(L.bind(function(hints) {
1030 hints.getMACHints().map(L.bind(function(hint) {
1031 this.value(hint[0], hint[1] ? '%s (%s)'.format(hint[0], hint[1]) : hint[0]);
1032 }, this));
1033
1034 return form.DynamicList.prototype.load.apply(this, [section_id]);
1035 }, this));
1036 };
1037
1038 mode.value('ap-wds', '%s (%s)'.format(_('Access Point'), _('WDS')));
1039 mode.value('sta-wds', '%s (%s)'.format(_('Client'), _('WDS')));
1040
1041 mode.write = function(section_id, value) {
1042 switch (value) {
1043 case 'ap-wds':
1044 uci.set('wireless', section_id, 'mode', 'ap');
1045 uci.set('wireless', section_id, 'wds', '1');
1046 break;
1047
1048 case 'sta-wds':
1049 uci.set('wireless', section_id, 'mode', 'sta');
1050 uci.set('wireless', section_id, 'wds', '1');
1051 break;
1052
1053 default:
1054 uci.set('wireless', section_id, 'mode', value);
1055 uci.unset('wireless', section_id, 'wds');
1056 break;
1057 }
1058 };
1059
1060 mode.cfgvalue = function(section_id) {
1061 var mode = uci.get('wireless', section_id, 'mode'),
1062 wds = uci.get('wireless', section_id, 'wds');
1063
1064 if (mode == 'ap' && wds)
1065 return 'ap-wds';
1066 else if (mode == 'sta' && wds)
1067 return 'sta-wds';
1068
1069 return mode;
1070 };
1071
1072 o = ss.taboption('general', form.Flag, 'hidden', _('Hide <abbr title="Extended Service Set Identifier">ESSID</abbr>'));
1073 o.depends('mode', 'ap');
1074 o.depends('mode', 'ap-wds');
1075
1076 o = ss.taboption('general', form.Flag, 'wmm', _('WMM Mode'));
1077 o.depends('mode', 'ap');
1078 o.depends('mode', 'ap-wds');
1079 o.default = o.enabled;
1080
1081 o = ss.taboption('advanced', form.Flag, 'isolate', _('Isolate Clients'), _('Prevents client-to-client communication'));
1082 o.depends('mode', 'ap');
1083 o.depends('mode', 'ap-wds');
1084
1085 o = ss.taboption('advanced', form.Value, 'ifname', _('Interface name'), _('Override default interface name'));
1086 o.optional = true;
1087 o.placeholder = radioNet.getIfname();
1088 if (/^radio\d+\.network/.test(o.placeholder))
1089 o.placeholder = '';
1090
1091 o = ss.taboption('advanced', form.Flag, 'short_preamble', _('Short Preamble'));
1092 o.default = o.enabled;
1093
1094 o = ss.taboption('advanced', form.Value, 'dtim_period', _('DTIM Interval'), _('Delivery Traffic Indication Message Interval'));
1095 o.optional = true;
1096 o.placeholder = 2;
1097 o.datatype = 'range(1,255)';
1098
1099 o = ss.taboption('advanced', form.Value, 'wpa_group_rekey', _('Time interval for rekeying GTK'), _('sec'));
1100 o.optional = true;
1101 o.placeholder = 600;
1102 o.datatype = 'uinteger';
1103
1104 o = ss.taboption('advanced', form.Flag , 'skip_inactivity_poll', _('Disable Inactivity Polling'));
1105 o.optional = true;
1106 o.datatype = 'uinteger';
1107
1108 o = ss.taboption('advanced', form.Value, 'max_inactivity', _('Station inactivity limit'), _('sec'));
1109 o.optional = true;
1110 o.placeholder = 300;
1111 o.datatype = 'uinteger';
1112
1113 o = ss.taboption('advanced', form.Value, 'max_listen_interval', _('Maximum allowed Listen Interval'));
1114 o.optional = true;
1115 o.placeholder = 65535;
1116 o.datatype = 'uinteger';
1117
1118 o = ss.taboption('advanced', form.Flag, 'disassoc_low_ack', _('Disassociate On Low Acknowledgement'), _('Allow AP mode to disconnect STAs based on low ACK condition'));
1119 o.default = o.enabled;
1120 }
1121
1122
1123 encr = o = ss.taboption('encryption', form.ListValue, 'encryption', _('Encryption'));
1124 o.depends('mode', 'ap');
1125 o.depends('mode', 'sta');
1126 o.depends('mode', 'adhoc');
1127 o.depends('mode', 'ahdemo');
1128 o.depends('mode', 'ap-wds');
1129 o.depends('mode', 'sta-wds');
1130 o.depends('mode', 'mesh');
1131
1132 o.cfgvalue = function(section_id) {
1133 var v = String(uci.get('wireless', section_id, 'encryption'));
1134 if (v == 'wep')
1135 return 'wep-open';
1136 else if (v.match(/\+/))
1137 return v.replace(/\+.+$/, '');
1138 return v;
1139 };
1140
1141 o.write = function(section_id, value) {
1142 var e = this.section.children.filter(function(o) { return o.option == 'encryption' })[0].formvalue(section_id),
1143 co = this.section.children.filter(function(o) { return o.option == 'cipher' })[0], c = co.formvalue(section_id);
1144
1145 if (value == 'wpa' || value == 'wpa2' || value == 'wpa3' || value == 'wpa3-mixed')
1146 uci.unset('wireless', section_id, 'key');
1147
1148 if (co.isActive(section_id) && e && (c == 'tkip' || c == 'ccmp' || c == 'tkip+ccmp'))
1149 e += '+' + c;
1150
1151 uci.set('wireless', section_id, 'encryption', e);
1152 };
1153
1154 o = ss.taboption('encryption', form.ListValue, 'cipher', _('Cipher'));
1155 o.depends('encryption', 'wpa');
1156 o.depends('encryption', 'wpa2');
1157 o.depends('encryption', 'wpa3');
1158 o.depends('encryption', 'wpa3-mixed');
1159 o.depends('encryption', 'psk');
1160 o.depends('encryption', 'psk2');
1161 o.depends('encryption', 'wpa-mixed');
1162 o.depends('encryption', 'psk-mixed');
1163 o.value('auto', _('auto'));
1164 o.value('ccmp', _('Force CCMP (AES)'));
1165 o.value('tkip', _('Force TKIP'));
1166 o.value('tkip+ccmp', _('Force TKIP and CCMP (AES)'));
1167 o.write = ss.children.filter(function(o) { return o.option == 'encryption' })[0].write;
1168
1169 o.cfgvalue = function(section_id) {
1170 var v = String(uci.get('wireless', section_id, 'encryption'));
1171 if (v.match(/\+/)) {
1172 v = v.replace(/^[^+]+\+/, '');
1173 if (v == 'aes')
1174 v = 'ccmp';
1175 else if (v == 'tkip+aes' || v == 'aes+tkip' || v == 'ccmp+tkip')
1176 v = 'tkip+ccmp';
1177 }
1178 return v;
1179 };
1180
1181
1182 var crypto_modes = [];
1183
1184 if (hwtype == 'mac80211') {
1185 var has_supplicant = L.hasSystemFeature('wpasupplicant'),
1186 has_hostapd = L.hasSystemFeature('hostapd');
1187
1188 // Probe EAP support
1189 var has_ap_eap = L.hasSystemFeature('hostapd', 'eap'),
1190 has_sta_eap = L.hasSystemFeature('wpasupplicant', 'eap');
1191
1192 // Probe SAE support
1193 var has_ap_sae = L.hasSystemFeature('hostapd', 'sae'),
1194 has_sta_sae = L.hasSystemFeature('wpasupplicant', 'sae');
1195
1196 // Probe OWE support
1197 var has_ap_owe = L.hasSystemFeature('hostapd', 'owe'),
1198 has_sta_owe = L.hasSystemFeature('wpasupplicant', 'owe');
1199
1200 // Probe Suite-B support
1201 var has_ap_eap192 = L.hasSystemFeature('hostapd', 'suiteb192'),
1202 has_sta_eap192 = L.hasSystemFeature('wpasupplicant', 'suiteb192');
1203
1204 // Probe WEP support
1205 var has_ap_wep = L.hasSystemFeature('hostapd', 'wep'),
1206 has_sta_wep = L.hasSystemFeature('wpasupplicant', 'wep');
1207
1208 if (has_hostapd || has_supplicant) {
1209 crypto_modes.push(['psk2', 'WPA2-PSK', 35]);
1210 crypto_modes.push(['psk-mixed', 'WPA-PSK/WPA2-PSK Mixed Mode', 22]);
1211 crypto_modes.push(['psk', 'WPA-PSK', 21]);
1212 }
1213 else {
1214 encr.description = _('WPA-Encryption requires wpa_supplicant (for client mode) or hostapd (for AP and ad-hoc mode) to be installed.');
1215 }
1216
1217 if (has_ap_sae || has_sta_sae) {
1218 crypto_modes.push(['sae', 'WPA3-SAE', 31]);
1219 crypto_modes.push(['sae-mixed', 'WPA2-PSK/WPA3-SAE Mixed Mode', 30]);
1220 }
1221
1222 if (has_ap_wep || has_sta_wep) {
1223 crypto_modes.push(['wep-open', _('WEP Open System'), 11]);
1224 crypto_modes.push(['wep-shared', _('WEP Shared Key'), 10]);
1225 }
1226
1227 if (has_ap_eap || has_sta_eap) {
1228 if (has_ap_eap192 || has_sta_eap192) {
1229 crypto_modes.push(['wpa3', 'WPA3-EAP', 33]);
1230 crypto_modes.push(['wpa3-mixed', 'WPA2-EAP/WPA3-EAP Mixed Mode', 32]);
1231 }
1232
1233 crypto_modes.push(['wpa2', 'WPA2-EAP', 34]);
1234 crypto_modes.push(['wpa', 'WPA-EAP', 20]);
1235 }
1236
1237 if (has_ap_owe || has_sta_owe) {
1238 crypto_modes.push(['owe', 'OWE', 1]);
1239 }
1240
1241 encr.crypto_support = {
1242 'ap': {
1243 'wep-open': has_ap_wep || _('Requires hostapd with WEP support'),
1244 'wep-shared': has_ap_wep || _('Requires hostapd with WEP support'),
1245 'psk': has_hostapd || _('Requires hostapd'),
1246 'psk2': has_hostapd || _('Requires hostapd'),
1247 'psk-mixed': has_hostapd || _('Requires hostapd'),
1248 'sae': has_ap_sae || _('Requires hostapd with SAE support'),
1249 'sae-mixed': has_ap_sae || _('Requires hostapd with SAE support'),
1250 'wpa': has_ap_eap || _('Requires hostapd with EAP support'),
1251 'wpa2': has_ap_eap || _('Requires hostapd with EAP support'),
1252 'wpa3': has_ap_eap192 || _('Requires hostapd with EAP Suite-B support'),
1253 'wpa3-mixed': has_ap_eap192 || _('Requires hostapd with EAP Suite-B support'),
1254 'owe': has_ap_owe || _('Requires hostapd with OWE support')
1255 },
1256 'sta': {
1257 'wep-open': has_sta_wep || _('Requires wpa-supplicant with WEP support'),
1258 'wep-shared': has_sta_wep || _('Requires wpa-supplicant with WEP support'),
1259 'psk': has_supplicant || _('Requires wpa-supplicant'),
1260 'psk2': has_supplicant || _('Requires wpa-supplicant'),
1261 'psk-mixed': has_supplicant || _('Requires wpa-supplicant'),
1262 'sae': has_sta_sae || _('Requires wpa-supplicant with SAE support'),
1263 'sae-mixed': has_sta_sae || _('Requires wpa-supplicant with SAE support'),
1264 'wpa': has_sta_eap || _('Requires wpa-supplicant with EAP support'),
1265 'wpa2': has_sta_eap || _('Requires wpa-supplicant with EAP support'),
1266 'wpa3': has_sta_eap192 || _('Requires wpa-supplicant with EAP Suite-B support'),
1267 'wpa3-mixed': has_sta_eap192 || _('Requires wpa-supplicant with EAP Suite-B support'),
1268 'owe': has_sta_owe || _('Requires wpa-supplicant with OWE support')
1269 },
1270 'adhoc': {
1271 'wep-open': true,
1272 'wep-shared': true,
1273 'psk': has_supplicant || _('Requires wpa-supplicant'),
1274 'psk2': has_supplicant || _('Requires wpa-supplicant'),
1275 'psk-mixed': has_supplicant || _('Requires wpa-supplicant'),
1276 },
1277 'mesh': {
1278 'sae': has_sta_sae || _('Requires wpa-supplicant with SAE support')
1279 },
1280 'ahdemo': {
1281 'wep-open': true,
1282 'wep-shared': true
1283 },
1284 'wds': {
1285 'wep-open': true,
1286 'wep-shared': true
1287 }
1288 };
1289
1290 encr.crypto_support['ap-wds'] = encr.crypto_support['ap'];
1291 encr.crypto_support['sta-wds'] = encr.crypto_support['sta'];
1292
1293 encr.validate = function(section_id, value) {
1294 var modeopt = this.section.children.filter(function(o) { return o.option == 'mode' })[0],
1295 modeval = modeopt.formvalue(section_id),
1296 modetitle = modeopt.vallist[modeopt.keylist.indexOf(modeval)],
1297 enctitle = this.vallist[this.keylist.indexOf(value)];
1298
1299 if (value == 'none')
1300 return true;
1301
1302 if (!L.isObject(this.crypto_support[modeval]) || !this.crypto_support[modeval].hasOwnProperty(value))
1303 return _('The selected %s mode is incompatible with %s encryption').format(modetitle, enctitle);
1304
1305 return this.crypto_support[modeval][value];
1306 };
1307 }
1308 else if (hwtype == 'broadcom') {
1309 crypto_modes.push(['psk2', 'WPA2-PSK', 33]);
1310 crypto_modes.push(['psk+psk2', 'WPA-PSK/WPA2-PSK Mixed Mode', 22]);
1311 crypto_modes.push(['psk', 'WPA-PSK', 21]);
1312 crypto_modes.push(['wep-open', _('WEP Open System'), 11]);
1313 crypto_modes.push(['wep-shared', _('WEP Shared Key'), 10]);
1314 }
1315
1316 crypto_modes.push(['none', _('No Encryption'), 0]);
1317
1318 crypto_modes.sort(function(a, b) { return b[2] - a[2] });
1319
1320 for (var i = 0; i < crypto_modes.length; i++) {
1321 var security_level = (crypto_modes[i][2] >= 30) ? _('strong security')
1322 : (crypto_modes[i][2] >= 20) ? _('medium security')
1323 : (crypto_modes[i][2] >= 10) ? _('weak security') : _('open network');
1324
1325 encr.value(crypto_modes[i][0], '%s (%s)'.format(crypto_modes[i][1], security_level));
1326 }
1327
1328
1329 o = ss.taboption('encryption', form.Value, 'auth_server', _('Radius-Authentication-Server'));
1330 add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1331 o.rmempty = true;
1332 o.datatype = 'host(0)';
1333
1334 o = ss.taboption('encryption', form.Value, 'auth_port', _('Radius-Authentication-Port'), _('Default %d').format(1812));
1335 add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1336 o.rmempty = true;
1337 o.datatype = 'port';
1338
1339 o = ss.taboption('encryption', form.Value, 'auth_secret', _('Radius-Authentication-Secret'));
1340 add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1341 o.rmempty = true;
1342 o.password = true;
1343
1344 o = ss.taboption('encryption', form.Value, 'acct_server', _('Radius-Accounting-Server'));
1345 add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1346 o.rmempty = true;
1347 o.datatype = 'host(0)';
1348
1349 o = ss.taboption('encryption', form.Value, 'acct_port', _('Radius-Accounting-Port'), _('Default %d').format(1813));
1350 add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1351 o.rmempty = true;
1352 o.datatype = 'port';
1353
1354 o = ss.taboption('encryption', form.Value, 'acct_secret', _('Radius-Accounting-Secret'));
1355 add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1356 o.rmempty = true;
1357 o.password = true;
1358
1359 o = ss.taboption('encryption', form.Value, 'dae_client', _('DAE-Client'));
1360 add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1361 o.rmempty = true;
1362 o.datatype = 'host(0)';
1363
1364 o = ss.taboption('encryption', form.Value, 'dae_port', _('DAE-Port'), _('Default %d').format(3799));
1365 add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1366 o.rmempty = true;
1367 o.datatype = 'port';
1368
1369 o = ss.taboption('encryption', form.Value, 'dae_secret', _('DAE-Secret'));
1370 add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1371 o.rmempty = true;
1372 o.password = true;
1373
1374
1375 o = ss.taboption('encryption', form.Value, '_wpa_key', _('Key'));
1376 o.depends('encryption', 'psk');
1377 o.depends('encryption', 'psk2');
1378 o.depends('encryption', 'psk+psk2');
1379 o.depends('encryption', 'psk-mixed');
1380 o.depends('encryption', 'sae');
1381 o.depends('encryption', 'sae-mixed');
1382 o.datatype = 'wpakey';
1383 o.rmempty = true;
1384 o.password = true;
1385
1386 o.cfgvalue = function(section_id) {
1387 var key = uci.get('wireless', section_id, 'key');
1388 return /^[1234]$/.test(key) ? null : key;
1389 };
1390
1391 o.write = function(section_id, value) {
1392 uci.set('wireless', section_id, 'key', value);
1393 uci.unset('wireless', section_id, 'key1');
1394 uci.unset('wireless', section_id, 'key2');
1395 uci.unset('wireless', section_id, 'key3');
1396 uci.unset('wireless', section_id, 'key4');
1397 };
1398
1399
1400 o = ss.taboption('encryption', form.ListValue, '_wep_key', _('Used Key Slot'));
1401 o.depends('encryption', 'wep-open');
1402 o.depends('encryption', 'wep-shared');
1403 o.value('1', _('Key #%d').format(1));
1404 o.value('2', _('Key #%d').format(2));
1405 o.value('3', _('Key #%d').format(3));
1406 o.value('4', _('Key #%d').format(4));
1407
1408 o.cfgvalue = function(section_id) {
1409 var slot = +uci.get('wireless', section_id, 'key');
1410 return (slot >= 1 && slot <= 4) ? String(slot) : '';
1411 };
1412
1413 o.write = function(section_id, value) {
1414 uci.set('wireless', section_id, 'key', value);
1415 };
1416
1417 for (var slot = 1; slot <= 4; slot++) {
1418 o = ss.taboption('encryption', form.Value, 'key%d'.format(slot), _('Key #%d').format(slot));
1419 o.depends('encryption', 'wep-open');
1420 o.depends('encryption', 'wep-shared');
1421 o.datatype = 'wepkey';
1422 o.rmempty = true;
1423 o.password = true;
1424
1425 o.write = function(section_id, value) {
1426 if (value != null && (value.length == 5 || value.length == 13))
1427 value = 's:%s'.format(value);
1428 uci.set('wireless', section_id, this.option, value);
1429 };
1430 }
1431
1432
1433 if (hwtype == 'mac80211') {
1434 // Probe 802.11r support (and EAP support as a proxy for Openwrt)
1435 var has_80211r = L.hasSystemFeature('hostapd', '11r') || L.hasSystemFeature('hostapd', 'eap');
1436
1437 o = ss.taboption('encryption', form.Flag, 'ieee80211r', _('802.11r Fast Transition'), _('Enables fast roaming among access points that belong to the same Mobility Domain'));
1438 add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1439 if (has_80211r)
1440 add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['psk', 'psk2', 'psk-mixed', 'sae', 'sae-mixed'] });
1441 o.rmempty = true;
1442
1443 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.'));
1444 add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1445 o.depends({ ieee80211r: '1' });
1446 o.rmempty = true;
1447
1448 o = ss.taboption('encryption', form.Value, 'mobility_domain', _('Mobility Domain'), _('4-character hexadecimal ID'));
1449 o.depends({ ieee80211r: '1' });
1450 o.placeholder = '4f57';
1451 o.datatype = 'and(hexstring,length(4))';
1452 o.rmempty = true;
1453
1454 o = ss.taboption('encryption', form.Value, 'reassociation_deadline', _('Reassociation Deadline'), _('time units (TUs / 1.024 ms) [1000-65535]'));
1455 o.depends({ ieee80211r: '1' });
1456 o.placeholder = '1000';
1457 o.datatype = 'range(1000,65535)';
1458 o.rmempty = true;
1459
1460 o = ss.taboption('encryption', form.ListValue, 'ft_over_ds', _('FT protocol'));
1461 o.depends({ ieee80211r: '1' });
1462 o.value('1', _('FT over DS'));
1463 o.value('0', _('FT over the Air'));
1464 o.rmempty = true;
1465
1466 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.'));
1467 o.depends({ ieee80211r: '1' });
1468 o.default = o.enabled;
1469 o.rmempty = false;
1470
1471 o = ss.taboption('encryption', form.Value, 'r0_key_lifetime', _('R0 Key Lifetime'), _('minutes'));
1472 o.depends({ ieee80211r: '1' });
1473 o.placeholder = '10000';
1474 o.datatype = 'uinteger';
1475 o.rmempty = true;
1476
1477 o = ss.taboption('encryption', form.Value, 'r1_key_holder', _('R1 Key Holder'), _('6-octet identifier as a hex string - no colons'));
1478 o.depends({ ieee80211r: '1' });
1479 o.placeholder = '00004f577274';
1480 o.datatype = 'and(hexstring,length(12))';
1481 o.rmempty = true;
1482
1483 o = ss.taboption('encryption', form.Flag, 'pmk_r1_push', _('PMK R1 Push'));
1484 o.depends({ ieee80211r: '1' });
1485 o.placeholder = '0';
1486 o.rmempty = true;
1487
1488 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.'));
1489 o.depends({ ieee80211r: '1' });
1490 o.rmempty = true;
1491
1492 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.'));
1493 o.depends({ ieee80211r: '1' });
1494 o.rmempty = true;
1495 // End of 802.11r options
1496
1497 o = ss.taboption('encryption', form.ListValue, 'eap_type', _('EAP-Method'));
1498 o.value('tls', 'TLS');
1499 o.value('ttls', 'TTLS');
1500 o.value('peap', 'PEAP');
1501 o.value('fast', 'FAST');
1502 add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1503
1504 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"));
1505 o.enabled = '1';
1506 o.disabled = '0';
1507 o.default = o.disabled;
1508 add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1509 o.validate = function(section_id, value) {
1510 if (value == '1' && !L.hasSystemFeature('cabundle')) {
1511 return _("This option cannot be used because the ca-bundle package is not installed.");
1512 }
1513 return true;
1514 };
1515
1516 o = ss.taboption('encryption', form.FileUpload, 'ca_cert', _('Path to CA-Certificate'));
1517 add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'], ca_cert_usesystem: ['0'] });
1518
1519 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"));
1520 add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1521
1522 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"));
1523 add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1524
1525 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)"));
1526 add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1527
1528 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)"));
1529 add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1530
1531 o = ss.taboption('encryption', form.FileUpload, 'client_cert', _('Path to Client-Certificate'));
1532 add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'], eap_type: ['tls'] });
1533
1534 o = ss.taboption('encryption', form.FileUpload, 'priv_key', _('Path to Private Key'));
1535 add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'], eap_type: ['tls'] });
1536
1537 o = ss.taboption('encryption', form.Value, 'priv_key_pwd', _('Password of Private Key'));
1538 add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'], eap_type: ['tls'] });
1539 o.password = true;
1540
1541 o = ss.taboption('encryption', form.ListValue, 'auth', _('Authentication'));
1542 o.value('PAP', 'PAP');
1543 o.value('CHAP', 'CHAP');
1544 o.value('MSCHAP', 'MSCHAP');
1545 o.value('MSCHAPV2', 'MSCHAPv2');
1546 o.value('EAP-GTC', 'EAP-GTC');
1547 o.value('EAP-MD5', 'EAP-MD5');
1548 o.value('EAP-MSCHAPV2', 'EAP-MSCHAPv2');
1549 o.value('EAP-TLS', 'EAP-TLS');
1550 add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'], eap_type: ['fast', 'peap', 'ttls'] });
1551
1552 o.validate = function(section_id, value) {
1553 var eo = this.section.children.filter(function(o) { return o.option == 'eap_type' })[0],
1554 ev = eo.formvalue(section_id);
1555
1556 if (ev != 'ttls' && (value == 'PAP' || value == 'CHAP' || value == 'MSCHAP' || value == 'MSCHAPV2'))
1557 return _('This authentication type is not applicable to the selected EAP method.');
1558
1559 return true;
1560 };
1561
1562 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"));
1563 o.enabled = '1';
1564 o.disabled = '0';
1565 o.default = o.disabled;
1566 add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'], auth: ['EAP-TLS'] });
1567 o.validate = function(section_id, value) {
1568 if (value == '1' && !L.hasSystemFeature('cabundle')) {
1569 return _("This option cannot be used because the ca-bundle package is not installed.");
1570 }
1571 return true;
1572 };
1573
1574 o = ss.taboption('encryption', form.FileUpload, 'ca_cert2', _('Path to inner CA-Certificate'));
1575 add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'], auth: ['EAP-TLS'], ca_cert2_usesystem: ['0'] });
1576
1577 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"));
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, '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"));
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.DynamicList, 'domain_match2', _('Inner certificate constraint (Domain)'), _("Certificate constraint(s) against DNS SAN values (if available)<br />or Subject CN (exact match)"));
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.DynamicList, 'domain_suffix_match2', _('Inner certificate constraint (Wildcard)'), _("Certificate constraint(s) against DNS SAN values (if available)<br />or Subject CN (suffix match)"));
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.FileUpload, 'client_cert2', _('Path to inner Client-Certificate'));
1590 add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'], auth: ['EAP-TLS'] });
1591
1592 o = ss.taboption('encryption', form.FileUpload, 'priv_key2', _('Path to inner Private Key'));
1593 add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'], auth: ['EAP-TLS'] });
1594
1595 o = ss.taboption('encryption', form.Value, 'priv_key2_pwd', _('Password of inner Private Key'));
1596 add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'], auth: ['EAP-TLS'] });
1597 o.password = true;
1598
1599 o = ss.taboption('encryption', form.Value, 'identity', _('Identity'));
1600 add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'], eap_type: ['fast', 'peap', 'tls', 'ttls'] });
1601
1602 o = ss.taboption('encryption', form.Value, 'anonymous_identity', _('Anonymous Identity'));
1603 add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'], eap_type: ['fast', 'peap', 'tls', 'ttls'] });
1604
1605 o = ss.taboption('encryption', form.Value, 'password', _('Password'));
1606 add_dependency_permutations(o, { mode: ['sta', 'sta-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'], eap_type: ['fast', 'peap', 'ttls'] });
1607 o.password = true;
1608
1609
1610 if (hwtype == 'mac80211') {
1611 // ieee802.11w options
1612 if (L.hasSystemFeature('hostapd', '11w')) {
1613 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)"));
1614 o.value('', _('Disabled'));
1615 o.value('1', _('Optional'));
1616 o.value('2', _('Required'));
1617 add_dependency_permutations(o, { mode: ['ap', 'ap-wds', 'sta', 'sta-wds'], encryption: ['owe', 'psk2', 'psk-mixed', 'sae', 'sae-mixed', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1618
1619 o.defaults = {
1620 '2': [{ encryption: 'sae' }, { encryption: 'owe' }, { encryption: 'wpa3' }, { encryption: 'wpa3-mixed' }],
1621 '1': [{ encryption: 'sae-mixed'}],
1622 '': []
1623 };
1624
1625 o = ss.taboption('encryption', form.Value, 'ieee80211w_max_timeout', _('802.11w maximum timeout'), _('802.11w Association SA Query maximum timeout'));
1626 o.depends('ieee80211w', '1');
1627 o.depends('ieee80211w', '2');
1628 o.datatype = 'uinteger';
1629 o.placeholder = '1000';
1630 o.rmempty = true;
1631
1632 o = ss.taboption('encryption', form.Value, 'ieee80211w_retry_timeout', _('802.11w retry timeout'), _('802.11w Association SA Query retry timeout'));
1633 o.depends('ieee80211w', '1');
1634 o.depends('ieee80211w', '2');
1635 o.datatype = 'uinteger';
1636 o.placeholder = '201';
1637 o.rmempty = true;
1638 };
1639
1640 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.'));
1641 add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['psk2', 'psk-mixed', 'sae', 'sae-mixed', 'wpa2', 'wpa3', 'wpa3-mixed'] });
1642
1643 if (L.hasSystemFeature('hostapd', 'wps') && L.hasSystemFeature('wpasupplicant')) {
1644 o = ss.taboption('encryption', form.Flag, 'wps_pushbutton', _('Enable WPS pushbutton, requires WPA(2)-PSK/WPA3-SAE'))
1645 o.enabled = '1';
1646 o.disabled = '0';
1647 o.default = o.disabled;
1648 o.depends('encryption', 'psk');
1649 o.depends('encryption', 'psk2');
1650 o.depends('encryption', 'psk-mixed');
1651 o.depends('encryption', 'sae');
1652 o.depends('encryption', 'sae-mixed');
1653 }
1654 }
1655 }
1656 });
1657 };
1658
1659 s.handleRemove = function(section_id, ev) {
1660 document.querySelector('.cbi-section-table-row[data-sid="%s"]'.format(section_id)).style.opacity = 0.5;
1661 return form.TypedSection.prototype.handleRemove.apply(this, [section_id, ev]);
1662 };
1663
1664 s.handleScan = function(radioDev, ev) {
1665 var table = E('table', { 'class': 'table' }, [
1666 E('tr', { 'class': 'tr table-titles' }, [
1667 E('th', { 'class': 'th col-2 middle center' }, _('Signal')),
1668 E('th', { 'class': 'th col-4 middle left' }, _('SSID')),
1669 E('th', { 'class': 'th col-2 middle center hide-xs' }, _('Channel')),
1670 E('th', { 'class': 'th col-2 middle left hide-xs' }, _('Mode')),
1671 E('th', { 'class': 'th col-3 middle left hide-xs' }, _('BSSID')),
1672 E('th', { 'class': 'th col-3 middle left' }, _('Encryption')),
1673 E('th', { 'class': 'th cbi-section-actions right' }, ' '),
1674 ])
1675 ]);
1676
1677 var stop = E('button', {
1678 'class': 'btn',
1679 'click': L.bind(this.handleScanStartStop, this),
1680 'style': 'display:none',
1681 'data-state': 'stop'
1682 }, _('Stop refresh'));
1683
1684 cbi_update_table(table, [], E('em', { class: 'spinning' }, _('Starting wireless scan...')));
1685
1686 var md = ui.showModal(_('Join Network: Wireless Scan'), [
1687 table,
1688 E('div', { 'class': 'right' }, [
1689 stop,
1690 ' ',
1691 E('button', {
1692 'class': 'btn',
1693 'click': L.bind(this.handleScanAbort, this)
1694 }, _('Dismiss'))
1695 ])
1696 ]);
1697
1698 md.style.maxWidth = '90%';
1699 md.style.maxHeight = 'none';
1700
1701 this.pollFn = L.bind(this.handleScanRefresh, this, radioDev, {}, table, stop);
1702
1703 poll.add(this.pollFn);
1704 poll.start();
1705 };
1706
1707 s.handleScanRefresh = function(radioDev, scanCache, table, stop) {
1708 return radioDev.getScanList().then(L.bind(function(results) {
1709 var rows = [];
1710
1711 for (var i = 0; i < results.length; i++)
1712 scanCache[results[i].bssid] = results[i];
1713
1714 for (var k in scanCache)
1715 if (scanCache[k].stale)
1716 results.push(scanCache[k]);
1717
1718 results.sort(function(a, b) {
1719 var diff = (b.quality - a.quality) || (a.channel - b.channel);
1720
1721 if (diff)
1722 return diff;
1723
1724 if (a.ssid < b.ssid)
1725 return -1;
1726 else if (a.ssid > b.ssid)
1727 return 1;
1728
1729 if (a.bssid < b.bssid)
1730 return -1;
1731 else if (a.bssid > b.bssid)
1732 return 1;
1733 });
1734
1735 for (var i = 0; i < results.length; i++) {
1736 var res = results[i],
1737 qv = res.quality || 0,
1738 qm = res.quality_max || 0,
1739 q = (qv > 0 && qm > 0) ? Math.floor((100 / qm) * qv) : 0,
1740 s = res.stale ? 'opacity:0.5' : '';
1741
1742 rows.push([
1743 E('span', { 'style': s }, render_signal_badge(q, res.signal, res.noise)),
1744 E('span', { 'style': s }, (res.ssid != null) ? '%h'.format(res.ssid) : E('em', _('hidden'))),
1745 E('span', { 'style': s }, '%d'.format(res.channel)),
1746 E('span', { 'style': s }, '%h'.format(res.mode)),
1747 E('span', { 'style': s }, '%h'.format(res.bssid)),
1748 E('span', { 'style': s }, '%h'.format(network.formatWifiEncryption(res.encryption))),
1749 E('div', { 'class': 'right' }, E('button', {
1750 'class': 'cbi-button cbi-button-action important',
1751 'click': ui.createHandlerFn(this, 'handleJoin', radioDev, res)
1752 }, _('Join Network')))
1753 ]);
1754
1755 res.stale = true;
1756 }
1757
1758 cbi_update_table(table, rows);
1759
1760 stop.disabled = false;
1761 stop.style.display = '';
1762 stop.classList.remove('spinning');
1763 }, this));
1764 };
1765
1766 s.handleScanStartStop = function(ev) {
1767 var btn = ev.currentTarget;
1768
1769 if (btn.getAttribute('data-state') == 'stop') {
1770 poll.remove(this.pollFn);
1771 btn.firstChild.data = _('Start refresh');
1772 btn.setAttribute('data-state', 'start');
1773 }
1774 else {
1775 poll.add(this.pollFn);
1776 btn.firstChild.data = _('Stop refresh');
1777 btn.setAttribute('data-state', 'stop');
1778 btn.classList.add('spinning');
1779 btn.disabled = true;
1780 }
1781 };
1782
1783 s.handleScanAbort = function(ev) {
1784 var md = dom.parent(ev.target, 'div[aria-modal="true"]');
1785 if (md) {
1786 md.style.maxWidth = '';
1787 md.style.maxHeight = '';
1788 }
1789
1790 ui.hideModal();
1791 poll.remove(this.pollFn);
1792
1793 this.pollFn = null;
1794 };
1795
1796 s.handleJoinConfirm = function(radioDev, bss, form, ev) {
1797 var nameopt = L.toArray(form.lookupOption('name', '_new_'))[0],
1798 passopt = L.toArray(form.lookupOption('password', '_new_'))[0],
1799 ssidopt = L.toArray(form.lookupOption('ssid', '_new_'))[0],
1800 bssidopt = L.toArray(form.lookupOption('bssid', '_new_'))[0],
1801 zoneopt = L.toArray(form.lookupOption('zone', '_new_'))[0],
1802 replopt = L.toArray(form.lookupOption('replace', '_new_'))[0],
1803 nameval = (nameopt && nameopt.isValid('_new_')) ? nameopt.formvalue('_new_') : null,
1804 passval = (passopt && passopt.isValid('_new_')) ? passopt.formvalue('_new_') : null,
1805 ssidval = (ssidopt && ssidopt.isValid('_new_')) ? ssidopt.formvalue('_new_') : null,
1806 bssidval = (bssidopt && bssidopt.isValid('_new_')) ? bssidopt.formvalue('_new_') : null,
1807 zoneval = zoneopt ? zoneopt.formvalue('_new_') : null,
1808 enc = L.isObject(bss.encryption) ? bss.encryption : null,
1809 is_wep = (enc && Array.isArray(enc.wep)),
1810 is_psk = (enc && Array.isArray(enc.wpa) && L.toArray(enc.authentication).filter(function(a) { return a == 'psk' }).length > 0),
1811 is_sae = (enc && Array.isArray(enc.wpa) && L.toArray(enc.authentication).filter(function(a) { return a == 'sae' }).length > 0);
1812
1813 if (nameval == null || (passopt && passval == null))
1814 return;
1815
1816 var section_id = null;
1817
1818 return this.map.save(function() {
1819 var wifi_sections = uci.sections('wireless', 'wifi-iface');
1820
1821 if (replopt.formvalue('_new_') == '1') {
1822 for (var i = 0; i < wifi_sections.length; i++)
1823 if (wifi_sections[i].device == radioDev.getName())
1824 uci.remove('wireless', wifi_sections[i]['.name']);
1825 }
1826
1827 if (uci.get('wireless', radioDev.getName(), 'disabled') == '1') {
1828 for (var i = 0; i < wifi_sections.length; i++)
1829 if (wifi_sections[i].device == radioDev.getName())
1830 uci.set('wireless', wifi_sections[i]['.name'], 'disabled', '1');
1831
1832 uci.unset('wireless', radioDev.getName(), 'disabled');
1833 }
1834
1835 section_id = next_free_sid(wifi_sections.length);
1836
1837 uci.add('wireless', 'wifi-iface', section_id);
1838 uci.set('wireless', section_id, 'device', radioDev.getName());
1839 uci.set('wireless', section_id, 'mode', (bss.mode == 'Ad-Hoc') ? 'adhoc' : 'sta');
1840 uci.set('wireless', section_id, 'network', nameval);
1841
1842 if (bss.ssid != null) {
1843 uci.set('wireless', section_id, 'ssid', bss.ssid);
1844
1845 if (bssidval == '1')
1846 uci.set('wireless', section_id, 'bssid', bss.bssid);
1847 }
1848 else if (bss.bssid != null) {
1849 uci.set('wireless', section_id, 'bssid', bss.bssid);
1850 }
1851
1852 if (ssidval != null)
1853 uci.set('wireless', section_id, 'ssid', ssidval);
1854
1855 if (is_sae) {
1856 uci.set('wireless', section_id, 'encryption', 'sae');
1857 uci.set('wireless', section_id, 'key', passval);
1858 }
1859 else if (is_psk) {
1860 for (var i = enc.wpa.length - 1; i >= 0; i--) {
1861 if (enc.wpa[i] == 2) {
1862 uci.set('wireless', section_id, 'encryption', 'psk2');
1863 break;
1864 }
1865 else if (enc.wpa[i] == 1) {
1866 uci.set('wireless', section_id, 'encryption', 'psk');
1867 break;
1868 }
1869 }
1870
1871 uci.set('wireless', section_id, 'key', passval);
1872 }
1873 else if (is_wep) {
1874 uci.set('wireless', section_id, 'encryption', 'wep-open');
1875 uci.set('wireless', section_id, 'key', '1');
1876 uci.set('wireless', section_id, 'key1', passval);
1877 }
1878 else {
1879 uci.set('wireless', section_id, 'encryption', 'none');
1880 }
1881
1882 return network.addNetwork(nameval, { proto: 'dhcp' }).then(function(net) {
1883 firewall.deleteNetwork(net.getName());
1884
1885 var zonePromise = zoneval
1886 ? firewall.getZone(zoneval).then(function(zone) { return zone || firewall.addZone(zoneval) })
1887 : Promise.resolve();
1888
1889 return zonePromise.then(function(zone) {
1890 if (zone)
1891 zone.addNetwork(net.getName());
1892 });
1893 });
1894 }).then(L.bind(function() {
1895 return this.renderMoreOptionsModal(section_id);
1896 }, this));
1897 };
1898
1899 s.handleJoin = function(radioDev, bss, ev) {
1900 poll.remove(this.pollFn);
1901
1902 var m2 = new form.Map('wireless'),
1903 s2 = m2.section(form.NamedSection, '_new_'),
1904 enc = L.isObject(bss.encryption) ? bss.encryption : null,
1905 is_wep = (enc && Array.isArray(enc.wep)),
1906 is_psk = (enc && Array.isArray(enc.wpa) && L.toArray(enc.authentication).filter(function(a) { return a == 'psk' || a == 'sae' })),
1907 replace, passphrase, name, bssid, zone;
1908
1909 var nameUsed = function(name) {
1910 var s = uci.get('network', name);
1911 if (s != null && s['.type'] != 'interface')
1912 return true;
1913
1914 var net = (s != null) ? network.instantiateNetwork(name) : null;
1915 return (net != null && !net.isEmpty());
1916 };
1917
1918 s2.render = function() {
1919 return Promise.all([
1920 {},
1921 this.renderUCISection('_new_')
1922 ]).then(this.renderContents.bind(this));
1923 };
1924
1925 if (bss.ssid == null) {
1926 name = s2.option(form.Value, 'ssid', _('Network SSID'), _('The correct SSID must be manually specified when joining a hidden wireless network'));
1927 name.rmempty = false;
1928 };
1929
1930 replace = s2.option(form.Flag, 'replace', _('Replace wireless configuration'), _('Check this option to delete the existing networks from this radio.'));
1931
1932 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>'));
1933 name.datatype = 'uciname';
1934 name.default = 'wwan';
1935 name.rmempty = false;
1936 name.validate = function(section_id, value) {
1937 if (nameUsed(value))
1938 return _('The network name is already used');
1939
1940 return true;
1941 };
1942
1943 for (var i = 2; nameUsed(name.default); i++)
1944 name.default = 'wwan%d'.format(i);
1945
1946 if (is_wep || is_psk) {
1947 passphrase = s2.option(form.Value, 'password', is_wep ? _('WEP passphrase') : _('WPA passphrase'), _('Specify the secret encryption key here.'));
1948 passphrase.datatype = is_wep ? 'wepkey' : 'wpakey';
1949 passphrase.password = true;
1950 passphrase.rmempty = false;
1951 }
1952
1953 if (bss.ssid != null) {
1954 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));
1955 bssid.default = '0';
1956 }
1957
1958 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.'));
1959 zone.default = 'wan';
1960
1961 return m2.render().then(L.bind(function(nodes) {
1962 ui.showModal(_('Joining Network: %q').replace(/%q/, '"%h"'.format(bss.ssid)), [
1963 nodes,
1964 E('div', { 'class': 'right' }, [
1965 E('button', {
1966 'class': 'btn',
1967 'click': ui.hideModal
1968 }, _('Cancel')), ' ',
1969 E('button', {
1970 'class': 'cbi-button cbi-button-positive important',
1971 'click': ui.createHandlerFn(this, 'handleJoinConfirm', radioDev, bss, m2)
1972 }, _('Submit'))
1973 ])
1974 ], 'cbi-modal').querySelector('[id="%s"] input[class][type]'.format((passphrase || name).cbid('_new_'))).focus();
1975 }, this));
1976 };
1977
1978 s.handleAdd = function(radioDev, ev) {
1979 var section_id = next_free_sid(uci.sections('wireless', 'wifi-iface').length);
1980
1981 uci.unset('wireless', radioDev.getName(), 'disabled');
1982
1983 uci.add('wireless', 'wifi-iface', section_id);
1984 uci.set('wireless', section_id, 'device', radioDev.getName());
1985 uci.set('wireless', section_id, 'mode', 'ap');
1986 uci.set('wireless', section_id, 'ssid', 'OpenWrt');
1987 uci.set('wireless', section_id, 'encryption', 'none');
1988
1989 this.addedSection = section_id;
1990 return this.renderMoreOptionsModal(section_id);
1991 };
1992
1993 o = s.option(form.DummyValue, '_badge');
1994 o.modalonly = false;
1995 o.textvalue = function(section_id) {
1996 var inst = this.section.lookupRadioOrNetwork(section_id),
1997 node = E('div', { 'class': 'center' });
1998
1999 if (inst.getWifiNetworks)
2000 node.appendChild(render_radio_badge(inst));
2001 else
2002 node.appendChild(render_network_badge(inst));
2003
2004 return node;
2005 };
2006
2007 o = s.option(form.DummyValue, '_stat');
2008 o.modalonly = false;
2009 o.textvalue = function(section_id) {
2010 var inst = this.section.lookupRadioOrNetwork(section_id);
2011
2012 if (inst.getWifiNetworks)
2013 return render_radio_status(inst, this.section.wifis.filter(function(e) {
2014 return (e.getWifiDeviceName() == inst.getName());
2015 }));
2016 else
2017 return render_network_status(inst);
2018 };
2019
2020 return m.render().then(L.bind(function(m, nodes) {
2021 poll.add(L.bind(function() {
2022 var section_ids = m.children[0].cfgsections(),
2023 tasks = [ network.getHostHints(), network.getWifiDevices() ];
2024
2025 for (var i = 0; i < section_ids.length; i++) {
2026 var row = nodes.querySelector('.cbi-section-table-row[data-sid="%s"]'.format(section_ids[i])),
2027 dsc = row.querySelector('[data-name="_stat"] > div'),
2028 btns = row.querySelectorAll('.cbi-section-actions button');
2029
2030 if (dsc.getAttribute('restart') == '') {
2031 dsc.setAttribute('restart', '1');
2032 tasks.push(fs.exec('/sbin/wifi', ['up', section_ids[i]]).catch(function(e) {
2033 ui.addNotification(null, E('p', e.message));
2034 }));
2035 }
2036 else if (dsc.getAttribute('restart') == '1') {
2037 dsc.removeAttribute('restart');
2038 btns[0].classList.remove('spinning');
2039 btns[0].disabled = false;
2040 }
2041 }
2042
2043 return Promise.all(tasks)
2044 .then(L.bind(function(hosts_radios) {
2045 var tasks = [];
2046
2047 for (var i = 0; i < hosts_radios[1].length; i++)
2048 tasks.push(hosts_radios[1][i].getWifiNetworks());
2049
2050 return Promise.all(tasks).then(function(data) {
2051 hosts_radios[2] = [];
2052
2053 for (var i = 0; i < data.length; i++)
2054 hosts_radios[2].push.apply(hosts_radios[2], data[i]);
2055
2056 return hosts_radios;
2057 });
2058 }, network))
2059 .then(L.bind(function(hosts_radios_wifis) {
2060 var tasks = [];
2061
2062 for (var i = 0; i < hosts_radios_wifis[2].length; i++)
2063 tasks.push(hosts_radios_wifis[2][i].getAssocList());
2064
2065 return Promise.all(tasks).then(function(data) {
2066 hosts_radios_wifis[3] = [];
2067
2068 for (var i = 0; i < data.length; i++) {
2069 var wifiNetwork = hosts_radios_wifis[2][i],
2070 radioDev = hosts_radios_wifis[1].filter(function(d) { return d.getName() == wifiNetwork.getWifiDeviceName() })[0];
2071
2072 for (var j = 0; j < data[i].length; j++)
2073 hosts_radios_wifis[3].push(Object.assign({ radio: radioDev, network: wifiNetwork }, data[i][j]));
2074 }
2075
2076 return hosts_radios_wifis;
2077 });
2078 }, network))
2079 .then(L.bind(this.poll_status, this, nodes));
2080 }, this), 5);
2081
2082 var table = E('table', { 'class': 'table assoclist', 'id': 'wifi_assoclist_table' }, [
2083 E('tr', { 'class': 'tr table-titles' }, [
2084 E('th', { 'class': 'th nowrap' }, _('Network')),
2085 E('th', { 'class': 'th hide-xs' }, _('MAC-Address')),
2086 E('th', { 'class': 'th' }, _('Host')),
2087 E('th', { 'class': 'th' }, _('Signal / Noise')),
2088 E('th', { 'class': 'th' }, _('RX Rate / TX Rate'))
2089 ])
2090 ]);
2091
2092 cbi_update_table(table, [], E('em', { 'class': 'spinning' }, _('Collecting data...')))
2093
2094 return E([ nodes, E('h3', _('Associated Stations')), table ]);
2095 }, this, m));
2096 }
2097 });