Initial commit — FlipperDroid v0.1.0-poc

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.
This commit is contained in:
sssnake
2026-03-31 21:26:58 -07:00
commit be81a92d44
22 changed files with 4191 additions and 0 deletions

376
webroot/js/app.js Normal file
View File

@@ -0,0 +1,376 @@
// 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);
});