Streamable-HTTP MCP server on POST /mcp: device.state, attack.deploy/stop/ status/deauth/capture/export_hc22000, loot.handshakes/enterprise_creds, recon.aps/isearch/devices, pineap.kick_client/set_filter tools; recon DB + bundled opencode skills resources; attack playbook prompts. Cookie or Bearer auth. Harness page shows endpoint, token, curl snippet, capability explorer and a copy-paste pi.dev prompt. scripts/harness_stdio.py for stdio-only agents.
503 lines
19 KiB
JavaScript
503 lines
19 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: 'attacks', label: 'Attacks', hash: '#/attacks', icon: 'attack' },
|
|
{ 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: 'extension' },
|
|
{ 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();
|
|
const hash = (location.hash || '#/dashboard').replace(/\/+$/, '');
|
|
if (hash === '#/recon/survey') {
|
|
location.hash = '#/recon';
|
|
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) {
|
|
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') {
|
|
PagerAPI.post('/api/logout')
|
|
.then(() => showLogin())
|
|
.catch((error) => toast(error.message || 'Logout failed', 'error'));
|
|
} else if (action === 'reboot') {
|
|
if (!window.confirm('Reboot Mark VIII now?')) return;
|
|
PagerAPI.post('/api/settings/reboot')
|
|
.then(() => toast('Reboot requested. Mark VIII will disconnect shortly.'))
|
|
.catch((error) => toast(error.message || 'Reboot failed', 'error'));
|
|
}
|
|
}
|
|
|
|
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')));
|
|
});
|
|
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;
|
|
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; });
|
|
});
|
|
|
|
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',
|
|
'#/attacks': 'attacks',
|
|
'#/attacks/wpa': 'attacks_wpa',
|
|
'#/attacks/open': 'attacks_open',
|
|
'#/attacks/enterprise': 'attacks_enterprise',
|
|
'#/pineap': 'pineap',
|
|
'#/pineap/open': 'pineap_open',
|
|
'#/pineap/evilwpa': 'pineap_evilwpa',
|
|
'#/pineap/impersonation': 'pineap_impersonation',
|
|
'#/pineap/clients': 'pineap_clients',
|
|
'#/pineap/filtering': 'pineap_filtering',
|
|
'#/recon': 'recon',
|
|
'#/recon/reports': 'recon_reports',
|
|
'#/recon/handshakes': 'recon_handshakes',
|
|
'#/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;
|
|
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();
|
|
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;
|
|
}
|
|
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());
|