fix: never re-enable SSID pool broadcast (crash guard) + top-bar health chip
Mode 'active' and the advertise toggle could re-enable the SSID-pool broadcast that segfaults pineapd. active preset now skips ssidpool/enable (advertise stays false), the advertise endpoint refuses with an explanation when the pool is disabled, get_ap reports the real pool state, and the PineAP overview disables the toggle with a notice. Added a top-bar health chip (PINEAP OK / POOL OFF / PINEAPD DOWN) polled every 15s.
This commit is contained in:
@@ -2581,7 +2581,8 @@ def h_pineap_wifi_get_ap(ctx):
|
|||||||
'channel': _ent_summary()['channel'],
|
'channel': _ent_summary()['channel'],
|
||||||
'live': _ent_summary()['live'],
|
'live': _ent_summary()['live'],
|
||||||
},
|
},
|
||||||
'pool': {'disabled': None, 'collecting': bool(pinecfg.get('autossidpool'))},
|
'pool': {'disabled': pool.get('disable') == '1',
|
||||||
|
'collecting': bool(pinecfg.get('autossidpool'))},
|
||||||
'radios': {
|
'radios': {
|
||||||
'radio0': _radio_dict('radio0'),
|
'radio0': _radio_dict('radio0'),
|
||||||
'radio1': _radio_dict('radio1'),
|
'radio1': _radio_dict('radio1'),
|
||||||
@@ -2759,6 +2760,13 @@ def h_pineap_wifi_set_ap(ctx):
|
|||||||
|
|
||||||
def h_pineap_advertise(ctx):
|
def h_pineap_advertise(ctx):
|
||||||
enable = bool((ctx.body or {}).get('enable'))
|
enable = bool((ctx.body or {}).get('enable'))
|
||||||
|
if enable:
|
||||||
|
# The SSID-pool broadcast segfaults pineapd on this firmware; the
|
||||||
|
# pool stays disabled or the crash-loop returns.
|
||||||
|
pool = _uci_section('pineapd.@ssidpool[0]')
|
||||||
|
if pool.get('disable') == '1':
|
||||||
|
return 400, {'error': 'SSID pool broadcast is disabled on this firmware '
|
||||||
|
'(pineapd SIGSEGV crash-loop) and cannot be re-enabled'}
|
||||||
status, data = _daemon_proxy('POST', 'ssidpool/enable' if enable else 'ssidpool/disable', {'enable': enable})
|
status, data = _daemon_proxy('POST', 'ssidpool/enable' if enable else 'ssidpool/disable', {'enable': enable})
|
||||||
if status == 200:
|
if status == 200:
|
||||||
update_pineap_state(mode='advanced', advertise=enable)
|
update_pineap_state(mode='advanced', advertise=enable)
|
||||||
@@ -3911,16 +3919,16 @@ def h_pineap_mode_post(ctx):
|
|||||||
(('enable' if enabled else 'disable') + ' PineAP response engine',
|
(('enable' if enabled else 'disable') + ' PineAP response engine',
|
||||||
'PUT', 'hostapd/enable_pineap', {'enable': enabled}),
|
'PUT', 'hostapd/enable_pineap', {'enable': enabled}),
|
||||||
('enable SSID collection', 'POST', 'ssidpool/enable_collect', {'enable': True}),
|
('enable SSID collection', 'POST', 'ssidpool/enable_collect', {'enable': True}),
|
||||||
(('enable' if mode == 'active' else 'disable') + ' SSID pool broadcasting',
|
|
||||||
'POST', 'ssidpool/enable' if mode == 'active' else 'ssidpool/disable',
|
|
||||||
{'enable': mode == 'active'}),
|
|
||||||
]
|
]
|
||||||
|
# NOTE: the SSID-pool broadcast step is intentionally omitted. The pool
|
||||||
|
# broadcast segfaults pineapd on this firmware (SIGSEGV crash-loop); the
|
||||||
|
# health monitor disables it and 'active' must not re-enable it.
|
||||||
for label, method, path, body in steps:
|
for label, method, path, body in steps:
|
||||||
status, data = _daemon_proxy(method, path, body)
|
status, data = _daemon_proxy(method, path, body)
|
||||||
if status != 200:
|
if status != 200:
|
||||||
return status, {'error': 'failed to ' + label, 'detail': data}
|
return status, {'error': 'failed to ' + label, 'detail': data}
|
||||||
return 200, update_pineap_state(mode=mode, enabled=enabled, karma=enabled,
|
return 200, update_pineap_state(mode=mode, enabled=enabled, karma=enabled,
|
||||||
collect=True, advertise=(mode == 'active'))
|
collect=True, advertise=False)
|
||||||
|
|
||||||
|
|
||||||
def h_filter_get(ctx, kind):
|
def h_filter_get(ctx, kind):
|
||||||
|
|||||||
@@ -187,6 +187,13 @@ body {
|
|||||||
.badge.off { background: #fff3e0; color: #e65100; }
|
.badge.off { background: #fff3e0; color: #e65100; }
|
||||||
.badge.warn { background: #fff8e1; color: #f57f17; }
|
.badge.warn { background: #fff8e1; color: #f57f17; }
|
||||||
.badge.unknown { background: #eeeeee; color: #616161; }
|
.badge.unknown { background: #eeeeee; color: #616161; }
|
||||||
|
.health-chip { font-size: 11px; font-weight: 600; letter-spacing: .04em; padding: 2px 8px; border-radius: 10px; margin-left: 10px; align-self: center; }
|
||||||
|
.health-chip.good { background: #e8f5e9; color: #2e7d32; }
|
||||||
|
.health-chip.warn { background: #fff8e1; color: #f57f17; }
|
||||||
|
.health-chip.bad { background: #fdecea; color: #b71c1c; }
|
||||||
|
html.dark .health-chip.good { background: #1b3a24; color: #81c784; }
|
||||||
|
html.dark .health-chip.warn { background: #3d3313; color: #ffd54f; }
|
||||||
|
html.dark .health-chip.bad { background: #4a2020; color: #ffb4a9; }
|
||||||
.btn {
|
.btn {
|
||||||
background: var(--primary); color: #fff; border: 0; border-radius: 2px;
|
background: var(--primary); color: #fff; border: 0; border-radius: 2px;
|
||||||
padding: 8px 14px; font-size: 14px; cursor: pointer;
|
padding: 8px 14px; font-size: 14px; cursor: pointer;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
<span id="brand-text" class="brand-text">Mark VIII</span>
|
<span id="brand-text" class="brand-text">Mark VIII</span>
|
||||||
<span class="toolbar-spacer"></span>
|
<span class="toolbar-spacer"></span>
|
||||||
<span id="live-status"></span>
|
<span id="live-status"></span>
|
||||||
|
<span id="health-status" class="health-chip"></span>
|
||||||
<div class="toolbar-action">
|
<div class="toolbar-action">
|
||||||
<button id="notifications-btn" class="toolbar-icon-btn" type="button" title="Notifications"
|
<button id="notifications-btn" class="toolbar-icon-btn" type="button" title="Notifications"
|
||||||
aria-label="Notifications" aria-haspopup="menu" aria-controls="notifications-menu" aria-expanded="false"></button>
|
aria-label="Notifications" aria-haspopup="menu" aria-controls="notifications-menu" aria-expanded="false"></button>
|
||||||
|
|||||||
@@ -434,6 +434,7 @@ const Live = (() => {
|
|||||||
let ws = null;
|
let ws = null;
|
||||||
let ever = false;
|
let ever = false;
|
||||||
let poll = null;
|
let poll = null;
|
||||||
|
let pollHealthTimer = null;
|
||||||
const subs = [];
|
const subs = [];
|
||||||
let timer = null;
|
let timer = null;
|
||||||
function stopPoll() {
|
function stopPoll() {
|
||||||
@@ -442,6 +443,10 @@ const Live = (() => {
|
|||||||
function start() {
|
function start() {
|
||||||
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) return;
|
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) return;
|
||||||
stopPoll();
|
stopPoll();
|
||||||
|
if (!pollHealthTimer) {
|
||||||
|
pollHealthTimer = setInterval(pollHealth, 15000);
|
||||||
|
pollHealth();
|
||||||
|
}
|
||||||
try { ws = new WebSocket(App.wsUrl('/api/ws')); }
|
try { ws = new WebSocket(App.wsUrl('/api/ws')); }
|
||||||
catch (e) { fallback(); return; }
|
catch (e) { fallback(); return; }
|
||||||
ws.onopen = () => { ever = true; };
|
ws.onopen = () => { ever = true; };
|
||||||
@@ -489,6 +494,23 @@ const Live = (() => {
|
|||||||
const el = document.getElementById('live-status');
|
const el = document.getElementById('live-status');
|
||||||
if (el) el.textContent = 'BAT ' + (b.level == null ? '--' : b.level + '%' + (b.charging ? '+' : '')) + ' CLIENTS ' + n;
|
if (el) el.textContent = 'BAT ' + (b.level == null ? '--' : b.level + '%' + (b.charging ? '+' : '')) + ' CLIENTS ' + n;
|
||||||
}
|
}
|
||||||
|
function pollHealth() {
|
||||||
|
fetch(API_BASE + '/api/health', { credentials: 'include' }).then((r) => r.json())
|
||||||
|
.then((h) => {
|
||||||
|
const el = document.getElementById('health-status');
|
||||||
|
if (!el) return;
|
||||||
|
if (h.pineap_up === false) {
|
||||||
|
el.textContent = 'PINEAPD DOWN';
|
||||||
|
el.className = 'health-chip bad';
|
||||||
|
} else if (h.pool_disabled) {
|
||||||
|
el.textContent = 'POOL OFF';
|
||||||
|
el.className = 'health-chip warn';
|
||||||
|
} else if (h.pineap_up) {
|
||||||
|
el.textContent = 'PINEAP OK';
|
||||||
|
el.className = 'health-chip good';
|
||||||
|
}
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
function onTick(fn) {
|
function onTick(fn) {
|
||||||
subs.push(fn);
|
subs.push(fn);
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
@@ -276,6 +276,9 @@ views.pineap = (root) => {
|
|||||||
h('div', { class: 'pineap-card-title' }, 'Quick Settings'));
|
h('div', { class: 'pineap-card-title' }, 'Quick Settings'));
|
||||||
quickCard.appendChild(h('label', { class: 'switch' }, quick.collect, h('span', { class: 'track' }), 'Capture SSIDs to Pool'));
|
quickCard.appendChild(h('label', { class: 'switch' }, quick.collect, h('span', { class: 'track' }), 'Capture SSIDs to Pool'));
|
||||||
quickCard.appendChild(h('label', { class: 'switch' }, quick.advertise, h('span', { class: 'track' }), 'Advertise AP Impersonation Pool'));
|
quickCard.appendChild(h('label', { class: 'switch' }, quick.advertise, h('span', { class: 'track' }), 'Advertise AP Impersonation Pool'));
|
||||||
|
const poolNotice = h('div', { class: 'pineap-infobox warn', style: 'margin-top:8px',
|
||||||
|
text: 'Pool broadcast disabled: it segfaults pineapd on this firmware (crash-loop fix).' });
|
||||||
|
quickCard.appendChild(poolNotice);
|
||||||
quickCard.appendChild(h('div', { class: 'muted', style: 'margin-top:8px;font-size:12px' },
|
quickCard.appendChild(h('div', { class: 'muted', style: 'margin-top:8px;font-size:12px' },
|
||||||
'Client connect/disconnect notifications are handled by the Pager alert payload system.'));
|
'Client connect/disconnect notifications are handled by the Pager alert payload system.'));
|
||||||
|
|
||||||
@@ -305,10 +308,10 @@ views.pineap = (root) => {
|
|||||||
on(requested).then(() => {
|
on(requested).then(() => {
|
||||||
if (remember) remember(requested);
|
if (remember) remember(requested);
|
||||||
load();
|
load();
|
||||||
}).catch(() => {
|
}).catch((e) => {
|
||||||
cb.checked = !requested;
|
cb.checked = !requested;
|
||||||
load();
|
load();
|
||||||
App.toast('Failed', 'error');
|
App.toast((e && e.message) || 'Failed', 'error');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -332,7 +335,7 @@ views.pineap = (root) => {
|
|||||||
}
|
}
|
||||||
const descriptions = {
|
const descriptions = {
|
||||||
passive: ['Capture SSIDs to the impersonation pool', 'Do not broadcast the pool', 'Keep the PineAP response engine disabled'],
|
passive: ['Capture SSIDs to the impersonation pool', 'Do not broadcast the pool', 'Keep the PineAP response engine disabled'],
|
||||||
active: ['Capture SSIDs to the impersonation pool', 'Broadcast the impersonation pool', 'Enable the PineAP response engine']
|
active: ['Capture SSIDs to the impersonation pool', 'Enable the PineAP response engine', 'Pool broadcast stays disabled (firmware crash fix)']
|
||||||
};
|
};
|
||||||
if (m === 'advanced') {
|
if (m === 'advanced') {
|
||||||
modeInfo.textContent = 'All supported PineAP features are individually customizable from Quick Settings and the PineAP tabs.';
|
modeInfo.textContent = 'All supported PineAP features are individually customizable from Quick Settings and the PineAP tabs.';
|
||||||
@@ -401,6 +404,10 @@ views.pineap = (root) => {
|
|||||||
: Object.prototype.hasOwnProperty.call(c, 'autossidpool') ? !!c.autossidpool : null;
|
: Object.prototype.hasOwnProperty.call(c, 'autossidpool') ? !!c.autossidpool : null;
|
||||||
setKnownCheckbox(quick.collect, collect);
|
setKnownCheckbox(quick.collect, collect);
|
||||||
const pool = a.pool || {};
|
const pool = a.pool || {};
|
||||||
|
const poolDisabled = pool.disabled === true;
|
||||||
|
poolNotice.classList.toggle('hidden', !poolDisabled);
|
||||||
|
quick.advertise.disabled = poolDisabled;
|
||||||
|
quick.advertise.title = poolDisabled ? 'Disabled by firmware crash fix' : '';
|
||||||
if (pool.disabled != null) PINEAP_SESSION.advertise = pool.disabled === false;
|
if (pool.disabled != null) PINEAP_SESSION.advertise = pool.disabled === false;
|
||||||
setKnownCheckbox(quick.advertise, PINEAP_SESSION.advertise);
|
setKnownCheckbox(quick.advertise, PINEAP_SESSION.advertise);
|
||||||
cards.karma.value.textContent = PINEAP_SESSION.karma == null ? 'Unknown' : (PINEAP_SESSION.karma ? 'On' : 'Off');
|
cards.karma.value.textContent = PINEAP_SESSION.karma == null ? 'Unknown' : (PINEAP_SESSION.karma ? 'On' : 'Off');
|
||||||
|
|||||||
@@ -37,8 +37,7 @@ class PineapModeTest(unittest.TestCase):
|
|||||||
status, payload = server.h_pineap_mode_post(ctx({'mode': 'passive'}))
|
status, payload = server.h_pineap_mode_post(ctx({'mode': 'passive'}))
|
||||||
self.assertEqual(status, 200)
|
self.assertEqual(status, 200)
|
||||||
self.assertEqual([c[1] for c in calls], [
|
self.assertEqual([c[1] for c in calls], [
|
||||||
'hostapd/enable_pineap', 'ssidpool/enable_collect',
|
'hostapd/enable_pineap', 'ssidpool/enable_collect'])
|
||||||
'ssidpool/disable'])
|
|
||||||
self.assertEqual(calls[0][2], {'enable': False})
|
self.assertEqual(calls[0][2], {'enable': False})
|
||||||
self.assertEqual(payload['mode'], 'passive')
|
self.assertEqual(payload['mode'], 'passive')
|
||||||
self.assertTrue(payload['collect'])
|
self.assertTrue(payload['collect'])
|
||||||
@@ -46,7 +45,7 @@ class PineapModeTest(unittest.TestCase):
|
|||||||
self.assertFalse(payload['karma'])
|
self.assertFalse(payload['karma'])
|
||||||
self.assertFalse(payload['enabled'])
|
self.assertFalse(payload['enabled'])
|
||||||
|
|
||||||
def test_active_enables_response_engine_and_pool_broadcasting(self):
|
def test_active_enables_response_engine_but_never_pool_broadcast(self):
|
||||||
calls = []
|
calls = []
|
||||||
server._daemon_proxy = lambda method, path, body=None, timeout=15: (
|
server._daemon_proxy = lambda method, path, body=None, timeout=15: (
|
||||||
calls.append((method, path, body)) or (200, {'success': True}))
|
calls.append((method, path, body)) or (200, {'success': True}))
|
||||||
@@ -54,12 +53,24 @@ class PineapModeTest(unittest.TestCase):
|
|||||||
self.assertEqual(status, 200)
|
self.assertEqual(status, 200)
|
||||||
self.assertEqual(calls[0], ('PUT', 'hostapd/enable_pineap', {'enable': True}))
|
self.assertEqual(calls[0], ('PUT', 'hostapd/enable_pineap', {'enable': True}))
|
||||||
self.assertNotIn('mimic/disable', [c[1] for c in calls])
|
self.assertNotIn('mimic/disable', [c[1] for c in calls])
|
||||||
self.assertEqual(calls[-1], ('POST', 'ssidpool/enable', {'enable': True}))
|
# The SSID-pool broadcast segfaults pineapd on this firmware: the
|
||||||
|
# active preset must never call ssidpool/enable.
|
||||||
|
self.assertNotIn('ssidpool/enable', [c[1] for c in calls])
|
||||||
self.assertEqual(payload['mode'], 'active')
|
self.assertEqual(payload['mode'], 'active')
|
||||||
self.assertTrue(payload['advertise'])
|
self.assertFalse(payload['advertise'])
|
||||||
self.assertTrue(payload['karma'])
|
self.assertTrue(payload['karma'])
|
||||||
self.assertTrue(payload['enabled'])
|
self.assertTrue(payload['enabled'])
|
||||||
|
|
||||||
|
def test_advertise_refuses_when_pool_disabled(self):
|
||||||
|
server._uci_section = lambda name: {'disable': '1'}
|
||||||
|
calls = []
|
||||||
|
server._daemon_proxy = lambda method, path, body=None, timeout=15: (
|
||||||
|
calls.append((method, path, body)) or (200, {'success': True}))
|
||||||
|
status, payload = server.h_pineap_advertise(ctx({'enable': True}))
|
||||||
|
self.assertEqual(status, 400)
|
||||||
|
self.assertIn('cannot be re-enabled', payload['error'])
|
||||||
|
self.assertEqual(calls, [])
|
||||||
|
|
||||||
def test_advanced_preserves_device_settings(self):
|
def test_advanced_preserves_device_settings(self):
|
||||||
calls = []
|
calls = []
|
||||||
server._daemon_proxy = lambda *args, **kwargs: (calls.append(args) or (200, {}))
|
server._daemon_proxy = lambda *args, **kwargs: (calls.append(args) or (200, {}))
|
||||||
|
|||||||
Reference in New Issue
Block a user