- Recon AP focus sidebar: 'Deauth All Clients' with engagement-scope confirm - Deauth Targeting card: 'Deauth All' behind the same scope confirmation - New POST /api/attacks/deauth/bulk (max 32 targets, per-target results) - Evil Portal tab: import EvilPortalNano-format portal zips into /mmc/mk8/portals, serve active portal on port 80 to unauthenticated clients via a minimal PHP shim, capture all form POSTs (.logs in stock MyPortal.php format + captures.jsonl), dnsmasq address=/#/ DNS hijack - OpenAP: Evil Portal template dropdown (greyed when none), activated with the attack and stopped with it - Monitor Capture fix: iface-less status now reports whichever monitor is actually capturing; pcap dir mkdir'd; tcpdump stderr surfaced instead of discarded
642 lines
24 KiB
JavaScript
642 lines
24 KiB
JavaScript
'use strict';
|
|
|
|
const Theme = (() => {
|
|
const KEY = 'pw_theme';
|
|
function apply() {
|
|
document.documentElement.classList.toggle('dark', localStorage.getItem(KEY) === 'dark');
|
|
}
|
|
function toggle() {
|
|
localStorage.setItem(KEY, Theme.current() === 'dark' ? 'light' : 'dark');
|
|
apply();
|
|
}
|
|
function current() { return document.documentElement.classList.contains('dark') ? 'dark' : 'light'; }
|
|
return { apply, toggle, current, KEY };
|
|
})();
|
|
|
|
const App = (() => {
|
|
const cfg = window.PAGER_CONFIG || {};
|
|
const API_BASE = cfg.apiBase || '';
|
|
const WS_BASE = (cfg.wsBase || location.origin).replace(/^http/, 'ws');
|
|
const TERMINAL_WS = cfg.terminalWs || ('ws://' + location.hostname + ':1471/api/terminal/openWs');
|
|
const PAGER_SCREEN_WS = cfg.pagerScreenWs || (WS_BASE + '/api/pager/display/screen.ws');
|
|
const PAGER_KEYS_WS = cfg.pagerKeysWs || (WS_BASE + '/api/pager/input/keys.ws');
|
|
|
|
const els = {};
|
|
let currentView = null;
|
|
let notifications = [];
|
|
let unreadNotifications = 0;
|
|
const NOTIFICATIONS_KEY = 'markviii_notifications';
|
|
|
|
const railItems = [
|
|
{ key: 'dashboard', label: 'Dashboard', hash: '#/dashboard', icon: 'dashboard' },
|
|
{ key: 'pineap', label: 'PineAP', hash: '#/pineap', icon: 'wifi' },
|
|
{ key: 'recon', label: 'Recon', hash: '#/recon', icon: 'recon' },
|
|
{ key: 'evilportal', label: 'Evil Portal', hash: '#/evilportal', icon: 'portal' },
|
|
{ key: 'logging', label: 'Logging', hash: '#/logging', icon: 'logging' },
|
|
{ key: 'modules', label: 'Payloads', hash: '#/modules', icon: 'modules' },
|
|
{ key: 'harness', label: 'Harness', hash: '#/harness', icon: 'robot' },
|
|
{ key: 'settings', label: 'Settings', hash: '#/settings', icon: 'settings' }
|
|
];
|
|
const railDividers = new Set(['logging']);
|
|
|
|
function keyOf(hash) {
|
|
const seg = (hash || '#/dashboard').replace(/^#\//, '').split('/')[0];
|
|
return seg || 'dashboard';
|
|
}
|
|
|
|
function buildRail() {
|
|
const rail = els.rail;
|
|
rail.innerHTML = '';
|
|
function railEntry(it) {
|
|
const a = document.createElement('a');
|
|
a.className = 'entry';
|
|
a.href = it.hash;
|
|
a.title = it.label;
|
|
a.innerHTML = '<span class="entry-icon">' + PineappleIcons[it.icon] + '</span><span class="entry-text">' + it.label + '</span>';
|
|
a.addEventListener('click', (e) => { e.preventDefault(); location.hash = it.hash; });
|
|
return a;
|
|
}
|
|
railItems.filter((it) => it.key !== 'settings').forEach((it) => {
|
|
if (railDividers.has(it.key)) {
|
|
const d = document.createElement('div');
|
|
d.className = 'entry divider';
|
|
rail.appendChild(d);
|
|
}
|
|
rail.appendChild(railEntry(it));
|
|
});
|
|
const foot = document.createElement('div');
|
|
foot.className = 'rail-footer';
|
|
const settings = railItems.find((it) => it.key === 'settings');
|
|
if (settings) foot.appendChild(railEntry(settings));
|
|
const open = document.createElement('a');
|
|
open.className = 'entry';
|
|
open.title = 'Open Menu';
|
|
open.innerHTML = '<span class="entry-icon">' + PineappleIcons.chevron_right + '</span><span class="entry-text">Open Menu</span>';
|
|
open.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
const openState = localStorage.getItem('pw_rail') === 'open';
|
|
localStorage.setItem('pw_rail', openState ? 'closed' : 'open');
|
|
els.rail.classList.toggle('open', !openState);
|
|
});
|
|
foot.appendChild(open);
|
|
rail.appendChild(foot);
|
|
rail.classList.toggle('open', localStorage.getItem('pw_rail') === 'open');
|
|
}
|
|
|
|
function route() {
|
|
closeToolbarMenus();
|
|
let hash = (location.hash || '#/dashboard').replace(/\/+$/, '');
|
|
if (hash === '#/recon/survey') {
|
|
location.hash = '#/recon';
|
|
return;
|
|
}
|
|
if (hash.indexOf('#/attacks') === 0) {
|
|
const map = {
|
|
'#/attacks': '#/pineap',
|
|
'#/attacks/wpa': '#/pineap/evilwpa',
|
|
'#/attacks/open': '#/pineap/open',
|
|
'#/attacks/enterprise': '#/pineap/enterprise'
|
|
};
|
|
hash = map[hash] || '#/pineap';
|
|
location.replace(hash);
|
|
return;
|
|
}
|
|
const name = routes[hash];
|
|
if (currentView && currentView.destroy) currentView.destroy();
|
|
els.content.innerHTML = '';
|
|
if (!name || !views[name]) {
|
|
const ph = document.createElement('div');
|
|
ph.className = 'section empty';
|
|
ph.textContent = 'View not available.';
|
|
els.content.appendChild(ph);
|
|
currentView = null;
|
|
} else {
|
|
currentView = views[name](els.content);
|
|
}
|
|
const key = keyOf(hash);
|
|
Array.prototype.forEach.call(els.rail.querySelectorAll('.entry'), (a) => {
|
|
const href = a.getAttribute('href');
|
|
a.classList.toggle('active', !!href && (href === hash || keyOf(href) === key));
|
|
});
|
|
}
|
|
|
|
function loadNotifications() {
|
|
try {
|
|
const stored = JSON.parse(sessionStorage.getItem(NOTIFICATIONS_KEY) || '{}');
|
|
notifications = Array.isArray(stored.items) ? stored.items.slice(0, 30) : [];
|
|
unreadNotifications = Math.max(0, Number(stored.unread) || 0);
|
|
} catch (e) {
|
|
notifications = [];
|
|
unreadNotifications = 0;
|
|
}
|
|
}
|
|
|
|
function saveNotifications() {
|
|
try {
|
|
sessionStorage.setItem(NOTIFICATIONS_KEY, JSON.stringify({
|
|
items: notifications.slice(0, 30), unread: unreadNotifications
|
|
}));
|
|
} catch (e) {}
|
|
}
|
|
|
|
function notificationTime(timestamp) {
|
|
const date = new Date(timestamp);
|
|
if (Number.isNaN(date.getTime())) return '';
|
|
const today = new Date();
|
|
if (date.toDateString() === today.toDateString()) {
|
|
return date.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
|
|
}
|
|
return date.toLocaleDateString([], { month: 'short', day: 'numeric' });
|
|
}
|
|
|
|
function renderNotifications() {
|
|
if (!els.notificationsList) return;
|
|
els.notificationsList.innerHTML = '';
|
|
els.notificationBadge.classList.toggle('hidden', unreadNotifications === 0);
|
|
els.notificationBadge.textContent = unreadNotifications > 99 ? '99+' : String(unreadNotifications);
|
|
els.notificationsClear.disabled = notifications.length === 0;
|
|
if (!notifications.length) {
|
|
const empty = document.createElement('div');
|
|
empty.className = 'notification-empty';
|
|
empty.textContent = 'No Notifications';
|
|
els.notificationsList.appendChild(empty);
|
|
return;
|
|
}
|
|
notifications.forEach((notice) => {
|
|
const item = document.createElement('div');
|
|
item.className = 'notification-item';
|
|
const marker = document.createElement('span');
|
|
marker.className = 'notification-kind ' + (notice.kind || 'info');
|
|
const message = document.createElement('span');
|
|
message.className = 'notification-message';
|
|
message.textContent = notice.message;
|
|
const time = document.createElement('time');
|
|
time.className = 'notification-time';
|
|
time.dateTime = new Date(notice.timestamp).toISOString();
|
|
time.textContent = notificationTime(notice.timestamp);
|
|
item.append(marker, message, time);
|
|
els.notificationsList.appendChild(item);
|
|
});
|
|
}
|
|
|
|
function addNotification(message, kind) {
|
|
const normalizedKind = ['error', 'success'].indexOf(kind) === -1 ? 'info' : kind;
|
|
notifications.unshift({
|
|
message: String(message).slice(0, 300), kind: normalizedKind, timestamp: Date.now()
|
|
});
|
|
notifications = notifications.slice(0, 30);
|
|
unreadNotifications += 1;
|
|
saveNotifications();
|
|
renderNotifications();
|
|
}
|
|
|
|
function toast(msg, kind, options) {
|
|
if (!options || options.notify !== false) addNotification(msg, kind);
|
|
const d = document.createElement('div');
|
|
d.className = 'toast ' + (kind || 'info');
|
|
d.textContent = msg;
|
|
els.toasts.appendChild(d);
|
|
setTimeout(() => d.remove(), 4000);
|
|
}
|
|
|
|
function closeToolbarMenus() {
|
|
if (!els.notificationsMenu) return;
|
|
els.notificationsMenu.classList.add('hidden');
|
|
els.overflowMenu.classList.add('hidden');
|
|
els.notificationsButton.setAttribute('aria-expanded', 'false');
|
|
els.overflowButton.setAttribute('aria-expanded', 'false');
|
|
}
|
|
|
|
function toggleToolbarMenu(name) {
|
|
const menu = name === 'notifications' ? els.notificationsMenu : els.overflowMenu;
|
|
const button = name === 'notifications' ? els.notificationsButton : els.overflowButton;
|
|
const opening = menu.classList.contains('hidden');
|
|
closeToolbarMenus();
|
|
if (!opening) return;
|
|
menu.classList.remove('hidden');
|
|
button.setAttribute('aria-expanded', 'true');
|
|
if (name === 'notifications') {
|
|
unreadNotifications = 0;
|
|
saveNotifications();
|
|
renderNotifications();
|
|
}
|
|
}
|
|
|
|
function setInternetStatus(state) {
|
|
const labels = { checking: 'Checking\u2026', online: 'Online', offline: 'Offline' };
|
|
els.internetStatus.textContent = labels[state];
|
|
els.internetStatusDot.className = 'connection-dot ' + state;
|
|
}
|
|
|
|
function checkInternet(announce) {
|
|
setInternetStatus('checking');
|
|
return PagerAPI.get('/api/settings/internet')
|
|
.then((response) => {
|
|
const online = !!response.data.online;
|
|
setInternetStatus(online ? 'online' : 'offline');
|
|
if (announce) toast(online ? 'Internet connection is online' : 'Internet connection is offline', online ? 'success' : 'error');
|
|
})
|
|
.catch(() => {
|
|
setInternetStatus('offline');
|
|
if (announce) toast('Unable to check the internet connection', 'error');
|
|
});
|
|
}
|
|
|
|
function handleMenuAction(action, button) {
|
|
closeToolbarMenus();
|
|
if (action === 'help') {
|
|
location.hash = '#/settings/help';
|
|
} else if (action === 'updates') {
|
|
location.hash = '#/settings/advanced';
|
|
toast('Update controls are available in Advanced settings');
|
|
} else if (action === 'internet') {
|
|
if (typeof views.openClientModeModal === 'function') views.openClientModeModal();
|
|
else checkInternet(true);
|
|
} else if (action === 'logout') {
|
|
if (button) { button.disabled = true; button.classList.add('busy'); }
|
|
PagerAPI.post('/api/logout')
|
|
.then(() => showLogin())
|
|
.catch((error) => toast(error.message || 'Logout failed', 'error'))
|
|
.finally(() => { if (button) { button.disabled = false; button.classList.remove('busy'); } });
|
|
} else if (action === 'reboot') {
|
|
if (!window.confirm('Reboot Mark VIII now?')) return;
|
|
if (button) { button.disabled = true; button.classList.add('busy'); }
|
|
PagerAPI.post('/api/settings/reboot')
|
|
.then(() => toast('Reboot requested. Mark VIII will disconnect shortly.'))
|
|
.catch((error) => toast(error.message || 'Reboot failed', 'error'))
|
|
.finally(() => { if (button) { button.disabled = false; button.classList.remove('busy'); } });
|
|
}
|
|
}
|
|
|
|
function showDock(name) {
|
|
if (name === 'pager') {
|
|
if (typeof Term !== 'undefined' && Term.isOpen()) Term.close();
|
|
} else {
|
|
if (typeof Pager !== 'undefined' && Pager.isOpen()) Pager.close();
|
|
}
|
|
}
|
|
|
|
function showApp() {
|
|
els.login.classList.add('hidden');
|
|
els.app.classList.remove('hidden');
|
|
Theme.apply();
|
|
Live.start();
|
|
route();
|
|
checkInternet(false);
|
|
}
|
|
|
|
function showLogin() {
|
|
closeToolbarMenus();
|
|
if (typeof Live !== 'undefined') Live.stop();
|
|
if (typeof Term !== 'undefined' && Term.isOpen()) Term.close();
|
|
if (typeof Pager !== 'undefined' && Pager.isOpen()) Pager.close();
|
|
els.app.classList.add('hidden');
|
|
els.login.classList.remove('hidden');
|
|
}
|
|
|
|
function init() {
|
|
els.login = document.getElementById('login-screen');
|
|
els.app = document.getElementById('app');
|
|
els.rail = document.getElementById('rail');
|
|
els.content = document.getElementById('content');
|
|
els.toasts = document.getElementById('toast-container');
|
|
els.notificationsButton = document.getElementById('notifications-btn');
|
|
els.notificationsMenu = document.getElementById('notifications-menu');
|
|
els.notificationsList = document.getElementById('notifications-list');
|
|
els.notificationsClear = document.getElementById('notifications-clear');
|
|
els.notificationBadge = document.getElementById('notification-badge');
|
|
els.overflowButton = document.getElementById('overflow-btn');
|
|
els.overflowMenu = document.getElementById('overflow-menu');
|
|
els.internetStatus = document.getElementById('internet-status');
|
|
els.internetStatusDot = document.getElementById('internet-status-dot');
|
|
|
|
els.notificationsButton.innerHTML = PineappleIcons.notifications;
|
|
document.getElementById('terminal-btn').innerHTML = PineappleIcons.terminal;
|
|
document.getElementById('pager-btn').innerHTML = PineappleIcons.pager;
|
|
els.overflowButton.innerHTML = PineappleIcons.more_vert;
|
|
Array.prototype.forEach.call(document.querySelectorAll('[data-icon]'), (icon) => {
|
|
icon.innerHTML = PineappleIcons[icon.getAttribute('data-icon')] || '';
|
|
});
|
|
loadNotifications();
|
|
renderNotifications();
|
|
|
|
els.notificationsButton.addEventListener('click', (event) => {
|
|
event.stopPropagation();
|
|
toggleToolbarMenu('notifications');
|
|
});
|
|
els.overflowButton.addEventListener('click', (event) => {
|
|
event.stopPropagation();
|
|
toggleToolbarMenu('overflow');
|
|
});
|
|
els.notificationsMenu.addEventListener('click', (event) => event.stopPropagation());
|
|
els.overflowMenu.addEventListener('click', (event) => event.stopPropagation());
|
|
els.notificationsClear.addEventListener('click', () => {
|
|
notifications = [];
|
|
unreadNotifications = 0;
|
|
saveNotifications();
|
|
renderNotifications();
|
|
});
|
|
Array.prototype.forEach.call(els.overflowMenu.querySelectorAll('[data-menu-action]'), (item) => {
|
|
item.addEventListener('click', () => handleMenuAction(item.getAttribute('data-menu-action'), item));
|
|
});
|
|
document.addEventListener('click', closeToolbarMenus);
|
|
|
|
document.getElementById('login-form').addEventListener('submit', (e) => {
|
|
e.preventDefault();
|
|
const btn = document.getElementById('login-button');
|
|
const pw = document.getElementById('login-password').value;
|
|
document.getElementById('login-error').textContent = '';
|
|
btn.disabled = true;
|
|
btn.classList.add('busy');
|
|
PagerAPI.login('root', pw)
|
|
.then(() => { document.getElementById('login-password').value = ''; showApp(); toast('Logged in'); })
|
|
.catch((err) => {
|
|
document.getElementById('login-error').textContent = (err && err.message && err.message !== 'unauthorized')
|
|
? 'Login failed.' : 'Invalid credentials.';
|
|
})
|
|
.finally(() => { btn.disabled = false; btn.classList.remove('busy'); });
|
|
});
|
|
|
|
document.getElementById('terminal-btn').addEventListener('click', () => {
|
|
if (typeof Term === 'undefined') { toast('Terminal not available yet'); return; }
|
|
showDock('terminal');
|
|
Term.toggle();
|
|
});
|
|
document.getElementById('terminal-close').addEventListener('click', () => {
|
|
if (typeof Term === 'undefined') { toast('Terminal not available yet'); return; }
|
|
showDock('terminal');
|
|
Term.toggle();
|
|
});
|
|
document.getElementById('pager-btn').addEventListener('click', () => {
|
|
if (typeof Pager === 'undefined') { toast('Virtual Pager not available yet'); return; }
|
|
showDock('pager');
|
|
Pager.toggle();
|
|
});
|
|
document.getElementById('pager-close').addEventListener('click', () => {
|
|
if (typeof Pager === 'undefined') { toast('Virtual Pager not available yet'); return; }
|
|
showDock('pager');
|
|
Pager.toggle();
|
|
});
|
|
window.addEventListener('hashchange', route);
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape') {
|
|
closeToolbarMenus();
|
|
return;
|
|
}
|
|
if (localStorage.getItem('pw_hotkeys') === 'false') return;
|
|
const t = e.target;
|
|
if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.tagName === 'SELECT')) return;
|
|
if (e.ctrlKey || e.metaKey || e.altKey) return;
|
|
const map = { d: '#/dashboard', p: '#/pineap', r: '#/recon', l: '#/logging', m: '#/modules' };
|
|
if (map[e.key.toLowerCase()]) { location.hash = map[e.key.toLowerCase()]; }
|
|
else if (e.key === '`') {
|
|
if (typeof Term === 'undefined') return;
|
|
if (typeof Pager !== 'undefined' && Pager.isOpen()) Pager.close();
|
|
Term.toggle();
|
|
}
|
|
});
|
|
|
|
PagerAPI.setBase(API_BASE);
|
|
PagerAPI.on401(() => showLogin());
|
|
buildRail();
|
|
Theme.apply();
|
|
checkSession();
|
|
}
|
|
|
|
function checkSession() {
|
|
PagerAPI.get('/api/api_ping')
|
|
.then(() => showApp())
|
|
.catch(() => showLogin());
|
|
}
|
|
|
|
const routes = {
|
|
'#/dashboard': 'dashboard',
|
|
'#/pineap': 'pineap',
|
|
'#/pineap/evilwpa': 'pineap_evilwpa',
|
|
'#/pineap/enterprise': 'pineap_enterprise',
|
|
'#/pineap/open': 'pineap_open',
|
|
'#/pineap/impersonation': 'pineap_impersonation',
|
|
'#/pineap/clients': 'pineap_clients',
|
|
'#/pineap/filtering': 'pineap_filtering',
|
|
'#/recon': 'recon',
|
|
'#/recon/reports': 'recon_reports',
|
|
'#/recon/handshakes': 'recon_handshakes',
|
|
'#/evilportal': 'evilportal',
|
|
'#/logging': 'logging',
|
|
'#/logging/system': 'logging_system',
|
|
'#/modules': 'modules',
|
|
'#/modules/online': 'modules_online',
|
|
'#/modules/running': 'modules_running',
|
|
'#/modules/develop': 'modules_develop',
|
|
'#/settings': 'settings',
|
|
'#/settings/networking': 'settings_networking',
|
|
'#/settings/wifi': 'settings_wifi',
|
|
'#/settings/led': 'settings_led',
|
|
'#/settings/advanced': 'settings_advanced',
|
|
'#/settings/help': 'settings_help',
|
|
'#/harness': 'harness'
|
|
};
|
|
|
|
return { init, route, toast, showLogin, checkInternet, wsUrl: (p) => WS_BASE + p,
|
|
terminalWs: TERMINAL_WS, pagerScreenWs: PAGER_SCREEN_WS,
|
|
pagerKeysWs: PAGER_KEYS_WS, apiBase: API_BASE,
|
|
keyOf, railItems, go: (h) => { location.hash = h; },
|
|
get key() { return keyOf(location.hash); } };
|
|
})();
|
|
|
|
const Live = (() => {
|
|
let ws = null;
|
|
let ever = false;
|
|
let poll = null;
|
|
let pollHealthTimer = null;
|
|
let pollEventsTimer = null;
|
|
let pollRfTimer = null;
|
|
let rfState = null;
|
|
let rfChannel = null;
|
|
const lastEvents = { hsSeen: {}, hsPrimed: false, creds: null, credsPrimed: false,
|
|
pineapUp: null, mon0: null, mon1: null };
|
|
const subs = [];
|
|
let timer = null;
|
|
function stopPoll() {
|
|
if (poll) { clearInterval(poll); poll = null; }
|
|
}
|
|
function start() {
|
|
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) return;
|
|
stopPoll();
|
|
if (!pollHealthTimer) {
|
|
pollHealthTimer = setInterval(pollHealth, 15000);
|
|
pollHealth();
|
|
}
|
|
if (!pollEventsTimer) {
|
|
pollEventsTimer = setInterval(pollEvents, 15000);
|
|
pollEvents();
|
|
}
|
|
if (!pollRfTimer) {
|
|
pollRfTimer = setInterval(pollRfplan, 15000);
|
|
pollRfplan();
|
|
}
|
|
try { ws = new WebSocket(App.wsUrl('/api/ws')); }
|
|
catch (e) { fallback(); return; }
|
|
ws.onopen = () => { ever = true; };
|
|
ws.onmessage = (ev) => {
|
|
let msg;
|
|
try { msg = JSON.parse(ev.data); } catch (e) { return; }
|
|
subs.forEach((fn) => fn(msg));
|
|
updateBar(msg);
|
|
};
|
|
ws.onclose = () => {
|
|
ws = null;
|
|
clearTimeout(timer);
|
|
if (ever) timer = setTimeout(start, 5000);
|
|
else fallback();
|
|
};
|
|
ws.onerror = () => { try { ws.close(); } catch (e) {} };
|
|
}
|
|
function stop() {
|
|
ever = false;
|
|
clearTimeout(timer);
|
|
timer = null;
|
|
stopPoll();
|
|
if (ws) {
|
|
const active = ws;
|
|
ws = null;
|
|
active.onclose = null;
|
|
try { active.close(); } catch (e) {}
|
|
}
|
|
}
|
|
function fallback() {
|
|
stopPoll();
|
|
const seconds = Math.max(2, Math.min(120, parseInt(localStorage.getItem('pw-poll') || '5', 10) || 5));
|
|
poll = setInterval(async () => {
|
|
try {
|
|
const r = await PagerAPI.get('/api/status');
|
|
const msg = { type: 'tick', status: r.data, clients: r.data.clients };
|
|
subs.forEach((fn) => fn(msg));
|
|
updateBar(msg);
|
|
} catch (e) {}
|
|
}, seconds * 1000);
|
|
}
|
|
function updateBar(msg) {
|
|
const b = (msg.status || {}).battery || {};
|
|
const n = (msg.clients || []).length;
|
|
const el = document.getElementById('live-status');
|
|
if (el) el.textContent = 'BAT ' + (b.level == null ? '--' : b.level + '%' + (b.charging ? '+' : '')) + ' CLIENTS ' + n;
|
|
const wifi = (msg.status || {}).wifi;
|
|
if (Array.isArray(wifi)) {
|
|
const up = wifi.find((w) => w && w.iface === 'wlan1up');
|
|
rfChannel = up && up.channel != null ? Number(up.channel) : null;
|
|
renderRfChip();
|
|
}
|
|
}
|
|
function pollRfplan() {
|
|
fetch(App.apiBase + '/api/rfplan', { credentials: 'include' }).then((r) => {
|
|
if (!r.ok) throw new Error('http ' + r.status);
|
|
return r.json();
|
|
}).then((d) => {
|
|
rfState = d && typeof d.role === 'string' ? d : null;
|
|
renderRfChip();
|
|
}).catch(() => {
|
|
rfState = null;
|
|
const el = document.getElementById('rf-chip');
|
|
if (el) {
|
|
el.textContent = 'PHY1: ?';
|
|
el.title = 'RF plan unavailable';
|
|
el.className = 'health-chip warn';
|
|
}
|
|
});
|
|
}
|
|
function renderRfChip() {
|
|
const el = document.getElementById('rf-chip');
|
|
if (!el) return;
|
|
if (!rfState) {
|
|
el.textContent = '';
|
|
el.title = '';
|
|
el.className = 'health-chip';
|
|
return;
|
|
}
|
|
let text;
|
|
let cls = '';
|
|
if (rfState.role === 'uplink') {
|
|
text = 'PHY1: UPLINK' + (rfState.assoc && rfChannel ? ' ch' + rfChannel : '');
|
|
cls = rfState.assoc ? 'good' : 'warn';
|
|
} else if (rfState.role === 'attack') {
|
|
text = 'PHY1: ATTACK';
|
|
cls = 'warn';
|
|
} else if (rfState.role === 'idle') {
|
|
text = 'PHY1: IDLE';
|
|
} else {
|
|
text = 'PHY1: ' + String(rfState.role).toUpperCase();
|
|
}
|
|
el.textContent = text;
|
|
el.className = 'health-chip' + (cls ? ' ' + cls : '');
|
|
el.title = 'radio1 role: ' + rfState.role +
|
|
' \u00b7 assoc ' + (rfState.assoc || 'none') +
|
|
' \u00b7 hop ' + (rfState.hop_paused == null ? 'unknown' : rfState.hop_paused ? 'paused' : 'running');
|
|
}
|
|
function pollHealth() {
|
|
fetch(App.apiBase + '/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.env && h.env.overall === 'fail') {
|
|
el.textContent = 'ENV CHECK FAIL';
|
|
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';
|
|
}
|
|
const prev = lastEvents;
|
|
if (prev.pineapUp === false && h.pineap_up) {
|
|
toast('PineAPd recovered', 'success');
|
|
} else if (prev.pineapUp === true && h.pineap_up === false) {
|
|
toast('PineAPd is down — health monitor is repairing it', 'error');
|
|
}
|
|
lastEvents.pineapUp = !!h.pineap_up;
|
|
const monState = [h.wlan0mon_up, h.wlan1mon_up];
|
|
if (prev.mon0 === true && monState[0] === false) toast('wlan0mon went down', 'error');
|
|
if (prev.mon1 === true && monState[1] === false) toast('wlan1mon went down', 'error');
|
|
lastEvents.mon0 = !!monState[0];
|
|
lastEvents.mon1 = !!monState[1];
|
|
}).catch(() => {});
|
|
}
|
|
function pollEvents() {
|
|
Promise.all([
|
|
fetch(App.apiBase + '/api/pineap/handshakes', { credentials: 'include' }).then((r) => r.json()).catch(() => ({})),
|
|
fetch(App.apiBase + '/api/attacks/status', { credentials: 'include' }).then((r) => r.json()).catch(() => ({}))
|
|
]).then(([hs, atk]) => {
|
|
const files = (hs && hs.files) || [];
|
|
const fresh = files.filter((f) => !lastEvents.hsSeen[f.name]);
|
|
if (lastEvents.hsPrimed && fresh.length) {
|
|
fresh.forEach((f) => {
|
|
const bssid = (f.name.match(/^[0-9]+_([0-9A-F]+)_/) || [])[1] || '';
|
|
toast('Handshake captured: ' + (bssid || f.name), 'success');
|
|
});
|
|
}
|
|
files.forEach((f) => { lastEvents.hsSeen[f.name] = true; });
|
|
lastEvents.hsPrimed = true;
|
|
const creds = ((atk.enterprise || {}).creds) == null ? null : atk.enterprise.creds;
|
|
if (lastEvents.credsPrimed && creds !== null && creds > lastEvents.creds) {
|
|
toast('Enterprise credential captured (' + (creds - lastEvents.creds) + ' new)', 'success');
|
|
}
|
|
if (creds !== null) lastEvents.creds = creds;
|
|
lastEvents.credsPrimed = true;
|
|
}).catch(() => {});
|
|
}
|
|
function onTick(fn) {
|
|
subs.push(fn);
|
|
return () => {
|
|
const i = subs.indexOf(fn);
|
|
if (i !== -1) subs.splice(i, 1);
|
|
};
|
|
}
|
|
return { start, stop, onTick };
|
|
})();
|
|
|
|
document.addEventListener('DOMContentLoaded', () => App.init());
|