JJ_MKVMaker/lib/core/tor_control.dart

105 lines
3.8 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import '../l10n/tr.dart';
/// Tor 화면의 연결 상태
enum TorPhase { off, starting, ready, failed }
class TorStatus {
final TorPhase phase;
/// 연결 (bootstrap) 진행률 0 ~ 100
final int percent;
/// 지금 하는 일 · 실패 이유
final String message;
const TorStatus(this.phase, {this.percent = 0, this.message = ''});
static const off = TorStatus(TorPhase.off);
bool get ready => phase == TorPhase.ready;
}
/// 새는 것 막기 (사용자 결정 10/10): Tor 화면의 웹 뷰는 이 셋이 모두 맞을 때만 만든다 (그 전에는 아무것도 읽지 않는다).
/// - Tor 연결이 100% 끝났다
/// - 웹 뷰가 Tor 로 나가도록 프록시를 걸었다 (Android 는 앱 전체 웹 뷰 프록시를 걸고 끝날 때까지 기다림)
/// - Tor 화면 전용 프로필 (쿠키 · 기록 · 캐시를 일반 브라우저와 따로) 을 준비했다
bool torViewMayLoad({required TorStatus status, required bool proxyApplied, required bool profileReady}) =>
status.ready && proxyApplied && profileReady;
/// tor 의 알림 줄에서 연결 진행 ("Bootstrapped 45% (loading_descriptors): Loading relay descriptors") 을 읽는다
(int, String)? parseBootstrap(String line) {
final m = RegExp(r'Bootstrapped (\d+)%(?: \(([^)]*)\))?:?\s*(.*)$').firstMatch(line);
if (m == null) return null;
return (int.parse(m.group(1)!), m.group(3)?.trim() ?? '');
}
/// tor 가 연 SOCKS 포트 ("Opened Socks listener connection (ready) on 127.0.0.1:9150")
int? parseSocksPort(String line) {
final m = RegExp(r'Opened Socks listener.* on 127\.0\.0\.1:(\d+)').firstMatch(line);
return m == null ? null : int.parse(m.group(1)!);
}
/// tor 설정 (torrc). 경로는 앱 데이터 폴더 아래 (사용자 기본 torrc 를 읽지 않게 모두 적는다)
String buildTorrc({
required String dataDir,
required String controlPortFile,
required int ownerPid,
String? geoip,
String? geoip6,
}) {
final lines = [
'DataDirectory $dataDir',
// 127.0.0.1 의 빈 포트 (다른 프로그램 · 공식 Tor Browser 와 겹치지 않게)
'SocksPort 127.0.0.1:auto',
'ControlPort 127.0.0.1:auto',
'CookieAuthentication 1',
'ControlPortWriteToFile $controlPortFile',
'ClientOnly 1',
'AvoidDiskWrites 1',
'Log notice stdout',
// 앱이 끝나면 (비정상 종료 포함) tor 도 끝난다
'__OwningControllerProcess $ownerPid',
if (geoip != null) 'GeoIPFile $geoip',
if (geoip6 != null) 'GeoIPv6File $geoip6',
];
return '${lines.join('\n')}\n';
}
/// Tor 제어 포트에 명령 보내기 (쿠키 인증). [commands] 의 답 (250 줄들) 을 돌려준다
Future<List<String>> torControl(int port, List<int> cookie, List<String> commands,
{Duration timeout = const Duration(seconds: 10)}) async {
final s = await Socket.connect(InternetAddress.loopbackIPv4, port, timeout: timeout);
final lines = StreamIterator(s.cast<List<int>>().transform(utf8.decoder).transform(const LineSplitter()));
Future<List<String>> reply() async {
final out = <String>[];
while (await lines.moveNext().timeout(timeout)) {
final l = lines.current;
out.add(l);
// "250 OK" 처럼 넷째 글자가 빈칸이면 답의 끝
if (l.length >= 4 && l[3] == ' ') break;
}
if (out.isEmpty || !out.last.startsWith('250')) {
throw SocketException(trf('Tor 제어 명령 실패: {0}', [out.join(' / ')]));
}
return out;
}
try {
final hex = cookie.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
s.write('AUTHENTICATE $hex\r\n');
await reply();
final all = <String>[];
for (final c in commands) {
s.write('$c\r\n');
all.addAll(await reply());
}
s.write('QUIT\r\n');
return all;
} finally {
await lines.cancel();
s.destroy();
}
}