152 lines
6.6 KiB
Dart
152 lines
6.6 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:jj_mkvmaker/app/net_guard.dart';
|
|
import 'package:jj_mkvmaker/app/web_proxy.dart';
|
|
import 'package:jj_mkvmaker/core/dns_resolver.dart';
|
|
|
|
/// Android 웹 뷰 프록시 문 (관리자 결정 10/11 A+B): 인증 없이, 실행마다 고른 루프백 주소에,
|
|
/// 웹 뷰 화면이 보이는 동안만 연다. 앱이 뒤로 가거나 가려지면 닫고 웹 뷰는 닫힌 포트로 (직접 나가지 않음)
|
|
void main() {
|
|
final calls = <(String, bool)>[];
|
|
setUp(() async {
|
|
await WebProxy.debugReset();
|
|
WebProxy.debugAndroid = true;
|
|
WebProxy.debugSetProxy = (url, tor) async => calls.add((url, tor));
|
|
WebProxy.torOpen.value = false;
|
|
NetGuard.instance.config = () => const DnsConfig(mode: 'custom', primary: '8.8.8.8', secondary: '8.8.4.4', dohUrl: '');
|
|
calls.clear();
|
|
});
|
|
tearDown(() async {
|
|
WebProxy.torOpen.value = false;
|
|
await WebProxy.debugReset();
|
|
WebProxy.debugAndroid = false;
|
|
WebProxy.debugSetProxy = null;
|
|
NetGuard.instance.config = () => const DnsConfig.system();
|
|
});
|
|
|
|
/// 그 주소로 CONNECT 를 보내 첫 줄 (닫혀 있으면 null)
|
|
Future<String?> probe(String url) async {
|
|
final u = Uri.parse(url);
|
|
final Socket s;
|
|
try {
|
|
s = await Socket.connect(u.host, u.port, timeout: const Duration(seconds: 2));
|
|
} on SocketException {
|
|
return null;
|
|
}
|
|
// 아무도 듣지 않는 곳 (127.0.0.1:1) 으로: 인증을 묻지 않으면 위쪽 연결 실패 (502) 가 온다
|
|
s.write('CONNECT 127.0.0.1:1 HTTP/1.1\r\nHost: 127.0.0.1:1\r\n\r\n');
|
|
final first = await s.cast<List<int>>().transform(latin1.decoder).first.timeout(const Duration(seconds: 10));
|
|
s.destroy();
|
|
return first.split('\r\n').first;
|
|
}
|
|
|
|
test('보이는 웹 뷰가 없으면 문 없이 닫힌 포트로 · 보이면 숨긴 루프백 주소에 인증 없는 문', () async {
|
|
expect(await WebProxy.apply(), WebProxy.closedProxy);
|
|
expect(WebProxy.dnsGateUrl, isNull);
|
|
final url = await WebProxy.show();
|
|
expect(url, WebProxy.dnsGateUrl);
|
|
final host = Uri.parse(url).host;
|
|
expect(InternetAddress(host).isLoopback, isTrue);
|
|
expect(calls.last, (url, false));
|
|
// 인증을 묻지 않는다 (Android 웹 뷰는 407 에 답하지 못함) - 위쪽에 잇지 못해 502
|
|
expect(await probe(url), startsWith('HTTP/1.1 502'));
|
|
// 화면이 가려지면 닫는다
|
|
expect(await WebProxy.hide(), WebProxy.closedProxy);
|
|
expect(WebProxy.dnsGateUrl, isNull);
|
|
expect(await probe(url), isNull, reason: '닫힌 문');
|
|
});
|
|
|
|
test('앱이 뒤로 가면 닫고 돌아오면 다시 연다 (같은 실행에서는 같은 주소)', () async {
|
|
final url = await WebProxy.show();
|
|
expect(await WebProxy.setForeground(false), WebProxy.closedProxy);
|
|
expect(await probe(url), isNull);
|
|
final again = await WebProxy.setForeground(true);
|
|
expect(again, isNotNull);
|
|
expect(Uri.parse(again).host, Uri.parse(url).host);
|
|
expect(await probe(again), startsWith('HTTP/1.1 502'));
|
|
});
|
|
|
|
test('Tor 화면: Tor 문만 열고 DNS 문은 닫는다 · Tor 가 준비되지 않았으면 직접 잇지 않음 · 닫으면 Tor 문도 닫힘', () async {
|
|
final dnsUrl = await WebProxy.show();
|
|
WebProxy.torOpen.value = true;
|
|
final torUrl = await WebProxy.apply();
|
|
expect(torUrl, WebProxy.torGateUrl);
|
|
expect(calls.last, (torUrl, true), reason: 'Tor 는 localhost 까지 Tor 로 (removeImplicitRules)');
|
|
expect(WebProxy.dnsGateUrl, isNull);
|
|
expect(await probe(dnsUrl), isNull);
|
|
expect(await probe(torUrl), startsWith('HTTP/1.1 502'));
|
|
WebProxy.torOpen.value = false;
|
|
await WebProxy.hide();
|
|
expect(WebProxy.torGateUrl, isNull);
|
|
expect(await probe(torUrl), isNull);
|
|
});
|
|
|
|
test('"다른 화면으로 가면 소리 멈춤" 이 꺼져 있으면 일반 브라우저가 살아 있는 동안 DNS 문 유지 (Tor 는 아님)', () async {
|
|
var keepPlaying = false;
|
|
WebProxy.keepPlayingWhenHidden = () => keepPlaying;
|
|
WebProxy.browserViewCreated();
|
|
final url = await WebProxy.show();
|
|
// 설정 켜짐 (처음 값): 앱이 뒤로 가면 닫힘
|
|
expect(await WebProxy.setForeground(false), WebProxy.closedProxy);
|
|
expect(await probe(url), isNull);
|
|
await WebProxy.setForeground(true);
|
|
// 설정 꺼짐: 앱이 뒤로 가도 · 다른 화면이 위로 와도 열어 둔다 (다음 조각 · 다음 곡)
|
|
keepPlaying = true;
|
|
final kept = await WebProxy.setForeground(false);
|
|
expect(kept, WebProxy.dnsGateUrl);
|
|
expect(await probe(kept), startsWith('HTTP/1.1 502'));
|
|
expect(await WebProxy.hide(), kept);
|
|
// 일반 브라우저가 없어지면 닫는다
|
|
WebProxy.browserViewDisposed();
|
|
await WebProxy.apply();
|
|
expect(WebProxy.dnsGateUrl, isNull);
|
|
expect(await probe(kept), isNull);
|
|
// Tor 는 설정과 상관없이 보이는 동안만
|
|
WebProxy.browserViewCreated();
|
|
await WebProxy.show();
|
|
WebProxy.torOpen.value = true;
|
|
await WebProxy.setForeground(true);
|
|
expect(WebProxy.torGateUrl, isNotNull);
|
|
expect(await WebProxy.setForeground(false), WebProxy.closedProxy);
|
|
expect(WebProxy.torGateUrl, isNull);
|
|
WebProxy.torOpen.value = false;
|
|
});
|
|
|
|
test('DNS 강제가 꺼져 있고 Tor 도 아니면 프록시 없음 (문도 없음)', () async {
|
|
NetGuard.instance.config = () => const DnsConfig.system();
|
|
expect(await WebProxy.show(), '');
|
|
expect(WebProxy.dnsGateUrl, isNull);
|
|
expect(calls.where((c) => c.$1.isNotEmpty), isEmpty, reason: '프록시를 걸지 않음');
|
|
});
|
|
|
|
testWidgets('웹 뷰 화면 위에 다른 화면이 오면 닫고, 돌아오면 다시 연다', (t) async {
|
|
final nav = GlobalKey<NavigatorState>();
|
|
await t.pumpWidget(MaterialApp(navigatorKey: nav, home: const _WebScreen()));
|
|
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 200)));
|
|
expect(WebProxy.shownForTest, 1);
|
|
nav.currentState!.push(MaterialPageRoute<void>(builder: (_) => const Text('other')));
|
|
await t.pumpAndSettle();
|
|
expect(WebProxy.shownForTest, 0);
|
|
nav.currentState!.pop();
|
|
await t.pumpAndSettle();
|
|
expect(WebProxy.shownForTest, 1);
|
|
await t.pumpWidget(const SizedBox());
|
|
expect(WebProxy.shownForTest, 0);
|
|
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 200)));
|
|
});
|
|
}
|
|
|
|
class _WebScreen extends StatefulWidget {
|
|
const _WebScreen();
|
|
@override
|
|
State<_WebScreen> createState() => _WebScreenState();
|
|
}
|
|
|
|
class _WebScreenState extends State<_WebScreen> with WebProxyVisibility {
|
|
@override
|
|
Widget build(BuildContext context) => const Text('web');
|
|
}
|