espilon-source/tools/C3PO/templates/ota.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

271 lines
14 KiB
HTML

{% extends "base.html" %}
{% block title %}OTA - ESPILON{% endblock %}
{% block content %}
<div class="page" x-data="otaApp()" x-init="init()">
<div class="split-v" style="flex:1;">
<!-- Top: Build + Source -->
<div class="split-h" style="flex:0 0 auto;max-height:50%;">
<!-- Build Firmware -->
<div class="panel flex-1">
<div class="panel-header">
<span>Build Firmware</span>
<span class="badge" :class="buildStatusClass" x-text="buildStatus"></span>
</div>
<div class="panel-body panel-body-pad" style="overflow-y:auto;">
<div class="form-row">
<span class="form-label">Device ID</span>
<input type="text" class="input flex-1" x-model="buildDeviceId" placeholder="e.g. ce4f626b"
list="dev-list">
<datalist id="dev-list">
<template x-for="d in $store.app.devices" :key="d.id">
<option :value="d.id"></option>
</template>
</datalist>
</div>
<div class="form-row">
<span class="form-label">Hostname</span>
<input type="text" class="input flex-1" x-model="buildHostname" placeholder="optional">
</div>
<div class="form-row">
<span class="form-label">Modules</span>
<div class="module-checkboxes">
<label class="checkbox-label"><input type="checkbox" x-model="modules.network"> Network</label>
<label class="checkbox-label"><input type="checkbox" x-model="modules.fakeap"> FakeAP</label>
<label class="checkbox-label"><input type="checkbox" x-model="modules.honeypot"> Honeypot</label>
<label class="checkbox-label"><input type="checkbox" x-model="modules.recon"> Recon</label>
<label class="checkbox-label"><input type="checkbox" x-model="modules.redteam"> Red Team</label>
<label class="checkbox-label"><input type="checkbox" x-model="modules.canbus"> CAN Bus</label>
<label class="checkbox-label"><input type="checkbox" x-model="modules.ota" checked> OTA</label>
</div>
</div>
<div x-show="modules.recon" class="form-row" style="padding-left:80px;">
<label class="checkbox-label"><input type="checkbox" x-model="modules.recon_camera"> Camera</label>
<label class="checkbox-label"><input type="checkbox" x-model="modules.recon_ble_trilat"> BLE Trilat</label>
</div>
<div class="form-row">
<span class="form-label"></span>
<label class="checkbox-label"><input type="checkbox" x-model="otaAllowHttp"> Allow OTA over HTTP</label>
</div>
<div class="form-row">
<span class="form-label"></span>
<button class="btn btn-primary" @click="startBuild()" :disabled="buildStatus === 'building'">Build</button>
</div>
<template x-if="buildLog.length > 0">
<div>
<div class="text-xs text-muted" style="margin-top:8px;" x-text="buildHint"></div>
<pre class="build-log" x-text="buildLog.join('\n')"></pre>
</div>
</template>
</div>
</div>
<div class="resizer"></div>
<!-- Firmware Source -->
<div class="panel" style="width:300px;min-width:200px;">
<div class="panel-header">
<span>Firmware (<span x-text="firmware.length"></span>)</span>
</div>
<div class="panel-body panel-body-pad" style="overflow-y:auto;">
<div class="form-group">
<label>Deploy URL</label>
<input type="text" class="input w-full" x-model="fwUrl" placeholder="https://...firmware.bin">
</div>
<div class="form-group">
<label>Upload .bin</label>
<input type="file" accept=".bin" @change="uploadFile($event)" style="font-size:11px;">
</div>
<template x-for="fw in firmware" :key="fw.filename">
<div style="display:flex;align-items:center;gap:4px;padding:3px 0;border-bottom:1px solid var(--border-subtle);">
<span class="text-mono text-xs flex-1 truncate" x-text="fw.filename"></span>
<span class="text-xs text-muted" x-text="formatBytes(fw.size)"></span>
<button class="btn btn-sm" @click="useFirmware(fw.filename)">Use</button>
<button class="btn btn-sm btn-danger" @click="deleteFirmware(fw.filename)">Del</button>
</div>
</template>
<template x-if="firmware.length === 0">
<div class="text-muted text-xs">No firmware uploaded</div>
</template>
</div>
</div>
</div>
<div class="resizer resizer-h"></div>
<!-- Bottom: Devices -->
<div class="panel flex-1">
<div class="panel-header">
<span>Devices</span>
<button class="btn btn-sm btn-primary" @click="deployAll()">Deploy All</button>
</div>
<div class="panel-body">
<table class="dt">
<thead>
<tr>
<th>ID</th>
<th class="col-shrink">Status</th>
<th>Chip</th>
<th class="col-shrink">OTA</th>
<th class="col-shrink">Action</th>
</tr>
</thead>
<tbody>
<template x-for="d in $store.app.devices" :key="d.id">
<tr>
<td x-text="d.id"></td>
<td><span class="badge" :class="d.status==='Connected'?'badge-ok':'badge-warn'" x-text="d.status"></span></td>
<td x-text="d.chip || '-'"></td>
<td>
<span class="badge" :class="hasOta(d)?'badge-ok':'badge-err'"
x-text="hasOta(d)?'Yes':'No'"></span>
</td>
<td>
<button class="btn btn-sm btn-primary" @click="deployDevice(d.id)"
:disabled="d.status!=='Connected' || !hasOta(d) || !fwUrl">Deploy</button>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function otaApp() {
return {
fwUrl: '',
firmware: [],
buildDeviceId: '',
buildHostname: '',
buildStatus: 'idle',
buildHint: '',
buildLog: [],
buildLogOffset: 0,
buildPoll: null,
otaAllowHttp: true,
modules: { network: true, fakeap: false, honeypot: false, recon: false, recon_camera: false, recon_ble_trilat: false, redteam: false, canbus: false, ota: true },
get buildStatusClass() {
if (this.buildStatus === 'building') return 'badge-warn';
if (this.buildStatus === 'success') return 'badge-ok';
if (this.buildStatus === 'failed') return 'badge-err';
return '';
},
init() {
this.loadFirmware();
this.loadBuildDefaults();
this.checkBuild();
setInterval(() => this.loadFirmware(), 15000);
},
hasOta(d) { return d.modules && d.modules.split(',').includes('ota'); },
async loadFirmware() {
try { const r = await fetch('/api/ota/firmware'); const d = await r.json(); this.firmware = d.firmware || []; } catch (e) {}
},
async loadBuildDefaults() {
try {
const r = await fetch('/api/ota/build/defaults'); const d = await r.json();
if (d.modules) Object.assign(this.modules, d.modules);
if (d.ota) this.otaAllowHttp = !!d.ota.allow_http;
} catch (e) {}
},
useFirmware(filename) {
const origin = this.getOrigin();
if (origin) this.fwUrl = origin + '/api/ota/fw/' + filename;
},
getOrigin() {
const h = window.location.hostname;
if (h === '0.0.0.0' || h === '127.0.0.1' || h === 'localhost') {
const ip = localStorage.getItem('ota_server_ip') || prompt('Enter server LAN IP:');
if (ip) { localStorage.setItem('ota_server_ip', ip); return window.location.protocol + '//' + ip + ':' + window.location.port; }
return null;
}
return window.location.origin;
},
async deployDevice(deviceId) {
if (!this.fwUrl) { toast('Enter a firmware URL', 'error'); return; }
try {
const r = await fetch('/api/ota/deploy', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({url:this.fwUrl, device_ids:[deviceId]}) });
const d = await r.json();
if (d.results && d.results[0] && d.results[0].status === 'error') toast('Deploy failed: ' + d.results[0].message, 'error');
else toast('Deploy sent to ' + deviceId, 'ok');
} catch (e) { toast('Deploy failed', 'error'); }
},
async deployAll() {
if (!this.fwUrl) { toast('Enter a firmware URL', 'error'); return; }
const ids = this.$store.app.connectedDevices().filter(d => this.hasOta(d)).map(d => d.id);
if (!ids.length) { toast('No OTA-capable devices', 'error'); return; }
try {
await fetch('/api/ota/deploy', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({url:this.fwUrl, device_ids:ids}) });
toast('Deploy sent to ' + ids.length + ' device(s)', 'ok');
} catch (e) { toast('Deploy failed', 'error'); }
},
async uploadFile(e) {
const file = e.target.files[0];
if (!file) return;
const fd = new FormData(); fd.append('file', file);
try {
const r = await fetch('/api/ota/upload', { method: 'POST', body: fd });
const d = await r.json();
if (d.error) toast('Upload failed: ' + d.error, 'error');
else { toast('Uploaded: ' + d.filename, 'ok'); this.useFirmware(d.filename); this.loadFirmware(); }
} catch (e) { toast('Upload failed', 'error'); }
e.target.value = '';
},
async deleteFirmware(filename) {
try { await fetch('/api/ota/firmware/' + filename, { method: 'DELETE' }); this.loadFirmware(); toast('Deleted', 'ok'); } catch (e) {}
},
async startBuild() {
if (!this.buildDeviceId) { toast('Enter device ID', 'error'); return; }
const body = { device_id: this.buildDeviceId, hostname: this.buildHostname, modules: this.modules, ota: { enabled: true, allow_http: this.otaAllowHttp } };
try {
const r = await fetch('/api/ota/build/start', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(body) });
const d = await r.json();
if (d.error) { toast('Build error: ' + d.error, 'error'); return; }
this.buildStatus = 'building'; this.buildLog = []; this.buildLogOffset = 0;
this.buildPoll = setInterval(() => this.pollBuild(), 2000);
} catch (e) { toast('Build failed', 'error'); }
},
async checkBuild() {
try {
const r = await fetch('/api/ota/build/status'); const d = await r.json();
this.buildStatus = d.status;
if (d.status === 'building') { this.buildPoll = setInterval(() => this.pollBuild(), 2000); }
if (d.status === 'success' && d.output_filename) this.useFirmware(d.output_filename);
} catch (e) {}
},
async pollBuild() {
try {
const [sr, lr] = await Promise.all([fetch('/api/ota/build/status'), fetch('/api/ota/build/log?offset=' + this.buildLogOffset)]);
const s = await sr.json(); const l = await lr.json();
this.buildStatus = s.status; this.buildHint = s.progress_hint || '';
if (l.lines && l.lines.length > 0) { this.buildLog.push(...l.lines); this.buildLogOffset = l.total; }
if (s.status !== 'building') {
clearInterval(this.buildPoll); this.buildPoll = null;
if (s.status === 'success' && s.output_filename) { this.useFirmware(s.output_filename); this.loadFirmware(); }
}
} catch (e) {}
}
};
}
</script>
{% endblock %}