import 'dart:async'; import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import '../l10n/tr.dart'; import 'net_guard.dart'; import 'tor_service.dart'; /// Android 웹 뷰 프록시 (androidx.webkit ProxyController - 앱의 모든 웹 뷰에 한꺼번에 걸린다). /// - Tor 화면이 열려 있으면: 모든 웹 뷰가 Tor 다리로 (관리자 결정 10/10 - 일반 브라우저에는 안내 띠) /// - 아니면: DNS 강제면 DNS 프록시로, 아니면 프록시 없음 /// 바꿀 때는 끝날 때까지 기다린다 (그 전에 페이지를 읽지 않게). Windows 는 웹 뷰마다 따로라 여기서 하지 않는다. class WebProxy { WebProxy._(); static const _ch = MethodChannel('jj_mkvmaker/android'); /// Tor 화면이 열려 있는지 (일반 브라우저의 안내 띠도 이것을 본다) static final torOpen = ValueNotifier(false); /// 지금 걸린 프록시 ('' = 없음) static String _applied = ''; static String get applied => _applied; static Future _chain = Future.value(); /// 이 기기 웹 뷰가 Tor 화면에 필요한 기능을 갖췄는지: 프록시 걸기 · 따로 프로필 · 페이지보다 먼저 스크립트 (WebRTC 끄기). /// 없는 것의 이름 목록 (모두 있으면 빈 목록) static Future> torMissingFeatures() async { if (!Platform.isAndroid) return const []; try { final m = await _ch.invokeMapMethod('torWebSupport') ?? const {}; return [ if (m['proxyOverride'] != true) 'PROXY_OVERRIDE', if (m['multiProfile'] != true) 'MULTI_PROFILE', if (m['documentStartScript'] != true) 'DOCUMENT_START_SCRIPT', ]; } catch (e) { return ['$e']; } } /// 지금 상태에 맞게 다시 건다 (차례로 - 겹치지 않게). 끝나면 걸린 프록시 static Future apply() { if (!Platform.isAndroid) return Future.value(''); final f = _chain.then((_) => _apply()); _chain = f.then((_) {}, onError: (_) {}); return f; } static Future _apply() async { String want = ''; if (torOpen.value) { want = 'http://127.0.0.1:${await TorService.instance.bridgePort()}'; } else if (NetGuard.instance.forcing) { want = NetGuard.instance.browserProxy ?? ''; } if (want == _applied) return _applied; final pc = ProxyController.instance(); if (want.isEmpty) { await pc.clearProxyOverride(); } else { await pc.setProxyOverride( settings: ProxySettings( proxyRules: [ProxyRule(url: want)], // Tor 화면: localhost 까지 Tor 로 (이 기기 · 집 안을 들여다보지 못하게). DNS 강제: 기본 (이 기기 주소는 직접) removeImplicitRules: torOpen.value, )); } _applied = want; debugPrint(trf('웹 뷰 프록시: {0}', [want.isEmpty ? tr('없음') : want])); return want; } /// Tor 화면 전용 프로필 지우기 (쿠키 · 저장소 · 캐시). 웹 뷰가 없어진 뒤에 부른다 static Future clearTorProfile() async { if (!Platform.isAndroid) return true; try { return await _ch.invokeMethod('torProfileClear', {'name': torProfileName}) ?? false; } catch (_) { return false; } } /// Tor 화면 웹 뷰의 프로필 이름 (androidx.webkit Profile) static const torProfileName = 'jj_tor'; }