Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ed2d4744c | ||
|
|
9b646b99f8 | ||
|
|
a2811cced6 | ||
|
|
07e7d2866f | ||
|
|
0fe631b4ed |
@@ -537,7 +537,9 @@ It writes the same netplan file as Core Settings → WiFi (`/etc/netplan/60-kios
|
|||||||
|
|
||||||
**About Fn:** the Fn key is handled inside most keyboards and never reaches the computer, so it isn't part of the combo - holding it too does no harm where Fn+W is still W. On compact keyboards where Fn+W is something else (often Up arrow), leave Fn out.
|
**About Fn:** the Fn key is handled inside most keyboards and never reaches the computer, so it isn't part of the combo - holding it too does no harm where Fn+W is still W. On compact keyboards where Fn+W is something else (often Up arrow), leave Fn out.
|
||||||
|
|
||||||
The root part is `/usr/local/bin/kiosk-wifi-helper` (status/scan/connect only), which the kiosk user may run via sudo through `/etc/sudoers.d/kiosk-wifi` - nothing else. Existing kiosks get it with Advanced → Upgrade.
|
**More than one WiFi adapter** (e.g. a built-in card and a USB dongle): a *WiFi adapter* dropdown appears at the top, and the status box lists every adapter with what it's connected to. Status, scan and connect act on the one you pick; by default it's the adapter that's connected right now.
|
||||||
|
|
||||||
|
The root part is `/usr/local/bin/kiosk-wifi-helper` (status/scan/connect and region only). The app reaches it through a root-owned socket, `/run/kiosk-wifi-helper.sock` (`kiosk-wifi-helper.socket`, group-restricted to the kiosk user, `0660`), rather than `sudo`: the app can end up running with Linux's "no new privileges" flag, and then sudo refuses with *"The no new privileges flag is set"*. A passwordless-sudo rule (`/etc/sudoers.d/kiosk-wifi`) stays as a fallback. Existing kiosks get both with Advanced → Upgrade.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+65
-7
@@ -1,5 +1,6 @@
|
|||||||
const {app,BrowserWindow,BrowserView,globalShortcut,ipcMain,dialog,session}=require('electron');
|
const {app,BrowserWindow,BrowserView,globalShortcut,ipcMain,dialog,session}=require('electron');
|
||||||
const {exec,spawn}=require('child_process');
|
const {exec,spawn}=require('child_process');
|
||||||
|
const net=require('net');
|
||||||
const fs=require('fs');
|
const fs=require('fs');
|
||||||
const path=require('path');
|
const path=require('path');
|
||||||
const os=require('os');
|
const os=require('os');
|
||||||
@@ -1215,7 +1216,56 @@ const WIFI_HELPER='/usr/local/bin/kiosk-wifi-helper';
|
|||||||
const WIFI_IDLE_TIMEOUT=5*60000; // closes after 5 minutes untouched
|
const WIFI_IDLE_TIMEOUT=5*60000; // closes after 5 minutes untouched
|
||||||
let wifiWindow=null,wifiUnlocked=false,wifiIdleTimer=null;
|
let wifiWindow=null,wifiUnlocked=false,wifiIdleTimer=null;
|
||||||
|
|
||||||
function runWifiHelper(action,input){
|
const WIFI_SOCKET='/run/kiosk-wifi-helper.sock';
|
||||||
|
|
||||||
|
// Parse the helper's one JSON reply line (the last line of its output).
|
||||||
|
function parseWifiReply(out){
|
||||||
|
const line=out.trim().split('\n').pop();
|
||||||
|
return JSON.parse(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preferred route: a root-owned socket (kiosk-wifi-helper.socket). Unlike
|
||||||
|
// sudo it keeps working when this app has the "no new privileges" flag set.
|
||||||
|
// Resolves null when the socket isn't usable (not installed yet, or this
|
||||||
|
// user isn't allowed), so the caller falls back to sudo.
|
||||||
|
function runWifiHelperSocket(action,input){
|
||||||
|
return new Promise(resolve=>{
|
||||||
|
let out='',done=false;
|
||||||
|
const finish=result=>{if(!done){done=true;clearTimeout(killer);resolve(result);}};
|
||||||
|
const sock=net.connect({path:WIFI_SOCKET,allowHalfOpen:true});
|
||||||
|
const killer=setTimeout(()=>{sock.destroy();finish({ok:false,error:'The WiFi helper timed out'});},120000);
|
||||||
|
sock.on('connect',()=>{
|
||||||
|
// Request = action line, then the JSON body; half-close so the
|
||||||
|
// helper sees end of input.
|
||||||
|
sock.end(action+'\n'+(input?JSON.stringify(input):''));
|
||||||
|
});
|
||||||
|
sock.on('data',d=>{out+=d;});
|
||||||
|
sock.on('error',e=>{
|
||||||
|
if(out){
|
||||||
|
// A reset after the reply arrived - use the reply.
|
||||||
|
try{finish(parseWifiReply(out));return;}catch(_){}
|
||||||
|
}
|
||||||
|
if(!out&&['ENOENT','ECONNREFUSED','EACCES','EPERM'].includes(e.code)){
|
||||||
|
console.log('[WIFI] Helper socket unavailable ('+e.code+') - falling back to sudo');
|
||||||
|
finish(null);
|
||||||
|
}else{
|
||||||
|
finish({ok:false,error:'Could not reach the WiFi helper: '+e.message});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
sock.on('close',()=>{
|
||||||
|
if(done)return;
|
||||||
|
try{
|
||||||
|
finish(parseWifiReply(out));
|
||||||
|
}catch(e){
|
||||||
|
console.error('[WIFI] helper socket output:',out);
|
||||||
|
finish({ok:false,error:'The WiFi helper gave no usable answer'});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: the helper through passwordless sudo (/etc/sudoers.d/kiosk-wifi).
|
||||||
|
function runWifiHelperSudo(action,input){
|
||||||
return new Promise(resolve=>{
|
return new Promise(resolve=>{
|
||||||
let out='',err='',done=false;
|
let out='',err='',done=false;
|
||||||
const finish=result=>{if(!done){done=true;resolve(result);}};
|
const finish=result=>{if(!done){done=true;resolve(result);}};
|
||||||
@@ -1232,12 +1282,11 @@ function runWifiHelper(action,input){
|
|||||||
child.on('error',e=>{clearTimeout(killer);finish({ok:false,error:'Could not run the WiFi helper: '+e.message});});
|
child.on('error',e=>{clearTimeout(killer);finish({ok:false,error:'Could not run the WiFi helper: '+e.message});});
|
||||||
child.on('close',()=>{
|
child.on('close',()=>{
|
||||||
clearTimeout(killer);
|
clearTimeout(killer);
|
||||||
const line=out.trim().split('\n').pop();
|
|
||||||
try{
|
try{
|
||||||
finish(JSON.parse(line));
|
finish(parseWifiReply(out));
|
||||||
}catch(e){
|
}catch(e){
|
||||||
// Not our JSON - typically sudo refusing ("a password is required")
|
// Not our JSON - typically sudo refusing ("a password is required",
|
||||||
// on a kiosk set up before this feature: Upgrade installs the rule.
|
// or the "no new privileges" flag) - Upgrade installs the socket.
|
||||||
console.error('[WIFI] helper output:',out,err);
|
console.error('[WIFI] helper output:',out,err);
|
||||||
finish({ok:false,error:(err.trim()||'The WiFi helper is not installed')+' - run Advanced -> Upgrade from the admin menu'});
|
finish({ok:false,error:(err.trim()||'The WiFi helper is not installed')+' - run Advanced -> Upgrade from the admin menu'});
|
||||||
}
|
}
|
||||||
@@ -1247,6 +1296,12 @@ function runWifiHelper(action,input){
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function runWifiHelper(action,input){
|
||||||
|
const viaSocket=await runWifiHelperSocket(action,input);
|
||||||
|
if(viaSocket!==null)return viaSocket;
|
||||||
|
return runWifiHelperSudo(action,input);
|
||||||
|
}
|
||||||
|
|
||||||
function resetWifiIdleTimer(){
|
function resetWifiIdleTimer(){
|
||||||
if(wifiIdleTimer)clearTimeout(wifiIdleTimer);
|
if(wifiIdleTimer)clearTimeout(wifiIdleTimer);
|
||||||
wifiIdleTimer=setTimeout(()=>{
|
wifiIdleTimer=setTimeout(()=>{
|
||||||
@@ -1327,7 +1382,10 @@ ipcMain.handle('wifi-request',async(event,req)=>{
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
if(!wifiUnlocked)return{ok:false,error:'Enter the PIN first'};
|
if(!wifiUnlocked)return{ok:false,error:'Enter the PIN first'};
|
||||||
if(action==='status'||action==='scan')return runWifiHelper(action);
|
if(action==='ifaces')return runWifiHelper('ifaces');
|
||||||
|
if(action==='status'||action==='scan'){
|
||||||
|
return runWifiHelper(action,req.iface?{iface:String(req.iface)}:null);
|
||||||
|
}
|
||||||
if(action==='region-status')return runWifiHelper('region-status');
|
if(action==='region-status')return runWifiHelper('region-status');
|
||||||
if(action==='region-set'){
|
if(action==='region-set'){
|
||||||
const input={};
|
const input={};
|
||||||
@@ -1337,7 +1395,7 @@ ipcMain.handle('wifi-request',async(event,req)=>{
|
|||||||
}
|
}
|
||||||
if(action==='connect'){
|
if(action==='connect'){
|
||||||
console.log('[WIFI] Connecting to',JSON.stringify(String(req.ssid||'')));
|
console.log('[WIFI] Connecting to',JSON.stringify(String(req.ssid||'')));
|
||||||
const result=await runWifiHelper('connect',{ssid:String(req.ssid||''),password:String(req.password||'')});
|
const result=await runWifiHelper('connect',{ssid:String(req.ssid||''),password:String(req.password||''),iface:req.iface?String(req.iface):''});
|
||||||
console.log('[WIFI] Connect result:',result.ok?('ok '+result.ip):result.error);
|
console.log('[WIFI] Connect result:',result.ok?('ok '+result.ip):result.error);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
+49
-8
@@ -95,7 +95,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="wifi-step" class="hidden">
|
<div id="wifi-step" class="hidden">
|
||||||
<div id="status">Checking…</div>
|
<div id="adapter-row" class="hidden">
|
||||||
|
<label for="adapter" style="margin-top: 0">WiFi adapter</label>
|
||||||
|
<select id="adapter"></select>
|
||||||
|
</div>
|
||||||
|
<div id="status" style="margin-top: 12px">Checking…</div>
|
||||||
|
|
||||||
<div class="row" style="margin-top: 14px">
|
<div class="row" style="margin-top: 14px">
|
||||||
<label style="margin: 0; flex: 1">Networks nearby</label>
|
<label style="margin: 0; flex: 1">Networks nearby</label>
|
||||||
@@ -139,6 +143,7 @@
|
|||||||
const {ipcRenderer} = require('electron');
|
const {ipcRenderer} = require('electron');
|
||||||
const $ = id => document.getElementById(id);
|
const $ = id => document.getElementById(id);
|
||||||
let busy = false;
|
let busy = false;
|
||||||
|
let adapter = ''; // the adapter picked in the dropdown ('' = let the helper choose)
|
||||||
|
|
||||||
function request(action, extra) {
|
function request(action, extra) {
|
||||||
return ipcRenderer.invoke('wifi-request', Object.assign({action}, extra || {}))
|
return ipcRenderer.invoke('wifi-request', Object.assign({action}, extra || {}))
|
||||||
@@ -163,16 +168,47 @@
|
|||||||
return '▂';
|
return '▂';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function adapterLabel(a) {
|
||||||
|
const kind = a.kind === 'usb' ? 'USB' : 'built-in';
|
||||||
|
return `${a.name} (${kind}) - ` + (a.connected ? `connected to "${a.ssid}"` : 'not connected');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill the dropdown from the helper's adapter list. Shown only when
|
||||||
|
// there is more than one adapter; the choice sticks across refreshes.
|
||||||
|
function fillAdapters(r) {
|
||||||
|
const row = $('adapter-row');
|
||||||
|
const sel = $('adapter');
|
||||||
|
if (!r || !r.ok || !r.adapters || r.adapters.length === 0) { row.classList.add('hidden'); return r; }
|
||||||
|
const names = r.adapters.map(a => a.name);
|
||||||
|
if (!names.includes(adapter)) adapter = r.selected || names[0];
|
||||||
|
sel.textContent = '';
|
||||||
|
for (const a of r.adapters) {
|
||||||
|
const o = document.createElement('option');
|
||||||
|
o.value = a.name;
|
||||||
|
o.textContent = adapterLabel(a);
|
||||||
|
if (a.name === adapter) o.selected = true;
|
||||||
|
sel.appendChild(o);
|
||||||
|
}
|
||||||
|
row.classList.toggle('hidden', r.adapters.length < 2);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
async function refreshStatus() {
|
async function refreshStatus() {
|
||||||
const s = await request('status');
|
const ad = fillAdapters(await request('ifaces'));
|
||||||
|
const s = await request('status', adapter ? {iface: adapter} : {});
|
||||||
const box = $('status');
|
const box = $('status');
|
||||||
if (!s.ok) { box.textContent = s.error || 'Status unavailable'; return; }
|
if (!s.ok) { box.textContent = s.error || 'Status unavailable'; return; }
|
||||||
if (!s.iface) { box.textContent = 'No WiFi adapter found'; return; }
|
if (!s.iface) { box.textContent = 'No WiFi adapter found'; return; }
|
||||||
box.textContent = '';
|
box.textContent = '';
|
||||||
const line = (text) => { const d = document.createElement('div'); d.textContent = text; box.appendChild(d); };
|
const line = (text) => { const d = document.createElement('div'); d.textContent = text; box.appendChild(d); };
|
||||||
line(s.connected ? `Connected to "${s.ssid}" - ${s.ip}` : 'Not connected');
|
if (ad && ad.ok && ad.adapters && ad.adapters.length > 1) {
|
||||||
|
// Every adapter at a glance, the selected one marked.
|
||||||
|
for (const a of ad.adapters) line((a.name === s.iface ? '▶ ' : ' ') + adapterLabel(a) + (a.ip ? ` - ${a.ip}` : ''));
|
||||||
|
} else {
|
||||||
|
line(s.connected ? `Connected to "${s.ssid}" - ${s.ip}` : 'Not connected');
|
||||||
|
line(`Adapter: ${s.iface}`);
|
||||||
|
}
|
||||||
if (s.configured && s.configured !== s.ssid) line(`Saved network: "${s.configured}"`);
|
if (s.configured && s.configured !== s.ssid) line(`Saved network: "${s.configured}"`);
|
||||||
line(`Adapter: ${s.iface}`);
|
|
||||||
if (!$('ssid').value && (s.ssid || s.configured)) $('ssid').value = s.ssid || s.configured;
|
if (!$('ssid').value && (s.ssid || s.configured)) $('ssid').value = s.ssid || s.configured;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,7 +217,7 @@
|
|||||||
setBusy(true);
|
setBusy(true);
|
||||||
message('Scanning…', 'busy');
|
message('Scanning…', 'busy');
|
||||||
const list = $('networks');
|
const list = $('networks');
|
||||||
const r = await request('scan');
|
const r = await request('scan', adapter ? {iface: adapter} : {});
|
||||||
list.textContent = '';
|
list.textContent = '';
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
message(r.error, 'error');
|
message(r.error, 'error');
|
||||||
@@ -217,7 +253,7 @@
|
|||||||
if (!ssid) { message('Pick a network or type its name', 'error'); return; }
|
if (!ssid) { message('Pick a network or type its name', 'error'); return; }
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
message(`Connecting to "${ssid}" (up to 30 seconds)…`, 'busy');
|
message(`Connecting to "${ssid}" (up to 30 seconds)…`, 'busy');
|
||||||
const r = await request('connect', {ssid, password});
|
const r = await request('connect', {ssid, password, iface: adapter});
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
message(`✓ Connected to "${r.ssid}" - ${r.ip}`, 'ok');
|
message(`✓ Connected to "${r.ssid}" - ${r.ip}`, 'ok');
|
||||||
$('password').value = '';
|
$('password').value = '';
|
||||||
@@ -287,14 +323,19 @@
|
|||||||
message('');
|
message('');
|
||||||
$('pin-step').classList.add('hidden');
|
$('pin-step').classList.add('hidden');
|
||||||
$('wifi-step').classList.remove('hidden');
|
$('wifi-step').classList.remove('hidden');
|
||||||
refreshStatus();
|
refreshStatus().then(() => scan());
|
||||||
loadRegion();
|
loadRegion();
|
||||||
scan();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$('pin-ok').addEventListener('click', unlock);
|
$('pin-ok').addEventListener('click', unlock);
|
||||||
$('pin').addEventListener('keydown', e => { if (e.key === 'Enter') unlock(); });
|
$('pin').addEventListener('keydown', e => { if (e.key === 'Enter') unlock(); });
|
||||||
$('scan').addEventListener('click', scan);
|
$('scan').addEventListener('click', scan);
|
||||||
|
$('adapter').addEventListener('change', () => {
|
||||||
|
adapter = $('adapter').value;
|
||||||
|
$('networks').innerHTML = '<li class="empty">Scanning…</li>';
|
||||||
|
refreshStatus();
|
||||||
|
scan();
|
||||||
|
});
|
||||||
$('region-save').addEventListener('click', saveRegion);
|
$('region-save').addEventListener('click', saveRegion);
|
||||||
$('region-from-locale').addEventListener('click', countryFromLocale);
|
$('region-from-locale').addEventListener('click', countryFromLocale);
|
||||||
$('connect').addEventListener('click', connect);
|
$('connect').addEventListener('click', connect);
|
||||||
|
|||||||
+23
-1
@@ -198,7 +198,12 @@ provision_configure_display() {
|
|||||||
# a fresh kiosk ships with; menus/advanced_virtual_consoles.sh can
|
# a fresh kiosk ships with; menus/advanced_virtual_consoles.sh can
|
||||||
# flip this later, and does below if declined at the end of setup.
|
# flip this later, and does below if declined at the end of setup.
|
||||||
provision_install_file "etc/X11/xorg.conf.d/99-finger-libinput.conf" /etc/X11/xorg.conf.d/99-finger-libinput.conf
|
provision_install_file "etc/X11/xorg.conf.d/99-finger-libinput.conf" /etc/X11/xorg.conf.d/99-finger-libinput.conf
|
||||||
provision_install_file "etc/X11/xorg.conf.d/10-serverflags.conf" /etc/X11/xorg.conf.d/10-serverflags.conf
|
# Only on first install: this file is also where Advanced -> Virtual
|
||||||
|
# Consoles records the admin's choice, and Upgrade re-runs this step -
|
||||||
|
# overwriting it would silently turn consoles back off (or on).
|
||||||
|
if [[ ! -f /etc/X11/xorg.conf.d/10-serverflags.conf ]]; then
|
||||||
|
provision_install_file "etc/X11/xorg.conf.d/10-serverflags.conf" /etc/X11/xorg.conf.d/10-serverflags.conf
|
||||||
|
fi
|
||||||
|
|
||||||
provision_install_file "usr/local/bin/kiosk-mirror-display.sh" /usr/local/bin/kiosk-mirror-display.sh 755
|
provision_install_file "usr/local/bin/kiosk-mirror-display.sh" /usr/local/bin/kiosk-mirror-display.sh 755
|
||||||
provision_install_file "usr/local/bin/kiosk-audio-route.sh" /usr/local/bin/kiosk-audio-route.sh 755
|
provision_install_file "usr/local/bin/kiosk-audio-route.sh" /usr/local/bin/kiosk-audio-route.sh 755
|
||||||
@@ -242,6 +247,23 @@ provision_install_wifi_helper() {
|
|||||||
log_warning "Couldn't set up the kiosk WiFi screen's permission - Ctrl+Alt+Super+W won't be able to connect"
|
log_warning "Couldn't set up the kiosk WiFi screen's permission - Ctrl+Alt+Super+W won't be able to connect"
|
||||||
fi
|
fi
|
||||||
rm -f "$tmp"
|
rm -f "$tmp"
|
||||||
|
|
||||||
|
# The same helper through a root-owned socket, which is what the kiosk
|
||||||
|
# app uses first: it can end up running with the "no new privileges"
|
||||||
|
# flag, and sudo then refuses to run as root. The sudoers rule above
|
||||||
|
# stays as a fallback. Socket access is limited to the kiosk user's
|
||||||
|
# group (0660) - the same reach as the sudoers rule.
|
||||||
|
provision_install_file "usr/local/bin/kiosk-wifi-helper-socket" "$BIN_DIR/kiosk-wifi-helper-socket" 755
|
||||||
|
provision_install_file "etc/systemd/system/kiosk-wifi-helper@.service" "$SYSTEMD_DIR/kiosk-wifi-helper@.service"
|
||||||
|
provision_install_file "etc/systemd/system/kiosk-wifi-helper.socket" "$SYSTEMD_DIR/kiosk-wifi-helper.socket"
|
||||||
|
local kiosk_group
|
||||||
|
kiosk_group=$(id -gn "$KIOSK_USER" 2>/dev/null || echo "$KIOSK_USER")
|
||||||
|
sudo sed -i "s/@KIOSK_GROUP@/${kiosk_group}/" "$SYSTEMD_DIR/kiosk-wifi-helper.socket"
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
if ! sudo systemctl enable kiosk-wifi-helper.socket &>/dev/null || ! sudo systemctl restart kiosk-wifi-helper.socket; then
|
||||||
|
log_warning "Couldn't start the kiosk WiFi helper socket - the WiFi screen will fall back to sudo"
|
||||||
|
fi
|
||||||
|
|
||||||
# Flaky cheap dongles (RTL8188CUS...) need WiFi power saving off.
|
# Flaky cheap dongles (RTL8188CUS...) need WiFi power saving off.
|
||||||
provision_install_file "etc/udev/rules.d/70-kiosk-wifi-power.rules" /etc/udev/rules.d/70-kiosk-wifi-power.rules
|
provision_install_file "etc/udev/rules.d/70-kiosk-wifi-power.rules" /etc/udev/rules.d/70-kiosk-wifi-power.rules
|
||||||
sudo udevadm control --reload-rules 2>/dev/null || true
|
sudo udevadm control --reload-rules 2>/dev/null || true
|
||||||
|
|||||||
@@ -112,6 +112,8 @@ action_complete_uninstall() {
|
|||||||
sudo rm -rf "$KIOSK_HOME"
|
sudo rm -rf "$KIOSK_HOME"
|
||||||
|
|
||||||
echo "[6/12] Removing remaining systemd units..."
|
echo "[6/12] Removing remaining systemd units..."
|
||||||
|
sudo systemctl disable --now kiosk-wifi-helper.socket 2>/dev/null || true
|
||||||
|
sudo rm -f "$SYSTEMD_DIR"/kiosk-*.socket
|
||||||
sudo rm -f "$SYSTEMD_DIR"/kiosk-*.service
|
sudo rm -f "$SYSTEMD_DIR"/kiosk-*.service
|
||||||
sudo rm -f "$SYSTEMD_DIR"/kiosk-*.timer
|
sudo rm -f "$SYSTEMD_DIR"/kiosk-*.timer
|
||||||
sudo systemctl daemon-reload 2>/dev/null || true
|
sudo systemctl daemon-reload 2>/dev/null || true
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ Section "ServerFlags"
|
|||||||
# Disable Ctrl+Alt+Backspace (X server kill)
|
# Disable Ctrl+Alt+Backspace (X server kill)
|
||||||
Option "DontZap" "true"
|
Option "DontZap" "true"
|
||||||
|
|
||||||
# Disable VT switching (Ctrl+Alt+F1-F12)
|
# Allow VT switching (Ctrl+Alt+F1-F8): virtual consoles are ENABLED by
|
||||||
Option "DontVTSwitch" "true"
|
# default, as the installer says. Advanced -> Virtual Consoles (or the
|
||||||
|
# install prompt) turns this to "true" to disable them.
|
||||||
|
Option "DontVTSwitch" "false"
|
||||||
|
|
||||||
# Don't allow clients to disconnect on exit
|
# Don't allow clients to disconnect on exit
|
||||||
Option "AllowClosedownGrabs" "false"
|
Option "AllowClosedownGrabs" "false"
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Kiosk WiFi helper socket (used by the kiosk app's WiFi screen)
|
||||||
|
|
||||||
|
[Socket]
|
||||||
|
ListenStream=/run/kiosk-wifi-helper.sock
|
||||||
|
SocketUser=root
|
||||||
|
# Replaced with the kiosk user's group when installed (lib/provision.sh).
|
||||||
|
SocketGroup=@KIOSK_GROUP@
|
||||||
|
SocketMode=0660
|
||||||
|
Accept=yes
|
||||||
|
MaxConnections=4
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=sockets.target
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Kiosk WiFi helper (one request)
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/usr/local/bin/kiosk-wifi-helper-socket
|
||||||
|
StandardInput=socket
|
||||||
|
StandardOutput=socket
|
||||||
|
StandardError=journal
|
||||||
|
# A connect waits up to ~30s for an address, plus netplan; never run forever.
|
||||||
|
RuntimeMaxSec=180
|
||||||
@@ -4,10 +4,12 @@
|
|||||||
# (Ctrl+Alt+Super+W), for joining a WiFi network from the kiosk
|
# (Ctrl+Alt+Super+W), for joining a WiFi network from the kiosk
|
||||||
# itself when the console (Ctrl+Alt+F1..F6) is switched off.
|
# itself when the console (Ctrl+Alt+F1..F6) is switched off.
|
||||||
#
|
#
|
||||||
# The kiosk user may run this one script via passwordless sudo
|
# The kiosk app reaches it through a root-owned socket
|
||||||
# (/etc/sudoers.d/kiosk-wifi, written by lib/provision.sh) - it does a
|
# (kiosk-wifi-helper.socket, group kiosk; see kiosk-wifi-helper-socket) -
|
||||||
|
# or, as a fallback, passwordless sudo (/etc/sudoers.d/kiosk-wifi) - it does a
|
||||||
# fixed set of things and nothing else:
|
# fixed set of things and nothing else:
|
||||||
#
|
#
|
||||||
|
# kiosk-wifi-helper ifaces {"adapters":[{"name","kind","connected","ssid","ip"}],"selected"}
|
||||||
# kiosk-wifi-helper status {"iface","connected","ssid","ip","configured"}
|
# kiosk-wifi-helper status {"iface","connected","ssid","ip","configured"}
|
||||||
# kiosk-wifi-helper scan {"iface","networks":[{"ssid","signal","secure"}]}
|
# kiosk-wifi-helper scan {"iface","networks":[{"ssid","signal","secure"}]}
|
||||||
# kiosk-wifi-helper connect stdin {"ssid","password"} -> {"ok","ip"|"error"}
|
# kiosk-wifi-helper connect stdin {"ssid","password"} -> {"ok","ip"|"error"}
|
||||||
@@ -17,8 +19,10 @@
|
|||||||
# WiFi country (regulatory domain), system
|
# WiFi country (regulatory domain), system
|
||||||
# locale and keyboard layout; every field optional
|
# locale and keyboard layout; every field optional
|
||||||
#
|
#
|
||||||
# Every reply is one JSON line on stdout, exit 0 on success. connect
|
# Every reply is one JSON line on stdout, exit 0 on success. status, scan
|
||||||
# writes the same netplan file as Core Settings -> WiFi (menus/wifi.sh,
|
# and connect accept an optional {"iface": "<adapter>"} in the stdin JSON
|
||||||
|
# to act on that adapter; without it the connected adapter is used (see
|
||||||
|
# pick_iface). connect writes the same netplan file as Core Settings -> WiFi (menus/wifi.sh,
|
||||||
# 60-kiosk-wifi.yaml) and puts the previous one back if the interface
|
# 60-kiosk-wifi.yaml) and puts the previous one back if the interface
|
||||||
# gets no address - a typo in the password doesn't lose a WiFi network
|
# gets no address - a typo in the password doesn't lose a WiFi network
|
||||||
# that was working. The password comes in on stdin, never argv (argv is
|
# that was working. The password comes in on stdin, never argv (argv is
|
||||||
@@ -168,8 +172,76 @@ default_country() {
|
|||||||
echo "${c^^}"
|
echo "${c^^}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Every wireless interface, by what it is (see detect_iface).
|
||||||
|
list_wifi_ifaces() {
|
||||||
|
local d
|
||||||
|
for d in $SYS_CLASS/net/*; do
|
||||||
|
if [[ -d "$d/wireless" || -e "$d/phy80211" ]]; then
|
||||||
|
basename "$d"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# The wireless adapter that is connected right now (associated and has an
|
||||||
|
# address), if any. With a built-in card plus a USB dongle, the connected
|
||||||
|
# one is often not the first in name order - status/scan/connect must act
|
||||||
|
# on the adapter that is actually carrying the connection.
|
||||||
|
connected_iface() {
|
||||||
|
local i
|
||||||
|
for i in $(list_wifi_ifaces); do
|
||||||
|
if [[ -n "$(current_ssid "$i")" && -n "$(iface_ip "$i")" ]]; then
|
||||||
|
echo "$i"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Which adapter to use: the connected one, else the one our netplan file
|
||||||
|
# configures, else the first found.
|
||||||
|
pick_iface() {
|
||||||
|
connected_iface || prefer_configured_iface || detect_iface
|
||||||
|
}
|
||||||
|
|
||||||
|
# The request body (JSON on stdin), read once in main. REQ_IFACE is the
|
||||||
|
# adapter the caller asked for, or empty.
|
||||||
|
REQ_INPUT=""
|
||||||
|
REQ_IFACE=""
|
||||||
|
|
||||||
|
# Only a real wireless interface name is accepted - never pass caller text
|
||||||
|
# to a command or a YAML file unchecked.
|
||||||
|
valid_iface() {
|
||||||
|
local i
|
||||||
|
[[ "$1" =~ ^[A-Za-z0-9._-]{1,15}$ ]] || return 1
|
||||||
|
for i in $(list_wifi_ifaces); do
|
||||||
|
[[ "$i" == "$1" ]] && return 0
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# "usb" or "built-in", from where the device sits in sysfs.
|
||||||
|
iface_kind() {
|
||||||
|
if readlink -f "$SYS_CLASS/net/$1/device" 2>/dev/null | grep -q '/usb'; then
|
||||||
|
echo usb
|
||||||
|
else
|
||||||
|
echo built-in
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
pick_requested_or_default_iface() {
|
||||||
|
if [[ -n "$REQ_IFACE" ]]; then
|
||||||
|
valid_iface "$REQ_IFACE" || return 1
|
||||||
|
echo "$REQ_IFACE"
|
||||||
|
else
|
||||||
|
pick_iface
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
need_iface() {
|
need_iface() {
|
||||||
IFACE=$(prefer_configured_iface || detect_iface) || reply_error "No WiFi adapter found. If it's a USB adapter, check it's plugged in."
|
if [[ -n "$REQ_IFACE" ]] && ! valid_iface "$REQ_IFACE"; then
|
||||||
|
reply_error "WiFi adapter $REQ_IFACE not found"
|
||||||
|
fi
|
||||||
|
IFACE=$(pick_requested_or_default_iface) || reply_error "No WiFi adapter found. If it's a USB adapter, check it's plugged in."
|
||||||
command -v iw >/dev/null || reply_error "WiFi support (iw, wpasupplicant) isn't installed - run Core Settings -> WiFi once from the admin menu"
|
command -v iw >/dev/null || reply_error "WiFi support (iw, wpasupplicant) isn't installed - run Core Settings -> WiFi once from the admin menu"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,7 +288,7 @@ set_country() {
|
|||||||
|
|
||||||
do_region_set() {
|
do_region_set() {
|
||||||
local input country locale keyboard
|
local input country locale keyboard
|
||||||
input=$(head -c 4096)
|
input="$REQ_INPUT"
|
||||||
country=$(jq -r '.country // "" | strings' <<< "$input" 2>/dev/null) || reply_error "Bad request"
|
country=$(jq -r '.country // "" | strings' <<< "$input" 2>/dev/null) || reply_error "Bad request"
|
||||||
locale=$(jq -r '.locale // "" | strings' <<< "$input" 2>/dev/null) || reply_error "Bad request"
|
locale=$(jq -r '.locale // "" | strings' <<< "$input" 2>/dev/null) || reply_error "Bad request"
|
||||||
keyboard=$(jq -r '.keyboard // "" | strings' <<< "$input" 2>/dev/null) || reply_error "Bad request"
|
keyboard=$(jq -r '.keyboard // "" | strings' <<< "$input" 2>/dev/null) || reply_error "Bad request"
|
||||||
@@ -251,10 +323,27 @@ do_region_set() {
|
|||||||
'{ok:true,country:$c,locale:$l,keyboard:$k,restart:$restart}'
|
'{ok:true,country:$c,locale:$l,keyboard:$k,restart:$restart}'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
do_ifaces() {
|
||||||
|
local i rows="" sel
|
||||||
|
sel=$(pick_iface) || sel=""
|
||||||
|
for i in $(list_wifi_ifaces); do
|
||||||
|
local ssid ip
|
||||||
|
ssid=$(current_ssid "$i")
|
||||||
|
ip=$(iface_ip "$i")
|
||||||
|
rows+=$(jq -cn --arg n "$i" --arg k "$(iface_kind "$i")" --arg s "$ssid" --arg a "$ip" \
|
||||||
|
'{name:$n,kind:$k,connected:($s != "" and $a != ""),ssid:$s,ip:$a}')$'\n'
|
||||||
|
done
|
||||||
|
jq -cn --arg sel "$sel" --argjson a "$(printf '%s' "$rows" | jq -cs .)" \
|
||||||
|
'{ok:true,selected:$sel,adapters:$a}'
|
||||||
|
}
|
||||||
|
|
||||||
do_status() {
|
do_status() {
|
||||||
local iface ssid="" ip="" configured
|
local iface ssid="" ip="" configured
|
||||||
configured=$(configured_ssid)
|
configured=$(configured_ssid)
|
||||||
if ! iface=$(detect_iface); then
|
if [[ -n "$REQ_IFACE" ]] && ! valid_iface "$REQ_IFACE"; then
|
||||||
|
reply_error "WiFi adapter $REQ_IFACE not found"
|
||||||
|
fi
|
||||||
|
if ! iface=$(pick_requested_or_default_iface); then
|
||||||
jq -cn --arg c "$configured" '{ok:true,iface:"",connected:false,ssid:"",ip:"",configured:$c}'
|
jq -cn --arg c "$configured" '{ok:true,iface:"",connected:false,ssid:"",ip:"",configured:$c}'
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
@@ -351,7 +440,7 @@ PYEOF
|
|||||||
do_connect() {
|
do_connect() {
|
||||||
need_iface
|
need_iface
|
||||||
local input ssid password
|
local input ssid password
|
||||||
input=$(head -c 4096)
|
input="$REQ_INPUT"
|
||||||
ssid=$(jq -er '.ssid | strings' <<< "$input" 2>/dev/null) || reply_error "No network name given"
|
ssid=$(jq -er '.ssid | strings' <<< "$input" 2>/dev/null) || reply_error "No network name given"
|
||||||
password=$(jq -r '.password // "" | strings' <<< "$input" 2>/dev/null) || reply_error "Bad request"
|
password=$(jq -r '.password // "" | strings' <<< "$input" 2>/dev/null) || reply_error "Bad request"
|
||||||
|
|
||||||
@@ -454,7 +543,14 @@ trap 'rm -f "$TMP_SCAN"' EXIT
|
|||||||
exec 9> "$LOCK_FILE"
|
exec 9> "$LOCK_FILE"
|
||||||
flock -w 60 9 || reply_error "Another WiFi change is still running - try again in a moment"
|
flock -w 60 9 || reply_error "Another WiFi change is still running - try again in a moment"
|
||||||
|
|
||||||
|
# One JSON request body on stdin (not when run by hand from a terminal).
|
||||||
|
if [[ ! -t 0 ]]; then
|
||||||
|
REQ_INPUT=$(head -c 4096)
|
||||||
|
REQ_IFACE=$(jq -r '.iface // "" | strings' <<< "$REQ_INPUT" 2>/dev/null) || REQ_IFACE=""
|
||||||
|
fi
|
||||||
|
|
||||||
case "${1:-}" in
|
case "${1:-}" in
|
||||||
|
ifaces) do_ifaces ;;
|
||||||
status) do_status ;;
|
status) do_status ;;
|
||||||
scan) do_scan ;;
|
scan) do_scan ;;
|
||||||
connect) do_connect ;;
|
connect) do_connect ;;
|
||||||
|
|||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
################################################################################
|
||||||
|
# kiosk-wifi-helper-socket - socket front end for kiosk-wifi-helper.
|
||||||
|
#
|
||||||
|
# Run by systemd (kiosk-wifi-helper@.service, one instance per connection to
|
||||||
|
# /run/kiosk-wifi-helper.sock) as root, with the connection as stdin AND
|
||||||
|
# stdout. The kiosk app connects, sends
|
||||||
|
#
|
||||||
|
# <action>\n<JSON request body>
|
||||||
|
#
|
||||||
|
# and half-closes; the reply is the helper's one JSON line.
|
||||||
|
#
|
||||||
|
# Why a socket and not `sudo`: the kiosk app can end up running with the
|
||||||
|
# Linux "no new privileges" flag, and sudo then refuses to run as root
|
||||||
|
# ("The 'no new privileges' flag is set"). A root service reached through a
|
||||||
|
# socket isn't affected by that flag. Who may connect is decided by the
|
||||||
|
# socket's owner/group/mode (root:kiosk 0660) - the same people who could
|
||||||
|
# run the helper through the old sudoers rule, no more.
|
||||||
|
#
|
||||||
|
# Only the fixed actions below are passed on; anything else is refused.
|
||||||
|
################################################################################
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
HELPER="${KIOSK_WIFI_HELPER:-/usr/local/bin/kiosk-wifi-helper}"
|
||||||
|
|
||||||
|
# First line only; bash's read doesn't consume past the newline on a
|
||||||
|
# socket, so the JSON body stays on stdin for the helper.
|
||||||
|
IFS= read -r -t 10 -n 32 action || action=""
|
||||||
|
|
||||||
|
case "$action" in
|
||||||
|
ifaces|status|scan|connect|region-status|region-set)
|
||||||
|
exec "$HELPER" "$action"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo '{"ok":false,"error":"unknown action"}'
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
Reference in New Issue
Block a user