- PackageInstallerModule: DigestInputStream边写边算SHA256, 原生层校验消除TOCTOU - updater: remove expo-crypto, add download domain whitelist, 错误消息去敏感路径 - package.json: 移除expo-crypto依赖(不再需要JS侧SHA256)
115 lines
3.6 KiB
TypeScript
115 lines
3.6 KiB
TypeScript
import { parseVersion, checkUpdate } from '@yuzu-gca/shared';
|
||
import Constants from 'expo-constants';
|
||
import * as FileSystem from 'expo-file-system';
|
||
import { NativeModules, Platform } from 'react-native';
|
||
|
||
// ============================================================
|
||
// 1. 版本号读取
|
||
// ============================================================
|
||
|
||
export function getLocalVersion(): string {
|
||
return Constants.expoConfig?.version ?? '0.1.0';
|
||
}
|
||
|
||
export function getLocalBuildTag(): string {
|
||
const v = getLocalVersion();
|
||
const versionCode = Constants.expoConfig?.android?.versionCode ?? 1;
|
||
return `v${v}-dav-android-${versionCode}`;
|
||
}
|
||
|
||
// ============================================================
|
||
// 2. JS Bundle OTA(expo-updates 自动处理)
|
||
// ============================================================
|
||
|
||
export async function checkJsBundleUpdate(): Promise<{
|
||
hasUpdate: boolean;
|
||
remoteVersion: string;
|
||
}> {
|
||
const localVersion = getLocalVersion();
|
||
const localTag = getLocalBuildTag();
|
||
const local = parseVersion(localTag);
|
||
if (!local) return { hasUpdate: false, remoteVersion: localVersion };
|
||
|
||
const result = await checkUpdate('android', local);
|
||
if (!result.shouldUpdate || !result.remoteVersion) {
|
||
return { hasUpdate: false, remoteVersion: localVersion };
|
||
}
|
||
|
||
return { hasUpdate: true, remoteVersion: result.remoteVersion.raw };
|
||
}
|
||
|
||
// ============================================================
|
||
// 3. APK 静默安装(大更新)
|
||
// ============================================================
|
||
|
||
const ALLOWED_DOWNLOAD_HOSTS = [
|
||
'git.childish-ghost.com',
|
||
'github.com',
|
||
];
|
||
|
||
const { PackageInstallerModule } = NativeModules;
|
||
|
||
function validateUrl(url: string): void {
|
||
try {
|
||
const host = new URL(url).hostname;
|
||
if (!ALLOWED_DOWNLOAD_HOSTS.some(h => host === h || host.endsWith('.' + h))) {
|
||
throw new Error(`不允许的下载域名: ${host}`);
|
||
}
|
||
} catch (e) {
|
||
if (e instanceof TypeError) throw new Error('下载URL格式无效');
|
||
throw e;
|
||
}
|
||
}
|
||
|
||
export async function downloadAndInstallApk(apkUrl: string, sha256: string): Promise<void> {
|
||
validateUrl(apkUrl);
|
||
|
||
const localPath = `${FileSystem.cacheDirectory ?? ''}gca-update.apk`;
|
||
|
||
const download = FileSystem.createDownloadResumable(
|
||
apkUrl,
|
||
localPath,
|
||
{},
|
||
(progress) => {
|
||
const pct = (progress.totalBytesWritten / progress.totalBytesExpectedToWrite) * 100;
|
||
console.log(`[OTA] 下载进度: ${pct.toFixed(0)}%`);
|
||
},
|
||
);
|
||
|
||
const result = await download.downloadAsync();
|
||
if (!result?.uri) throw new Error('APK 下载失败');
|
||
|
||
if (Platform.OS === 'android' && PackageInstallerModule) {
|
||
// SHA256 校验在原生层完成(避免 TOCTOU + 密码学误用)
|
||
await PackageInstallerModule.installApk(result.uri, sha256);
|
||
console.log('[OTA] SHA256校验通过,APK已提交安装');
|
||
} else {
|
||
throw new Error('PackageInstaller 原生模块不可用');
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// 4. 启动时一键检查
|
||
// ============================================================
|
||
|
||
export async function onAppStartCheckUpdate(): Promise<{
|
||
jsUpdate: boolean;
|
||
apkUpdate: boolean;
|
||
}> {
|
||
const localTag = getLocalBuildTag();
|
||
const local = parseVersion(localTag);
|
||
if (!local) return { jsUpdate: false, apkUpdate: false };
|
||
|
||
const result = await checkUpdate('android', local);
|
||
if (!result.shouldUpdate || !result.build) {
|
||
return { jsUpdate: false, apkUpdate: false };
|
||
}
|
||
|
||
if (result.build.type === 'apk') {
|
||
await downloadAndInstallApk(result.build.url, result.build.sha256);
|
||
return { jsUpdate: false, apkUpdate: true };
|
||
}
|
||
|
||
return { jsUpdate: true, apkUpdate: false };
|
||
}
|