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:
376
webroot/js/app.js
Normal file
376
webroot/js/app.js
Normal 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);
|
||||
});
|
||||
Reference in New Issue
Block a user