Files
CellGuard/webroot/js/app.js

136 lines
5.3 KiB
JavaScript
Raw Normal View History

// 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 =>
({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[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 `<span class="badge ${cls}">${esc(r || "?")}</span>`;
}
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
? `<span class="badge badge-5g">LTE + 5G only</span>`
: `<span class="badge badge-2g">2G/3G allowed</span>`;
const bad = s.suspect && s.suspect !== "none";
ui.dVerdict.innerHTML = bad
? `<span class="badge badge-2g">${esc(s.suspect)}</span>`
: `<span class="badge badge-5g">clear</span>`;
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 =>
`<div class="cell-row"><div class="left">${esc(l)}</div></div>`).join("")
: `<div class="empty">No alerts.</div>`;
}
}
async function onToggle() {
ui.toggle.disabled = true;
ui.footMsg.textContent = ui.toggle.checked ? "enabling 2G/3G block…" : "disabling…";
const r = await ctl("block " + (ui.toggle.checked ? "on" : "off"));
if (!r || !r.ok) { ui.toggle.checked = !ui.toggle.checked; ui.footMsg.textContent = "failed"; }
ui.toggle.disabled = false;
setTimeout(refresh, 800);
}
async function onDetectToggle() {
ui.dToggle.disabled = true;
ui.footMsg.textContent = ui.dToggle.checked ? "enabling detection…" : "pausing detection…";
const r = await ctl("detect " + (ui.dToggle.checked ? "on" : "off"));
if (!r || !r.ok) { ui.dToggle.checked = !ui.dToggle.checked; ui.footMsg.textContent = "failed"; }
ui.dToggle.disabled = false;
setTimeout(refresh, 800);
}
async function onClear() {
await exec("sh -c ': > /data/adb/cellguard/alerts'");
refresh();
}
ui.toggle.disabled = true;
ui.dToggle.disabled = true;
ui.toggle.addEventListener("change", onToggle);
ui.dToggle.addEventListener("change", onDetectToggle);
ui.clrBtn.addEventListener("click", onClear);
refresh();
setInterval(refresh, 3000);
})();