'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: '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 = '' + PineappleIcons[it.icon] + '' + it.label + ''; 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 = '' + PineappleIcons.chevron_right + 'Open Menu'; 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', '#/pineap/evilportal': 'pineap_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());