'require network';
'require firewall';
'require tools.widgets as widgets';
+'require uqr';
const isReadonlyView = !L.hasViewPermission();
]);
}
+function buildSVGQRCode(data, code, options, dummy=false) {
+ const opts = {
+ pixelSize: 4,
+ whiteColor: 'white',
+ blackColor: 'black',
+ ecc: 'M',
+ ...options
+ };
+ const svg = uqr.renderSVG(data, opts);
+ if (dummy)
+ return svg;
+ else {
+ code.style.opacity = '';
+ dom.content(code, Object.assign(E(svg), { style: 'width:100%;height:auto' }));
+ }
+}
+
function render_signal_badge(signalPercent, signalValue, noiseValue, wrap, mode) {
let icon = L.resource('icons/signal-075-100.svg'), title, value;
encr.value(crypto_mode[0], '%s (%s)'.format(crypto_mode[1], security_level));
});
+ // QR Code
+ o = ss.taboption('encryption', form.DummyValue, '_qrops', _('QR Code'),
+ _('SSID and passwords with URIencoded sequences (e.g. %20) may not work.'));
+ o.modalonly = true;
+
+ o.createWiFiPassword = function(section_id) {
+ // https://www.wi-fi.org/system/files/WPA3%20Specification%20v3.5.pdf#page=33
+ /*
+ WIFI:T:WPA;S:mynetwork;P:mypass;;
+
+ WIFI-qr = "WIFI:" [type ";"] [trdisable ";"] ssid ";" [hidden ";"] [id ";"] [password ";"] [public-key ";"] ";"
+
+ Param Description
+ type "T:" *(unreserved) ; security type
+ trdisable "R:" *(HEXDIG) ; Transition Disable value
+ ssid "S:" *(printable / pct-encoded) ; SSID of the network
+ hidden "H:true" ; when present, indicates a hidden (stealth) SSID is used
+ id "I:" *(printable / pct-encoded) ; UTF-8 encoded password identifier, present if the password has an SAE password identifier
+ password "P:" *(printable / pct-encoded) ; password, present for password-based authentication
+ public-key "K:" *PKCHAR ; DER of ASN.1 SubjectPublicKeyInfo in compressed form and encoded in "base64" as per [6], present when the network supports SAE-PK, else absent
+
+ printable = %x20-3a / %x3c-7e ; semi-colon excluded
+ PKCHAR = ALPHA / DIGIT / %x2b / %x2f / %x3d
+ */
+
+ function pctEncode(str) {
+ const bytes = new TextEncoder().encode(str);
+ let out = "";
+ for (const b of bytes) {
+ // printable = 0x20–0x3A and 0x3C–0x7E, but semicolon (0x3B) excluded
+ // anything *within* this range %encoded should be treated as printable literal(?)
+ // There seems to be a glaring bug in this WiFi spec. Ofc there are bugs.
+ // By not encoding the "%" character, a string literal % with two successive
+ // digits is ambiguous. If the password contains "%20" which
+ // should be interpreted literally ['%', '2', '0'] and not " ", some
+ // clients interpret this as " ". YMMV.
+ const printable = (b >= 0x20 && b <= 0x3A && b !== 0x3B)
+ || (b >= 0x3C && b <= 0x7E);
+
+ if (printable) {
+ out += String.fromCharCode(b);
+ } else {
+ out += "%" + b.toString(16).toUpperCase().padStart(2, "0");
+ }
+ }
+ return out;
+ }
+
+ const wifiSSID = this.section.formvalue(section_id, 'ssid'); // S
+ const wifiEncr = this.section.formvalue(section_id, 'encryption'); // T
+ const wifiKey = this.section.formvalue(section_id, '_wpa_key'); // P
+ const wifiHide = this.section.formvalue(section_id, 'hidden') === '1'; // H
+
+ /* trdisable:
+ 0 WPA3-Personal
+ 1 SAE-PK
+ 2 WPA3-Enterprise
+ 3 WiFi-Enhanced Open */
+ let trdisable = ''; // R
+ switch (true) {
+ case (wifiEncr === 'sae'): trdisable = 0; break; // 'sae' i.e. WPA3-Personal
+ // case (???): trdisable = 1; break; // SAE-PK
+ case (wifiEncr.startsWith('wpa3')): trdisable = 2; break; // 'wpa3*' i.e. WPA3-Enterprise
+ case (wifiEncr === 'owe'): trdisable = 3; break; // 'open' i.e. WiFi-Enhanced Open
+ default: trdisable = ''; break;
+ }
+
+ return [
+ `WIFI:`,
+ (wifiKey) ? `T:WPA;`: null, // absent indicates [open || Wi-Fi Enhanced Open ]
+ (trdisable !== '') ? `R:${trdisable};` : null,
+ `S:${wifiSSID};`,
+ (wifiHide) ? `H:${wifiHide};` : null,
+ (wifiKey) ? `P:${pctEncode(wifiKey)};`: null,
+ ].filter(Boolean).join('') + ';';
+ };
+
+ o.handleGenerateQR = function(section_id, ev) {
+ const parent = s.map;
+ const mapNode = document.querySelector('body.modal-overlay-active > #modal_overlay > .modal.cbi-modal > .cbi-map:not(.hidden)');
+ const headNode = mapNode.parentNode.querySelector('h4');
+ const wifiQRGenerator = this.createWiFiPassword.bind(this, section_id);
+
+ return Promise.all([
+ parent.save(null, true)
+ ]).then(function(data) {
+ let qrm, qrs, qro;
+
+ qrm = new form.JSONMap({ qrcode: { } },
+ null, _('Scan this QR code with the client device.'));
+ qrm.parent = parent;
+
+ qrs = qrm.section(form.NamedSection, 'qrcode');
+
+ function handleQRParamChange(ev, section_id, value) {
+ const code = this.map.findElement('.qr-code');
+ const conf = this.map.findElement('.wifi-qr-code-content');
+ const ecc = this.section.getUIElement(section_id, 'ecc');
+
+ if (this.isValid(section_id)) {
+ conf.firstChild.data = wifiQRGenerator(section_id);
+ code.style.opacity = '.5';
+
+ buildSVGQRCode(conf.firstChild.data, code, {ecc: ecc.getValue()});
+ }
+ };
+
+ qro = qrs.option(form.ListValue, 'ecc', _('QR Error Correction Code Level'));
+ qro.value('L', _('Low'));
+ qro.value('M', _('Medium'));
+ qro.value('Q', _('Quartile'));
+ qro.value('H', _('High'));
+ qro.onchange = handleQRParamChange;
+
+
+ qro = qrs.option(form.DummyValue, 'output');
+ qro.renderWidget = function() {
+ const wifi_qr = wifiQRGenerator(section_id);
+ const ecc = this.section.formvalue(section_id, 'ecc');
+
+ return E('div', {
+ 'class': 'qr-code-display',
+ 'style': 'display:flex; flex-wrap:wrap; align-items:center; gap:.5em',
+ }, [
+ E('div', {
+ 'class': 'qr-code',
+ // any width and height should be ~360: enough for QR with K: field and High ECC.
+ }, [
+ // fill initial QR code
+ E(buildSVGQRCode(wifi_qr, null, {ecc: ecc || undefined}, true))
+ ]),
+ E('pre', {
+ 'class': 'wifi-qr-code-content',
+ 'style': 'flex:1; overflow:auto; word-break:break-all; ',
+ 'click': function(ev) {
+ const sel = window.getSelection();
+ const range = document.createRange();
+
+ range.selectNodeContents(ev.currentTarget);
+
+ sel.removeAllRanges();
+ sel.addRange(range);
+ }
+ }, [ wifi_qr ])
+ ]);
+ };
+
+ return qrm.render().then(function(nodes) {
+ // stash the current dialogue style (visible)
+ const dStyle = mapNode.style;
+ // hide the current modal window
+ mapNode.style.display = 'none';
+ // stash the current button row style (visible)
+ const bRowStyle = mapNode.nextElementSibling.style;
+ // hide the [ Dismiss | Save ] button row
+ mapNode.nextElementSibling.style.display = 'none';
+
+ headNode.appendChild(E('span', [ ' » ', _('Generate WiFi QR…') ]));
+ mapNode.parentNode.appendChild(E([], [
+ nodes,
+ E('div', {
+ 'class': 'right'
+ }, [
+ E('button', {
+ 'class': 'btn',
+ 'click': function() {
+ // Remove QR code button (row)
+ nodes.parentNode.removeChild(nodes.nextSibling);
+ // Remove QR code form
+ nodes.parentNode.removeChild(nodes);
+ // unhide the WiFi modal dialogue
+ mapNode.style = dStyle;
+ // Revert button row style to visible again
+ mapNode.nextSibling.style = bRowStyle;
+ // Remove the H4 span (») title
+ headNode.removeChild(headNode.lastChild);
+ }
+ }, [ _('Back to settings') ])
+ ])
+ ]));
+ });
+ });
+ };
+
+ o.cfgvalue = function(section_id, value) {
+ return E('button', {
+ 'class': 'btn qr-code',
+ 'style': 'display:inline-flex;align-items:center;gap:.5em',
+ 'click': ui.createHandlerFn(this, 'handleGenerateQR', section_id),
+ }, [
+ // inject dummy QR code
+ E(buildSVGQRCode('openwrt.org', null, {pixelSize: 1, ecc: 'L'}, true)),
+ _('Generate QR…')
+ ]);
+ };
+ // End QR Code
o = ss.taboption('encryption', form.Flag, 'ppsk', _('Enable Private PSK (PPSK)'), _('Private Pre-Shared Key (PPSK) allows the use of different Pre-Shared Key for each STA MAC address. Private MAC PSKs are stored on the RADIUS server.'));
add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['psk', 'psk2', 'psk+psk2', 'psk-mixed'] });