import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:path/path.dart' as p; import '../core/local_proxy.dart'; import '../core/tor_control.dart'; import '../l10n/tr.dart'; import 'tor_bundle.dart'; /// Tor 실행 (Tor 화면이 열려 있는 동안). tor 를 자식 프로세스로 띄우고 (SOCKS · 제어 포트는 127.0.0.1 의 빈 포트), /// 웹 뷰가 쓸 HTTP 프록시 다리 ([LocalProxy] + [TorConnector]) 를 연다. 다리는 이름을 Tor 에 넘긴다 (이 기기의 DNS 를 쓰지 않음). class TorService { TorService._(); static final instance = TorService._(); /// 앱 데이터 폴더 (main 에서 정함). tor 의 DataDirectory 는 그 아래 tor_data static String dataDir = Directory.systemTemp.path; final status = ValueNotifier(TorStatus.off); /// Tor 화면을 떠난 뒤 tor 를 끌 때 (기다리는 중이면 그 시각, 아니면 null) - 관리자 결정 10/11: /// 화면을 떠나면 웹 뷰 · 다리 · 프록시는 바로 정리하고 tor 만 잠시 (환경 설정, 처음 2분) 켜 둔다 (다시 들어오면 바로 쓰게) final idleStopAt = ValueNotifier(null); Timer? _idleTimer; /// [after] 뒤에 끈다 (0 이면 바로). 그 사이 [cancelIdleStop] 하면 그대로 켜 둔다 Future stopAfter(Duration after) async { cancelIdleStop(); if (_proc == null && !debugRunning) return; if (after <= Duration.zero) return stop(); idleStopAt.value = DateTime.now().add(after); log?.call(trf('Tor 대기 ({0}분 뒤 끔)', [after.inMinutes])); _idleTimer = Timer(after, () { _idleTimer = null; idleStopAt.value = null; unawaited(stop()); }); } /// 기다렸다 끄기를 그만둔다 (Tor 화면으로 돌아옴) void cancelIdleStop() { _idleTimer?.cancel(); _idleTimer = null; idleStopAt.value = null; } /// 작업 기록 void Function(String)? log; /// 시험용: 실행 파일 · 인수를 바꾼다 @visibleForTesting Future Function() executable = TorBundle.executable; Process? _proc; int? _socksPort; int? _controlPort; LocalProxy? _bridge; Timer? _watchdog; DateTime _lastProgress = DateTime.now(); final _recent = []; Future? _starting; /// 지금 Tor SOCKS 포트 (준비되지 않았으면 null) int? get socksPort => status.value.ready ? _socksPort : null; /// 웹 뷰가 쓸 HTTP 프록시 (127.0.0.1:포트). Tor 화면이 열린 동안만 열고 ([closeBridge]), 다시 열 때는 될 수 있으면 같은 포트 /// (Windows 웹 뷰 환경은 만든 뒤 프록시 주소를 바꿀 수 없다). 이 앱만 쓰게 인증을 요구한다 ([ProxyAuth]) Future bridgePort() async { final b = _bridge ??= LocalProxy(TorConnector(() => socksPort), auth: ProxyAuth.instance); return _lastBridgePort = await b.start(prefer: _lastBridgePort); } int? _lastBridgePort; /// 다리가 열려 있으면 그 포트 int? get bridgeOpenPort => _bridge?.running == true ? _bridge!.port : null; /// Tor 다리를 닫는다 (Tor 화면을 닫은 뒤) Future closeBridge() async { final b = _bridge; _bridge = null; await b?.close(); } bool get running => _proc != null || debugRunning; /// 시험용: tor 가 떠 있는 것처럼 ([stopAfter] 시험) @visibleForTesting bool debugRunning = false; /// 켜기 (이미 켜져 있거나 켜는 중이면 그것을 기다린다) Future start() { if (status.value.ready && _proc != null) return Future.value(); return _starting ??= _start().whenComplete(() => _starting = null); } Future _start() async { final exe = await executable(); if (exe == null) { _set(TorStatus(TorPhase.failed, message: tr('Tor 가 설치되어 있지 않습니다 (환경 설정 > 컴포넌트)'))); return; } await bridgePort(); final data = Directory(p.join(dataDir, 'tor_data'))..createSync(recursive: true); final portFile = File(p.join(data.path, 'control-port.txt')); if (portFile.existsSync()) portFile.deleteSync(); final torrc = File(p.join(data.path, 'torrc')); torrc.writeAsStringSync(buildTorrc( dataDir: data.path, controlPortFile: portFile.path, ownerPid: pid, geoip: Platform.isWindows ? TorBundle.geoip('geoip') : null, geoip6: Platform.isWindows ? TorBundle.geoip('geoip6') : null, )); final defaults = File(p.join(data.path, 'torrc-defaults'))..writeAsStringSync(''); _recent.clear(); _socksPort = null; _controlPort = null; _set(TorStatus(TorPhase.starting, message: tr('Tor 를 시작하는 중'))); final Process proc; try { proc = await Process.start(exe, ['-f', torrc.path, '--defaults-torrc', defaults.path], workingDirectory: data.path, environment: Platform.isAndroid ? {'HOME': data.path} : null); } catch (e) { _set(TorStatus(TorPhase.failed, message: trf('Tor 를 실행할 수 없습니다: {0}', [e]))); return; } _proc = proc; log?.call(trf('Tor 시작 ({0})', [exe])); _lastProgress = DateTime.now(); void onLine(String line) { if (_recent.length > 20) _recent.removeAt(0); _recent.add(line); final port = parseSocksPort(line); if (port != null) _socksPort = port; final b = parseBootstrap(line); if (b != null) { _lastProgress = DateTime.now(); if (b.$1 >= 100) { unawaited(_becomeReady(portFile)); } else { _set(TorStatus(TorPhase.starting, percent: b.$1, message: b.$2)); } } } proc.stdout.transform(utf8.decoder).transform(const LineSplitter()).listen(onLine); proc.stderr.transform(utf8.decoder).transform(const LineSplitter()).listen(onLine); unawaited(proc.exitCode.then((code) { if (!identical(_proc, proc)) return; _proc = null; _watchdog?.cancel(); if (status.value.phase != TorPhase.off) { final why = _recent.lastWhere((l) => l.contains('[err]') || l.contains('[warn]'), orElse: () => ''); _set(TorStatus(TorPhase.failed, message: trf('Tor 가 끝났습니다 (코드 {0}) {1}', [code, why]))); log?.call(trf('Tor 끝남 (코드 {0}): {1}', [code, _recent.join(' | ')])); } })); // 2분 동안 진행이 없으면 실패로 (망이 막혀 있는 등) _watchdog?.cancel(); _watchdog = Timer.periodic(const Duration(seconds: 5), (_) { if (status.value.phase != TorPhase.starting) return; if (DateTime.now().difference(_lastProgress) > const Duration(minutes: 2)) { final why = _recent.lastWhere((l) => l.contains('[warn]'), orElse: () => ''); unawaited(stop(keepStatus: true)); // 사용자에게: 무엇이 일어났고 무엇을 할지 ([다시 시도] 는 상태 줄에). 마지막 경고는 기록에도 남긴다 log?.call(trf('Tor 연결 시간 초과 {0}', [why])); _set(TorStatus(TorPhase.failed, percent: status.value.percent, message: trf('{0}% 에서 2분 넘게 나아가지 않아 멈췄습니다. 망이 Tor 를 막고 있거나 느릴 수 있습니다. 잠시 뒤 [다시 시도] 하세요.', [status.value.percent]))); } }); } Future _becomeReady(File portFile) async { // 제어 포트 (새 경로에 씀) 와 SOCKS 포트 확인 for (var i = 0; i < 50 && !portFile.existsSync(); i++) { await Future.delayed(const Duration(milliseconds: 100)); } try { final m = RegExp(r'PORT=127\.0\.0\.1:(\d+)').firstMatch(portFile.readAsStringSync()); _controlPort = m == null ? null : int.parse(m.group(1)!); } catch (_) {} if (_socksPort == null && _controlPort != null) { try { final r = await _control(['GETINFO net/listeners/socks']); final m = RegExp(r'127\.0\.0\.1:(\d+)').firstMatch(r.join(' ')); if (m != null) _socksPort = int.parse(m.group(1)!); } catch (_) {} } if (_socksPort == null) { _set(TorStatus(TorPhase.failed, message: tr('Tor SOCKS 포트를 찾지 못했습니다'))); await stop(keepStatus: true); return; } _set(TorStatus(TorPhase.ready, percent: 100, message: tr('연결됨'))); log?.call(trf('Tor 연결됨 (SOCKS 127.0.0.1:{0})', [_socksPort])); } Future> _control(List commands) async { final port = _controlPort; if (port == null) throw SocketException(tr('Tor 제어 포트가 없습니다')); final cookie = await File(p.join(dataDir, 'tor_data', 'control_auth_cookie')).readAsBytes(); return torControl(port, cookie, commands); } /// [새 경로]: 앞으로의 연결은 새 Tor 회로로 (NEWNYM) Future newIdentity() async { try { await _control(['SIGNAL NEWNYM']); log?.call(tr('Tor 새 경로')); } catch (e) { // 제어 포트를 쓸 수 없으면 다시 시작 log?.call(trf('Tor 새 경로 실패 ({0}) - 다시 시작합니다', [e])); await stop(); await start(); } } /// 끄기 (Tor 화면을 닫을 때 · 앱이 끝날 때 · 제거할 때) Future stop({bool keepStatus = false}) async { cancelIdleStop(); debugRunning = false; _watchdog?.cancel(); final proc = _proc; _proc = null; _socksPort = null; if (!keepStatus) _set(TorStatus.off); if (proc != null) { try { await _control(['SIGNAL SHUTDOWN']).timeout(const Duration(seconds: 2)); } catch (_) {} proc.kill(); log?.call(tr('Tor 끔')); } } void _set(TorStatus s) => status.value = s; }