JJ_MKVMaker/lib/app/tor_bundle.dart

181 lines
7.5 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:crypto/crypto.dart';
import 'package:flutter/services.dart';
import 'package:path/path.dart' as p;
import '../l10n/tr.dart';
import 'component_store.dart';
/// Tor 컴포넌트 (환경 설정 > 컴포넌트 > Tor 브라우저).
/// - Windows: 공식 Tor Expert Bundle 을 받아 (판 · SHA-256 고정) 앱 데이터 폴더 components/tor 에 tor.exe · geoip 를 둔다
/// - Android: 받은 실행 파일은 실행할 수 없어 (W^X) 같은 판의 Android 번들에서 꺼낸 libTor.so (실행 파일) 를 APK 에 넣었다
/// (tool/fetch_tor_android.ps1 → third_party/tor/android/arm64-v8a/libtor.so). 켜기 · 끄기만 한다.
class TorBundle {
/// Tor Browser 15.0.24 의 Expert Bundle (tor 0.4.9.13). 올릴 때: 판 · 아래 SHA-256 (공식 sha256sums-signed-build.txt) ·
/// tool/fetch_tor_android.ps1 · tool/licenses/fetch_licenses.py 를 함께 바꾼다
static const version = '15.0.24';
static const torVersion = '0.4.9.13';
static const windowsFile = 'tor-expert-bundle-windows-x86_64-$version.tar.gz';
static const windowsSha256 = 'e9dc6ccc93cd6afa507193f4de284d6424233ff5102155cd2c94b259e8a22b65';
static const androidFile = 'tor-expert-bundle-android-aarch64-$version.tar.gz';
static const androidSha256 = '2a7a02484149b05f64b582a4a1e8eb8aabb9f6cd32daca4e03c58e09839bb7a8';
/// 받을 곳: 공식 보관소 (판이 지워지지 않음), 안 되면 공식 배포처
static List<String> urls(String file) => [
'https://archive.torproject.org/tor-package-archive/torbrowser/$version/$file',
'https://dist.torproject.org/torbrowser/$version/$file',
];
/// 받을 크기 (안내용)
static const approxDownloadMb = 22;
static const id = 'tor';
static Directory get dir => ComponentStore.shared.dirOf(id);
/// Windows: 설치한 tor.exe (없으면 null)
static String? get windowsExe {
final f = File(p.join(dir.path, 'tor.exe'));
return f.existsSync() && ComponentStore.shared.isInstalled(id) ? f.path : null;
}
/// 설치한 판 (installed.json)
static String? installedVersion() {
try {
final j = jsonDecode(File(p.join(dir.path, 'installed.json')).readAsStringSync()) as Map;
return j['version'] as String?;
} catch (_) {
return null;
}
}
/// Android: APK 에 들어 있는 tor (libtor.so). 없으면 (이 APK 에 넣지 않음) null
static Future<String?> androidExe() async {
try {
final d = await const MethodChannel('jj_mkvmaker/android').invokeMethod<String>('nativeLibDir');
final f = d == null ? null : File(p.join(d, 'libtor.so'));
return f != null && f.existsSync() ? f.path : null;
} catch (_) {
return null;
}
}
/// 이 기기에서 쓸 tor 실행 파일
static Future<String?> executable() async => Platform.isAndroid ? androidExe() : windowsExe;
/// geoip 파일 (있으면 tor 에 알려 준다 - 없어도 동작)
static String? geoip(String name) {
final f = File(p.join(dir.path, name));
return f.existsSync() ? f.path : null;
}
/// Windows 설치: 받기 (SHA-256 확인) → 풀기 → tor.exe · geoip · 라이선스 문서만 두기.
/// [onProgress] (단계, 0~1 또는 null). [urlsOverride] · [sha256Override] 는 시험용
static Future<void> installWindows({
void Function(String step, double? progress)? onProgress,
bool Function()? isCancelled,
List<String>? urlsOverride,
String? sha256Override,
String? targetOverride,
}) async {
final target = Directory(targetOverride ?? dir.path);
final work = await Directory.systemTemp.createTemp('jj_tor_');
final client = HttpClient()..userAgent = 'JJMKVMaker';
try {
final archive = File(p.join(work.path, windowsFile));
Object? last;
var ok = false;
for (final url in urlsOverride ?? urls(windowsFile)) {
try {
await _download(client, url, archive, sha256Override ?? windowsSha256,
(d) => onProgress?.call(trf('받는 중: {0}', [windowsFile]), d), isCancelled);
ok = true;
break;
} on _Cancelled {
rethrow;
} on ComponentException {
rethrow; // 손상 (SHA-256 다름) 은 다른 곳에서 받아도 다시 시도하지 않는다
} catch (e) {
last = e;
}
}
if (!ok) throw ComponentException(trf('Tor 를 받을 수 없습니다: {0}', [last]));
onProgress?.call(tr('푸는 중'), null);
final x = Directory(p.join(work.path, 'x'))..createSync();
final r = await Process.run('tar', ['-xzf', archive.path, '-C', x.path]);
if (r.exitCode != 0) throw ComponentException(trf('압축을 풀 수 없습니다: {0}', [r.stderr]));
final exe = File(p.join(x.path, 'tor', 'tor.exe'));
if (!exe.existsSync()) throw ComponentException(tr('받은 파일에 tor.exe 가 없습니다'));
// 새 폴더에 모은 뒤 바꿔 넣는다 (중간에 실패해도 반쯤 깔린 것이 남지 않게)
final staging = Directory('${target.path}.new');
if (staging.existsSync()) staging.deleteSync(recursive: true);
Directory(p.join(staging.path, 'docs')).createSync(recursive: true);
exe.copySync(p.join(staging.path, 'tor.exe'));
for (final g in ['geoip', 'geoip6']) {
final f = File(p.join(x.path, 'data', g));
if (f.existsSync()) f.copySync(p.join(staging.path, g));
}
final docs = Directory(p.join(x.path, 'docs'));
if (docs.existsSync()) {
for (final f in docs.listSync().whereType<File>()) {
f.copySync(p.join(staging.path, 'docs', p.basename(f.path)));
}
}
File(p.join(staging.path, 'installed.json')).writeAsStringSync(
jsonEncode({'version': version, 'tor': torVersion, 'sha256': sha256Override ?? windowsSha256, 'installed': DateTime.now().toIso8601String()}));
if (target.existsSync()) target.deleteSync(recursive: true);
target.parent.createSync(recursive: true);
staging.renameSync(target.path);
onProgress?.call(tr('완료'), 1);
} on _Cancelled {
throw ComponentException(tr('취소했습니다'));
} finally {
client.close(force: true);
try {
work.deleteSync(recursive: true);
} catch (_) {}
}
}
static Future<void> _download(HttpClient client, String url, File out, String sha,
void Function(double?) onProgress, bool Function()? isCancelled) async {
final req = await client.getUrl(Uri.parse(url));
final res = await req.close();
if (res.statusCode != 200) throw HttpException(trf('받을 수 없습니다 ({0}): {1}', [res.statusCode, url]));
final total = res.contentLength;
final sink = out.openWrite();
final hash = AccumulatorSink<Digest>();
final hasher = sha256.startChunkedConversion(hash);
var got = 0;
try {
await for (final chunk in res) {
if (isCancelled?.call() ?? false) throw const _Cancelled();
sink.add(chunk);
hasher.add(chunk);
got += chunk.length;
onProgress(total > 0 ? got / total : null);
}
} finally {
await sink.close();
hasher.close();
}
if (hash.events.single.toString() != sha.toLowerCase()) {
await out.delete();
throw ComponentException(trf('파일이 손상되었습니다 (SHA-256 다름): {0}', [p.basename(out.path)]));
}
}
/// Windows 제거 (Tor 를 먼저 끈 뒤)
static Future<void> removeWindows() async {
if (dir.existsSync()) await dir.delete(recursive: true);
}
}
class _Cancelled implements Exception {
const _Cancelled();
}