espilon-source/tools/C3PO/templates/terminal.html
Eun0us 79c2a4d4bf c3po: full server rewrite with modular routes and honeypot dashboard
Replace monolithic CLI and web server with route-based Flask API.
New routes: api_commands, api_build, api_can, api_monitor, api_ota,
api_tunnel. Add honeypot security dashboard with real-time SSE,
MITRE ATT&CK mapping, kill chain analysis.

New TUI with commander/help modules. Add session management,
tunnel proxy core, CAN bus data store. Docker support.
2026-02-28 20:12:27 +01:00

298 lines
14 KiB
HTML

{% extends "base.html" %}
{% block title %}Terminal - ESPILON{% endblock %}
{% block content %}
<div class="page" x-data="terminalApp()" x-init="init()">
<div class="split-h" style="flex:1;">
<!-- Command Terminal -->
<div class="panel flex-1">
<div class="toolbar">
<span class="toolbar-label">Command</span>
<select class="select" x-model="selectedDevice" @change="updatePrompt()">
<option value="all">all devices</option>
<template x-for="d in $store.app.devices" :key="d.id">
<option :value="d.id" x-text="d.id + (d.status==='Connected' ? '' : ' (offline)')"></option>
</template>
</select>
<div class="toolbar-sep"></div>
<button class="btn btn-sm" @click="showMonitor = !showMonitor; if(showMonitor) refreshPorts();"
:class="showMonitor ? 'btn-primary' : ''">Monitor</button>
<button class="btn btn-sm" @click="lines = []">Clear</button>
</div>
<div class="term-output" id="term-out" x-ref="termOut">
<div class="term-line term-system">ESPILON C2 Terminal — Type a command and press Enter.</div>
<template x-for="(l, i) in lines" :key="i">
<div class="term-line" :class="l.cls || ''" x-html="l.html"></div>
</template>
</div>
<div class="term-input-row">
<span class="term-prompt" x-html="promptHtml"></span>
<input type="text" class="term-input" x-model="inputText" @keydown="handleKey($event)"
autocomplete="off" spellcheck="false" placeholder="system_info" x-ref="termInput">
</div>
</div>
<!-- Serial Monitor (toggleable) -->
<template x-if="showMonitor">
<div class="flex" style="flex:0 0 auto;">
<div class="resizer" @mousedown="startResize($event)"></div>
<div class="panel" :style="'width:' + monitorWidth + 'px'">
<div class="toolbar">
<span class="toolbar-label">Serial</span>
<select class="select" x-model="selectedPort">
<option value="">select port...</option>
<template x-for="p in ports" :key="p.port">
<option :value="p.port" x-text="p.port + (p.device_id ? ' (' + p.device_id + ')' : '') + (p.monitoring ? ' *' : '')"></option>
</template>
</select>
<button class="btn btn-sm" :class="monitorConnected ? 'btn-danger' : 'btn-success'"
@click="monitorConnected ? disconnectMonitor() : connectMonitor()"
x-text="monitorConnected ? 'Disconnect' : 'Connect'"></button>
<button class="btn btn-sm" @click="monitorLines = []">Clear</button>
<button class="btn btn-sm" @click="showMonitor = false; disconnectMonitor();">X</button>
</div>
<div class="term-output">
<template x-for="(l, i) in monitorLines" :key="i">
<div class="term-line" :class="l.cls || ''" x-text="l.text"></div>
</template>
</div>
</div>
</div>
</template>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function terminalApp() {
const LOCAL_COMMANDS = ['help', 'clear', 'devices'];
const MODULE_COMMANDS = {
system: ['system_info', 'system_mem', 'system_uptime', 'system_reboot'],
network: ['ping', 'arp_scan', 'proxy_start', 'proxy_stop', 'dos_tcp'],
fakeap: ['fakeap_start', 'fakeap_stop', 'fakeap_status', 'fakeap_clients',
'fakeap_portal_start', 'fakeap_portal_stop',
'fakeap_sniffer_on', 'fakeap_sniffer_off'],
recon: ['cam_start', 'cam_stop', 'mlat', 'trilat'],
honeypot: ['hp_svc', 'hp_wifi', 'hp_net', 'hp_status', 'hp_config_set', 'hp_config_get', 'hp_config_list', 'hp_config_reset'],
ota: ['ota_update', 'ota_status'],
canbus: ['can_start', 'can_stop', 'can_send', 'can_sniff', 'can_record', 'can_replay',
'can_dump', 'can_status', 'can_filter_add', 'can_filter_del', 'can_filter_list', 'can_filter_clear',
'can_scan_ecu', 'can_uds', 'can_uds_session', 'can_uds_read', 'can_uds_dump', 'can_uds_auth',
'can_obd', 'can_obd_vin', 'can_obd_dtc', 'can_obd_supported', 'can_obd_monitor', 'can_obd_monitor_stop'],
redteam: ['rt_hunt', 'rt_stop', 'rt_status', 'rt_scan', 'rt_net_add', 'rt_net_list', 'rt_mesh'],
};
return {
selectedDevice: 'all',
inputText: '',
lines: [],
promptHtml: '<span style="color:var(--accent)">all</span>&gt;',
// Command history
history: JSON.parse(localStorage.getItem('term_history') || '[]'),
historyIdx: -1,
// Polling
pendingPolls: {},
// Monitor
showMonitor: false,
monitorWidth: 450,
selectedPort: '',
ports: [],
monitorLines: [],
monitorConnected: false,
monitorES: null,
init() {
this.$nextTick(() => this.$refs.termInput && this.$refs.termInput.focus());
},
updatePrompt() {
this.promptHtml = '<span style="color:var(--accent)">' + escapeHtml(this.selectedDevice) + '</span>&gt;';
},
appendLine(html, cls) {
this.lines.push({ html, cls });
if (this.lines.length > 2000) this.lines.splice(0, this.lines.length - 1500);
this.$nextTick(() => {
const el = this.$refs.termOut;
if (el && el.scrollHeight - el.scrollTop - el.clientHeight < 200) {
el.scrollTop = el.scrollHeight;
}
});
},
handleKey(e) {
if (e.key === 'Enter') {
const line = this.inputText.trim();
if (!line) return;
this.history.unshift(line);
if (this.history.length > 100) this.history.length = 100;
try { localStorage.setItem('term_history', JSON.stringify(this.history)); } catch(ex) {}
this.historyIdx = -1;
this.inputText = '';
this.execute(line);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
if (this.historyIdx < this.history.length - 1) { this.historyIdx++; this.inputText = this.history[this.historyIdx]; }
} else if (e.key === 'ArrowDown') {
e.preventDefault();
if (this.historyIdx > 0) { this.historyIdx--; this.inputText = this.history[this.historyIdx]; }
else { this.historyIdx = -1; this.inputText = ''; }
} else if (e.key === 'Tab') {
e.preventDefault();
if (!this.inputText) return;
const cmds = this.getAutocomplete();
const matches = cmds.filter(c => c.startsWith(this.inputText));
if (matches.length === 1) this.inputText = matches[0] + ' ';
else if (matches.length > 1) this.appendLine(' ' + matches.join(' '), 'term-system');
} else if (e.key === 'l' && e.ctrlKey) {
e.preventDefault();
this.lines = [];
}
},
getAutocomplete() {
let cmds = [...LOCAL_COMMANDS, ...MODULE_COMMANDS.system];
if (this.selectedDevice === 'all') {
Object.values(MODULE_COMMANDS).forEach(c => cmds.push(...c));
} else {
const dev = this.$store.app.devices.find(d => d.id === this.selectedDevice);
if (dev && dev.modules) {
dev.modules.split(',').forEach(m => {
if (MODULE_COMMANDS[m]) cmds.push(...MODULE_COMMANDS[m]);
});
}
}
return [...new Set(cmds)];
},
async execute(line) {
const parts = line.split(/\s+/);
const cmd = parts[0];
const argv = parts.slice(1);
if (cmd === 'clear') { this.lines = []; return; }
if (cmd === 'help') {
this.appendLine('Available commands:', 'term-system');
for (const [mod, cmds] of Object.entries(MODULE_COMMANDS)) {
this.appendLine(' <span class="term-cmd">' + mod + ':</span> ' + cmds.join(', '));
}
this.appendLine(' <span class="term-cmd">local:</span> ' + LOCAL_COMMANDS.join(', '));
return;
}
if (cmd === 'devices') {
await this.$store.app.fetchDevices();
const devs = this.$store.app.devices;
if (!devs.length) { this.appendLine('No devices.', 'term-error'); return; }
devs.forEach(d => {
const st = d.status === 'Connected'
? '<span class="term-success">online</span>'
: '<span class="term-error">offline</span>';
this.appendLine('<span class="term-device">' + escapeHtml(d.id) + '</span> ' + st + ' ' + escapeHtml(d.ip||'') + ' [' + escapeHtml(d.modules||'') + ']');
});
return;
}
const target = this.selectedDevice;
const deviceIds = target === 'all' ? 'all' : [target];
this.appendLine('<span class="term-cmd">' + escapeHtml(target) + '&gt; ' + escapeHtml(line) + '</span>');
try {
const res = await fetch('/api/commands', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ device_ids: deviceIds, command: cmd, argv })
});
const data = await res.json();
if (data.error) { this.appendLine('Error: ' + escapeHtml(data.error), 'term-error'); return; }
for (const r of (data.results || [])) {
if (r.status === 'ok') {
const idx = this.lines.length;
this.appendLine('<span class="term-device">[' + escapeHtml(r.device_id) + ']</span> <span class="term-pending">pending...</span>');
this.startPolling(r.request_id, r.device_id, idx);
} else {
this.appendLine('<span class="term-device">[' + escapeHtml(r.device_id) + ']</span> <span class="term-error">' + escapeHtml(r.message || 'error') + '</span>');
}
}
} catch (e) {
this.appendLine('Network error: ' + escapeHtml(e.message), 'term-error');
}
},
startPolling(requestId, deviceId, lineIdx) {
let attempts = 0;
const poll = async () => {
attempts++;
try {
const res = await fetch('/api/commands/' + encodeURIComponent(requestId));
const data = await res.json();
if (data.status === 'completed' || data.status === 'error' || attempts >= 60) {
clearInterval(this.pendingPolls[requestId]);
delete this.pendingPolls[requestId];
let html;
if (data.output && data.output.length > 0) {
html = '<span class="term-device">[' + escapeHtml(deviceId) + ']</span> ' + escapeHtml(data.output.join('\n'));
} else if (attempts >= 60) {
html = '<span class="term-device">[' + escapeHtml(deviceId) + ']</span> <span class="term-error">Timeout</span>';
} else {
html = '<span class="term-device">[' + escapeHtml(deviceId) + ']</span> <span class="term-success">OK</span>';
}
if (this.lines[lineIdx]) this.lines[lineIdx].html = html;
}
} catch (e) {}
};
this.pendingPolls[requestId] = setInterval(poll, 500);
setTimeout(poll, 300);
},
// Monitor
async refreshPorts() {
try {
const res = await fetch('/api/monitor/ports');
const data = await res.json();
this.ports = data.ports || [];
} catch (e) {}
},
connectMonitor() {
if (!this.selectedPort) return;
this.disconnectMonitor();
const path = '/api/monitor/stream' + this.selectedPort;
this.monitorES = new EventSource(path);
this.monitorConnected = true;
this.monitorES.onmessage = (e) => {
let cls = '';
if (/\bE\s*\(/.test(e.data) || /error/i.test(e.data)) cls = 'monitor-error';
else if (/\bW\s*\(/.test(e.data) || /warn/i.test(e.data)) cls = 'monitor-warn';
else if (/\bI\s*\(/.test(e.data)) cls = 'monitor-info';
this.monitorLines.push({ text: e.data, cls });
if (this.monitorLines.length > 2000) this.monitorLines.splice(0, 500);
};
this.monitorES.onerror = () => {
this.monitorLines.push({ text: '[connection lost]', cls: 'monitor-error' });
this.disconnectMonitor();
};
},
disconnectMonitor() {
if (this.monitorES) { this.monitorES.close(); this.monitorES = null; }
this.monitorConnected = false;
},
startResize(e) {
const startX = e.clientX;
const startW = this.monitorWidth;
const onMove = (ev) => { this.monitorWidth = Math.max(200, startW - (ev.clientX - startX)); };
const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); };
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
}
};
}
</script>
{% endblock %}