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