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