228 lines
9.1 KiB
Dart
228 lines
9.1 KiB
Dart
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';
|
|
import '../core/idle_stop.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>(TorStatus.off);
|
|
|
|
/// Tor 화면을 떠난 뒤 tor 를 끌 때 (기다리는 중이면 그 시각, 아니면 null) - 관리자 결정 10/11:
|
|
/// 화면을 떠나면 웹 뷰 · 다리 · 프록시는 바로 정리하고 tor 만 잠시 (환경 설정, 처음 2분) 켜 둔다 (다시 들어오면 바로 쓰게)
|
|
ValueNotifier<DateTime?> get idleStopAt => idle.stopAt;
|
|
|
|
/// 떠난 뒤 잠시 켜 두기 (시간 재기 · 지금 끄기) - 환경 설정 > Tor 가 이것을 받는다
|
|
late final idle = IdleStop(stop);
|
|
|
|
/// [after] 뒤에 끈다 (0 이면 바로). 그 사이 [cancelIdleStop] 하면 그대로 켜 둔다 (시간 재기는 [IdleStop])
|
|
Future<void> stopAfter(Duration after) async {
|
|
cancelIdleStop();
|
|
if (_proc == null) return;
|
|
if (after > Duration.zero) log?.call(trf('Tor 대기 ({0}분 뒤 끔)', [after.inMinutes]));
|
|
await idle.schedule(after);
|
|
}
|
|
|
|
/// 기다렸다 끄기를 그만둔다 (Tor 화면으로 돌아옴)
|
|
void cancelIdleStop() => idle.cancel();
|
|
|
|
/// 작업 기록
|
|
void Function(String)? log;
|
|
|
|
Process? _proc;
|
|
int? _socksPort;
|
|
int? _controlPort;
|
|
LocalProxy? _bridge;
|
|
Timer? _watchdog;
|
|
DateTime _lastProgress = DateTime.now();
|
|
final _recent = <String>[];
|
|
Future<void>? _starting;
|
|
|
|
/// 지금 Tor SOCKS 포트 (준비되지 않았으면 null)
|
|
int? get socksPort => status.value.ready ? _socksPort : null;
|
|
|
|
/// 웹 뷰가 쓸 HTTP 프록시 (127.0.0.1:포트). Tor 화면이 열린 동안만 열고 ([closeBridge]), 다시 열 때는 될 수 있으면 같은 포트
|
|
/// (Windows 웹 뷰 환경은 만든 뒤 프록시 주소를 바꿀 수 없다). 이 앱만 쓰게 인증을 요구한다 ([ProxyAuth])
|
|
Future<int> 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<void> closeBridge() async {
|
|
final b = _bridge;
|
|
_bridge = null;
|
|
await b?.close();
|
|
}
|
|
|
|
bool get running => _proc != null;
|
|
|
|
/// 켜기 (이미 켜져 있거나 켜는 중이면 그것을 기다린다)
|
|
Future<void> start() {
|
|
if (status.value.ready && _proc != null) return Future.value();
|
|
return _starting ??= _start().whenComplete(() => _starting = null);
|
|
}
|
|
|
|
Future<void> _start() async {
|
|
final exe = await TorBundle.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<void> _becomeReady(File portFile) async {
|
|
// 제어 포트 (새 경로에 씀) 와 SOCKS 포트 확인
|
|
for (var i = 0; i < 50 && !portFile.existsSync(); i++) {
|
|
await Future<void>.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<List<String>> _control(List<String> 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<void> 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<void> stop({bool keepStatus = false}) async {
|
|
cancelIdleStop();
|
|
_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;
|
|
}
|