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.
263 lines
12 KiB
HTML
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 %}
|