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