// CellGuard WebUI — KernelSU-Next ksu.exec() bridge. // // The WebUI is a viewer/controller only. All work runs in background daemons: // ratlock.sh — holds LTE+5G(NR) only (the 2G/3G-block toggle) // cgdetect.sh — always-on IMSI-catcher detection -> status + alerts files // cgcap.ko — raw CP-frame capture (diag) // It talks to them through cgctl.sh, which emits one JSON line per command. (function () { const $ = (id) => document.getElementById(id); const MOD = "/data/adb/modules/cellguard"; const ui = { toggle: $("toggle"), dToggle: $("dToggle"), headerState: $("headerState"), dVerdict: $("dVerdict"), sRat: $("sRat"), sOp: $("sOp"), sPlmn: $("sPlmn"), sCell: $("sCell"), sEarfcn: $("sEarfcn"), sFrames: $("sFrames"), sLock: $("sLock"), alerts: $("alerts"), footMsg: $("footMsg"), clrBtn: $("clrBtn"), }; // KernelSU-Next bridge: ksu.exec(command, optionsJson, callbackFuncName). // Results arrive via window[callbackFuncName](errno, stdout, stderr) — the // callback is passed BY NAME (a string), not as a function object. function exec(cmd) { return new Promise((resolve) => { if (typeof ksu === "undefined" || !ksu.exec) { resolve({ code: -1, stdout: "", stderr: "ksu bridge missing" }); return; } const cb = "cg_cb_" + Math.random().toString(36).slice(2); let done = false; const finish = (code, stdout, stderr) => { if (done) return; done = true; try { delete window[cb]; } catch (_) {} resolve({ code: (code | 0), stdout: stdout || "", stderr: stderr || "" }); }; window[cb] = finish; try { ksu.exec(cmd, "{}", cb); } catch (e) { // very old bridge with a direct function callback try { ksu.exec(cmd, (c, o, er) => finish(c, o, er)); } catch (_) { finish(-1, "", String(e)); } } setTimeout(() => finish(-1, "", "timeout"), 8000); }); } async function ctl(sub) { const r = await exec("sh " + MOD + "/cgctl.sh " + sub); try { return JSON.parse(r.stdout.trim()); } catch (e) { return { ok: false, error: r.stderr || r.stdout || `exit ${r.code}` }; } } function esc(s) { return String(s).replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); } function ratBadge(r) { const cls = ({ GSM: "badge-2g", GERAN: "badge-2g", GPRS: "badge-2g", EDGE: "badge-2g", UMTS: "badge-3g", WCDMA: "badge-3g", HSPA: "badge-3g", HSPAP: "badge-3g", UTRAN: "badge-3g", LTE: "badge-lte", NR: "badge-5g", })[r] || "badge-unk"; return `${esc(r || "?")}`; } async function refresh() { const s = await ctl("status"); if (!s || !s.ok) { ui.footMsg.textContent = (s && s.error) || "backend unavailable"; return; } ui.toggle.checked = !!s.block; ui.toggle.disabled = false; ui.dToggle.checked = !!s.detect; ui.dToggle.disabled = false; ui.sRat.innerHTML = ratBadge(s.rat); ui.sOp.textContent = (s.operator && s.operator !== "?") ? s.operator : "no service"; ui.sPlmn.textContent = s.plmn || "?"; ui.sCell.textContent = `${s.ci || "?"} / ${s.pci || "?"} / ${s.tac || "?"}`; ui.sEarfcn.textContent = s.earfcn || "?"; ui.sFrames.textContent = s.frames || 0; ui.sLock.innerHTML = s.lock ? `LTE + 5G only` : `2G/3G allowed`; const bad = s.suspect && s.suspect !== "none"; ui.dVerdict.innerHTML = bad ? `${esc(s.suspect)}` : `clear`; ui.headerState.textContent = bad ? "ALERT" : (s.block ? "armed" : "off"); ui.headerState.className = "state " + (bad ? "err" : (s.block ? "on" : "off")); ui.footMsg.textContent = `RAT ${s.rat || "?"} · ${s.frames || 0} frames`; const a = await ctl("alerts"); if (a && a.ok) { ui.alerts.innerHTML = (a.lines && a.lines.length) ? a.lines.slice().reverse().map(l => `