Add a WiFi screen to the kiosk app (Shift+W+S+F+H)
With the virtual consoles turned off there was no way to join a WiFi network at the kiosk itself. Holding Shift+W+S+F+H together (or Ctrl+Alt+Shift+Super+W) now opens a PIN-protected WiFi screen: scan, pick or type a network, enter the password, connect. The root side is a small helper (kiosk-wifi-helper: status, scan, connect only) that the kiosk user may run through a validated sudoers.d rule. It writes the same netplan file as Core Settings -> WiFi and puts the previous one back if the new network gives no address. Installed by provisioning and Advanced -> Upgrade, removed by Complete Uninstall. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cjz8a58eB5P9sUweqd3NHm
This commit is contained in:
@@ -492,6 +492,19 @@ Both can be used at the same time — they serve different purposes:
|
|||||||
- **Multi-method WiFi scan** - nmcli, iw, wpa_cli fallbacks
|
- **Multi-method WiFi scan** - nmcli, iw, wpa_cli fallbacks
|
||||||
- **Watchdog support** - Auto-revert bad WiFi configs
|
- **Watchdog support** - Auto-revert bad WiFi configs
|
||||||
- **Emergency hotspot** - Fallback if no internet
|
- **Emergency hotspot** - Fallback if no internet
|
||||||
|
- **WiFi from the kiosk screen** - hold **Shift+W+S+F+H** together (or press Ctrl+Alt+Shift+Super+W) to join a WiFi network without the admin console - see below
|
||||||
|
|
||||||
|
### WiFi from the kiosk screen (consoles turned off)
|
||||||
|
|
||||||
|
With virtual consoles switched off, there's no way to reach the admin menu at the kiosk itself. To join a WiFi network anyway, plug in a keyboard and **hold Shift, W, S, F and H all at once** (or press Ctrl+Alt+Shift+Super+W). A WiFi screen opens:
|
||||||
|
|
||||||
|
1. Enter the hidden-sites PIN (the same one as F10 - default `1234`; skipped if the PIN is turned off).
|
||||||
|
2. Pick a network from the scan (strongest first, 🔒 = needs a password), or type its name.
|
||||||
|
3. Type the password (empty for an open network) and press **Connect**.
|
||||||
|
|
||||||
|
It writes the same netplan file as Core Settings → WiFi (`/etc/netplan/60-kiosk-wifi.yaml`), so the connection persists across reboots. If the kiosk gets no address within 30 seconds (usually a wrong password) the previous WiFi setting is put back. Esc or Close shuts the screen; it also closes itself after 5 minutes untouched, and five wrong PINs close it. The keys only work as a chord (typing W-S-F-H one at a time does nothing), and they don't interfere with the tab or power shortcuts.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -1512,6 +1525,7 @@ full migration pass.
|
|||||||
- **Local files as sites:** an absolute path (`/home/kiosk/docs/menu.pdf`) shows a local page, PDF or image; a folder (`/home/kiosk/photos/`) plays as a full-screen **image slideshow** that picks up added/removed images within a minute. Works in both the Sites menu and the Web UI.
|
- **Local files as sites:** an absolute path (`/home/kiosk/docs/menu.pdf`) shows a local page, PDF or image; a folder (`/home/kiosk/photos/`) plays as a full-screen **image slideshow** that picks up added/removed images within a minute. Works in both the Sites menu and the Web UI.
|
||||||
- **Fix: the first-boot kiosk setup never started on 26.04.** Its service was ordered after cloud-init's final stage, which itself runs after `multi-user.target` - an ordering cycle that systemd broke by dropping the kiosk setup. It also "conflicted" with the tty1 login prompt, which systemd can resolve the same way. Both removed; checked with `systemd-analyze verify` against 26.04's own units. Rebuild the ISO to pick this up.
|
- **Fix: the first-boot kiosk setup never started on 26.04.** Its service was ordered after cloud-init's final stage, which itself runs after `multi-user.target` - an ordering cycle that systemd broke by dropping the kiosk setup. It also "conflicted" with the tty1 login prompt, which systemd can resolve the same way. Both removed; checked with `systemd-analyze verify` against 26.04's own units. Rebuild the ISO to pick this up.
|
||||||
- **WiFi tools in the base install:** `wpasupplicant` and `iw` are now installed (and bundled offline). A default Ubuntu Server install only has them if WiFi was set up in the installer; without them, Core Settings → WiFi couldn't connect an offline kiosk. The WiFi menu also installs them itself if missing.
|
- **WiFi tools in the base install:** `wpasupplicant` and `iw` are now installed (and bundled offline). A default Ubuntu Server install only has them if WiFi was set up in the installer; without them, Core Settings → WiFi couldn't connect an offline kiosk. The WiFi menu also installs them itself if missing.
|
||||||
|
- **New: WiFi from the kiosk screen** - hold Shift+W+S+F+H (or Ctrl+Alt+Shift+Super+W) to scan for and join a WiFi network right on the kiosk, PIN-protected, for kiosks with the consoles turned off. Reverts to the previous WiFi if the new one doesn't connect. See "WiFi from the kiosk screen".
|
||||||
- **New: `iso/e2b-contig.sh`** - makes a file on an Easy2Boot (or any NTFS/FAT32/exFAT) USB drive contiguous from Linux, so ISOs that need to be in one piece boot.
|
- **New: `iso/e2b-contig.sh`** - makes a file on an Easy2Boot (or any NTFS/FAT32/exFAT) USB drive contiguous from Linux, so ISOs that need to be in one piece boot.
|
||||||
- **Offline addons:** the `--offline` bundle now also carries LMS (Lyrion) + Squeezelite, Asterisk Intercom, VNC, WireGuard and the Emergency Hotspot; their normal installs use it automatically when there's no internet.
|
- **Offline addons:** the `--offline` bundle now also carries LMS (Lyrion) + Squeezelite, Asterisk Intercom, VNC, WireGuard and the Emergency Hotspot; their normal installs use it automatically when there's no internet.
|
||||||
- **Emergency Hotspot fix:** by default it now starts only when the local network is unreachable (router *and* internet), not merely when there's no internet - on a LAN without internet it used to start on every boot. Existing setups keep the old check until Advanced → Emergency Hotspot → Reconfigure.
|
- **Emergency Hotspot fix:** by default it now starts only when the local network is unreachable (router *and* internet), not merely when there's no internet - on a LAN without internet it used to start on every boot. Existing setups keep the old check until Advanced → Emergency Hotspot → Reconfigure.
|
||||||
|
|||||||
+169
-1
@@ -1,5 +1,5 @@
|
|||||||
const {app,BrowserWindow,BrowserView,globalShortcut,ipcMain,dialog,session}=require('electron');
|
const {app,BrowserWindow,BrowserView,globalShortcut,ipcMain,dialog,session}=require('electron');
|
||||||
const {exec}=require('child_process');
|
const {exec,spawn}=require('child_process');
|
||||||
const fs=require('fs');
|
const fs=require('fs');
|
||||||
const path=require('path');
|
const path=require('path');
|
||||||
const os=require('os');
|
const os=require('os');
|
||||||
@@ -159,6 +159,7 @@ function showLockoutScreen(){
|
|||||||
|
|
||||||
isLockedOut=true;
|
isLockedOut=true;
|
||||||
console.log('[LOCKOUT] Showing lockout screen');
|
console.log('[LOCKOUT] Showing lockout screen');
|
||||||
|
closeWifiWindow();
|
||||||
|
|
||||||
// Detach all browser views to prevent content from being visible
|
// Detach all browser views to prevent content from being visible
|
||||||
console.log('[LOCKOUT] Detaching all browser views for security');
|
console.log('[LOCKOUT] Detaching all browser views for security');
|
||||||
@@ -1189,7 +1190,170 @@ function showHiddenTab(index){
|
|||||||
showingHidden=true;
|
showingHidden=true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WiFi screen (hold Shift+W+S+F+H, or Ctrl+Alt+Shift+Super+W): join a WiFi network from the kiosk
|
||||||
|
// itself, for when the console (Ctrl+Alt+F1..F6) is switched off and the
|
||||||
|
// admin menu can't be reached. Gated by the hidden-tab PIN (.jitsi-pin,
|
||||||
|
// same rules as pin-entry.html: no file = 1234, NOPIN = none). The root
|
||||||
|
// work is done by /usr/local/bin/kiosk-wifi-helper, which the kiosk user
|
||||||
|
// may run through sudo and nothing else (/etc/sudoers.d/kiosk-wifi).
|
||||||
|
const WIFI_HELPER='/usr/local/bin/kiosk-wifi-helper';
|
||||||
|
const WIFI_IDLE_TIMEOUT=5*60000; // closes after 5 minutes untouched
|
||||||
|
const WIFI_MAX_PIN_TRIES=5;
|
||||||
|
let wifiWindow=null,wifiUnlocked=false,wifiPinTries=0,wifiIdleTimer=null;
|
||||||
|
|
||||||
|
function readHiddenPin(){
|
||||||
|
try{
|
||||||
|
const stored=fs.readFileSync(path.join(__dirname,'.jitsi-pin'),'utf8').trim();
|
||||||
|
if(stored==='NOPIN')return null;
|
||||||
|
if(stored)return stored;
|
||||||
|
}catch(e){}
|
||||||
|
return '1234';
|
||||||
|
}
|
||||||
|
|
||||||
|
function runWifiHelper(action,input){
|
||||||
|
return new Promise(resolve=>{
|
||||||
|
let out='',err='',done=false;
|
||||||
|
const finish=result=>{if(!done){done=true;resolve(result);}};
|
||||||
|
let child;
|
||||||
|
try{
|
||||||
|
child=spawn('sudo',['-n',WIFI_HELPER,action],{stdio:['pipe','pipe','pipe']});
|
||||||
|
}catch(e){
|
||||||
|
finish({ok:false,error:'Could not run the WiFi helper: '+e.message});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const killer=setTimeout(()=>{child.kill();finish({ok:false,error:'The WiFi helper timed out'});},120000);
|
||||||
|
child.stdout.on('data',d=>{out+=d;});
|
||||||
|
child.stderr.on('data',d=>{err+=d;});
|
||||||
|
child.on('error',e=>{clearTimeout(killer);finish({ok:false,error:'Could not run the WiFi helper: '+e.message});});
|
||||||
|
child.on('close',()=>{
|
||||||
|
clearTimeout(killer);
|
||||||
|
const line=out.trim().split('\n').pop();
|
||||||
|
try{
|
||||||
|
finish(JSON.parse(line));
|
||||||
|
}catch(e){
|
||||||
|
// Not our JSON - typically sudo refusing ("a password is required")
|
||||||
|
// on a kiosk set up before this feature: Upgrade installs the rule.
|
||||||
|
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'});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
child.stdin.on('error',()=>{});
|
||||||
|
child.stdin.end(input?JSON.stringify(input):'');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetWifiIdleTimer(){
|
||||||
|
if(wifiIdleTimer)clearTimeout(wifiIdleTimer);
|
||||||
|
wifiIdleTimer=setTimeout(()=>{
|
||||||
|
console.log('[WIFI] Closing after 5 minutes idle');
|
||||||
|
closeWifiWindow();
|
||||||
|
},WIFI_IDLE_TIMEOUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeWifiWindow(){
|
||||||
|
if(wifiWindow&&!wifiWindow.isDestroyed())wifiWindow.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showWifiWindow(){
|
||||||
|
if(isLockedOut)return;
|
||||||
|
if(wifiWindow&&!wifiWindow.isDestroyed()){
|
||||||
|
wifiWindow.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
wifiUnlocked=(readHiddenPin()===null);
|
||||||
|
wifiPinTries=0;
|
||||||
|
let width=720,height=820;
|
||||||
|
if(mainWindow&&!mainWindow.isDestroyed()){
|
||||||
|
const[w,h]=mainWindow.getContentSize();
|
||||||
|
width=Math.min(width,w);
|
||||||
|
height=Math.min(height,h);
|
||||||
|
}
|
||||||
|
wifiWindow=new BrowserWindow({
|
||||||
|
width,height,
|
||||||
|
frame:false,
|
||||||
|
alwaysOnTop:true,
|
||||||
|
parent:mainWindow,
|
||||||
|
modal:true,
|
||||||
|
webPreferences:{nodeIntegration:true,contextIsolation:false}
|
||||||
|
});
|
||||||
|
wifiWindow.setMenu(null);
|
||||||
|
wifiWindow.loadFile(path.join(__dirname,'wifi.html'));
|
||||||
|
wifiWindow.on('closed',()=>{
|
||||||
|
if(wifiIdleTimer){clearTimeout(wifiIdleTimer);wifiIdleTimer=null;}
|
||||||
|
wifiWindow=null;
|
||||||
|
wifiUnlocked=false;
|
||||||
|
});
|
||||||
|
resetWifiIdleTimer();
|
||||||
|
console.log('[WIFI] WiFi screen opened');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shift+W+S+F+H, all held together, opens the WiFi screen. A globalShortcut
|
||||||
|
// can only have one non-modifier key, so this watches the raw key events
|
||||||
|
// of every page/window instead (before-input-event): a key counts as held
|
||||||
|
// from its keyDown (autorepeat keeps refreshing it) until its keyUp, or
|
||||||
|
// for 1.5s if the keyUp never arrives (focus moved mid-press). Physical
|
||||||
|
// keys (input.code), so it's the same keys on any keyboard layout. The
|
||||||
|
// key presses that complete the chord are swallowed, not typed into the
|
||||||
|
// page.
|
||||||
|
const WIFI_CHORD_KEYS=['KeyW','KeyS','KeyF','KeyH'];
|
||||||
|
const WIFI_CHORD_HOLD_MS=1500;
|
||||||
|
const wifiChordDown=new Map();
|
||||||
|
|
||||||
|
function wifiChordInput(event,input){
|
||||||
|
if(!WIFI_CHORD_KEYS.includes(input.code))return;
|
||||||
|
if(input.type==='keyUp'){wifiChordDown.delete(input.code);return;}
|
||||||
|
if(input.type!=='keyDown')return;
|
||||||
|
const now=Date.now();
|
||||||
|
wifiChordDown.set(input.code,now);
|
||||||
|
if(!input.shift||input.control||input.alt||input.meta)return;
|
||||||
|
const all=WIFI_CHORD_KEYS.every(k=>now-(wifiChordDown.get(k)||0)<WIFI_CHORD_HOLD_MS);
|
||||||
|
if(!all)return;
|
||||||
|
event.preventDefault();
|
||||||
|
if(input.isAutoRepeat)return;
|
||||||
|
wifiChordDown.clear();
|
||||||
|
console.log('[WIFI] Shift+W+S+F+H pressed');
|
||||||
|
showWifiWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
app.on('web-contents-created',(e,contents)=>{
|
||||||
|
contents.on('before-input-event',wifiChordInput);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('wifi-request',async(event,req)=>{
|
||||||
|
if(!wifiWindow||wifiWindow.isDestroyed()||event.sender!==wifiWindow.webContents){
|
||||||
|
return{ok:false,error:'Not allowed'};
|
||||||
|
}
|
||||||
|
resetWifiIdleTimer();
|
||||||
|
const action=req&&req.action;
|
||||||
|
if(action==='close'){closeWifiWindow();return{ok:true};}
|
||||||
|
if(action==='needs-pin')return{ok:true,needsPin:!wifiUnlocked};
|
||||||
|
if(action==='unlock'){
|
||||||
|
if(wifiUnlocked)return{ok:true};
|
||||||
|
if(String(req.pin||'')===readHiddenPin()){
|
||||||
|
wifiUnlocked=true;
|
||||||
|
return{ok:true};
|
||||||
|
}
|
||||||
|
wifiPinTries++;
|
||||||
|
console.log('[WIFI] Wrong PIN, attempt',wifiPinTries);
|
||||||
|
if(wifiPinTries>=WIFI_MAX_PIN_TRIES){
|
||||||
|
setTimeout(closeWifiWindow,1500);
|
||||||
|
return{ok:false,error:'Too many wrong PINs'};
|
||||||
|
}
|
||||||
|
return{ok:false,error:'Incorrect PIN'};
|
||||||
|
}
|
||||||
|
if(!wifiUnlocked)return{ok:false,error:'Enter the PIN first'};
|
||||||
|
if(action==='status'||action==='scan')return runWifiHelper(action);
|
||||||
|
if(action==='connect'){
|
||||||
|
console.log('[WIFI] Connecting to',JSON.stringify(String(req.ssid||'')));
|
||||||
|
const result=await runWifiHelper('connect',{ssid:String(req.ssid||''),password:String(req.password||'')});
|
||||||
|
console.log('[WIFI] Connect result:',result.ok?('ok '+result.ip):result.error);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return{ok:false,error:'Unknown request'};
|
||||||
|
});
|
||||||
|
|
||||||
function forceReturnToTabs(){
|
function forceReturnToTabs(){
|
||||||
|
closeWifiWindow();
|
||||||
if(pinWindow&&!pinWindow.isDestroyed()){
|
if(pinWindow&&!pinWindow.isDestroyed()){
|
||||||
pinWindow.close();
|
pinWindow.close();
|
||||||
pinWindow=null;
|
pinWindow=null;
|
||||||
@@ -1625,6 +1789,10 @@ async function createWindow(){
|
|||||||
globalShortcut.register('Control+Alt+Delete',showPowerMenu);
|
globalShortcut.register('Control+Alt+Delete',showPowerMenu);
|
||||||
globalShortcut.register('Control+Alt+P',showPowerMenu);
|
globalShortcut.register('Control+Alt+P',showPowerMenu);
|
||||||
globalShortcut.register('Control+Alt+Escape',showPowerMenu);
|
globalShortcut.register('Control+Alt+Escape',showPowerMenu);
|
||||||
|
// Five keys on purpose - not something a visitor hits by accident.
|
||||||
|
if(!globalShortcut.register('Control+Alt+Shift+Super+W',showWifiWindow)){
|
||||||
|
console.error('[WIFI] Could not register Ctrl+Alt+Shift+Super+W');
|
||||||
|
}
|
||||||
globalShortcut.register('Control+K',()=>{
|
globalShortcut.register('Control+K',()=>{
|
||||||
if(htmlKeyboardWindow&&!htmlKeyboardWindow.isDestroyed()){
|
if(htmlKeyboardWindow&&!htmlKeyboardWindow.isDestroyed()){
|
||||||
closeHTMLKeyboard();
|
closeHTMLKeyboard();
|
||||||
|
|||||||
@@ -0,0 +1,253 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>WiFi</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
|
background: #2c3e50;
|
||||||
|
color: #ecf0f1;
|
||||||
|
height: 100vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
h2 { text-align: center; margin-bottom: 16px; font-size: 26px; }
|
||||||
|
.hidden { display: none !important; }
|
||||||
|
label { display: block; margin: 14px 0 6px; font-size: 16px; color: #bdc3c7; }
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px;
|
||||||
|
font-size: 20px;
|
||||||
|
border: 2px solid #34495e;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #34495e;
|
||||||
|
color: #ecf0f1;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
input:focus { border-color: #3498db; }
|
||||||
|
.row { display: flex; gap: 10px; align-items: center; }
|
||||||
|
.row input { flex: 1; }
|
||||||
|
button {
|
||||||
|
padding: 14px 20px;
|
||||||
|
font-size: 18px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #fff;
|
||||||
|
background: #34495e;
|
||||||
|
}
|
||||||
|
button:disabled { opacity: 0.5; cursor: default; }
|
||||||
|
.primary { background: #27ae60; }
|
||||||
|
.scan { background: #2980b9; }
|
||||||
|
.close { background: #e74c3c; }
|
||||||
|
.actions { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 20px; }
|
||||||
|
#status {
|
||||||
|
background: #34495e;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
#message { margin-top: 16px; font-size: 17px; font-weight: bold; text-align: center; min-height: 24px; }
|
||||||
|
#message.error { color: #e74c3c; }
|
||||||
|
#message.ok { color: #2ecc71; }
|
||||||
|
#message.busy { color: #f1c40f; }
|
||||||
|
#networks {
|
||||||
|
list-style: none;
|
||||||
|
max-height: 260px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #34495e;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
#networks li {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 14px;
|
||||||
|
font-size: 18px;
|
||||||
|
border-bottom: 1px solid #2c3e50;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
#networks li:last-child { border-bottom: none; }
|
||||||
|
#networks li.selected { background: #2980b9; }
|
||||||
|
#networks li .meta { color: #bdc3c7; font-size: 15px; white-space: nowrap; margin-left: 12px; }
|
||||||
|
#networks li.empty { color: #95a5a6; cursor: default; }
|
||||||
|
.hint { text-align: center; font-size: 13px; color: #95a5a6; margin-top: 18px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>📶 WiFi</h2>
|
||||||
|
|
||||||
|
<div id="pin-step" class="hidden">
|
||||||
|
<label for="pin">Enter the PIN</label>
|
||||||
|
<input id="pin" type="password" inputmode="numeric" autocomplete="off">
|
||||||
|
<div class="actions">
|
||||||
|
<button class="primary" id="pin-ok">✓ OK</button>
|
||||||
|
<button class="close" data-close>✗ Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="wifi-step" class="hidden">
|
||||||
|
<div id="status">Checking…</div>
|
||||||
|
|
||||||
|
<div class="row" style="margin-top: 14px">
|
||||||
|
<label style="margin: 0; flex: 1">Networks nearby</label>
|
||||||
|
<button class="scan" id="scan">⟳ Scan</button>
|
||||||
|
</div>
|
||||||
|
<ul id="networks"><li class="empty">Scanning…</li></ul>
|
||||||
|
|
||||||
|
<label for="ssid">Network name</label>
|
||||||
|
<input id="ssid" type="text" autocomplete="off" spellcheck="false">
|
||||||
|
|
||||||
|
<label for="password">Password (empty for an open network)</label>
|
||||||
|
<div class="row">
|
||||||
|
<input id="password" type="password" autocomplete="off" spellcheck="false">
|
||||||
|
<button id="show">Show</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<button class="primary" id="connect">Connect</button>
|
||||||
|
<button class="close" data-close>✗ Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="message"></div>
|
||||||
|
<div class="hint">Esc closes this screen. Closes by itself after 5 minutes untouched.</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const {ipcRenderer} = require('electron');
|
||||||
|
const $ = id => document.getElementById(id);
|
||||||
|
let busy = false;
|
||||||
|
|
||||||
|
function request(action, extra) {
|
||||||
|
return ipcRenderer.invoke('wifi-request', Object.assign({action}, extra || {}))
|
||||||
|
.catch(e => ({ok: false, error: String(e && e.message || e)}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function message(text, kind) {
|
||||||
|
const m = $('message');
|
||||||
|
m.textContent = text || '';
|
||||||
|
m.className = kind || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBusy(on) {
|
||||||
|
busy = on;
|
||||||
|
for (const id of ['scan', 'connect', 'pin-ok']) $(id).disabled = on;
|
||||||
|
}
|
||||||
|
|
||||||
|
function signalBars(dbm) {
|
||||||
|
if (dbm >= -55) return '▂▄▆█';
|
||||||
|
if (dbm >= -67) return '▂▄▆';
|
||||||
|
if (dbm >= -78) return '▂▄';
|
||||||
|
return '▂';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshStatus() {
|
||||||
|
const s = await request('status');
|
||||||
|
const box = $('status');
|
||||||
|
if (!s.ok) { box.textContent = s.error || 'Status unavailable'; return; }
|
||||||
|
if (!s.iface) { box.textContent = 'No WiFi adapter found'; return; }
|
||||||
|
box.textContent = '';
|
||||||
|
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 (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;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function scan() {
|
||||||
|
if (busy) return;
|
||||||
|
setBusy(true);
|
||||||
|
message('Scanning…', 'busy');
|
||||||
|
const list = $('networks');
|
||||||
|
const r = await request('scan');
|
||||||
|
list.textContent = '';
|
||||||
|
if (!r.ok) {
|
||||||
|
message(r.error, 'error');
|
||||||
|
list.innerHTML = '<li class="empty">No scan results</li>';
|
||||||
|
} else {
|
||||||
|
message('');
|
||||||
|
if (!r.networks.length) list.innerHTML = '<li class="empty">No networks found - type the name below</li>';
|
||||||
|
for (const n of r.networks) {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
const name = document.createElement('span');
|
||||||
|
name.textContent = n.ssid;
|
||||||
|
const meta = document.createElement('span');
|
||||||
|
meta.className = 'meta';
|
||||||
|
meta.textContent = (n.secure ? '🔒 ' : '') + signalBars(n.signal);
|
||||||
|
li.append(name, meta);
|
||||||
|
li.addEventListener('click', () => {
|
||||||
|
for (const other of list.children) other.classList.remove('selected');
|
||||||
|
li.classList.add('selected');
|
||||||
|
$('ssid').value = n.ssid;
|
||||||
|
$('password').value = '';
|
||||||
|
$('password').focus();
|
||||||
|
});
|
||||||
|
list.appendChild(li);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function connect() {
|
||||||
|
if (busy) return;
|
||||||
|
const ssid = $('ssid').value;
|
||||||
|
const password = $('password').value;
|
||||||
|
if (!ssid) { message('Pick a network or type its name', 'error'); return; }
|
||||||
|
setBusy(true);
|
||||||
|
message(`Connecting to "${ssid}" (up to 30 seconds)…`, 'busy');
|
||||||
|
const r = await request('connect', {ssid, password});
|
||||||
|
if (r.ok) {
|
||||||
|
message(`✓ Connected to "${r.ssid}" - ${r.ip}`, 'ok');
|
||||||
|
$('password').value = '';
|
||||||
|
} else {
|
||||||
|
message(r.error || 'Connection failed', 'error');
|
||||||
|
}
|
||||||
|
setBusy(false);
|
||||||
|
refreshStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function unlock() {
|
||||||
|
if (busy) return;
|
||||||
|
setBusy(true);
|
||||||
|
const r = await request('unlock', {pin: $('pin').value});
|
||||||
|
setBusy(false);
|
||||||
|
$('pin').value = '';
|
||||||
|
if (r.ok) { showWifi(); } else { message(r.error, 'error'); $('pin').focus(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function showWifi() {
|
||||||
|
message('');
|
||||||
|
$('pin-step').classList.add('hidden');
|
||||||
|
$('wifi-step').classList.remove('hidden');
|
||||||
|
refreshStatus();
|
||||||
|
scan();
|
||||||
|
}
|
||||||
|
|
||||||
|
$('pin-ok').addEventListener('click', unlock);
|
||||||
|
$('pin').addEventListener('keydown', e => { if (e.key === 'Enter') unlock(); });
|
||||||
|
$('scan').addEventListener('click', scan);
|
||||||
|
$('connect').addEventListener('click', connect);
|
||||||
|
$('password').addEventListener('keydown', e => { if (e.key === 'Enter') connect(); });
|
||||||
|
$('ssid').addEventListener('keydown', e => { if (e.key === 'Enter') $('password').focus(); });
|
||||||
|
$('show').addEventListener('click', () => {
|
||||||
|
const p = $('password');
|
||||||
|
p.type = p.type === 'password' ? 'text' : 'password';
|
||||||
|
$('show').textContent = p.type === 'password' ? 'Show' : 'Hide';
|
||||||
|
});
|
||||||
|
for (const b of document.querySelectorAll('[data-close]')) b.addEventListener('click', () => request('close'));
|
||||||
|
|
||||||
|
request('needs-pin').then(r => {
|
||||||
|
if (r.ok && r.needsPin) {
|
||||||
|
$('pin-step').classList.remove('hidden');
|
||||||
|
$('pin').focus();
|
||||||
|
} else {
|
||||||
|
showWifi();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -216,10 +216,34 @@ provision_configure_display() {
|
|||||||
sudo udevadm control --reload-rules
|
sudo udevadm control --reload-rules
|
||||||
|
|
||||||
provision_configure_lightdm_autologin
|
provision_configure_lightdm_autologin
|
||||||
|
provision_install_wifi_helper
|
||||||
|
|
||||||
log_success "Display configured"
|
log_success "Display configured"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# The kiosk app's WiFi screen (Ctrl+Alt+Shift+Super+W) - joining WiFi from
|
||||||
|
# the kiosk itself when the console is switched off. The app runs as the
|
||||||
|
# kiosk user; the netplan write is done by this one root helper, which
|
||||||
|
# the kiosk user may run through sudo and nothing else (it only does
|
||||||
|
# status/scan/connect - see its header). Here rather than its own
|
||||||
|
# provisioning step so Advanced -> Upgrade, which re-runs this function,
|
||||||
|
# installs it on an existing kiosk too. The sudoers rule is validated
|
||||||
|
# before it goes in place, like the Web UI's: a broken sudoers.d file
|
||||||
|
# breaks sudo for everyone.
|
||||||
|
provision_install_wifi_helper() {
|
||||||
|
provision_install_file "usr/local/bin/kiosk-wifi-helper" "$BIN_DIR/kiosk-wifi-helper" 755
|
||||||
|
local tmp
|
||||||
|
tmp=$(mktemp)
|
||||||
|
echo "${KIOSK_USER} ALL=(root) NOPASSWD: ${BIN_DIR}/kiosk-wifi-helper" > "$tmp"
|
||||||
|
if sudo visudo -c -f "$tmp" &>/dev/null; then
|
||||||
|
sudo mkdir -p "$SUDOERS_D_DIR"
|
||||||
|
sudo install -m 0440 -o root -g root "$tmp" "$SUDOERS_D_DIR/kiosk-wifi"
|
||||||
|
else
|
||||||
|
log_warning "Couldn't set up the kiosk WiFi screen's permission - Ctrl+Alt+Shift+Super+W won't be able to connect"
|
||||||
|
fi
|
||||||
|
rm -f "$tmp"
|
||||||
|
}
|
||||||
|
|
||||||
provision_configure_firewall() {
|
provision_configure_firewall() {
|
||||||
echo "[6/10] Configuring firewall..."
|
echo "[6/10] Configuring firewall..."
|
||||||
sudo ufw --force enable
|
sudo ufw --force enable
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ action_complete_uninstall() {
|
|||||||
|
|
||||||
echo "[7/12] Removing remaining scripts..."
|
echo "[7/12] Removing remaining scripts..."
|
||||||
sudo rm -f "$BIN_DIR"/kiosk-*
|
sudo rm -f "$BIN_DIR"/kiosk-*
|
||||||
|
sudo rm -f "$SUDOERS_D_DIR/kiosk-wifi"
|
||||||
sudo rm -f /etc/udev/rules.d/99-kiosk-hotplug.rules
|
sudo rm -f /etc/udev/rules.d/99-kiosk-hotplug.rules
|
||||||
sudo udevadm control --reload-rules 2>/dev/null || true
|
sudo udevadm control --reload-rules 2>/dev/null || true
|
||||||
|
|
||||||
|
|||||||
+273
@@ -0,0 +1,273 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
################################################################################
|
||||||
|
# kiosk-wifi-helper - the root side of the kiosk app's WiFi screen
|
||||||
|
# (Ctrl+Alt+Shift+Super+W), for joining a WiFi network from the kiosk
|
||||||
|
# itself when the console (Ctrl+Alt+F1..F6) is switched off.
|
||||||
|
#
|
||||||
|
# The kiosk user may run this one script via passwordless sudo
|
||||||
|
# (/etc/sudoers.d/kiosk-wifi, written by lib/provision.sh) - it does a
|
||||||
|
# fixed set of things and nothing else:
|
||||||
|
#
|
||||||
|
# kiosk-wifi-helper status {"iface","connected","ssid","ip","configured"}
|
||||||
|
# kiosk-wifi-helper scan {"iface","networks":[{"ssid","signal","secure"}]}
|
||||||
|
# kiosk-wifi-helper connect stdin {"ssid","password"} -> {"ok","ip"|"error"}
|
||||||
|
#
|
||||||
|
# Every reply is one JSON line on stdout, exit 0 on success. 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
|
||||||
|
# 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
|
||||||
|
# visible to every user in ps).
|
||||||
|
################################################################################
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
NETPLAN_DIR="${NETPLAN_DIR:-/etc/netplan}"
|
||||||
|
SYS_CLASS="${KIOSK_WIFI_SYS_CLASS:-/sys/class}" # overridable for tests
|
||||||
|
WIFI_NETPLAN_FILE="$NETPLAN_DIR/60-kiosk-wifi.yaml"
|
||||||
|
LOCK_FILE="${KIOSK_WIFI_LOCK:-/run/kiosk-wifi-helper.lock}"
|
||||||
|
CONNECT_WAIT="${KIOSK_WIFI_CONNECT_WAIT:-30}"
|
||||||
|
|
||||||
|
reply_error() {
|
||||||
|
jq -cn --arg e "$1" '{ok:false,error:$e}'
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# The first wireless interface, by what it is (sysfs wireless/ or
|
||||||
|
# phy80211), falling back to the wl* naming convention. Same as
|
||||||
|
# menus/wifi.sh's wifi_detect_iface.
|
||||||
|
detect_iface() {
|
||||||
|
local d
|
||||||
|
for d in $SYS_CLASS/net/*; do
|
||||||
|
if [[ -d "$d/wireless" || -e "$d/phy80211" ]]; then
|
||||||
|
basename "$d"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
for d in $SYS_CLASS/net/wl*; do
|
||||||
|
[[ -e "$d" ]] || continue
|
||||||
|
basename "$d"
|
||||||
|
return 0
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Clear an rfkill soft block (a hard block can't be cleared from software).
|
||||||
|
unblock() {
|
||||||
|
local r
|
||||||
|
for r in $SYS_CLASS/rfkill/rfkill*; do
|
||||||
|
[[ "$(cat "$r/type" 2>/dev/null)" == "wlan" ]] || continue
|
||||||
|
[[ "$(cat "$r/soft" 2>/dev/null)" == "1" ]] && echo 0 > "$r/soft" 2>/dev/null
|
||||||
|
done
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
hard_blocked() {
|
||||||
|
local r
|
||||||
|
for r in $SYS_CLASS/rfkill/rfkill*; do
|
||||||
|
[[ "$(cat "$r/type" 2>/dev/null)" == "wlan" ]] || continue
|
||||||
|
[[ "$(cat "$r/hard" 2>/dev/null)" == "1" ]] && return 0
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
iface_ip() {
|
||||||
|
ip -4 -o addr show dev "$1" 2>/dev/null \
|
||||||
|
| awk '{for (i = 1; i < NF; i++) if ($i == "inet") { sub("/.*", "", $(i+1)); print $(i+1); exit }}'
|
||||||
|
}
|
||||||
|
|
||||||
|
# YAML single-quoted scalar - the quote itself doubled is the only escape.
|
||||||
|
yaml_quote() {
|
||||||
|
local v="$1"
|
||||||
|
printf "'%s'" "${v//\'/\'\'}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# iw prints a backslash, a leading/trailing space and anything
|
||||||
|
# unprintable in an SSID as \xNN (a backslash itself is always \x5c,
|
||||||
|
# so there are no other escapes) - %b turns those back into the bytes.
|
||||||
|
unescape_ssid() {
|
||||||
|
printf '%b' "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# The SSID the interface is associated with right now, if any.
|
||||||
|
current_ssid() {
|
||||||
|
unescape_ssid "$(iw dev "$1" link 2>/dev/null | sed -n 's/^[[:space:]]*SSID: //p' | head -1)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# The SSID in our netplan file, if any (the first access point's key).
|
||||||
|
configured_ssid() {
|
||||||
|
[[ -f "$WIFI_NETPLAN_FILE" ]] || return 0
|
||||||
|
python3 -I -c '
|
||||||
|
import sys, yaml
|
||||||
|
try:
|
||||||
|
d = yaml.safe_load(open(sys.argv[1]))
|
||||||
|
for w in (d.get("network", {}).get("wifis") or {}).values():
|
||||||
|
for ssid in (w.get("access-points") or {}):
|
||||||
|
print(ssid); sys.exit(0)
|
||||||
|
except Exception:
|
||||||
|
pass' "$WIFI_NETPLAN_FILE" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
need_iface() {
|
||||||
|
IFACE=$(detect_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"
|
||||||
|
}
|
||||||
|
|
||||||
|
do_status() {
|
||||||
|
local iface ssid="" ip="" configured
|
||||||
|
configured=$(configured_ssid)
|
||||||
|
if ! iface=$(detect_iface); then
|
||||||
|
jq -cn --arg c "$configured" '{ok:true,iface:"",connected:false,ssid:"",ip:"",configured:$c}'
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
ssid=$(current_ssid "$iface")
|
||||||
|
ip=$(iface_ip "$iface")
|
||||||
|
jq -cn --arg i "$iface" --arg s "$ssid" --arg a "$ip" --arg c "$configured" \
|
||||||
|
'{ok:true,iface:$i,connected:($s != "" and $a != ""),ssid:$s,ip:$a,configured:$c}'
|
||||||
|
}
|
||||||
|
|
||||||
|
do_scan() {
|
||||||
|
need_iface
|
||||||
|
unblock
|
||||||
|
hard_blocked && reply_error "WiFi is switched off by a hardware switch or in the BIOS/UEFI"
|
||||||
|
ip link set "$IFACE" up 2>/dev/null || reply_error "Couldn't switch on $IFACE"
|
||||||
|
|
||||||
|
# "Device or resource busy" while wpa_supplicant is mid-scan - retry.
|
||||||
|
local out="" try
|
||||||
|
for try in 1 2 3; do
|
||||||
|
out=$(iw dev "$IFACE" scan 2>/dev/null) && break
|
||||||
|
out=""
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
# wpa_supplicant owns the interface and refuses iw: use its last scan.
|
||||||
|
if [[ -z "$out" ]] && command -v wpa_cli >/dev/null; then
|
||||||
|
wpa_cli -i "$IFACE" scan >/dev/null 2>&1
|
||||||
|
sleep 4
|
||||||
|
wpa_cli -i "$IFACE" scan_results 2>/dev/null | awk -F'\t' '
|
||||||
|
NR > 1 && $5 != "" { secure = ($4 ~ /WPA|WEP|RSN|SAE/) ? 1 : 0; print $3 "\t" secure "\t" $5 }' \
|
||||||
|
> "$TMP_SCAN"
|
||||||
|
else
|
||||||
|
# BSS blocks: signal, SSID, and RSN/WPA/Privacy for "secured".
|
||||||
|
printf '%s\n' "$out" | awk '
|
||||||
|
function flush() { if (ssid != "") print sig "\t" sec "\t" ssid; ssid = ""; sig = -100; sec = 0 }
|
||||||
|
/^BSS / { flush() }
|
||||||
|
/^\tsignal:/ { sig = int($2) }
|
||||||
|
/^\tSSID: / { sub(/^\tSSID: /, ""); ssid = $0 }
|
||||||
|
/^\t(RSN|WPA):/ { sec = 1 }
|
||||||
|
/^\tcapability:.*Privacy/ { sec = 1 }
|
||||||
|
END { flush() }' \
|
||||||
|
| while IFS=$'\t' read -r sig sec ssid; do
|
||||||
|
printf '%s\t%s\t%s\n' "$sig" "$sec" "$(unescape_ssid "$ssid")"
|
||||||
|
done > "$TMP_SCAN"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Strongest first, one entry per SSID.
|
||||||
|
jq -cR --arg i "$IFACE" -n '
|
||||||
|
[inputs | split("\t") | select(length >= 3)
|
||||||
|
| {signal: (.[0] | tonumber? // -100), secure: (.[1] == "1"), ssid: (.[2:] | join("\t"))}]
|
||||||
|
| group_by(.ssid) | map(max_by(.signal)) | sort_by(-.signal)
|
||||||
|
| {ok: true, iface: $i, networks: .}' < "$TMP_SCAN"
|
||||||
|
}
|
||||||
|
|
||||||
|
do_connect() {
|
||||||
|
need_iface
|
||||||
|
local input ssid password
|
||||||
|
input=$(head -c 4096)
|
||||||
|
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"
|
||||||
|
|
||||||
|
local ssid_bytes
|
||||||
|
ssid_bytes=$(printf '%s' "$ssid" | wc -c)
|
||||||
|
(( ssid_bytes >= 1 && ssid_bytes <= 32 )) || reply_error "A network name is 1-32 characters"
|
||||||
|
# No control characters (newlines etc.) in either - they can't be in a
|
||||||
|
# real SSID/passphrase and would only serve to break the YAML.
|
||||||
|
[[ "$ssid$password" == *[[:cntrl:]]* ]] && reply_error "The name or password contains an invalid character"
|
||||||
|
if [[ -n "$password" ]]; then
|
||||||
|
local plen=${#password}
|
||||||
|
if ! (( plen >= 8 && plen <= 63 )) && ! [[ "$password" =~ ^[0-9A-Fa-f]{64}$ ]]; then
|
||||||
|
reply_error "A WiFi password is 8-63 characters"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
unblock
|
||||||
|
hard_blocked && reply_error "WiFi is switched off by a hardware switch or in the BIOS/UEFI"
|
||||||
|
|
||||||
|
local backup="" new
|
||||||
|
mkdir -p "$NETPLAN_DIR"
|
||||||
|
if [[ -f "$WIFI_NETPLAN_FILE" ]]; then
|
||||||
|
backup="${WIFI_NETPLAN_FILE}.backup-$(date +%Y%m%d-%H%M%S)"
|
||||||
|
cp -p "$WIFI_NETPLAN_FILE" "$backup"
|
||||||
|
fi
|
||||||
|
|
||||||
|
new=$(mktemp "$NETPLAN_DIR/.kiosk-wifi.XXXXXX")
|
||||||
|
{
|
||||||
|
echo "network:"
|
||||||
|
echo " version: 2"
|
||||||
|
echo " renderer: networkd"
|
||||||
|
echo " wifis:"
|
||||||
|
echo " $IFACE:"
|
||||||
|
echo " dhcp4: true"
|
||||||
|
echo " dhcp6: false"
|
||||||
|
echo " optional: true"
|
||||||
|
echo " access-points:"
|
||||||
|
if [[ -n "$password" ]]; then
|
||||||
|
echo " $(yaml_quote "$ssid"):"
|
||||||
|
echo " password: $(yaml_quote "$password")"
|
||||||
|
else
|
||||||
|
echo " $(yaml_quote "$ssid"): {}"
|
||||||
|
fi
|
||||||
|
} > "$new"
|
||||||
|
chmod 0600 "$new"
|
||||||
|
mv -f "$new" "$WIFI_NETPLAN_FILE"
|
||||||
|
|
||||||
|
local err
|
||||||
|
if ! err=$(netplan apply 2>&1); then
|
||||||
|
restore "$backup"
|
||||||
|
reply_error "netplan apply failed: ${err:0:300}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local i addr="" now
|
||||||
|
for (( i = 0; i < CONNECT_WAIT; i++ )); do
|
||||||
|
now=$(current_ssid "$IFACE")
|
||||||
|
addr=$(iface_ip "$IFACE")
|
||||||
|
[[ "$now" == "$ssid" && -n "$addr" ]] && break
|
||||||
|
addr=""
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$addr" ]]; then
|
||||||
|
restore "$backup"
|
||||||
|
if [[ -n "$backup" ]]; then
|
||||||
|
reply_error "Couldn't connect to \"$ssid\" - wrong password, or out of range. The previous WiFi setting was put back."
|
||||||
|
fi
|
||||||
|
reply_error "Couldn't connect to \"$ssid\" - wrong password, or out of range."
|
||||||
|
fi
|
||||||
|
jq -cn --arg s "$ssid" --arg a "$addr" --arg i "$IFACE" '{ok:true,ssid:$s,ip:$a,iface:$i}'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Put the previous netplan WiFi file back (or remove ours if there was
|
||||||
|
# none) and re-apply, after a failed connect.
|
||||||
|
restore() {
|
||||||
|
if [[ -n "$1" && -f "$1" ]]; then
|
||||||
|
mv -f "$1" "$WIFI_NETPLAN_FILE"
|
||||||
|
else
|
||||||
|
rm -f "$WIFI_NETPLAN_FILE"
|
||||||
|
fi
|
||||||
|
netplan apply >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
[[ $EUID -eq 0 ]] || reply_error "must run as root (sudo)"
|
||||||
|
command -v jq >/dev/null || { echo '{"ok":false,"error":"jq is not installed"}'; exit 1; }
|
||||||
|
|
||||||
|
TMP_SCAN=$(mktemp)
|
||||||
|
trap 'rm -f "$TMP_SCAN"' EXIT
|
||||||
|
|
||||||
|
# One at a time - two overlapping netplan applies/backups would race.
|
||||||
|
exec 9> "$LOCK_FILE"
|
||||||
|
flock -w 60 9 || reply_error "Another WiFi change is still running - try again in a moment"
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
status) do_status ;;
|
||||||
|
scan) do_scan ;;
|
||||||
|
connect) do_connect ;;
|
||||||
|
*) reply_error "unknown action: ${1:-}" ;;
|
||||||
|
esac
|
||||||
Reference in New Issue
Block a user