JJ_MKVMaker/integration_test/tor_dns_real_test.dart

191 lines
9.0 KiB
Dart

import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:jj_mkvmaker/app/app_controller.dart';
import 'package:jj_mkvmaker/app/bookmarks_controller.dart';
import 'package:jj_mkvmaker/app/component_store.dart';
import 'package:jj_mkvmaker/app/net_guard.dart';
import 'package:jj_mkvmaker/app/tor_bundle.dart';
import 'package:jj_mkvmaker/app/tor_service.dart';
import 'package:jj_mkvmaker/core/local_proxy.dart';
import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart';
import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart';
import 'package:jj_mkvmaker/services/platform_services.dart';
import 'package:jj_mkvmaker/ui/app_actions.dart';
import 'package:jj_mkvmaker/ui/browser_page.dart';
import 'package:jj_mkvmaker/ui/tor_page.dart';
import 'package:path/path.dart' as p;
/// 실제 망 (Windows): DNS 강제 (8.8.8.8) 로 앱의 HTTP · 일반 브라우저 (Edge) 가 이름을 푸는지, 닿지 않는 DNS 는 실패로 알리는지,
/// Tor 컴포넌트 (공식 번들, SHA-256 확인) 를 받아 Tor 화면이 check.torproject.org 의 "Congratulations" 까지 가는지.
/// 데이터는 JJ_MKVMAKER_DATA (없으면 임시 폴더) 에만 - 사용자 앱 데이터를 건드리지 않는다.
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('DNS 강제 · Tor 화면 실제 연결', (tester) async {
final data = Platform.environment['JJ_MKVMAKER_DATA'] ?? Directory.systemTemp.createTempSync('jj_tor_real_').path;
Directory(data).createSync(recursive: true);
ComponentStore.dataDirectory = data;
ComponentStore.shared = ComponentStore(data);
TorService.dataDir = data;
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
c.settings.webViewDataDir = p.join(data, 'webview');
final logs = <String>[];
final answered = <String>[];
final g = NetGuard.instance
..config = (() => c.settings.dnsConfig)
..log = logs.add;
g.resolver.onAnswer = (h, s) => answered.add('$h@$s');
TorService.instance.log = logs.add;
expect(c.settings.dnsMode, 'custom');
expect(c.settings.dnsPrimary, '8.8.8.8');
await tester.runAsync(g.ensure);
HttpOverrides.global = JjHttpOverrides();
Future<void> wait(int ms) async {
for (var i = 0; i < ms ~/ 200; i++) {
await tester.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 200)));
await tester.pump();
}
}
// 1. 앱의 HTTP (HttpClient) → 앱 안 프록시 → 8.8.8.8
final code = await tester.runAsync(() async {
final cl = HttpClient();
final r = await (await cl.getUrl(Uri.parse('https://www.google.com/generate_204'))).close();
await r.drain<void>();
cl.close();
return r.statusCode;
});
// ignore: avoid_print
print('RESULT HttpClient google 204: $code, 답한 DNS: $answered');
expect(code, 204);
expect(answered, contains('www.google.com@8.8.8.8'));
// 1-2. 같은 기기의 다른 프로그램처럼 비밀값 없이 프록시에 붙으면 407
final bare = await tester.runAsync(() async {
final s = await Socket.connect(InternetAddress.loopbackIPv4, g.port!);
s.write('CONNECT www.google.com:443 HTTP/1.1\r\nHost: www.google.com:443\r\n\r\n');
final first = await s.cast<List<int>>().transform(latin1.decoder).first;
s.destroy();
return first.split('\r\n').first;
});
// ignore: avoid_print
print('RESULT 비밀값 없는 연결: $bare');
expect(bare, startsWith('HTTP/1.1 407'));
// 2. 일반 브라우저 (Edge WebView2) 도 같은 프록시로
final bm = BookmarksController(p.join(data, 'bm.json'));
await tester.pumpWidget(MaterialApp(
builder: (context, child) => AppScope(controller: c, bookmarks: bm, child: child!),
home: BrowserPage(c: c, bookmarks: bm, initialUrl: 'https://example.com/'),
));
String title = '';
for (var i = 0; i < 60 && !title.contains('Example'); i++) {
await wait(500);
title = '${await tester.runAsync(() async => BrowserPage.debugNav?.evaluate('document.title'))}';
}
// ignore: avoid_print
print('RESULT Edge example.com 제목: $title, 답한 DNS: ${answered.where((a) => a.startsWith('example.com'))}');
expect(title, contains('Example'));
expect(answered, contains('example.com@8.8.8.8'));
await tester.pumpWidget(const SizedBox());
await wait(1000);
// 3. 닿지 않는 DNS (TEST-NET) → 기기 DNS 로 바꾸지 않고 실패 + 알림
c.settings
..dnsPrimary = '192.0.2.1'
..dnsSecondary = '';
g.resolver.clearCache();
final err = await tester.runAsync(() async {
final cl = HttpClient();
try {
final r = await (await cl.getUrl(Uri.parse('https://www.wikipedia.org/'))).close();
await r.drain<void>();
return 'HTTP ${r.statusCode}';
} catch (e) {
return '$e';
} finally {
cl.close(force: true);
}
});
// ignore: avoid_print
print('RESULT 닿지 않는 DNS: $err / 알림: ${g.failure.value?.reason}');
expect(err, contains('502'));
expect(g.failure.value, isNotNull);
c.settings
..dnsPrimary = '8.8.8.8'
..dnsSecondary = '8.8.4.4';
g.resolver.clearCache();
// 4. Tor 컴포넌트 받기 (공식 번들 · SHA-256) → Tor 화면 → check.torproject.org
await tester.runAsync(() => TorBundle.installWindows());
expect(TorBundle.windowsExe, isNotNull);
expect(TorBundle.installedVersion(), TorBundle.version);
c.settings
..components = [...c.settings.components, 'tor']
..torNoticeSeen = true;
await tester.pumpWidget(MaterialApp(
builder: (context, child) => AppScope(controller: c, bookmarks: bm, child: child!),
home: TorPage(c: c),
));
var body = '';
for (var i = 0; i < 360 && !body.contains('Congratulations'); i++) {
await wait(500);
if (TorPage.debugNav != null) body = '${await tester.runAsync(() async => TorPage.debugNav!.evaluate('document.body ? document.body.innerText : ""'))}';
}
final rtc = await tester.runAsync(() async => TorPage.debugNav?.evaluate('typeof RTCPeerConnection'));
// ignore: avoid_print
print('RESULT Tor 상태: ${TorService.instance.status.value.phase} ${TorService.instance.status.value.percent}% ${TorService.instance.status.value.message} / RTCPeerConnection: $rtc\n'
'RESULT check.torproject.org: ${body.split('\n').where((l) => l.trim().isNotEmpty).take(3).join(' | ')}');
// ignore: avoid_print
print('RESULT 기록: ${logs.where((l) => l.contains('Tor')).join(' | ')}');
expect(body, contains('Congratulations'));
expect(rtc, 'undefined');
// 5. 파일 링크: 받지 않고 안내 (Windows 는 packages/flutter_inappwebview_windows/JJ_PATCH.md 의 DownloadStarting)
// dist.torproject.org 는 예전 판을 지운다 (10/17 15.0.24 가 404) - 오래 남는 archive 의 같은 파일 (833B, application/pgp-keys)
const fileUrl = 'https://archive.torproject.org/tor-package-archive/torbrowser/15.0.24/tor-expert-bundle-windows-x86_64-15.0.24.tar.gz.asc';
await tester.runAsync(() async => TorPage.debugNav?.load(fileUrl));
var told = false;
for (var i = 0; i < 120 && !told; i++) {
await wait(500);
told = find.text('Tor 화면에서는 파일을 받지 않습니다 (Tor 밖으로 나가지 않게)').evaluate().isNotEmpty;
}
final downloads = Platform.environment['USERPROFILE'] == null ? null : Directory(p.join(Platform.environment['USERPROFILE']!, 'Downloads'));
final saved = downloads == null || !downloads.existsSync()
? <String>[]
: [for (final f in downloads.listSync()) if (p.basename(f.path).startsWith('tor-expert-bundle-windows-x86_64-15.0.24.tar.gz.asc')) f.path];
// ignore: avoid_print
print('RESULT 파일 링크 안내: $told / 다운로드 폴더에 생긴 것: $saved');
expect(told, isTrue);
expect(saved, isEmpty);
// 6. Tor 화면을 닫으면 Tor 다리도 닫힌다
final bridge = TorService.instance.bridgeOpenPort;
expect(bridge, isNotNull);
await tester.pumpWidget(const SizedBox());
for (var i = 0; i < 20 && TorService.instance.bridgeOpenPort != null; i++) {
await wait(500);
}
final closed = await tester.runAsync(() async {
try {
final s = await Socket.connect(InternetAddress.loopbackIPv4, bridge!, timeout: const Duration(seconds: 2));
s.destroy();
return false;
} catch (_) {
return true;
}
});
// ignore: avoid_print
print('RESULT Tor 화면을 닫은 뒤 다리 (127.0.0.1:$bridge) 닫힘: $closed');
expect(TorService.instance.bridgeOpenPort, isNull);
expect(closed, isTrue);
expect(ProxyAuth.instance.password, isNotEmpty);
await tester.runAsync(() => TorService.instance.stop());
await wait(1000);
}, timeout: const Timeout(Duration(minutes: 10)));
}