change: security-check, memory-check [untest]

- fix high risk security issues
- fix high memory usage
- change adb to connection pool single instance

Signed-off-by: pakintada@gmail.com <Pakin>
This commit is contained in:
pakintada@gmail.com 2026-06-22 10:55:47 +07:00
parent f0619c5a10
commit 4f464a8513
23 changed files with 413 additions and 73 deletions

View file

@ -17,8 +17,60 @@ import { adbWriter } from '../stores/adbWriter';
import { WritableStream } from '@yume-chan/stream-extra';
import { env } from '$env/dynamic/public';
import { get } from 'svelte/store';
import { adbConnectionStatus } from '../stores/adbConnectionStore';
let _connectionPromise: Promise<boolean> | null = null;
let syncConnection: any = null;
/**
* Centralized connection function that all pages and components should use.
*
* - If a device is already connected, returns `true` immediately.
* - If a connection is already in progress, waits for it and returns its result.
* - Otherwise attempts to connect using stored credentials first
* (AdbDaemonWebUsbDeviceManager.BROWSER.getDevices), falling back to
* prompting the user with the browser's WebUSB device picker.
*
* This deduplicates connection attempts across the app: no matter how many
* pages/components call it simultaneously, only one WebUSB handshake happens.
*
* @returns `true` when a device is (or became) connected, `false` otherwise.
*/
export async function ensureAdbConnection(): Promise<boolean> {
// Already connected.
if (getAdbInstance()) return true;
// Already in progress — wait for it.
if (_connectionPromise) return await _connectionPromise;
adbConnectionStatus.setConnecting();
_connectionPromise = (async () => {
try {
const deviceManager = AdbDaemonWebUsbDeviceManager.BROWSER;
if (deviceManager && 'usb' in navigator) {
const devices = await deviceManager.getDevices();
if (devices && devices.length === 1) {
const credStore = new AdbWebCredentialStore();
await connectDeviceByCred(devices[0], credStore);
return Boolean(getAdbInstance());
}
}
// Fallback: prompt user to pick a device.
await connnectViaWebUSB();
return Boolean(getAdbInstance());
} catch (e) {
logger.error('ensureAdbConnection failed', e);
adbConnectionStatus.setDisconnected();
return false;
} finally {
_connectionPromise = null;
}
})();
return await _connectionPromise;
}
let syncOperation: Promise<unknown> = Promise.resolve();
let recipeMenuAdbConnectPromise: Promise<Adb | undefined> | null = null;
let recipeMenuAndroidServerConnectPromise: Promise<void> | null = null;
@ -138,6 +190,7 @@ async function connectWithRetry<T>(
}
export async function connnectViaWebUSB(connectAndroidServer = true) {
adbConnectionStatus.setConnecting();
const device = await AdbDaemonWebUsbDeviceManager.BROWSER?.requestDevice();
logger.info('usb ok', globalThis.navigator.usb);
if (device) {
@ -180,6 +233,7 @@ export async function connectDeviceByCred(
credStore: AdbWebCredentialStore,
connectAndroidServer = true
) {
adbConnectionStatus.setConnecting();
try {
const connection = await device.connect();
const transport = await AdbDaemonTransport.authenticate({
@ -197,6 +251,7 @@ export async function connectDeviceByCred(
return true;
} catch (error) {
adbConnectionStatus.setDisconnected();
throw error;
}
}
@ -204,6 +259,11 @@ export async function connectDeviceByCred(
export async function saveAdbInstance(adb: Adb | undefined) {
await cleanupSync();
AdbInstance.instance = adb;
if (adb) {
adbConnectionStatus.setConnected();
} else {
adbConnectionStatus.setDisconnected();
}
}
export function getAdbInstance() {
@ -211,6 +271,7 @@ export function getAdbInstance() {
}
export async function connectRecipeMenuViaWebUSB() {
adbConnectionStatus.setConnecting();
const currentInstance = getAdbInstance();
if (currentInstance) {
await connectToAndroidRecipeMenuServer();
@ -250,6 +311,7 @@ export async function connectRecipeMenuDeviceByCred(
device: AdbDaemonWebUsbDevice,
credStore: AdbWebCredentialStore
) {
adbConnectionStatus.setConnecting();
const currentInstance = getAdbInstance();
if (currentInstance) {
await connectToAndroidRecipeMenuServer();
@ -316,7 +378,22 @@ export async function sendRecipeMenuMessageToAndroid(message: any) {
}
}
/**
* Reject shell metacharacters that enable command injection.
* Allows alphanumerics, spaces, common paths (/ - _ .), and ADB shell primitives.
*/
const UNSAFE_SHELL_PATTERN = /[;&|`$(){}[\]<>!#~*?\\\n\r]/;
function sanitizeAdbCommand(command: string): string {
if (UNSAFE_SHELL_PATTERN.test(command)) {
logger.warn(`Command contains unsafe characters, rejecting: ${command.slice(0, 80)}`);
throw new Error(`Command rejected: contains shell metacharacters`);
}
return command;
}
export async function executeCmd(command: string) {
command = sanitizeAdbCommand(command);
let instance = getAdbInstance();
if (!instance) {
@ -378,6 +455,7 @@ export async function executeStreamingCmd(
onExit?: (exitCode: number | undefined) => void;
}
): Promise<() => void> {
command = sanitizeAdbCommand(command);
const instance = getAdbInstance();
let aborted = false;
@ -637,14 +715,15 @@ async function connectToAndroidServer(maxRetries = 5) {
if (isRecoverableError(e)) {
void connectToAndroidServer();
}
} finally {
adbWriter.set(null);
addNotification('WARN:Brewing Mode T Offline ...');
}
})();
return;
} else {
addNotification('WARN:Brewing Mode T unavailable');
} finally {
adbWriter.set(null);
addNotification('WARN:Brewing Mode T Offline ...');
reader.cancel().catch(() => {});
}
})();
return;
} else {
addNotification('WARN:Brewing Mode T unavailable');
if (attempt < maxRetries - 1) {
const delay = Math.min(500 * Math.pow(2, attempt) + Math.random() * 500, 5000);
@ -752,6 +831,7 @@ async function connectToAndroidRecipeMenuServerOnce(notifyFailure = true, retryO
} finally {
adbWriter.set(null);
addNotification('WARN:Android recipe menu channel offline ...');
reader.cancel().catch(() => {});
if (retryOnFailure) {
scheduleRecipeMenuAndroidServerReconnect();
}
@ -773,3 +853,9 @@ async function connectToAndroidRecipeMenuServerOnce(notifyFailure = true, retryO
export function getScrcpyBinaryFromSource() {
//https://github.com/Genymobile/scrcpy/releases
}
// Initialize connection store based on existing instance
// (handles hot-reload / page refresh where instance survives)
if (AdbInstance.instance) {
adbConnectionStatus.setConnected();
}