mirror of
https://github.com/okhsunrog/vpnhide.git
synced 2026-04-28 14:44:43 +00:00
Add service.sh to zygisk module that resolves package names → UIDs and writes /data/system/vpnhide_uids.txt on boot — same contract as kmod's service.sh. This enables the lsposed system_server hooks to work with zygisk (previously they only worked with kmod). Also update the zygisk WebUI to resolve and write UIDs on save, so changes apply immediately without reboot.
330 lines
9.7 KiB
HTML
330 lines
9.7 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
<title>VPN Hide — targets</title>
|
|
<style>
|
|
:root {
|
|
color-scheme: dark light;
|
|
--fg: #e6e6e6;
|
|
--bg: #121212;
|
|
--bg-elev: #1e1e1e;
|
|
--bg-row: #242424;
|
|
--bg-row-hover: #2e2e2e;
|
|
--accent: #4caf50;
|
|
--accent-fg: #0b0b0b;
|
|
--muted: #9a9a9a;
|
|
--border: #333;
|
|
}
|
|
@media (prefers-color-scheme: light) {
|
|
:root {
|
|
--fg: #111;
|
|
--bg: #fafafa;
|
|
--bg-elev: #fff;
|
|
--bg-row: #fff;
|
|
--bg-row-hover: #f0f0f0;
|
|
--accent: #1b7f3a;
|
|
--accent-fg: #fff;
|
|
--muted: #555;
|
|
--border: #ddd;
|
|
}
|
|
}
|
|
* { box-sizing: border-box; }
|
|
html, body { margin: 0; padding: 0; height: 100%; }
|
|
body {
|
|
font: 14px/1.4 -apple-system, system-ui, Roboto, sans-serif;
|
|
color: var(--fg);
|
|
background: var(--bg);
|
|
display: flex;
|
|
flex-direction: column;
|
|
padding-top: env(safe-area-inset-top);
|
|
padding-bottom: env(safe-area-inset-bottom);
|
|
}
|
|
header {
|
|
padding: 12px 16px 8px;
|
|
background: var(--bg-elev);
|
|
border-bottom: 1px solid var(--border);
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 2;
|
|
}
|
|
h1 { margin: 0 0 8px; font-size: 16px; font-weight: 600; }
|
|
.sub { color: var(--muted); font-size: 12px; margin-bottom: 8px; }
|
|
input[type="search"] {
|
|
width: 100%;
|
|
padding: 10px 12px;
|
|
font-size: 14px;
|
|
color: var(--fg);
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
outline: none;
|
|
}
|
|
input[type="search"]:focus { border-color: var(--accent); }
|
|
#list {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 8px 0 80px;
|
|
}
|
|
.row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 10px 16px;
|
|
background: var(--bg-row);
|
|
border-bottom: 1px solid var(--border);
|
|
cursor: pointer;
|
|
user-select: none;
|
|
}
|
|
.row:hover { background: var(--bg-row-hover); }
|
|
.row input[type="checkbox"] {
|
|
width: 20px;
|
|
height: 20px;
|
|
accent-color: var(--accent);
|
|
flex-shrink: 0;
|
|
}
|
|
.row .pkg {
|
|
font-family: ui-monospace, Menlo, Consolas, monospace;
|
|
font-size: 13px;
|
|
word-break: break-all;
|
|
}
|
|
.row.selected { background: color-mix(in srgb, var(--accent) 12%, var(--bg-row)); }
|
|
.empty {
|
|
padding: 32px 16px;
|
|
text-align: center;
|
|
color: var(--muted);
|
|
}
|
|
footer {
|
|
position: fixed;
|
|
left: 0; right: 0; bottom: 0;
|
|
padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
|
|
background: var(--bg-elev);
|
|
border-top: 1px solid var(--border);
|
|
display: flex;
|
|
gap: 8px;
|
|
align-items: center;
|
|
}
|
|
#count { flex: 1; color: var(--muted); font-size: 12px; }
|
|
button {
|
|
padding: 10px 16px;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: var(--accent-fg);
|
|
background: var(--accent);
|
|
border: none;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
}
|
|
button:disabled { opacity: 0.5; cursor: default; }
|
|
#toast {
|
|
position: fixed;
|
|
left: 50%;
|
|
bottom: 80px;
|
|
transform: translateX(-50%) translateY(100px);
|
|
padding: 10px 16px;
|
|
background: var(--bg-elev);
|
|
border: 1px solid var(--border);
|
|
border-radius: 24px;
|
|
color: var(--fg);
|
|
font-size: 13px;
|
|
pointer-events: none;
|
|
opacity: 0;
|
|
transition: opacity .2s, transform .2s;
|
|
}
|
|
#toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<header>
|
|
<h1>VPN Hide — target apps</h1>
|
|
<div class="sub">Selected apps will have their <code>ioctl</code> VPN detection hooked.</div>
|
|
<input id="search" type="search" placeholder="Filter packages…" autocomplete="off">
|
|
</header>
|
|
|
|
<div id="list"><div class="empty">Loading…</div></div>
|
|
|
|
<footer>
|
|
<div id="count"></div>
|
|
<button id="save" disabled>Save</button>
|
|
</footer>
|
|
|
|
<div id="toast"></div>
|
|
|
|
<script>
|
|
const MODULE_ID = 'vpnhide_zygisk';
|
|
// Stored OUTSIDE the module directory so it survives module updates.
|
|
// customize.sh creates the parent directory on install. The save()
|
|
// path also runs `mkdir -p` defensively.
|
|
const PERSIST_DIR = `/data/adb/${MODULE_ID}`;
|
|
const TARGETS_PATH = `${PERSIST_DIR}/targets.txt`;
|
|
const SS_UIDS_FILE = '/data/system/vpnhide_uids.txt';
|
|
|
|
const listEl = document.getElementById('list');
|
|
const searchEl = document.getElementById('search');
|
|
const saveBtn = document.getElementById('save');
|
|
const countEl = document.getElementById('count');
|
|
const toastEl = document.getElementById('toast');
|
|
|
|
/** @type {{pkg: string, selected: boolean}[]} */
|
|
let packages = [];
|
|
let filter = '';
|
|
|
|
function toast(msg, isError = false) {
|
|
toastEl.textContent = msg;
|
|
toastEl.style.borderColor = isError ? '#e53935' : '';
|
|
toastEl.classList.add('show');
|
|
clearTimeout(toast._t);
|
|
toast._t = setTimeout(() => toastEl.classList.remove('show'), 2400);
|
|
}
|
|
|
|
/**
|
|
* Run a shell command via the KernelSU WebUI bridge. Returns a Promise
|
|
* resolving to { errno, stdout, stderr }. The bridge uses a callback
|
|
* mechanism — we install a uniquely-named global and wait for it.
|
|
*/
|
|
function ksuExec(cmd) {
|
|
return new Promise((resolve, reject) => {
|
|
if (typeof ksu === 'undefined' || !ksu.exec) {
|
|
reject(new Error('ksu.exec is not available. Open this page from the KernelSU-Next manager.'));
|
|
return;
|
|
}
|
|
const cbName = `__ksu_cb_${Math.random().toString(36).slice(2)}`;
|
|
window[cbName] = (errno, stdout, stderr) => {
|
|
delete window[cbName];
|
|
resolve({ errno: Number(errno), stdout: String(stdout || ''), stderr: String(stderr || '') });
|
|
};
|
|
try {
|
|
ksu.exec(cmd, '{}', cbName);
|
|
} catch (e) {
|
|
delete window[cbName];
|
|
reject(e);
|
|
}
|
|
});
|
|
}
|
|
|
|
async function loadPackages() {
|
|
// List user-installed packages.
|
|
const listed = await ksuExec('pm list packages -3');
|
|
if (listed.errno !== 0) {
|
|
throw new Error(`pm list packages failed: ${listed.stderr || 'errno=' + listed.errno}`);
|
|
}
|
|
const allPkgs = listed.stdout
|
|
.split('\n')
|
|
.map(l => l.trim())
|
|
.filter(l => l.startsWith('package:'))
|
|
.map(l => l.slice('package:'.length))
|
|
.sort();
|
|
|
|
// Read the current targets file (may not exist yet).
|
|
const existing = await ksuExec(`cat ${TARGETS_PATH} 2>/dev/null || true`);
|
|
const selected = new Set(
|
|
existing.stdout
|
|
.split('\n')
|
|
.map(l => l.trim())
|
|
.filter(l => l && !l.startsWith('#'))
|
|
);
|
|
|
|
packages = allPkgs.map(pkg => ({ pkg, selected: selected.has(pkg) }));
|
|
// Any entries in the file that no longer match an installed package
|
|
// are preserved at the top so the user can see and remove them.
|
|
for (const orphan of selected) {
|
|
if (!allPkgs.includes(orphan)) {
|
|
packages.unshift({ pkg: orphan, selected: true, orphan: true });
|
|
}
|
|
}
|
|
}
|
|
|
|
function render() {
|
|
const q = filter.trim().toLowerCase();
|
|
const rows = packages.filter(p => !q || p.pkg.toLowerCase().includes(q));
|
|
if (rows.length === 0) {
|
|
listEl.innerHTML = '<div class="empty">No matches.</div>';
|
|
} else {
|
|
listEl.innerHTML = '';
|
|
for (const entry of rows) {
|
|
const row = document.createElement('label');
|
|
row.className = 'row' + (entry.selected ? ' selected' : '');
|
|
const cb = document.createElement('input');
|
|
cb.type = 'checkbox';
|
|
cb.checked = entry.selected;
|
|
cb.addEventListener('change', () => {
|
|
entry.selected = cb.checked;
|
|
row.classList.toggle('selected', entry.selected);
|
|
updateCount();
|
|
saveBtn.disabled = false;
|
|
});
|
|
const name = document.createElement('span');
|
|
name.className = 'pkg';
|
|
name.textContent = entry.pkg + (entry.orphan ? ' (not installed)' : '');
|
|
row.appendChild(cb);
|
|
row.appendChild(name);
|
|
listEl.appendChild(row);
|
|
}
|
|
}
|
|
updateCount();
|
|
}
|
|
|
|
function updateCount() {
|
|
const n = packages.filter(p => p.selected).length;
|
|
countEl.textContent = `${n} selected`;
|
|
}
|
|
|
|
async function save() {
|
|
saveBtn.disabled = true;
|
|
try {
|
|
const selected = packages
|
|
.filter(p => p.selected)
|
|
.map(p => p.pkg)
|
|
.sort();
|
|
const body =
|
|
'# Managed by the vpnhide_zygisk WebUI.\n' +
|
|
'# One package name per line. Lines starting with # are comments.\n' +
|
|
selected.join('\n') +
|
|
(selected.length ? '\n' : '');
|
|
// base64 → shell, so we don't have to worry about shell quoting of
|
|
// package names or newlines.
|
|
const b64 = btoa(body);
|
|
|
|
// Step 1: save package names to targets.txt
|
|
const step1 = `mkdir -p ${PERSIST_DIR} && echo '${b64}' | base64 -d > ${TARGETS_PATH} && chmod 644 ${TARGETS_PATH}`;
|
|
const r1 = await ksuExec(step1);
|
|
if (r1.errno !== 0) throw new Error(r1.stderr || `step1 errno=${r1.errno}`);
|
|
|
|
// Step 2: resolve UIDs and write to /data/system/vpnhide_uids.txt
|
|
// for the LSPosed system_server hooks.
|
|
const step2 = [
|
|
`UIDS=""`,
|
|
...selected.map(pkg =>
|
|
`U=$(pm list packages -U '${pkg}' 2>/dev/null | grep '^package:${pkg} ' | sed 's/.*uid://') && [ -n "$U" ] && UIDS="$UIDS$U\\n"`
|
|
),
|
|
`printf "$UIDS" > ${SS_UIDS_FILE}`,
|
|
`chmod 644 ${SS_UIDS_FILE}`,
|
|
`chcon u:object_r:system_data_file:s0 ${SS_UIDS_FILE} 2>/dev/null`,
|
|
].join(' ; ');
|
|
await ksuExec(step2);
|
|
toast(`Saved ${selected.length} target(s).`);
|
|
} catch (e) {
|
|
toast('Save failed: ' + e.message, true);
|
|
saveBtn.disabled = false;
|
|
}
|
|
}
|
|
|
|
searchEl.addEventListener('input', () => {
|
|
filter = searchEl.value;
|
|
render();
|
|
});
|
|
saveBtn.addEventListener('click', save);
|
|
|
|
(async () => {
|
|
try {
|
|
await loadPackages();
|
|
render();
|
|
} catch (e) {
|
|
listEl.innerHTML = `<div class="empty">Failed to load: ${e.message}</div>`;
|
|
}
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|