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

263 lines
12 KiB
HTML

{% extends "base.html" %}
{% block title %}Tunnel - ESPILON{% endblock %}
{% block content %}
<div class="page" x-data="tunnelApp()" x-init="init()">
<div class="split-h" style="flex:1;">
<!-- Left: Controls -->
<div class="panel" style="width:360px;min-width:280px;">
<div class="panel-header">
<span>SOCKS5 Tunnel</span>
</div>
<div class="panel-body panel-body-pad" style="overflow-y:auto;">
<!-- Start tunnel on a device -->
<div style="border-bottom:1px solid var(--border-subtle);padding-bottom:12px;margin-bottom:12px;">
<div class="form-group">
<label>Target Device</label>
<select class="select w-full" x-model="device">
<option value="">select device...</option>
<template x-for="d in $store.app.connectedDevices()" :key="d.id">
<option :value="d.id" x-text="d.id"></option>
</template>
</select>
</div>
<div class="form-group">
<label>C3PO Tunnel Server (ESP32 connects back here)</label>
<div class="form-row" style="gap:6px;">
<input type="text" class="input flex-1" x-model="tunnelIp" placeholder="C3PO IP">
<input type="text" class="input" x-model="tunnelPort" placeholder="2627" style="width:70px;">
</div>
</div>
<div class="form-row" style="gap:6px;">
<button class="btn btn-sm btn-success flex-1" @click="startTunnel()" :disabled="!device || !tunnelIp">Start Tunnel</button>
<button class="btn btn-sm btn-danger flex-1" @click="stopTunnel()" :disabled="!device">Stop Tunnel</button>
</div>
</div>
<!-- Active device for SOCKS5 -->
<div style="border-bottom:1px solid var(--border-subtle);padding-bottom:12px;margin-bottom:12px;">
<div class="form-group">
<label>Active SOCKS5 Device</label>
<div class="form-row" style="gap:6px;">
<select class="select flex-1" x-model="activeDevice">
<option value="">none</option>
<template x-for="d in tunnels" :key="d.device_id">
<option :value="d.device_id" x-text="d.device_id"></option>
</template>
</select>
<button class="btn btn-sm btn-primary" @click="setActive()" :disabled="!activeDevice">Set</button>
</div>
</div>
</div>
<!-- SOCKS5 info -->
<div class="form-group">
<label>SOCKS5 Proxy</label>
<div class="text-mono text-xs" style="padding:8px;background:var(--bg-inset);border-radius:4px;">
<div><span class="text-muted">Address:</span> <span x-text="socksAddr"></span></div>
<div><span class="text-muted">Status:</span>
<span :class="socksRunning ? 'text-success' : 'text-muted'" x-text="socksRunning ? 'listening' : 'stopped'"></span>
</div>
<div style="margin-top:6px;" class="text-muted">
proxychains curl http://target/
</div>
</div>
</div>
</div>
</div>
<div class="resizer"></div>
<!-- Right: Status + Channels -->
<div class="panel flex-1">
<div class="panel-header">
<span>Tunnel Status</span>
<button class="btn btn-sm" @click="refresh()">Refresh</button>
</div>
<div class="panel-body" style="flex:1;overflow-y:auto;">
<!-- Connected tunnels -->
<template x-if="tunnels.length > 0">
<div>
<template x-for="tun in tunnels" :key="tun.device_id">
<div style="padding:12px;border-bottom:1px solid var(--border-subtle);">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
<span class="statusbar-dot ok"></span>
<strong x-text="tun.device_id"></strong>
<span class="text-muted text-xs" x-show="tun.device_id === currentActive">(active)</span>
</div>
<div class="text-xs text-mono" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:4px 16px;">
<div><span class="text-muted">Channels:</span> <span x-text="tun.active_channels + '/' + tun.max_channels"></span></div>
<div><span class="text-muted">TX:</span> <span x-text="formatBytes(tun.bytes_tx)"></span></div>
<div><span class="text-muted">RX:</span> <span x-text="formatBytes(tun.bytes_rx)"></span></div>
<div><span class="text-muted">Encrypted:</span> <span x-text="tun.encrypted ? 'yes' : 'no'"></span></div>
</div>
<!-- Channel list -->
<template x-if="tun.channels && tun.channels.length > 0">
<div style="margin-top:8px;">
<table class="data-table" style="font-size:11px;">
<thead>
<tr>
<th>CH</th>
<th>Target</th>
<th>State</th>
<th>TX</th>
<th>RX</th>
</tr>
</thead>
<tbody>
<template x-for="ch in tun.channels" :key="ch.id">
<tr>
<td x-text="ch.id"></td>
<td x-text="ch.target || '-'"></td>
<td x-text="ch.state"></td>
<td x-text="formatBytes(ch.bytes_tx)"></td>
<td x-text="formatBytes(ch.bytes_rx)"></td>
</tr>
</template>
</tbody>
</table>
</div>
</template>
</div>
</template>
</div>
</template>
<template x-if="tunnels.length === 0">
<div class="text-muted text-xs" style="padding:16px;text-align:center;">
No active tunnels. Start a tunnel on a connected device.
</div>
</template>
<!-- Output log -->
<template x-if="outputLines.length > 0">
<div style="border-top:1px solid var(--border-subtle);padding:8px;">
<div class="text-xs text-muted" style="margin-bottom:4px;">Log</div>
<div class="term-output" style="max-height:200px;">
<template x-for="(l, i) in outputLines" :key="i">
<div class="term-line" :class="l.cls || ''" x-html="l.html"></div>
</template>
</div>
</div>
</template>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function tunnelApp() {
return {
...commander(),
device: '',
tunnelIp: '',
tunnelPort: '2627',
activeDevice: '',
currentActive: '',
socksAddr: '',
socksRunning: false,
tunnels: [],
outputLines: [],
_interval: null,
init() {
this.refresh();
this._interval = setInterval(() => this.refresh(), 3000);
},
destroy() {
if (this._interval) clearInterval(this._interval);
},
formatBytes(n) {
if (!n || n === 0) return '0 B';
const units = ['B', 'KB', 'MB', 'GB'];
let i = 0;
while (n >= 1024 && i < units.length - 1) { n /= 1024; i++; }
return n.toFixed(i > 0 ? 1 : 0) + ' ' + units[i];
},
async refresh() {
try {
const res = await fetch('/api/tunnel/status');
if (!res.ok) return;
const data = await res.json();
this.socksRunning = data.socks_running || false;
this.socksAddr = data.socks_addr || '-';
this.currentActive = data.active_device || '';
this.tunnels = data.tunnels || [];
if (!this.activeDevice && this.currentActive) {
this.activeDevice = this.currentActive;
}
} catch (e) {}
},
async startTunnel() {
if (!this.device) { toast('Select a device', 'error'); return; }
if (!this.tunnelIp) { toast('Enter C3PO tunnel IP', 'error'); return; }
const port = this.tunnelPort || '2627';
this.log('Starting tunnel on ' + this.device + ' -> ' + this.tunnelIp + ':' + port);
try {
const data = await this.sendCommand([this.device], 'tun_start', [this.tunnelIp, port]);
const r = (data.results || [])[0];
if (r && r.status === 'ok') {
this.log('Tunnel start command sent (async)');
} else {
this.log('Error: ' + (r && r.message || 'unknown'), 'term-error');
}
} catch (e) {
this.log('Error: ' + e.message, 'term-error');
}
},
async stopTunnel() {
if (!this.device) { toast('Select a device', 'error'); return; }
this.log('Stopping tunnel on ' + this.device + '...');
try {
const data = await this.sendCommand([this.device], 'tun_stop', []);
const r = (data.results || [])[0];
if (r && r.status === 'ok') {
this.log('Tunnel stopped');
} else {
this.log('Error: ' + (r && r.message || 'unknown'), 'term-error');
}
} catch (e) {
this.log('Error: ' + e.message, 'term-error');
}
setTimeout(() => this.refresh(), 500);
},
async setActive() {
if (!this.activeDevice) return;
try {
const res = await fetch('/api/tunnel/active', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ device_id: this.activeDevice })
});
const data = await res.json();
if (data.status === 'ok') {
this.log('Active device set to ' + this.activeDevice);
this.currentActive = this.activeDevice;
} else {
this.log('Error: ' + (data.error || 'unknown'), 'term-error');
}
} catch (e) {
this.log('Error: ' + e.message, 'term-error');
}
},
log(msg, cls) {
this.outputLines.push({ html: escapeHtml(msg), cls: cls || '' });
}
};
}
</script>
{% endblock %}