238 lines
11 KiB
Dart
238 lines
11 KiB
Dart
import 'dart:async';
|
|
import 'dart:io';
|
|
|
|
import 'package:archive/archive.dart';
|
|
import 'package:crypto/crypto.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:jj_mkvmaker/app/app_controller.dart';
|
|
import 'package:jj_mkvmaker/app/component_store.dart';
|
|
import 'package:jj_mkvmaker/app/components.dart';
|
|
import 'package:jj_mkvmaker/app/settings.dart';
|
|
import 'package:jj_mkvmaker/app/tor_bundle.dart';
|
|
import 'package:jj_mkvmaker/app/tor_service.dart';
|
|
import 'package:jj_mkvmaker/core/tor_control.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/tor_page.dart';
|
|
|
|
void main() {
|
|
test('tor 알림 줄 읽기 · torrc', () {
|
|
expect(parseBootstrap('Oct 10 19:16:01.000 [notice] Bootstrapped 45% (loading_descriptors): Loading relay descriptors'),
|
|
(45, 'Loading relay descriptors'));
|
|
expect(parseBootstrap('[notice] Bootstrapped 100% (done): Done')?.$1, 100);
|
|
expect(parseBootstrap('[notice] Opening Socks listener on 127.0.0.1:0'), isNull);
|
|
expect(parseSocksPort('[notice] Opened Socks listener connection (ready) on 127.0.0.1:53017'), 53017);
|
|
final rc = buildTorrc(dataDir: r'C:\d\tor_data', controlPortFile: r'C:\d\tor_data\cp.txt', ownerPid: 1234, geoip: r'C:\g\geoip');
|
|
expect(rc, contains('SocksPort 127.0.0.1:auto'));
|
|
expect(rc, contains('ControlPort 127.0.0.1:auto'));
|
|
expect(rc, contains('CookieAuthentication 1'));
|
|
expect(rc, contains('__OwningControllerProcess 1234'));
|
|
expect(rc, contains(r'GeoIPFile C:\g\geoip'));
|
|
expect(rc, isNot(contains('GeoIPv6File')));
|
|
});
|
|
|
|
test('새는 것 막기: Tor 100% · 프록시 · 전용 프로필이 모두 되어야 웹 뷰를 만든다', () {
|
|
const ready = TorStatus(TorPhase.ready, percent: 100);
|
|
expect(torViewMayLoad(status: ready, proxyApplied: true, profileReady: true), isTrue);
|
|
expect(torViewMayLoad(status: ready, proxyApplied: false, profileReady: true), isFalse);
|
|
expect(torViewMayLoad(status: ready, proxyApplied: true, profileReady: false), isFalse);
|
|
for (final s in [TorStatus.off, const TorStatus(TorPhase.starting, percent: 99), const TorStatus(TorPhase.failed)]) {
|
|
expect(torViewMayLoad(status: s, proxyApplied: true, profileReady: true), isFalse, reason: '${s.phase}');
|
|
}
|
|
});
|
|
|
|
test('Tor 검색은 DuckDuckGo (Google 로 보내지 않음), 주소는 그대로', () {
|
|
expect(torAddress('tor project'), 'https://duckduckgo.com/?q=tor+project');
|
|
expect(torAddress('check.torproject.org'), 'https://check.torproject.org');
|
|
expect(torAddress('http://example.onion/'), 'http://example.onion/');
|
|
});
|
|
|
|
test('Edge 실행 인수: 모든 연결 (localhost 포함) 을 Tor 다리로 · WebRTC UDP 막기 · QUIC 끄기', () {
|
|
final a = torBrowserArguments(41234);
|
|
expect(a, contains('--proxy-server=http://127.0.0.1:41234'));
|
|
expect(a, contains('--proxy-bypass-list=<-loopback>'));
|
|
expect(a, contains('--force-webrtc-ip-handling-policy=disable_non_proxied_udp'));
|
|
expect(a, contains('--disable-quic'));
|
|
expect(torWebRtcBlockScript, contains('RTCPeerConnection'));
|
|
});
|
|
|
|
test('컴포넌트: Tor 는 처음 꺼짐 · 화면은 웹 브라우저 바로 뒤 (예전 순서에도)', () {
|
|
expect(AppComponent.tor.builtIn, isFalse);
|
|
expect(AppComponent.defaultInstalled, isNot(contains('tor')));
|
|
expect(AppComponent.defaultOrder.indexOf('tor'), AppComponent.defaultOrder.indexOf('browser') + 1);
|
|
final ids = [for (final p in AppComponent.pages(['mkv', 'browser', 'tor', 'explorer'], ['explorer', 'mkv', 'browser'])) p.id];
|
|
expect(ids, ['explorer', 'mkv', 'browser', 'tor']);
|
|
expect([for (final p in AppComponent.pages(['mkv', 'browser', 'tor'], ['browser', 'mkv'])) p.id], ['browser', 'tor', 'mkv']);
|
|
final s = AppSettings()..startScreen = 'tor';
|
|
expect(AppNavButtons.homeId(s), 'mkv', reason: '설치하지 않았으면 첫 화면');
|
|
s.components = [...s.components, 'tor'];
|
|
expect(AppNavButtons.homeId(s), 'tor');
|
|
});
|
|
|
|
group('Tor 컴포넌트 (Windows 번들)', () {
|
|
test('판 · SHA-256 고정 (공식 sha256sums-signed-build.txt)', () {
|
|
final hex = RegExp(r'^[0-9a-f]{64}$');
|
|
expect(TorBundle.windowsSha256, matches(hex));
|
|
expect(TorBundle.androidSha256, matches(hex));
|
|
expect(TorBundle.windowsSha256, 'e9dc6ccc93cd6afa507193f4de284d6424233ff5102155cd2c94b259e8a22b65');
|
|
expect(TorBundle.windowsFile, 'tor-expert-bundle-windows-x86_64-${TorBundle.version}.tar.gz');
|
|
for (final u in TorBundle.urls(TorBundle.windowsFile)) {
|
|
expect(u, startsWith('https://'));
|
|
expect(u, contains('torproject.org/'));
|
|
expect(u, endsWith('/${TorBundle.version}/${TorBundle.windowsFile}'));
|
|
}
|
|
// Android 준비 스크립트도 같은 판 · 같은 SHA-256
|
|
final ps = File('tool/fetch_tor_android.ps1').readAsStringSync();
|
|
expect(ps, contains("'${TorBundle.version}'"));
|
|
expect(ps, contains(TorBundle.androidSha256));
|
|
});
|
|
|
|
late HttpServer server;
|
|
late Directory tmp;
|
|
late List<int> bundle;
|
|
setUp(() async {
|
|
// 이 파일의 testWidgets 가 HttpClient 를 막아 둔다 - 이 시험은 실제 (로컬) 서버에 연결
|
|
HttpOverrides.global = null;
|
|
tmp = Directory.systemTemp.createTempSync('jj_tor_test_');
|
|
final a = Archive()
|
|
..addFile(ArchiveFile.bytes('tor/tor.exe', [77, 90, 1, 2]))
|
|
..addFile(ArchiveFile.bytes('data/geoip', [1]))
|
|
..addFile(ArchiveFile.bytes('docs/tor.txt', 'Tor license'.codeUnits));
|
|
bundle = GZipEncoder().encode(TarEncoder().encode(a));
|
|
server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
|
|
server.listen((req) async {
|
|
req.response.add(bundle);
|
|
await req.response.close();
|
|
});
|
|
});
|
|
tearDown(() async {
|
|
await server.close(force: true);
|
|
tmp.deleteSync(recursive: true);
|
|
});
|
|
|
|
test('SHA-256 이 다르면 설치하지 않는다', () async {
|
|
final target = '${tmp.path}${Platform.pathSeparator}tor';
|
|
await expectLater(
|
|
TorBundle.installWindows(
|
|
urlsOverride: ['http://127.0.0.1:${server.port}/b.tar.gz'], sha256Override: '0' * 64, targetOverride: target),
|
|
throwsA(isA<ComponentException>()),
|
|
);
|
|
expect(Directory(target).existsSync(), isFalse);
|
|
});
|
|
|
|
test('맞으면 tor.exe · geoip · 라이선스 문서 · 설치 표시 (판)', () async {
|
|
final target = '${tmp.path}${Platform.pathSeparator}tor';
|
|
await TorBundle.installWindows(
|
|
urlsOverride: ['http://127.0.0.1:${server.port}/b.tar.gz'],
|
|
sha256Override: sha256.convert(bundle).toString(),
|
|
targetOverride: target);
|
|
for (final f in ['tor.exe', 'geoip', 'docs/tor.txt', 'installed.json']) {
|
|
expect(File('$target/$f').existsSync(), isTrue, reason: f);
|
|
}
|
|
expect(File('$target/installed.json').readAsStringSync(), contains('"version":"${TorBundle.version}"'));
|
|
}, skip: Platform.isWindows ? false : 'Windows tar');
|
|
});
|
|
|
|
group('Tor 화면: 준비되기 전에는 아무것도 읽지 않는다', () {
|
|
late Directory tmp;
|
|
setUp(() => tmp = Directory.systemTemp.createTempSync('jj_tor_page_'));
|
|
tearDown(() {
|
|
TorService.instance.status.value = TorStatus.off;
|
|
tmp.deleteSync(recursive: true);
|
|
});
|
|
|
|
Future<AppController> open(WidgetTester t,
|
|
{required Future<void> Function() startTor,
|
|
Future<List<String>> Function()? missing,
|
|
required Future<bool> Function() applyProxy,
|
|
required List<String> loads}) async {
|
|
t.view.physicalSize = const Size(1400, 900);
|
|
t.view.devicePixelRatio = 1;
|
|
addTearDown(t.view.reset);
|
|
const pp = MethodChannel('plugins.flutter.io/path_provider');
|
|
t.binding.defaultBinaryMessenger.setMockMethodCallHandler(pp, (_) async => tmp.path);
|
|
addTearDown(() => t.binding.defaultBinaryMessenger.setMockMethodCallHandler(pp, null));
|
|
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
|
c.settings
|
|
..torNoticeSeen = true
|
|
..components = [...c.settings.components, 'tor'];
|
|
await t.pumpWidget(MaterialApp(
|
|
builder: (context, child) => AppScope(controller: c, child: child!),
|
|
home: TorPage(
|
|
c: c,
|
|
startTor: startTor,
|
|
missingFeatures: missing ?? () async => const [],
|
|
applyProxy: applyProxy,
|
|
viewBuilder: (host, url) {
|
|
loads.add(url);
|
|
return Text('VIEW $url');
|
|
},
|
|
),
|
|
));
|
|
await t.pump();
|
|
return c;
|
|
}
|
|
|
|
testWidgets('연결 중 → 100% → 프록시 적용 뒤에야 웹 뷰', (t) async {
|
|
final loads = <String>[];
|
|
final proxy = Completer<bool>();
|
|
final st = TorService.instance.status;
|
|
await open(t,
|
|
startTor: () async => st.value = const TorStatus(TorPhase.starting, percent: 10, message: 'x'),
|
|
applyProxy: () => proxy.future,
|
|
loads: loads);
|
|
await t.pump();
|
|
expect(find.textContaining('Tor 연결 중 (10%)'), findsOneWidget);
|
|
expect(loads, isEmpty);
|
|
st.value = const TorStatus(TorPhase.starting, percent: 90);
|
|
await t.pump();
|
|
expect(loads, isEmpty);
|
|
st.value = const TorStatus(TorPhase.ready, percent: 100);
|
|
await t.pump();
|
|
expect(loads, isEmpty, reason: '프록시를 걸기 전');
|
|
proxy.complete(true);
|
|
await t.pump();
|
|
await t.pump();
|
|
expect(loads, ['https://check.torproject.org/']);
|
|
expect(find.text('Tor 연결됨'), findsOneWidget);
|
|
// Tor 가 끊기면 웹 뷰를 치운다 (다리는 Tor 없이 잇지 않음)
|
|
st.value = const TorStatus(TorPhase.failed, message: 'x');
|
|
await t.pump();
|
|
expect(find.textContaining('VIEW'), findsNothing);
|
|
expect(find.text('다시 시도'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('프록시를 걸지 못하면 (Android 웹 뷰 프록시 실패) 읽지 않는다', (t) async {
|
|
final loads = <String>[];
|
|
await open(t,
|
|
startTor: () async => TorService.instance.status.value = const TorStatus(TorPhase.ready, percent: 100),
|
|
applyProxy: () async => false,
|
|
loads: loads);
|
|
await t.pump();
|
|
await t.pump();
|
|
expect(loads, isEmpty);
|
|
});
|
|
|
|
testWidgets('웹 뷰에 필요한 기능이 없으면 Tor 를 켜지 않고 안내만', (t) async {
|
|
final loads = <String>[];
|
|
var started = false;
|
|
await open(t,
|
|
startTor: () async {
|
|
started = true;
|
|
TorService.instance.status.value = const TorStatus(TorPhase.ready, percent: 100);
|
|
},
|
|
missing: () async => ['PROXY_OVERRIDE'],
|
|
applyProxy: () async => true,
|
|
loads: loads);
|
|
await t.pump();
|
|
expect(started, isFalse);
|
|
expect(loads, isEmpty);
|
|
expect(find.textContaining('PROXY_OVERRIDE'), findsOneWidget);
|
|
});
|
|
});
|
|
}
|