KernelSU module + Flipper Zero FAP that bridges both devices into a unified pentesting platform over USB CDC serial / BT rfcomm. Android side: bridge daemon, WebUI (:8089), bind mount namespace isolation stealth engine. Flipper side: proper FAP with 4-view GUI, GPIO/SubGHz/IR/file command handlers, async event streaming.
377 lines
12 KiB
JavaScript
377 lines
12 KiB
JavaScript
// FlipperDroid WebUI — Bridge Controller
|
|
|
|
let config = {};
|
|
let currentTab = 'dashboard';
|
|
let toastTimer = null;
|
|
let eventPollInterval = null;
|
|
|
|
// ---- API ----
|
|
async function api(path, opts = {}) {
|
|
try {
|
|
const res = await fetch(path, opts);
|
|
if (path.endsWith('.css') || path.endsWith('.js') || path.endsWith('.html')) return res;
|
|
const ct = res.headers.get('content-type') || '';
|
|
if (ct.includes('json')) return await res.json();
|
|
return await res.text();
|
|
} catch (e) {
|
|
showToast('Connection error', 'error');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
const post = (path, body) => api(path, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body)
|
|
});
|
|
|
|
// ---- Toast ----
|
|
function showToast(msg, type = 'success') {
|
|
const el = document.getElementById('toast');
|
|
el.textContent = msg;
|
|
el.className = 'toast ' + type + ' show';
|
|
clearTimeout(toastTimer);
|
|
toastTimer = setTimeout(() => el.className = 'toast', 2500);
|
|
}
|
|
|
|
// ---- Tabs ----
|
|
function switchTab(tab) {
|
|
currentTab = tab;
|
|
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
|
|
document.querySelectorAll('.tab-item').forEach(t => t.classList.remove('active'));
|
|
document.getElementById('page-' + tab).classList.add('active');
|
|
document.querySelector(`[data-tab="${tab}"]`).classList.add('active');
|
|
if (tab === 'events') loadEvents();
|
|
if (tab === 'gpio') initGpioGrid();
|
|
if (tab === 'stealth') stealthStatus();
|
|
}
|
|
|
|
// ---- Dashboard ----
|
|
async function loadStatus() {
|
|
const data = await api('/api/status');
|
|
if (!data) return;
|
|
|
|
config = data;
|
|
setText('info-status', data.status || 'unknown');
|
|
setText('info-transport', data.conn_type || 'none');
|
|
setText('info-device', data.device || '-');
|
|
setText('info-product', data.product || '-');
|
|
setText('info-serial', data.serial || '-');
|
|
setText('info-firmware', data.firmware_version || '-');
|
|
|
|
const badge = document.getElementById('status-badge');
|
|
if (data.status === 'connected') {
|
|
badge.textContent = `Connected — ${data.conn_type?.toUpperCase() || 'USB'} — ${data.product || 'Flipper Zero'}`;
|
|
} else {
|
|
badge.textContent = 'Disconnected';
|
|
}
|
|
|
|
// Status chips
|
|
setChip('chip-conn', data.status === 'connected');
|
|
setChip('chip-gpio', data.status === 'connected' && data.config?.enable_gpio);
|
|
setChip('chip-rf', data.status === 'connected' && data.config?.enable_subghz);
|
|
setChip('chip-nfc', data.status === 'connected' && data.config?.enable_nfc);
|
|
setChip('chip-cpu', data.status === 'connected' && data.config?.cpu_share);
|
|
|
|
// Toggles
|
|
if (data.config) {
|
|
setToggle('toggle-autoconnect', data.config.auto_connect);
|
|
setToggle('toggle-cpushare', data.config.cpu_share);
|
|
}
|
|
}
|
|
|
|
function setChip(id, on) {
|
|
const chip = document.getElementById(id);
|
|
if (!chip) return;
|
|
const dot = chip.querySelector('.dot');
|
|
if (dot) { dot.className = on ? 'dot' : 'dot off'; }
|
|
}
|
|
|
|
function setText(id, val) {
|
|
const el = document.getElementById(id);
|
|
if (el) el.textContent = val;
|
|
}
|
|
|
|
function setToggle(id, val) {
|
|
const el = document.getElementById(id);
|
|
if (el) el.checked = !!val;
|
|
}
|
|
|
|
async function setConfig(key, value) {
|
|
const r = await post('/api/config', { key, value: String(value) });
|
|
if (r && r.ok) showToast('Config updated');
|
|
else showToast('Config update failed', 'error');
|
|
}
|
|
|
|
async function reconnect() {
|
|
await post('/api/reconnect');
|
|
showToast('Reconnecting...');
|
|
setTimeout(loadStatus, 3000);
|
|
}
|
|
|
|
async function pingFlipper() {
|
|
const r = await post('/api/system/ping');
|
|
if (r && r.result && r.result.startsWith('OK')) showToast('Pong!');
|
|
else showToast('Ping failed: ' + (r?.result || 'no response'), 'error');
|
|
}
|
|
|
|
// ---- GPIO ----
|
|
const GPIO_PINS = [
|
|
{ num: 2, name: 'PA2' }, { num: 3, name: 'PA3' }, { num: 4, name: 'PA4' },
|
|
{ num: 6, name: 'PA6' }, { num: 7, name: 'PA7' }, { num: 13, name: 'PB2' },
|
|
{ num: 14, name: 'PB3' }, { num: 15, name: 'PB13' }, { num: 16, name: 'PB14' },
|
|
{ num: 17, name: 'PC0' }, { num: 18, name: 'PC1' }, { num: 19, name: 'PC3' }
|
|
];
|
|
|
|
function initGpioGrid() {
|
|
const grid = document.getElementById('gpio-grid');
|
|
if (!grid || grid.children.length > 0) return;
|
|
GPIO_PINS.forEach(pin => {
|
|
const div = document.createElement('div');
|
|
div.className = 'gpio-pin';
|
|
div.id = 'gpio-pin-' + pin.num;
|
|
div.innerHTML = `<div class="pin-name">${pin.name}</div><div class="pin-val">-</div>`;
|
|
div.onclick = () => quickGpioRead(pin.num);
|
|
grid.appendChild(div);
|
|
});
|
|
}
|
|
|
|
async function quickGpioRead(pinNum) {
|
|
const r = await post('/api/gpio/read', { pin: pinNum });
|
|
const el = document.getElementById('gpio-pin-' + pinNum);
|
|
if (r && r.result && r.result.startsWith('OK:')) {
|
|
const val = r.result.replace('OK:', '');
|
|
const valEl = el.querySelector('.pin-val');
|
|
valEl.textContent = val === '01' || val === '1' ? 'HIGH' : 'LOW';
|
|
el.className = 'gpio-pin ' + (val === '01' || val === '1' ? 'high' : 'low');
|
|
}
|
|
}
|
|
|
|
async function gpioInit() {
|
|
const pin = parseInt(document.getElementById('gpio-pin').value);
|
|
const mode = parseInt(document.getElementById('gpio-mode').value);
|
|
const r = await post('/api/gpio/init', { pin, mode });
|
|
showToast(r?.result || 'No response');
|
|
}
|
|
|
|
async function gpioRead() {
|
|
const pin = parseInt(document.getElementById('gpio-pin').value);
|
|
const r = await post('/api/gpio/read', { pin });
|
|
document.getElementById('gpio-result').textContent = r?.result || '-';
|
|
}
|
|
|
|
async function gpioWrite(val) {
|
|
const pin = parseInt(document.getElementById('gpio-pin').value);
|
|
const r = await post('/api/gpio/write', { pin, value: val });
|
|
showToast(r?.result || 'No response');
|
|
}
|
|
|
|
async function gpioPwm() {
|
|
const pin = parseInt(document.getElementById('gpio-pin').value);
|
|
const freq = parseInt(document.getElementById('gpio-pwm-freq').value);
|
|
const duty = parseInt(document.getElementById('gpio-pwm-duty').value);
|
|
const r = await post('/api/gpio/pwm', { pin, freq, duty });
|
|
showToast(r?.result || 'No response');
|
|
}
|
|
|
|
async function gpioAdc() {
|
|
const pin = parseInt(document.getElementById('gpio-pin').value);
|
|
const r = await post('/api/gpio/adc', { pin });
|
|
document.getElementById('gpio-adc-result').textContent = r?.result || '-';
|
|
}
|
|
|
|
// ---- SubGHz ----
|
|
async function subghzSetFreq() {
|
|
const freq = parseInt(document.getElementById('subghz-freq').value);
|
|
const r = await post('/api/subghz/set_freq', { freq });
|
|
showToast(r?.result || 'No response');
|
|
}
|
|
|
|
async function subghzTx() {
|
|
const data = document.getElementById('subghz-tx-data').value;
|
|
const r = await post('/api/subghz/tx', { data });
|
|
showToast(r?.result || 'No response');
|
|
}
|
|
|
|
async function subghzRxStart() {
|
|
const r = await post('/api/subghz/rx_start');
|
|
showToast(r?.result || 'No response');
|
|
}
|
|
|
|
async function subghzRxStop() {
|
|
const r = await post('/api/subghz/rx_stop');
|
|
showToast(r?.result || 'No response');
|
|
}
|
|
|
|
async function subghzGetRssi() {
|
|
const r = await post('/api/subghz/get_rssi');
|
|
document.getElementById('subghz-rssi').textContent = r?.result || '-';
|
|
}
|
|
|
|
async function subghzReplay() {
|
|
const slot = parseInt(document.getElementById('subghz-replay-slot').value);
|
|
const r = await post('/api/subghz/replay', { slot });
|
|
showToast(r?.result || 'No response');
|
|
}
|
|
|
|
// ---- NFC ----
|
|
async function nfcPoll() {
|
|
appendLog('nfc-log', 'Polling for NFC card...');
|
|
const r = await post('/api/nfc/poll');
|
|
appendLog('nfc-log', r?.result || 'No response');
|
|
}
|
|
|
|
async function nfcReadFull() {
|
|
appendLog('nfc-log', 'Reading full card...');
|
|
const r = await post('/api/nfc/read_full');
|
|
appendLog('nfc-log', r?.result || 'No response');
|
|
}
|
|
|
|
async function nfcRelayStart() {
|
|
const r = await post('/api/nfc/relay_start');
|
|
showToast(r?.result || 'No response');
|
|
}
|
|
|
|
async function nfcRelayStop() {
|
|
const r = await post('/api/nfc/relay_stop');
|
|
showToast(r?.result || 'No response');
|
|
}
|
|
|
|
// ---- RFID ----
|
|
async function rfidRead() {
|
|
appendLog('rfid-log', 'Reading RFID tag...');
|
|
const r = await post('/api/rfid/read');
|
|
appendLog('rfid-log', r?.result || 'No response');
|
|
}
|
|
|
|
async function rfidEmulate() {
|
|
const data = document.getElementById('rfid-emu-data').value;
|
|
const r = await post('/api/rfid/emulate', { data });
|
|
showToast(r?.result || 'No response');
|
|
}
|
|
|
|
// ---- IR ----
|
|
async function irTx() {
|
|
const protocol = parseInt(document.getElementById('ir-proto').value);
|
|
const address = parseInt(document.getElementById('ir-addr').value);
|
|
const command = parseInt(document.getElementById('ir-cmd').value);
|
|
const r = await post('/api/ir/tx', { protocol, address, command });
|
|
showToast(r?.result || 'No response');
|
|
}
|
|
|
|
async function irRxStart() {
|
|
const r = await post('/api/ir/rx_start');
|
|
showToast(r?.result || 'No response');
|
|
}
|
|
|
|
async function irRxStop() {
|
|
const r = await post('/api/ir/rx_stop');
|
|
showToast(r?.result || 'No response');
|
|
}
|
|
|
|
async function irReplay() {
|
|
const slot = parseInt(document.getElementById('ir-replay-slot').value);
|
|
const r = await post('/api/ir/replay', { slot });
|
|
showToast(r?.result || 'No response');
|
|
}
|
|
|
|
// ---- Stealth ----
|
|
async function stealthStatus() {
|
|
const r = await api('/api/stealth/status');
|
|
const el = document.getElementById('stealth-status-text');
|
|
const countEl = document.getElementById('stealth-mount-count');
|
|
const logEl = document.getElementById('stealth-log');
|
|
if (r && typeof r === 'object') {
|
|
el.textContent = r.active_bind_mounts > 0 ? 'ACTIVE' : 'INACTIVE';
|
|
el.style.color = r.active_bind_mounts > 0 ? 'var(--success)' : 'var(--text-muted)';
|
|
countEl.textContent = r.active_bind_mounts || '0';
|
|
if (logEl) {
|
|
logEl.textContent = JSON.stringify(r, null, 2);
|
|
}
|
|
} else {
|
|
el.textContent = 'unavailable';
|
|
}
|
|
}
|
|
|
|
async function stealthApply() {
|
|
const r = await post('/api/stealth/apply');
|
|
showToast(r?.message || 'Stealth applied');
|
|
setTimeout(stealthStatus, 1000);
|
|
}
|
|
|
|
async function stealthTeardown() {
|
|
const r = await post('/api/stealth/teardown');
|
|
showToast(r?.message || 'Stealth removed');
|
|
setTimeout(stealthStatus, 1000);
|
|
}
|
|
|
|
async function stealthHideDev() {
|
|
const r = await post('/api/stealth/hide');
|
|
showToast(r?.message || 'Device hidden');
|
|
setTimeout(stealthStatus, 1000);
|
|
}
|
|
|
|
async function stealthShowDev() {
|
|
const r = await post('/api/stealth/show');
|
|
showToast(r?.message || 'Device visible');
|
|
setTimeout(stealthStatus, 1000);
|
|
}
|
|
|
|
// ---- Events ----
|
|
async function loadEvents() {
|
|
const data = await api('/api/events');
|
|
const log = document.getElementById('events-log');
|
|
if (!log) return;
|
|
if (Array.isArray(data)) {
|
|
log.textContent = data.map(e =>
|
|
`[${new Date(e.timestamp * 1000).toLocaleTimeString()}] ${e.type}: ${e.data} ${e.extra || ''}`
|
|
).join('\n') || 'No events yet';
|
|
} else {
|
|
log.textContent = data || 'No events';
|
|
}
|
|
}
|
|
|
|
async function loadSysLog() {
|
|
const data = await api('/api/log');
|
|
const log = document.getElementById('sys-log');
|
|
if (log) log.textContent = data || 'No log data';
|
|
}
|
|
|
|
// ---- Helpers ----
|
|
function appendLog(id, msg) {
|
|
const el = document.getElementById(id);
|
|
if (!el) return;
|
|
const ts = new Date().toLocaleTimeString();
|
|
el.textContent += `[${ts}] ${msg}\n`;
|
|
el.scrollTop = el.scrollHeight;
|
|
}
|
|
|
|
// ---- Init ----
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
loadStatus();
|
|
setInterval(loadStatus, 5000);
|
|
|
|
// Poll events when on events tab
|
|
setInterval(() => {
|
|
if (currentTab === 'events') loadEvents();
|
|
}, 3000);
|
|
|
|
// Poll SubGHz log when on subghz tab
|
|
setInterval(async () => {
|
|
if (currentTab === 'subghz') {
|
|
const data = await api('/api/events');
|
|
if (Array.isArray(data)) {
|
|
const rfEvents = data.filter(e => e.type === 'subghz_rx');
|
|
if (rfEvents.length > 0) {
|
|
const log = document.getElementById('subghz-log');
|
|
if (log) {
|
|
log.textContent = rfEvents.map(e =>
|
|
`[${new Date(e.timestamp * 1000).toLocaleTimeString()}] RX: ${e.data} (${e.extra})`
|
|
).join('\n');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}, 2000);
|
|
});
|