mirror of
https://github.com/infinition/Bjorn.git
synced 2026-03-15 08:52:00 +00:00
- Implemented LokiUtils class with GET and POST endpoints for managing scripts, jobs, and payloads. - Added SentinelUtils class with GET and POST endpoints for managing events, rules, devices, and notifications. - Both classes include error handling and JSON response formatting.
537 lines
18 KiB
JavaScript
537 lines
18 KiB
JavaScript
/**
|
|
* Loki — HID Attack Suite SPA page
|
|
* Script editor, library, job management, quick-type.
|
|
*/
|
|
import { ResourceTracker } from '../core/resource-tracker.js';
|
|
import { api, Poller } from '../core/api.js';
|
|
import { el, $, $$, empty, toast, escapeHtml, confirmT } from '../core/dom.js';
|
|
import { t } from '../core/i18n.js';
|
|
|
|
const PAGE = 'loki';
|
|
|
|
/* ── State ─────────────────────────────────────────────── */
|
|
|
|
let tracker = null;
|
|
let poller = null;
|
|
let root = null;
|
|
|
|
let lokiEnabled = false;
|
|
let status = {};
|
|
let scripts = [];
|
|
let payloads = [];
|
|
let jobs = [];
|
|
let layouts = ['us'];
|
|
let currentScript = { id: null, name: '', content: '' };
|
|
|
|
/* ── Lifecycle ─────────────────────────────────────────── */
|
|
|
|
export async function mount(container) {
|
|
tracker = new ResourceTracker(PAGE);
|
|
root = buildShell();
|
|
container.appendChild(root);
|
|
bindEvents();
|
|
await refresh();
|
|
poller = new Poller(refreshJobs, 4000);
|
|
poller.start();
|
|
}
|
|
|
|
export function unmount() {
|
|
if (poller) { poller.stop(); poller = null; }
|
|
if (tracker) { tracker.cleanupAll(); tracker = null; }
|
|
root = null;
|
|
scripts = [];
|
|
payloads = [];
|
|
jobs = [];
|
|
}
|
|
|
|
/* ── Shell ─────────────────────────────────────────────── */
|
|
|
|
function buildShell() {
|
|
return el('div', { class: 'loki-page' }, [
|
|
|
|
/* ── Header ───────────────────────────────────────── */
|
|
el('div', { class: 'loki-header' }, [
|
|
el('h1', { class: 'loki-title' }, [
|
|
el('span', { class: 'loki-title-icon' }, ['\uD83D\uDC0D']),
|
|
el('span', { 'data-i18n': 'loki.title' }, [t('loki.title')]),
|
|
]),
|
|
el('div', { class: 'loki-controls' }, [
|
|
el('span', { 'data-i18n': 'loki.enable' }, [t('loki.enable')]),
|
|
el('input', { type: 'checkbox', class: 'loki-toggle', id: 'loki-toggle' }),
|
|
]),
|
|
]),
|
|
|
|
/* ── Status bar ───────────────────────────────────── */
|
|
el('div', { class: 'loki-status-bar', id: 'loki-status-bar' }),
|
|
|
|
/* ── Grid: editor + library ───────────────────────── */
|
|
el('div', { class: 'loki-grid', id: 'loki-grid' }, [
|
|
|
|
/* Editor column */
|
|
el('div', { class: 'loki-editor-panel' }, [
|
|
el('textarea', {
|
|
class: 'loki-editor',
|
|
id: 'loki-editor',
|
|
spellcheck: 'false',
|
|
placeholder: '// HIDScript editor\nlayout(\'us\');\ndelay(1000);\npress("GUI r");\ndelay(500);\ntype("notepad\\n");\ndelay(1000);\ntype("Hello from Loki!");',
|
|
}),
|
|
el('div', { class: 'loki-editor-toolbar' }, [
|
|
el('button', { class: 'loki-btn primary', id: 'loki-run' }, ['\u25B6 ', t('loki.run')]),
|
|
el('button', { class: 'loki-btn', id: 'loki-save' }, ['\uD83D\uDCBE ', t('loki.save')]),
|
|
el('button', { class: 'loki-btn', id: 'loki-new' }, ['\uD83D\uDCC4 ', t('loki.new')]),
|
|
el('select', { id: 'loki-layout-select' }),
|
|
]),
|
|
/* Quick type row */
|
|
el('div', { class: 'loki-quick-row' }, [
|
|
el('input', {
|
|
type: 'text', class: 'loki-quick-input', id: 'loki-quick-input',
|
|
placeholder: t('loki.quick_placeholder'),
|
|
}),
|
|
el('button', { class: 'loki-btn', id: 'loki-quick-send' }, [t('loki.quick_send')]),
|
|
]),
|
|
]),
|
|
|
|
/* Library column */
|
|
el('div', { class: 'loki-library' }, [
|
|
/* Payloads section */
|
|
el('div', { class: 'loki-library-section' }, [
|
|
el('div', { class: 'loki-library-heading', id: 'loki-payloads-heading' }, [t('loki.payloads')]),
|
|
el('ul', { class: 'loki-library-list', id: 'loki-payloads-list' }),
|
|
]),
|
|
/* Custom scripts section */
|
|
el('div', { class: 'loki-library-section' }, [
|
|
el('div', { class: 'loki-library-heading', id: 'loki-scripts-heading' }, [t('loki.custom_scripts')]),
|
|
el('ul', { class: 'loki-library-list', id: 'loki-scripts-list' }),
|
|
]),
|
|
]),
|
|
]),
|
|
|
|
/* ── Jobs panel ───────────────────────────────────── */
|
|
el('div', { class: 'loki-jobs' }, [
|
|
el('div', { class: 'loki-jobs-header' }, [
|
|
el('h3', {}, [t('loki.jobs')]),
|
|
el('button', { class: 'loki-btn', id: 'loki-clear-jobs' }, [t('loki.clear_completed')]),
|
|
]),
|
|
el('div', { id: 'loki-jobs-body' }),
|
|
]),
|
|
]);
|
|
}
|
|
|
|
/* ── Events ────────────────────────────────────────────── */
|
|
|
|
function bindEvents() {
|
|
// Toggle enable/disable
|
|
const tog = $('#loki-toggle', root);
|
|
if (tog) tog.addEventListener('change', async () => {
|
|
const enabled = tog.checked;
|
|
const res = await api.post('/api/loki/toggle', { enabled });
|
|
if (res?.status === 'ok') {
|
|
lokiEnabled = enabled;
|
|
toast(enabled ? t('loki.enabled_msg') : t('loki.disabled_msg'));
|
|
await refresh();
|
|
}
|
|
});
|
|
|
|
// Run
|
|
const runBtn = $('#loki-run', root);
|
|
if (runBtn) runBtn.addEventListener('click', runScript);
|
|
|
|
// Save
|
|
const saveBtn = $('#loki-save', root);
|
|
if (saveBtn) saveBtn.addEventListener('click', saveScript);
|
|
|
|
// New
|
|
const newBtn = $('#loki-new', root);
|
|
if (newBtn) newBtn.addEventListener('click', newScript);
|
|
|
|
// Quick type
|
|
const quickBtn = $('#loki-quick-send', root);
|
|
if (quickBtn) quickBtn.addEventListener('click', quickType);
|
|
const quickInput = $('#loki-quick-input', root);
|
|
if (quickInput) quickInput.addEventListener('keydown', e => {
|
|
if (e.key === 'Enter') quickType();
|
|
});
|
|
|
|
// Clear jobs
|
|
const clearBtn = $('#loki-clear-jobs', root);
|
|
if (clearBtn) clearBtn.addEventListener('click', async () => {
|
|
await api.post('/api/loki/jobs/clear', {});
|
|
await refreshJobs();
|
|
});
|
|
|
|
// Layout select
|
|
const layoutSel = $('#loki-layout-select', root);
|
|
if (layoutSel) layoutSel.addEventListener('change', () => {
|
|
// Layout is sent per-run, stored in editor state
|
|
});
|
|
|
|
// Tab on editor inserts two spaces
|
|
const editor = $('#loki-editor', root);
|
|
if (editor) editor.addEventListener('keydown', e => {
|
|
if (e.key === 'Tab') {
|
|
e.preventDefault();
|
|
const start = editor.selectionStart;
|
|
const end = editor.selectionEnd;
|
|
editor.value = editor.value.substring(0, start) + ' ' + editor.value.substring(end);
|
|
editor.selectionStart = editor.selectionEnd = start + 2;
|
|
}
|
|
});
|
|
}
|
|
|
|
/* ── Data fetch ────────────────────────────────────────── */
|
|
|
|
async function refresh() {
|
|
const [sRes, scrRes, payRes, jobRes, layRes] = await Promise.all([
|
|
api.get('/api/loki/status'),
|
|
api.get('/api/loki/scripts'),
|
|
api.get('/api/loki/payloads'),
|
|
api.get('/api/loki/jobs'),
|
|
api.get('/api/loki/layouts'),
|
|
]);
|
|
|
|
if (sRes) { status = sRes; lokiEnabled = sRes.enabled; }
|
|
if (scrRes) scripts = scrRes.scripts || [];
|
|
if (payRes) payloads = payRes.payloads || [];
|
|
if (jobRes) jobs = jobRes.jobs || [];
|
|
if (layRes) layouts = layRes.layouts || ['us'];
|
|
|
|
paint();
|
|
}
|
|
|
|
async function refreshJobs() {
|
|
const [sRes, jobRes] = await Promise.all([
|
|
api.get('/api/loki/status'),
|
|
api.get('/api/loki/jobs'),
|
|
]);
|
|
if (sRes) { status = sRes; lokiEnabled = sRes.enabled; }
|
|
if (jobRes) jobs = jobRes.jobs || [];
|
|
paintStatus();
|
|
paintJobs();
|
|
}
|
|
|
|
/* ── Render ────────────────────────────────────────────── */
|
|
|
|
function paint() {
|
|
paintToggle();
|
|
paintStatus();
|
|
paintLayouts();
|
|
paintPayloads();
|
|
paintScripts();
|
|
paintJobs();
|
|
paintDisabledState();
|
|
}
|
|
|
|
function paintToggle() {
|
|
const tog = $('#loki-toggle', root);
|
|
if (tog) tog.checked = lokiEnabled;
|
|
}
|
|
|
|
function paintStatus() {
|
|
const bar = $('#loki-status-bar', root);
|
|
if (!bar) return;
|
|
empty(bar);
|
|
|
|
const running = status.running;
|
|
const gadget = status.gadget_ready;
|
|
const installed = status.gadget_installed !== false;
|
|
|
|
if (!installed) {
|
|
bar.append(
|
|
statusItem(t('loki.gadget_label'), t('loki.not_installed') || 'Not installed', false),
|
|
);
|
|
return;
|
|
}
|
|
|
|
bar.append(
|
|
statusItem(t('loki.status_label'), running ? t('loki.running') : t('loki.idle'), running),
|
|
statusItem(t('loki.gadget_label'), gadget ? t('loki.ready') : t('loki.not_ready'), gadget),
|
|
statusItem(t('loki.layout_label'), (status.layout || 'us').toUpperCase()),
|
|
statusItem(t('loki.jobs_label'), `${status.jobs_running || 0} ${t('loki.running_lc')}`),
|
|
);
|
|
}
|
|
|
|
function statusItem(label, value, dotState) {
|
|
const children = [];
|
|
if (dotState !== undefined) {
|
|
children.push(el('span', { class: `dot ${dotState ? 'on' : 'off'}` }));
|
|
}
|
|
children.push(el('span', { class: 'label' }, [label + ': ']));
|
|
children.push(el('span', { class: 'value' }, [String(value)]));
|
|
return el('span', { class: 'loki-status-item' }, children);
|
|
}
|
|
|
|
function paintLayouts() {
|
|
const sel = $('#loki-layout-select', root);
|
|
if (!sel) return;
|
|
empty(sel);
|
|
for (const lay of layouts) {
|
|
const opt = el('option', { value: lay }, [lay.toUpperCase()]);
|
|
if (lay === (status.layout || 'us')) opt.selected = true;
|
|
sel.appendChild(opt);
|
|
}
|
|
}
|
|
|
|
function paintPayloads() {
|
|
const list = $('#loki-payloads-list', root);
|
|
if (!list) return;
|
|
empty(list);
|
|
for (const p of payloads) {
|
|
const item = el('li', { class: 'loki-library-item' }, [
|
|
el('span', { class: 'name', title: p.description || '' }, [p.name]),
|
|
]);
|
|
item.addEventListener('click', () => loadPayload(p));
|
|
list.appendChild(item);
|
|
}
|
|
if (!payloads.length) {
|
|
list.appendChild(el('li', { class: 'loki-library-item' }, [
|
|
el('span', { class: 'name', style: 'color:var(--muted)' }, [t('loki.no_payloads')]),
|
|
]));
|
|
}
|
|
}
|
|
|
|
function paintScripts() {
|
|
const list = $('#loki-scripts-list', root);
|
|
if (!list) return;
|
|
empty(list);
|
|
for (const s of scripts) {
|
|
const item = el('li', {
|
|
class: `loki-library-item${currentScript.id === s.id ? ' active' : ''}`,
|
|
}, [
|
|
el('span', { class: 'name' }, [s.name]),
|
|
el('button', {
|
|
class: 'loki-btn danger',
|
|
style: 'padding:2px 6px;font-size:0.65rem;',
|
|
title: t('loki.delete'),
|
|
}, ['\u2715']),
|
|
]);
|
|
// Click name → load
|
|
item.querySelector('.name').addEventListener('click', () => loadScript(s));
|
|
// Click delete
|
|
item.querySelector('.loki-btn').addEventListener('click', async (e) => {
|
|
e.stopPropagation();
|
|
if (!confirm(t('loki.confirm_delete', { name: s.name }))) return;
|
|
await api.post('/api/loki/script/delete', { id: s.id });
|
|
await refresh();
|
|
});
|
|
list.appendChild(item);
|
|
}
|
|
if (!scripts.length) {
|
|
list.appendChild(el('li', { class: 'loki-library-item' }, [
|
|
el('span', { class: 'name', style: 'color:var(--muted)' }, [t('loki.no_scripts')]),
|
|
]));
|
|
}
|
|
}
|
|
|
|
function paintJobs() {
|
|
const body = $('#loki-jobs-body', root);
|
|
if (!body) return;
|
|
empty(body);
|
|
|
|
if (!jobs.length) {
|
|
body.appendChild(el('div', { class: 'loki-jobs-empty' }, [t('loki.no_jobs')]));
|
|
return;
|
|
}
|
|
|
|
const table = el('table', { class: 'loki-jobs-table' }, [
|
|
el('thead', {}, [
|
|
el('tr', {}, [
|
|
el('th', {}, ['ID']),
|
|
el('th', {}, [t('loki.script')]),
|
|
el('th', {}, [t('loki.status_col')]),
|
|
el('th', {}, [t('loki.started')]),
|
|
el('th', {}, [t('loki.actions')]),
|
|
]),
|
|
]),
|
|
el('tbody', {}, jobs.slice(0, 20).map(j => {
|
|
const badge = el('span', { class: `loki-badge ${j.status}` }, [
|
|
statusIcon(j.status), ' ', j.status,
|
|
]);
|
|
const row = el('tr', {}, [
|
|
el('td', {}, [j.id ? j.id.substring(0, 6) : '...']),
|
|
el('td', {}, [j.script_name || '-']),
|
|
el('td', {}, [badge]),
|
|
el('td', {}, [formatTime(j.started_at)]),
|
|
el('td', {}),
|
|
]);
|
|
const actions = row.lastChild;
|
|
if (j.status === 'running') {
|
|
const cancelBtn = el('button', { class: 'loki-btn danger', style: 'padding:2px 8px;font-size:0.7rem;' }, [t('loki.cancel')]);
|
|
cancelBtn.addEventListener('click', async () => {
|
|
await api.post('/api/loki/job/cancel', { job_id: j.id });
|
|
await refreshJobs();
|
|
});
|
|
actions.appendChild(cancelBtn);
|
|
}
|
|
if (j.output) {
|
|
const outBtn = el('button', { class: 'loki-btn', style: 'padding:2px 8px;font-size:0.7rem;' }, [t('loki.output')]);
|
|
outBtn.addEventListener('click', () => {
|
|
alert(j.output || t('loki.no_output'));
|
|
});
|
|
actions.appendChild(outBtn);
|
|
}
|
|
return row;
|
|
})),
|
|
]);
|
|
body.appendChild(table);
|
|
}
|
|
|
|
function paintDisabledState() {
|
|
const grid = $('#loki-grid', root);
|
|
if (!grid) return;
|
|
|
|
const installed = status.gadget_installed !== false;
|
|
|
|
if (!installed) {
|
|
grid.classList.add('loki-disabled-overlay');
|
|
// Show install banner
|
|
let banner = $('#loki-install-banner', root);
|
|
if (!banner) {
|
|
banner = el('div', { id: 'loki-install-banner', class: 'loki-install-banner' }, [
|
|
el('p', {}, [t('loki.install_msg') || 'HID gadget not installed. Install it and reboot to enable Loki.']),
|
|
el('button', { class: 'loki-btn primary', id: 'loki-install-btn' }, [
|
|
t('loki.install_btn') || 'Install HID Gadget & Reboot',
|
|
]),
|
|
]);
|
|
grid.parentNode.insertBefore(banner, grid);
|
|
$('#loki-install-btn', root).addEventListener('click', installGadget);
|
|
}
|
|
} else if (!lokiEnabled) {
|
|
grid.classList.add('loki-disabled-overlay');
|
|
// Remove install banner if present
|
|
const banner = $('#loki-install-banner', root);
|
|
if (banner) banner.remove();
|
|
} else {
|
|
grid.classList.remove('loki-disabled-overlay');
|
|
const banner = $('#loki-install-banner', root);
|
|
if (banner) banner.remove();
|
|
}
|
|
}
|
|
|
|
async function installGadget() {
|
|
const btn = $('#loki-install-btn', root);
|
|
if (btn) { btn.disabled = true; btn.textContent = 'Installing...'; }
|
|
|
|
const res = await api.post('/api/loki/install', {});
|
|
if (res?.success) {
|
|
toast(res.message || 'Installed!');
|
|
if (res.reboot_required) {
|
|
if (confirm(t('loki.reboot_confirm') || 'HID gadget installed. Reboot now?')) {
|
|
await api.post('/api/loki/reboot', {});
|
|
toast('Rebooting...');
|
|
}
|
|
}
|
|
} else {
|
|
toast(res?.message || 'Installation failed', 'error');
|
|
if (btn) { btn.disabled = false; btn.textContent = t('loki.install_btn') || 'Install HID Gadget & Reboot'; }
|
|
}
|
|
}
|
|
|
|
/* ── Actions ───────────────────────────────────────────── */
|
|
|
|
async function runScript() {
|
|
const editor = $('#loki-editor', root);
|
|
if (!editor) return;
|
|
const content = editor.value.trim();
|
|
if (!content) { toast(t('loki.empty_script'), 'warn'); return; }
|
|
|
|
const name = currentScript.name || 'editor';
|
|
const res = await api.post('/api/loki/script/run', { content, name });
|
|
if (res?.status === 'ok') {
|
|
toast(t('loki.job_started', { id: res.job_id }));
|
|
await refreshJobs();
|
|
} else {
|
|
toast(res?.message || t('loki.run_error'), 'error');
|
|
}
|
|
}
|
|
|
|
async function saveScript() {
|
|
const editor = $('#loki-editor', root);
|
|
if (!editor) return;
|
|
const content = editor.value.trim();
|
|
if (!content) { toast(t('loki.empty_script'), 'warn'); return; }
|
|
|
|
let name = currentScript.name;
|
|
if (!name) {
|
|
name = prompt(t('loki.script_name_prompt'), 'my_script');
|
|
if (!name) return;
|
|
}
|
|
|
|
const res = await api.post('/api/loki/script/save', {
|
|
id: currentScript.id || undefined,
|
|
name,
|
|
content,
|
|
description: '',
|
|
});
|
|
if (res?.status === 'ok') {
|
|
toast(t('loki.saved'));
|
|
currentScript.name = name;
|
|
await refresh();
|
|
} else {
|
|
toast(res?.message || t('loki.save_error'), 'error');
|
|
}
|
|
}
|
|
|
|
function newScript() {
|
|
const editor = $('#loki-editor', root);
|
|
if (editor) editor.value = '';
|
|
currentScript = { id: null, name: '', content: '' };
|
|
paintScripts();
|
|
}
|
|
|
|
async function loadScript(s) {
|
|
// Fetch full content
|
|
const res = await api.get(`/api/loki/script?id=${s.id}`);
|
|
if (res?.script) {
|
|
const editor = $('#loki-editor', root);
|
|
if (editor) editor.value = res.script.content || '';
|
|
currentScript = { id: s.id, name: s.name, content: res.script.content };
|
|
paintScripts();
|
|
}
|
|
}
|
|
|
|
function loadPayload(p) {
|
|
const editor = $('#loki-editor', root);
|
|
if (editor) editor.value = p.content || '';
|
|
currentScript = { id: null, name: p.name, content: p.content };
|
|
paintScripts();
|
|
}
|
|
|
|
async function quickType() {
|
|
const input = $('#loki-quick-input', root);
|
|
if (!input) return;
|
|
const text = input.value;
|
|
if (!text) return;
|
|
|
|
const res = await api.post('/api/loki/quick', { text });
|
|
if (res?.status === 'ok') {
|
|
toast(t('loki.quick_sent'));
|
|
input.value = '';
|
|
await refreshJobs();
|
|
} else {
|
|
toast(res?.message || t('loki.quick_error'), 'error');
|
|
}
|
|
}
|
|
|
|
/* ── Helpers ───────────────────────────────────────────── */
|
|
|
|
function statusIcon(status) {
|
|
switch (status) {
|
|
case 'running': return '\u26A1';
|
|
case 'succeeded': return '\u2705';
|
|
case 'failed': return '\u274C';
|
|
case 'cancelled': return '\u23F9';
|
|
case 'pending': return '\u23F3';
|
|
default: return '\u2022';
|
|
}
|
|
}
|
|
|
|
function formatTime(iso) {
|
|
if (!iso) return '-';
|
|
try {
|
|
const d = new Date(iso);
|
|
return d.toLocaleTimeString();
|
|
} catch {
|
|
return iso;
|
|
}
|
|
}
|