JJ_MKVMaker/test/web_proxy_gate_test.dart

161 lines
7.0 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/app/web_view_proxy_target.dart';
import 'package:jj_mkvmaker/core/dns_resolver.dart';
/// Android 웹 뷰 프록시 문 (관리자 결정 10/11 A+B): 인증 없이, 실행마다 고른 루프백 주소에,
/// 웹 뷰 화면이 보이는 동안만 연다. 앱이 뒤로 가거나 가려지면 닫고 웹 뷰는 닫힌 포트로 (직접 나가지 않음)
void main() {
final calls = <(String, bool)>[];
setUp(() async {
await WebProxy.reset();
WebProxy.target = _RecordingProxy(calls);
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.reset();
WebProxy.target = const NoWebViewProxy();
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.shown, 1);
nav.currentState!.push(MaterialPageRoute<void>(builder: (_) => const Text('other')));
await t.pumpAndSettle();
expect(WebProxy.shown, 0);
nav.currentState!.pop();
await t.pumpAndSettle();
expect(WebProxy.shown, 1);
await t.pumpWidget(const SizedBox());
expect(WebProxy.shown, 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');
}
/// Android 처럼 앱 전체 웹 뷰 프록시를 쓰는 기기 - 거는 대신 기록 (시험 파일 안의 것, lib 에 흉내 장치 없음)
class _RecordingProxy implements WebViewProxyTarget {
final List<(String, bool)> calls;
_RecordingProxy(this.calls);
@override
bool get active => true;
@override
Future<void> set(String url, {required bool removeImplicitRules}) async => calls.add((url, removeImplicitRules));
}