mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-03 19:21:32 -07:00
fix(anime): harden the extension bridge against untrusted repos and hangs
Addresses CodeRabbit review feedback on the Anime Browser: - reject repository package/apk names that are not plain identifiers, and verify the install target resolves inside the extensions directory - count mpv's %n% option escape in UTF-8 bytes, and escape backslashes in header values so a trailing one cannot eat the list separator - key the bridge extension-id cache by APK content, so an in-place upgrade re-uploads instead of running the previous build - bound every bridge, release-listing, and download request with a timeout - enforce the APK size limit while streaming rather than after buffering - read APK bytes on demand instead of holding a base64 copy per extension for the lifetime of the browser - serialize preference mutations and write the file atomically - handle the sidecar spawn error event, and wait for the child to exit in stop() before returning - report a failed Anime Browser bootstrap instead of showing the starting banner forever - keep the preferences panel's save confirmation and in-flight multi-select edits by re-rendering only on a structural schema change
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
import { createReadStream } from 'node:fs';
|
||||
import { readdir, readFile } from 'node:fs/promises';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { pipeline } from 'node:stream/promises';
|
||||
import path from 'node:path';
|
||||
import type { AnimeBridgeClient } from './bridge-client';
|
||||
import type { BridgeSource } from './bridge-client';
|
||||
@@ -15,7 +18,11 @@ export interface InstalledExtension {
|
||||
file: string;
|
||||
/** File name without extension, used when the bridge reports no name. */
|
||||
fallbackName: string;
|
||||
apkBase64: string;
|
||||
/**
|
||||
* SHA-256 of the APK. Identifies the build rather than the slot, so the
|
||||
* bridge's extension-id cache misses after an in-place upgrade.
|
||||
*/
|
||||
sha256: string;
|
||||
}
|
||||
|
||||
export interface ExtensionSource {
|
||||
@@ -27,7 +34,14 @@ export interface ExtensionSource {
|
||||
file: string;
|
||||
}
|
||||
|
||||
/** Read every .apk in `directory`. A missing directory yields no extensions. */
|
||||
/**
|
||||
* Discover every .apk in `directory`. A missing directory yields no extensions.
|
||||
*
|
||||
* Only a hash is kept, never the bytes: APKs run to several MB each and a
|
||||
* base64 copy adds a third on top, so holding the whole set for the lifetime of
|
||||
* the Anime Browser would cost far more than re-reading a file on the rare
|
||||
* upload. Hashing streams, so peak memory stays flat regardless of APK size.
|
||||
*/
|
||||
export async function readInstalledExtensions(directory: string): Promise<InstalledExtension[]> {
|
||||
let entries;
|
||||
try {
|
||||
@@ -40,16 +54,21 @@ export async function readInstalledExtensions(directory: string): Promise<Instal
|
||||
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
||||
if (!entry.isFile() || !entry.name.toLowerCase().endsWith('.apk')) continue;
|
||||
const file = path.join(directory, entry.name);
|
||||
const bytes = await readFile(file);
|
||||
extensions.push({
|
||||
file,
|
||||
fallbackName: entry.name.replace(/\.apk$/i, ''),
|
||||
apkBase64: bytes.toString('base64'),
|
||||
sha256: await hashFile(file),
|
||||
});
|
||||
}
|
||||
return extensions;
|
||||
}
|
||||
|
||||
async function hashFile(file: string): Promise<string> {
|
||||
const hash = createHash('sha256');
|
||||
await pipeline(createReadStream(file), hash);
|
||||
return hash.digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Describe what is on disk, for the installed list in the Extensions tab.
|
||||
*
|
||||
@@ -75,9 +94,18 @@ export function toInstalledExtensionViews(
|
||||
});
|
||||
}
|
||||
|
||||
/** The bridge payload for a specific source inside an extension. */
|
||||
/**
|
||||
* The bridge payload for a specific source inside an extension.
|
||||
*
|
||||
* The APK is read on demand: after the first upload the bridge answers by
|
||||
* extension id, so most calls never touch the file at all.
|
||||
*/
|
||||
export function toBridgeSource(extension: InstalledExtension, sourceId?: string): BridgeSource {
|
||||
return { apkBase64: extension.apkBase64, ...(sourceId ? { sourceId } : {}) };
|
||||
return {
|
||||
fingerprint: extension.sha256,
|
||||
loadApkBase64: async () => (await readFile(extension.file)).toString('base64'),
|
||||
...(sourceId ? { sourceId } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user