464 lines
22 KiB
Dart
464 lines
22 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:jj_mkvmaker/app/net_guard.dart';
|
|
import 'package:jj_mkvmaker/app/settings.dart';
|
|
import 'package:jj_mkvmaker/core/dns_resolver.dart';
|
|
import 'package:jj_mkvmaker/core/local_proxy.dart';
|
|
|
|
import 'support/fake_dns.dart';
|
|
|
|
/// 정한 DNS 서버로 이름을 푸는지 · 앱 안 프록시가 그 이름으로 잇는지 · 같은 집 안 이름은 기기 DNS 로 · Tor 다리는 이름을 Tor 에 넘기는지
|
|
void main() {
|
|
DnsConfig custom(String primary, [String secondary = '']) =>
|
|
DnsConfig(mode: 'custom', primary: primary, secondary: secondary);
|
|
|
|
group('DNS 풀기 (가짜 서버)', () {
|
|
late FakeDns dns;
|
|
setUp(() async {
|
|
dns = FakeDns({'example.test': '10.1.2.3', 'up.test': '127.0.0.1'});
|
|
await dns.start();
|
|
});
|
|
tearDown(() => dns.close());
|
|
|
|
test('정한 서버에 묻고 답을 TTL 동안 기억한다', () async {
|
|
final r = DnsResolver(config: () => custom('127.0.0.1'), port: dns.port);
|
|
String? server;
|
|
r.onAnswer = (_, s) => server = s;
|
|
final a = await r.lookup('example.test');
|
|
expect(a.map((x) => x.address), ['10.1.2.3']);
|
|
expect(server, '127.0.0.1');
|
|
final n = dns.queries.length;
|
|
expect(await r.lookup('EXAMPLE.test.'), a); // 대소문자 · 끝 점은 같은 이름
|
|
expect(dns.queries.length, n, reason: '기억한 답');
|
|
});
|
|
|
|
test('없는 이름은 "찾을 수 없음" (서버에 닿지 않음이 아님)', () async {
|
|
final r = DnsResolver(config: () => custom('127.0.0.1'), port: dns.port);
|
|
final e = await r.lookup('nope.test').then<Object?>((_) => null, onError: (Object e) => e);
|
|
expect(e, isA<DnsException>());
|
|
expect((e as DnsException).unreachable, isFalse);
|
|
});
|
|
|
|
test('첫 서버가 답하지 않으면 보조 서버', () async {
|
|
// 127.0.0.2 에는 아무도 없다
|
|
final r = DnsResolver(config: () => custom('127.0.0.2', '127.0.0.1'), port: dns.port, timeout: const Duration(milliseconds: 400));
|
|
final a = await r.lookup('example.test');
|
|
expect(a.single.address, '10.1.2.3');
|
|
});
|
|
|
|
test('잘린 답 (TC) 은 TCP 로 다시 묻는다', () async {
|
|
final t = FakeDns({'big.test': '10.9.9.9'}, truncateUdp: true);
|
|
await t.start();
|
|
addTearDown(t.close);
|
|
final r = DnsResolver(config: () => custom('127.0.0.1'), port: t.port);
|
|
// 전체 실행에서만 실패한 적이 있다 - 실패하면 실제 오류와 가짜 DNS 의 기록을 남긴다
|
|
try {
|
|
expect((await r.lookup('big.test')).single.address, '10.9.9.9');
|
|
expect(t.tcpQueries, greaterThan(0));
|
|
} catch (e, st) {
|
|
// ignore: avoid_print
|
|
print('TC 시험 실패: $e (${e.runtimeType}${e is SocketException ? ', errno ${e.osError?.errorCode}' : ''})\n'
|
|
'가짜 DNS: port ${t.port}, ${t.events}, 질문 ${t.queries}\n$st');
|
|
rethrow;
|
|
}
|
|
});
|
|
|
|
test('서버에 닿지 않으면 unreachable 오류 (기기 DNS 로 바꾸지 않음)', () async {
|
|
final r = DnsResolver(config: () => custom('127.0.0.2'), port: dns.port, timeout: const Duration(milliseconds: 300));
|
|
final e = await r.lookup('www.google.com').then<Object?>((_) => null, onError: (Object e) => e);
|
|
expect(e, isA<DnsException>());
|
|
expect((e as DnsException).unreachable, isTrue);
|
|
expect(e.server, '127.0.0.2');
|
|
});
|
|
|
|
test('DNS over HTTPS (RFC 8484 POST)', () async {
|
|
final http = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
|
|
addTearDown(() => http.close(force: true));
|
|
String? contentType;
|
|
http.listen((req) async {
|
|
contentType = req.headers.contentType?.mimeType;
|
|
final q = Uint8List.fromList(await req.fold<List<int>>([], (a, b) => a..addAll(b)));
|
|
// 받은 질문을 가짜 DNS 서버에 그대로 넘겨 답을 돌려준다
|
|
final s = await RawDatagramSocket.bind(InternetAddress.loopbackIPv4, 0);
|
|
final got = Completer<Uint8List>();
|
|
s.listen((e) {
|
|
final d = s.receive();
|
|
if (d != null && !got.isCompleted) got.complete(d.data);
|
|
});
|
|
s.send(q, InternetAddress.loopbackIPv4, dns.port);
|
|
final a = await got.future;
|
|
s.close();
|
|
req.response.headers.contentType = ContentType('application', 'dns-message');
|
|
req.response.add(a);
|
|
await req.response.close();
|
|
});
|
|
final r = DnsResolver(
|
|
config: () => DnsConfig(mode: 'doh', dohUrl: 'http://127.0.0.1:${http.port}/dns-query', primary: '127.0.0.2'),
|
|
dohClient: HttpClient.new);
|
|
expect((await r.lookup('example.test')).single.address, '10.1.2.3');
|
|
expect(contentType, 'application/dns-message');
|
|
});
|
|
});
|
|
|
|
test('DNS 질문 · 답 읽기 (줄임 표시)', () {
|
|
final q = buildDnsQuery('a.example.com', 1, id: 0x1234);
|
|
expect(q.sublist(0, 2), [0x12, 0x34]);
|
|
// 답: 질문 + 줄임 표시 (0xc00c) A 1.2.3.4 TTL 60
|
|
final res = Uint8List.fromList([
|
|
0x12, 0x34, 0x81, 0x80, 0, 1, 0, 1, 0, 0, 0, 0, ...q.sublist(12), //
|
|
0xc0, 12, 0, 1, 0, 1, 0, 0, 0, 60, 0, 4, 1, 2, 3, 4,
|
|
]);
|
|
final a = parseDnsResponse(res, id: 0x1234);
|
|
expect(a.addresses.single.address, '1.2.3.4');
|
|
expect(a.ttl, 60);
|
|
expect(() => parseDnsResponse(res, id: 1), throwsFormatException);
|
|
});
|
|
|
|
test('같은 집 안 이름 · 주소 · DNS 서버 주소 확인', () {
|
|
for (final h in ['localhost', 'ds1815', 'nas.local', 'pc.lan', 'tab-s10.tail1234.ts.net', 'printer.home.arpa']) {
|
|
expect(isLocalName(h), isTrue, reason: h);
|
|
}
|
|
for (final h in ['www.youtube.com', 'github.com', '8.8.8.8']) {
|
|
expect(isLocalName(h), isFalse, reason: h);
|
|
}
|
|
for (final a in ['192.168.0.10', '10.0.0.1', '172.20.1.1', '100.80.195.34', '127.0.0.1', '::1', 'fe80::1']) {
|
|
expect(isPrivateAddress(InternetAddress(a)), isTrue, reason: a);
|
|
}
|
|
expect(isPrivateAddress(InternetAddress('8.8.8.8')), isFalse);
|
|
expect(parseDnsServer('8.8.8.8')?.address, '8.8.8.8');
|
|
expect(parseDnsServer(' 2001:4860:4860::8888 '), isNotNull);
|
|
expect(parseDnsServer('[2001:4860:4860::8844]'), isNotNull);
|
|
for (final bad in ['', '8.8.8', 'dns.google', '0.0.0.0', '::', '256.1.1.1']) {
|
|
expect(parseDnsServer(bad), isNull, reason: bad);
|
|
}
|
|
expect(isValidDohUrl('https://dns.google/dns-query'), isTrue);
|
|
expect(isValidDohUrl('https://cloudflare-dns.com/dns-query{?dns}'), isTrue);
|
|
expect(isValidDohUrl('http://dns.google/dns-query'), isFalse);
|
|
});
|
|
|
|
group('앱 안 프록시', () {
|
|
late FakeDns dns;
|
|
late HttpServer up;
|
|
late DnsResolver resolver;
|
|
late LocalProxy proxy;
|
|
final seen = <String>[];
|
|
setUp(() async {
|
|
dns = FakeDns({'up.test': '127.0.0.1'});
|
|
await dns.start();
|
|
up = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
|
|
up.listen((req) async {
|
|
seen.add('${req.method} ${req.uri} ${req.headers.value('connection')}');
|
|
req.response.write('hello ${req.uri.path}');
|
|
await req.response.close();
|
|
});
|
|
resolver = DnsResolver(config: () => custom('127.0.0.1'), port: dns.port, timeout: const Duration(milliseconds: 500));
|
|
proxy = LocalProxy(DnsConnector(resolver));
|
|
await proxy.start();
|
|
seen.clear();
|
|
});
|
|
tearDown(() async {
|
|
await proxy.close();
|
|
await up.close(force: true);
|
|
await dns.close();
|
|
});
|
|
|
|
HttpClient client() => HttpClient()..findProxy = ((_) => 'PROXY 127.0.0.1:${proxy.port}');
|
|
|
|
test('http: 이름을 정한 DNS 로 풀어 잇고, 요청 줄은 경로만 · 한 연결에 요청 하나', () async {
|
|
final c = client();
|
|
addTearDown(c.close);
|
|
final res = await (await c.getUrl(Uri.parse('http://up.test:${up.port}/a?b=1'))).close();
|
|
expect(await res.transform(utf8.decoder).join(), 'hello /a');
|
|
expect(seen.single, 'GET /a?b=1 close');
|
|
expect(dns.queries, contains('up.test/1'));
|
|
});
|
|
|
|
test('CONNECT: 터널 (https 와 같은 길) 을 정한 DNS 의 주소로', () async {
|
|
final s = await Socket.connect(InternetAddress.loopbackIPv4, proxy.port);
|
|
addTearDown(s.destroy);
|
|
final lines = StreamIterator(s.cast<List<int>>().transform(latin1.decoder));
|
|
s.write('CONNECT up.test:${up.port} HTTP/1.1\r\nHost: up.test:${up.port}\r\n\r\n');
|
|
var got = '';
|
|
while (!got.contains('\r\n\r\n') && await lines.moveNext()) {
|
|
got += lines.current;
|
|
}
|
|
expect(got, startsWith('HTTP/1.1 200'));
|
|
// 터널 안에서 원래 서버에 직접 말한다
|
|
s.write('GET /tunnel HTTP/1.1\r\nHost: up.test\r\nConnection: close\r\n\r\n');
|
|
var body = got.substring(got.indexOf('\r\n\r\n') + 4);
|
|
while (await lines.moveNext()) {
|
|
body += lines.current;
|
|
}
|
|
expect(body, contains('hello /tunnel'));
|
|
expect(dns.queries, contains('up.test/1'));
|
|
});
|
|
|
|
test('DNS 서버에 닿지 않으면 직접 잇지 않고 502 + 알림', () async {
|
|
final bad = LocalProxy(DnsConnector(DnsResolver(config: () => custom('127.0.0.2'), port: dns.port, timeout: const Duration(milliseconds: 300))));
|
|
Object? err;
|
|
bad.onError = (_, e) => err = e;
|
|
await bad.start();
|
|
addTearDown(bad.close);
|
|
final c = HttpClient()..findProxy = ((_) => 'PROXY 127.0.0.1:${bad.port}');
|
|
addTearDown(c.close);
|
|
final res = await (await c.getUrl(Uri.parse('http://up.test:${up.port}/x'))).close();
|
|
expect(res.statusCode, 502);
|
|
await res.drain<void>();
|
|
expect(seen, isEmpty, reason: '원래 서버에 닿지 않았다');
|
|
expect(err, isA<DnsException>().having((e) => e.unreachable, 'unreachable', isTrue));
|
|
});
|
|
|
|
test('같은 집 안 이름 (localhost) 은 정한 DNS 에 묻지 않고 기기 DNS 로', () async {
|
|
final c = client();
|
|
addTearDown(c.close);
|
|
final res = await (await c.getUrl(Uri.parse('http://localhost:${up.port}/lan'))).close();
|
|
expect(await res.transform(utf8.decoder).join(), 'hello /lan');
|
|
expect(dns.queries, isEmpty);
|
|
});
|
|
|
|
test('시스템 DNS (끄기) 면 정한 서버에 묻지 않는다', () async {
|
|
final sys = LocalProxy(DnsConnector(DnsResolver(config: () => const DnsConfig.system(), port: dns.port)));
|
|
await sys.start();
|
|
addTearDown(sys.close);
|
|
final s = await Socket.connect(InternetAddress.loopbackIPv4, sys.port);
|
|
addTearDown(s.destroy);
|
|
s.write('CONNECT 127.0.0.1:${up.port} HTTP/1.1\r\n\r\n');
|
|
final first = await s.cast<List<int>>().transform(latin1.decoder).first;
|
|
expect(first, startsWith('HTTP/1.1 200'));
|
|
expect(dns.queries, isEmpty);
|
|
});
|
|
});
|
|
|
|
group('Tor 다리 (SOCKS5)', () {
|
|
late ServerSocket socks;
|
|
late HttpServer up;
|
|
final asked = <String>[];
|
|
setUp(() async {
|
|
asked.clear();
|
|
up = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
|
|
up.listen((req) async {
|
|
req.response.write('via tor');
|
|
await req.response.close();
|
|
});
|
|
// 가짜 Tor SOCKS5: 이름 (ATYP 3) 을 받아 적고 원래 서버로 잇는다
|
|
socks = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0);
|
|
socks.listen((c) async {
|
|
final r = SocketReader(c);
|
|
await r.read(3);
|
|
c.add([5, 0]);
|
|
final h = await r.read(4);
|
|
String host;
|
|
if (h[3] == 3) {
|
|
final n = (await r.read(1))[0];
|
|
host = 'name:${latin1.decode(await r.read(n))}';
|
|
} else {
|
|
host = 'ip:${InternetAddress.fromRawAddress(await r.read(h[3] == 1 ? 4 : 16)).address}';
|
|
}
|
|
final pb = await r.read(2);
|
|
asked.add('$host:${(pb[0] << 8) | pb[1]}');
|
|
final u = await Socket.connect(InternetAddress.loopbackIPv4, up.port);
|
|
c.add([5, 0, 0, 1, 127, 0, 0, 1, 0, 0]);
|
|
unawaited(u.addStream(r.rest()).catchError((_) {}));
|
|
unawaited(c.addStream(u).then((_) => c.close()).catchError((_) {}));
|
|
});
|
|
});
|
|
tearDown(() async {
|
|
await socks.close();
|
|
await up.close(force: true);
|
|
});
|
|
|
|
test('이름을 그대로 Tor 에 넘긴다 (이 기기에서 DNS 를 묻지 않음)', () async {
|
|
final bridge = LocalProxy(TorConnector(() => socks.port));
|
|
await bridge.start();
|
|
addTearDown(bridge.close);
|
|
final c = HttpClient()..findProxy = ((_) => 'PROXY 127.0.0.1:${bridge.port}');
|
|
addTearDown(c.close);
|
|
final res = await (await c.getUrl(Uri.parse('http://some-site.example:${up.port}/'))).close();
|
|
expect(await res.transform(utf8.decoder).join(), 'via tor');
|
|
expect(asked.single, 'name:some-site.example:${up.port}');
|
|
});
|
|
|
|
test('Tor 가 준비되지 않았으면 502 (직접 잇지 않음)', () async {
|
|
final bridge = LocalProxy(TorConnector(() => null));
|
|
await bridge.start();
|
|
addTearDown(bridge.close);
|
|
final c = HttpClient()..findProxy = ((_) => 'PROXY 127.0.0.1:${bridge.port}');
|
|
addTearDown(c.close);
|
|
final res = await (await c.getUrl(Uri.parse('http://some-site.example:${up.port}/'))).close();
|
|
expect(res.statusCode, 502);
|
|
await res.drain<void>();
|
|
expect(asked, isEmpty);
|
|
});
|
|
});
|
|
|
|
group('앱 안 프록시 인증 (같은 기기의 다른 앱이 쓰지 못하게)', () {
|
|
late HttpServer up;
|
|
late LocalProxy proxy;
|
|
final auth = ProxyAuth('jj', 'secret-value');
|
|
setUp(() async {
|
|
up = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
|
|
up.listen((req) async {
|
|
req.response.write('ok');
|
|
await req.response.close();
|
|
});
|
|
proxy = LocalProxy(DnsConnector(DnsResolver(config: () => const DnsConfig.system())), auth: auth);
|
|
await proxy.start();
|
|
});
|
|
tearDown(() async {
|
|
await proxy.close();
|
|
await up.close(force: true);
|
|
});
|
|
|
|
/// CONNECT 를 보내고 답의 첫 줄
|
|
Future<String> connect(String? authHeader) async {
|
|
final s = await Socket.connect(InternetAddress.loopbackIPv4, proxy.port);
|
|
addTearDown(s.destroy);
|
|
s.write('CONNECT 127.0.0.1:${up.port} HTTP/1.1\r\nHost: 127.0.0.1:${up.port}\r\n'
|
|
'${authHeader == null ? '' : 'Proxy-Authorization: $authHeader\r\n'}\r\n');
|
|
final got = await s.cast<List<int>>().transform(latin1.decoder).firstWhere((x) => x.contains('\r\n'));
|
|
return got.split('\r\n').first;
|
|
}
|
|
|
|
test('인증 없음 · 틀린 비밀값 · 다른 아이디는 407 (위로 잇지 않음)', () async {
|
|
final asked = <String>[];
|
|
proxy.onRequest = (m, h, p) => asked.add(h);
|
|
expect(await connect(null), startsWith('HTTP/1.1 407'));
|
|
expect(await connect('Basic ${base64.encode(utf8.encode('jj:wrong'))}'), startsWith('HTTP/1.1 407'));
|
|
expect(await connect('Basic ${base64.encode(utf8.encode('xx:secret-value'))}'), startsWith('HTTP/1.1 407'));
|
|
expect(await connect('Bearer secret-value'), startsWith('HTTP/1.1 407'));
|
|
expect(asked, isEmpty);
|
|
});
|
|
|
|
test('맞는 비밀값은 통과 (CONNECT · 앱의 HttpClient)', () async {
|
|
expect(await connect(auth.header), startsWith('HTTP/1.1 200'));
|
|
final c = HttpClient()..findProxy = ((_) => 'PROXY ${auth.user}:${auth.password}@127.0.0.1:${proxy.port}');
|
|
addTearDown(c.close);
|
|
final res = await (await c.getUrl(Uri.parse('http://127.0.0.1:${up.port}/'))).close();
|
|
expect(res.statusCode, 200);
|
|
expect(await res.transform(utf8.decoder).join(), 'ok');
|
|
// 비밀값 없이는 407
|
|
final bare = HttpClient()..findProxy = ((_) => 'PROXY 127.0.0.1:${proxy.port}');
|
|
addTearDown(bare.close);
|
|
final res2 = await (await bare.getUrl(Uri.parse('http://127.0.0.1:${up.port}/'))).close();
|
|
expect(res2.statusCode, 407);
|
|
});
|
|
|
|
test('실행마다 다른 비밀값 · 127.0.0.1 에만 열림', () async {
|
|
expect(ProxyAuth.instance.password.length, greaterThanOrEqualTo(30));
|
|
expect(ProxyAuth.instance.password, isNot(contains(':')));
|
|
expect(ProxyAuth.instance.password, isNot(contains('@')));
|
|
// 127.0.0.1 이 아닌 이 기기 주소로는 닿지 않는다
|
|
final others = [
|
|
for (final i in await NetworkInterface.list(type: InternetAddressType.IPv4))
|
|
for (final a in i.addresses)
|
|
if (!a.isLoopback) a,
|
|
];
|
|
for (final a in others) {
|
|
await expectLater(Socket.connect(a, proxy.port, timeout: const Duration(seconds: 2)), throwsA(isA<SocketException>()));
|
|
}
|
|
});
|
|
});
|
|
|
|
test('일찍 만든 HttpClient 도 나중에 정한 DNS 를 따른다 (업데이트 확인 등이 정한 DNS 를 거치지 않던 것)', () async {
|
|
final g = NetGuard.instance;
|
|
final old = HttpOverrides.current;
|
|
HttpOverrides.global = JjHttpOverrides();
|
|
addTearDown(() {
|
|
HttpOverrides.global = old;
|
|
g.config = () => const DnsConfig.system();
|
|
});
|
|
g.config = () => const DnsConfig.system();
|
|
// 앱이 시작할 때 (설정을 읽기 전) 만들어 두는 것처럼
|
|
final early = HttpClient();
|
|
addTearDown(early.close);
|
|
// 설정을 읽어 DNS 강제가 되면 같은 HttpClient 도 프록시로: 닿지 않는 DNS 라 프록시가 502 로 답한다
|
|
// (프록시를 거치지 않았다면 이 기기의 DNS 로 이름을 찾다 SocketException)
|
|
g.config = () => custom('192.0.2.1');
|
|
await g.ensure();
|
|
final res = await (await early.getUrl(Uri.parse('http://jj-does-not-exist.example/'))).close();
|
|
await res.drain<void>();
|
|
expect(res.statusCode, 502);
|
|
expect(res.headers.value('x-jj-proxy-error'), '1');
|
|
});
|
|
|
|
test('207: Chrome 엔진 문 - 웹 뷰가 있을 때만 · 인증 없음 · 숨긴 루프백 주소 · 다시 열면 같은 주소 · 닫히면 접속 불가', () async {
|
|
final g = NetGuard.instance;
|
|
g.config = () => custom('8.8.8.8', '8.8.4.4');
|
|
addTearDown(() => g.config = () => const DnsConfig.system());
|
|
Future<String?> probe(String url) async {
|
|
final u = Uri.parse(url);
|
|
try {
|
|
final s = await Socket.connect(u.host, u.port, timeout: const Duration(seconds: 2));
|
|
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;
|
|
} on SocketException {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
final a = await g.openCefGate();
|
|
expect(a, isNotNull);
|
|
expect(InternetAddress(Uri.parse(a!).host).isLoopback, isTrue);
|
|
// 인증을 묻지 않는다 (Chrome 엔진은 407 에 답하지 못함) - 위로 잇지 못해 502
|
|
expect(await probe(a), startsWith('HTTP/1.1 502'));
|
|
// 웹 뷰가 둘이면 하나가 없어져도 열려 있다
|
|
expect(await g.openCefGate(), a);
|
|
await g.closeCefGate();
|
|
expect(g.cefGateUrl, a);
|
|
await g.closeCefGate();
|
|
expect(g.cefGateUrl, isNull);
|
|
expect(await probe(a), isNull, reason: '닫히면 접속 불가 (직접 나가지 않음)');
|
|
// 다시 열면 같은 주소 · 포트 (엔진은 시작할 때 정한 주소만 쓴다)
|
|
expect(await g.openCefGate(), a);
|
|
await g.closeCefGate();
|
|
// DNS 강제가 아니면 문 없음
|
|
g.config = () => const DnsConfig.system();
|
|
expect(await g.openCefGate(), isNull);
|
|
await g.closeCefGate();
|
|
});
|
|
|
|
test('NetGuard: 앱의 HTTP · yt-dlp · aria2 가 프록시를 쓰는 조건', () async {
|
|
final g = NetGuard.instance;
|
|
var cfg = custom('8.8.8.8', '8.8.4.4');
|
|
g.config = () => cfg;
|
|
await g.ensure();
|
|
final port = g.port!;
|
|
final a = ProxyAuth.instance;
|
|
// 비밀값을 함께 (같은 기기의 다른 앱은 이 프록시를 쓰지 못한다)
|
|
expect(g.findProxy(Uri.parse('https://www.youtube.com/')), 'PROXY ${a.user}:${a.password}@127.0.0.1:$port');
|
|
expect(g.findProxy(Uri.parse('http://192.168.0.5:5005/')), 'DIRECT');
|
|
expect(g.findProxy(Uri.parse('https://ds1815:5006/')), 'DIRECT');
|
|
expect(g.findProxy(Uri.parse('http://127.0.0.1:6800/jsonrpc')), 'DIRECT');
|
|
expect(g.ytDlpArgs(), ['--proxy', 'http://${a.user}:${a.password}@127.0.0.1:$port']);
|
|
final aria = ['--all-proxy=http://127.0.0.1:$port', '--all-proxy-user=${a.user}', '--all-proxy-passwd=${a.password}'];
|
|
expect(g.aria2Args(asyncDns: true), [...aria, '--async-dns=true', '--async-dns-server=8.8.8.8,8.8.4.4']);
|
|
expect(g.aria2Args(asyncDns: false), aria);
|
|
cfg = const DnsConfig.system();
|
|
expect(g.findProxy(Uri.parse('https://www.youtube.com/')), 'DIRECT');
|
|
expect(g.ytDlpArgs(), isEmpty);
|
|
expect(g.aria2Args(asyncDns: true), isEmpty);
|
|
expect(g.browserProxy, isNull);
|
|
});
|
|
|
|
test('설정: 처음은 직접 지정 8.8.8.8 · 8.8.4.4, 틀린 값은 처음 값, 사용자가 고른 값은 그대로', () {
|
|
final s = AppSettings();
|
|
expect((s.dnsMode, s.dnsPrimary, s.dnsSecondary, s.dnsDohUrl), ('custom', '8.8.8.8', '8.8.4.4', 'https://dns.google/dns-query'));
|
|
expect(AppSettings.fromJson({}).dnsMode, 'custom');
|
|
final bad = AppSettings.fromJson({'dnsMode': 'x', 'dnsPrimary': 'dns.google', 'dnsSecondary': '1.2.3', 'dnsDohUrl': 'ftp://x'});
|
|
expect((bad.dnsMode, bad.dnsPrimary, bad.dnsSecondary, bad.dnsDohUrl), ('custom', '8.8.8.8', '8.8.4.4', 'https://dns.google/dns-query'));
|
|
final mine = AppSettings.fromJson(
|
|
AppSettings().toJson()..addAll({'dnsMode': 'system', 'dnsPrimary': '1.1.1.1', 'dnsSecondary': '', 'dnsDohUrl': 'https://cloudflare-dns.com/dns-query'}));
|
|
expect((mine.dnsMode, mine.dnsPrimary, mine.dnsSecondary, mine.dnsDohUrl), ('system', '1.1.1.1', '', 'https://cloudflare-dns.com/dns-query'));
|
|
final back = AppSettings.fromJson(mine.toJson());
|
|
expect(back.dnsConfig.key, mine.dnsConfig.key);
|
|
// Tor 설정 처음 값
|
|
expect((s.torKeepData, s.torHomeUrl, s.torJavaScript, s.torNoticeSeen), (false, 'https://check.torproject.org/', true, false));
|
|
});
|
|
}
|