From 3b3d70fc1825664227974a037b02e48f6b47076e Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Fri, 23 Oct 2026 03:36:12 +0900 Subject: [PATCH] =?UTF-8?q?tor=5Fpage=20=EB=82=98=EB=88=84=EA=B8=B0=20(740?= =?UTF-8?q?=20=E2=86=92=20499):=20=EC=9B=B9=20=EB=B7=B0=20=EB=B6=80?= =?UTF-8?q?=EB=B6=84=20(tor=5Fweb=5Fview)=20=C2=B7=20=EC=95=88=EB=82=B4=20?= =?UTF-8?q?(tor=5Fmessages)=20=EB=A5=BC=20lib/ui/tor/=20=EB=AA=A8=EB=93=88?= =?UTF-8?q?=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 웹 뷰가 화면 상태의 _clearProfile 을 직접 부르던 것을 인자 (clearProfile) 로 - 모듈이 화면에 기대지 않게 - 약속은 tor_test 의 torAddress · torBrowserArguments · torWebRtcBlockScript · TorViewHost 시험이 본다 (20 통과) Co-Authored-By: Claude Opus 5.5 --- lib/ui/tor/tor_messages.dart | 42 ++++++ lib/ui/tor/tor_web_view.dart | 228 ++++++++++++++++++++++++++++++ lib/ui/tor_page.dart | 259 ++--------------------------------- 3 files changed, 279 insertions(+), 250 deletions(-) create mode 100644 lib/ui/tor/tor_messages.dart create mode 100644 lib/ui/tor/tor_web_view.dart diff --git a/lib/ui/tor/tor_messages.dart b/lib/ui/tor/tor_messages.dart new file mode 100644 index 0000000..91a1418 --- /dev/null +++ b/lib/ui/tor/tor_messages.dart @@ -0,0 +1,42 @@ +/// 약속: Tor 화면의 안내 - 공식 Tor Browser 가 아니라는 안내 창 ([showTorNotice]) · 웹 뷰 대신 보이는 안내 ([torMessage]). +library; + +import 'package:flutter/material.dart'; + +import '../../l10n/tr.dart'; +import '../theme.dart'; + +/// 처음 열 때 · 환경 설정에서: 공식 Tor Browser 가 아니라는 안내 +Future showTorNotice(BuildContext context) => showDialog( + context: context, + builder: (ctx) => AlertDialog( + scrollable: true, + title: Text(tr('Tor 브라우저 안내')), + content: SizedBox( + width: 480, + child: Text(tr('이 화면은 앱 안 웹 뷰의 연결을 Tor 로 보내는 것이며, 공식 Tor Browser 가 아닙니다.\n\n' + '· 연결 (이름 찾기 포함) 은 모두 Tor 로 나가고, 쿠키 · 기록 · 캐시는 일반 브라우저와 따로 두며 닫으면 지웁니다 (환경 설정에서 남기기 가능).\n' + '· 하지만 브라우저 지문 (화면 크기 · 글꼴 · 기기 정보 등) 을 감추는 보호는 공식 Tor Browser 보다 약합니다. ' + '사이트가 이 기기를 알아볼 수 있습니다.\n' + '· 강한 익명성이 필요하면 공식 Tor Browser (https://www.torproject.org) 를 쓰세요.')), + ), + actions: [FilledButton(onPressed: () => Navigator.pop(ctx), child: Text(tr('확인')))], + ), + ); + +/// 웹 뷰 대신 보이는 안내 (아이콘 또는 빙글이 · 제목 · 설명) +Widget torMessage(IconData icon, String title, String body, {bool busy = false}) => Center( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 520), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + busy ? const SizedBox(width: 40, height: 40, child: CircularProgressIndicator()) : Icon(icon, size: 40, color: JjColors.textDim), + const SizedBox(height: 12), + Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), textAlign: TextAlign.center), + const SizedBox(height: 8), + Text(body, style: TextStyle(color: JjColors.textDim), textAlign: TextAlign.center), + ]), + ), + ), + ); diff --git a/lib/ui/tor/tor_web_view.dart b/lib/ui/tor/tor_web_view.dart new file mode 100644 index 0000000..67c1960 --- /dev/null +++ b/lib/ui/tor/tor_web_view.dart @@ -0,0 +1,228 @@ +/// 약속: Tor 화면의 웹 뷰 부분 - 조작 ([TorNav]) · 화면으로 알리는 통로 ([TorViewHost]) · Windows 전용 환경 (모든 연결을 Tor 다리로, +/// QUIC · 프록시 밖 UDP 끄기) · Android 전용 프로필 · WebRTC 막는 스크립트 · 주소 고르기 (검색은 DuckDuckGo). +library; + +import 'dart:async'; +import 'dart:collection'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:path/path.dart' as p; + +import '../../app/web_proxy.dart'; +import '../../core/web_address.dart'; +import '../../platform/windows/com_guard.dart'; + +/// Tor 화면의 웹 뷰 조작 +abstract class TorNav { + Future load(String url); + Future back(); + Future forward(); + Future reload(); + Future stop(); + + /// 페이지에서 스크립트를 실행하고 결과를 받는다 (실패하면 null) + Future evaluate(String js); +} + +/// 웹 뷰 → 화면으로 알리는 통로 +class TorViewHost { + final void Function(String url) onUrl; + + /// 페이지가 실제로 열림 (읽기 끝 · 기록에 남음). 막은 파일 주소로 주소 칸이 바뀌었으면 이 주소로 되돌린다 + final void Function(String url)? onShown; + final void Function(double progress) onProgress; + final void Function(bool back, bool forward) onHistory; + final void Function(TorNav nav) attach; + + /// 파일 받기를 막았다 (Tor 화면은 받지 않는다 - 관리자 결정 10/11) + final void Function(String url) onDownloadBlocked; + const TorViewHost( + {required this.onUrl, + required this.onProgress, + required this.onHistory, + required this.attach, + required this.onDownloadBlocked, + this.onShown}); +} + +/// WebRTC 끄기 (모든 틀 · 페이지 스크립트보다 먼저): STUN 으로 진짜 주소가 새지 않게. +/// Windows 는 실행 인수 (--force-webrtc-ip-handling-policy) 로도 막는다 +const torWebRtcBlockScript = r''' +(function(){ + var names=['RTCPeerConnection','webkitRTCPeerConnection','mozRTCPeerConnection','RTCDataChannel','RTCIceCandidate','RTCSessionDescription','RTCRtpSender','RTCRtpReceiver','RTCRtpTransceiver']; + function strip(w){ + try{ for(var i=0;i? _torEnv; +int? _torEnvPort; + +/// Tor 화면 Edge 실행 인수: 프록시 (localhost 까지), QUIC 끄기, WebRTC 는 프록시를 거치지 않는 UDP 금지, SmartScreen (주소를 Microsoft 로 보냄) 끄기 +String torBrowserArguments(int bridgePort) => [ + '--proxy-server=http://127.0.0.1:$bridgePort', + '--proxy-bypass-list=<-loopback>', + '--disable-quic', + '--webrtc-ip-handling-policy=disable_non_proxied_udp', + '--force-webrtc-ip-handling-policy=disable_non_proxied_udp', + '--dns-prefetch-disable', + '--disable-features=msSmartScreenProtection,NetworkPrediction,msEdgeShoppingAssistant', + ].join(' '); + +Future torEnvironment(String dataDir, int bridgePort) { + // 다리를 다시 열 때 포트가 바뀌었으면 (그 사이 다른 프로그램이 씀) 새 환경으로 + if (_torEnvPort != bridgePort) _torEnv = null; + _torEnvPort = bridgePort; + return _torEnv ??= () async { + try { + ComGuard.ensure(); + return await WebViewEnvironment.create( + settings: WebViewEnvironmentSettings( + userDataFolder: p.join(dataDir, 'tor_webview'), additionalBrowserArguments: torBrowserArguments(bridgePort))); + } catch (_) { + _torEnv = null; + rethrow; + } + }(); +} + +/// Android 웹 뷰를 Tor 화면 전용 프로필로 만들게 하는 설정 (packages/flutter_inappwebview_android/JJ_PATCH.md: jjProfile) +class _TorWebSettings extends InAppWebViewSettings { + _TorWebSettings({required bool javaScript, required bool keepData}) + : super( + javaScriptEnabled: javaScript, + mediaPlaybackRequiresUserGesture: true, + supportMultipleWindows: false, + useHybridComposition: false, + thirdPartyCookiesEnabled: false, + geolocationEnabled: false, + saveFormData: false, + // 파일 링크는 웹 뷰가 스스로 받지 않고 알린다 (Windows: packages/flutter_inappwebview_windows/JJ_PATCH.md) + useOnDownloadStart: true, + // Windows: 남기지 않으면 InPrivate (닫으면 쿠키 · 캐시가 없어진다) + incognito: Platform.isWindows && !keepData, + ); + + @override + Map toMap() => { + ...super.toMap(), + if (Platform.isAndroid) 'jjProfile': WebProxy.torProfileName, + // Windows: Tor 다리의 인증에 답하게 (packages/flutter_inappwebview_windows/JJ_PATCH.md) + if (Platform.isWindows) 'jjProxyAuth': WebProxy.proxyAuthSetting(), + }; +} + +/// Tor 화면의 주소창 입력 → 주소 (주소가 아니면 DuckDuckGo 검색 - Google 로 보내지 않음) +String torAddress(String input) { + final u = normalizeAddress(input); + if (u.startsWith('https://www.google.com/search?q=')) { + return 'https://duckduckgo.com/?q=${u.substring('https://www.google.com/search?q='.length)}'; + } + return u; +} + +/// Tor 화면의 웹 뷰 (전용 환경 · 전용 프로필 · 파일 받기 막기 · WebRTC 끄기) +class TorWebView extends StatefulWidget { + final TorViewHost host; + final String url; + final WebViewEnvironment? env; + final bool javaScript; + final bool keepData; + + /// 마지막 Tor 웹 뷰가 없어질 때 프로필 (쿠키 · 기록 · 캐시) 을 지울 것 (Android, 남기지 않을 때) + final Future Function() clearProfile; + const TorWebView({super.key, required this.clearProfile, required this.host, required this.url, required this.env, required this.javaScript, required this.keepData}); + + @override + State createState() => _TorWebViewState(); +} + +/// 웹 뷰가 살아 있는 동안 WebProxy 에 알린다 ([WebProxy.torViewCreated]) - 없어지면 프록시를 다시 걸고, 남기지 않으면 프로필을 지운다 +class _TorWebViewState extends State { + @override + void initState() { + super.initState(); + WebProxy.torViewCreated(); + WebProxy.onLastTorView = widget.keepData || !Platform.isAndroid ? null : widget.clearProfile; + } + + @override + void dispose() { + WebProxy.torViewDisposed(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final host = widget.host, url = widget.url, env = widget.env, javaScript = widget.javaScript, keepData = widget.keepData; + Future history(InAppWebViewController c) async => host.onHistory(await c.canGoBack(), await c.canGoForward()); + return InAppWebView( + webViewEnvironment: env, + initialUrlRequest: URLRequest(url: WebUri(torAddress(url))), + initialSettings: _TorWebSettings(javaScript: javaScript, keepData: keepData), + initialUserScripts: UnmodifiableListView([ + UserScript(source: torWebRtcBlockScript, injectionTime: UserScriptInjectionTime.AT_DOCUMENT_START, forMainFrameOnly: false), + ]), + onWebViewCreated: (c) => host.attach(_InAppTorNav(c)), + // 받지 않는다 (useOnDownloadStart: 웹 뷰도 스스로 받지 않음) + onDownloadStartRequest: (c, r) => host.onDownloadBlocked(r.url.toString()), + onLoadStart: (c, u) => host.onUrl(u?.toString() ?? ''), + onLoadStop: (c, u) async { + host.onUrl(u?.toString() ?? ''); + host.onShown?.call(u?.toString() ?? ''); + host.onProgress(1); + await history(c); + }, + onUpdateVisitedHistory: (c, u, _) async { + host.onUrl(u?.toString() ?? ''); + host.onShown?.call(u?.toString() ?? ''); + await history(c); + }, + onProgressChanged: (c, v) => host.onProgress(v / 100), + // 카메라 · 마이크 · 위치는 묻지 않고 거절 (WebRTC 와 함께 진짜 기기 정보가 나가지 않게) + onPermissionRequest: (c, r) async => PermissionResponse(resources: r.resources, action: PermissionResponseAction.DENY), + onGeolocationPermissionsShowPrompt: (c, o) async => GeolocationPermissionShowPromptResponse(origin: o, allow: false, retain: false), + ); + } +} + +class _InAppTorNav implements TorNav { + final InAppWebViewController c; + _InAppTorNav(this.c); + @override + Future load(String url) => c.loadUrl(urlRequest: URLRequest(url: WebUri(url))); + @override + Future back() => c.goBack(); + @override + Future forward() => c.goForward(); + @override + Future reload() => c.reload(); + @override + Future stop() async { + try { + await c.stopLoading(); + } catch (_) {} + } + + @override + Future evaluate(String js) async { + try { + return await c.evaluateJavascript(source: js); + } catch (_) { + return null; + } + } +} diff --git a/lib/ui/tor_page.dart b/lib/ui/tor_page.dart index 6b151e7..92c8bb0 100644 --- a/lib/ui/tor_page.dart +++ b/lib/ui/tor_page.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:collection'; import 'dart:io'; import 'package:flutter/material.dart'; @@ -12,124 +11,16 @@ import '../app/app_controller.dart'; import '../app/tor_service.dart'; import '../app/web_proxy.dart'; import '../core/tor_control.dart'; -import '../core/web_address.dart'; -import '../platform/windows/com_guard.dart'; import '../l10n/tr.dart'; import 'app_actions.dart'; import 'key_hint.dart'; import 'settings_page.dart'; import 'theme.dart'; +import 'tor/tor_messages.dart'; +import 'tor/tor_web_view.dart'; -/// Tor 화면의 웹 뷰 조작 -abstract class TorNav { - Future load(String url); - Future back(); - Future forward(); - Future reload(); - Future stop(); - - /// 페이지에서 스크립트를 실행하고 결과를 받는다 (실패하면 null) - Future evaluate(String js); -} - -/// 웹 뷰 → 화면으로 알리는 통로 -class TorViewHost { - final void Function(String url) onUrl; - - /// 페이지가 실제로 열림 (읽기 끝 · 기록에 남음). 막은 파일 주소로 주소 칸이 바뀌었으면 이 주소로 되돌린다 - final void Function(String url)? onShown; - final void Function(double progress) onProgress; - final void Function(bool back, bool forward) onHistory; - final void Function(TorNav nav) attach; - - /// 파일 받기를 막았다 (Tor 화면은 받지 않는다 - 관리자 결정 10/11) - final void Function(String url) onDownloadBlocked; - const TorViewHost( - {required this.onUrl, - required this.onProgress, - required this.onHistory, - required this.attach, - required this.onDownloadBlocked, - this.onShown}); -} - -/// WebRTC 끄기 (모든 틀 · 페이지 스크립트보다 먼저): STUN 으로 진짜 주소가 새지 않게. -/// Windows 는 실행 인수 (--force-webrtc-ip-handling-policy) 로도 막는다 -const torWebRtcBlockScript = r''' -(function(){ - var names=['RTCPeerConnection','webkitRTCPeerConnection','mozRTCPeerConnection','RTCDataChannel','RTCIceCandidate','RTCSessionDescription','RTCRtpSender','RTCRtpReceiver','RTCRtpTransceiver']; - function strip(w){ - try{ for(var i=0;i? _torEnv; -int? _torEnvPort; - -/// Tor 화면 Edge 실행 인수: 프록시 (localhost 까지), QUIC 끄기, WebRTC 는 프록시를 거치지 않는 UDP 금지, SmartScreen (주소를 Microsoft 로 보냄) 끄기 -String torBrowserArguments(int bridgePort) => [ - '--proxy-server=http://127.0.0.1:$bridgePort', - '--proxy-bypass-list=<-loopback>', - '--disable-quic', - '--webrtc-ip-handling-policy=disable_non_proxied_udp', - '--force-webrtc-ip-handling-policy=disable_non_proxied_udp', - '--dns-prefetch-disable', - '--disable-features=msSmartScreenProtection,NetworkPrediction,msEdgeShoppingAssistant', - ].join(' '); - -Future torEnvironment(String dataDir, int bridgePort) { - // 다리를 다시 열 때 포트가 바뀌었으면 (그 사이 다른 프로그램이 씀) 새 환경으로 - if (_torEnvPort != bridgePort) _torEnv = null; - _torEnvPort = bridgePort; - return _torEnv ??= () async { - try { - ComGuard.ensure(); - return await WebViewEnvironment.create( - settings: WebViewEnvironmentSettings( - userDataFolder: p.join(dataDir, 'tor_webview'), additionalBrowserArguments: torBrowserArguments(bridgePort))); - } catch (_) { - _torEnv = null; - rethrow; - } - }(); -} - -/// Android 웹 뷰를 Tor 화면 전용 프로필로 만들게 하는 설정 (packages/flutter_inappwebview_android/JJ_PATCH.md: jjProfile) -class _TorWebSettings extends InAppWebViewSettings { - _TorWebSettings({required bool javaScript, required bool keepData}) - : super( - javaScriptEnabled: javaScript, - mediaPlaybackRequiresUserGesture: true, - supportMultipleWindows: false, - useHybridComposition: false, - thirdPartyCookiesEnabled: false, - geolocationEnabled: false, - saveFormData: false, - // 파일 링크는 웹 뷰가 스스로 받지 않고 알린다 (Windows: packages/flutter_inappwebview_windows/JJ_PATCH.md) - useOnDownloadStart: true, - // Windows: 남기지 않으면 InPrivate (닫으면 쿠키 · 캐시가 없어진다) - incognito: Platform.isWindows && !keepData, - ); - - @override - Map toMap() => { - ...super.toMap(), - if (Platform.isAndroid) 'jjProfile': WebProxy.torProfileName, - // Windows: Tor 다리의 인증에 답하게 (packages/flutter_inappwebview_windows/JJ_PATCH.md) - if (Platform.isWindows) 'jjProxyAuth': WebProxy.proxyAuthSetting(), - }; -} +export 'tor/tor_messages.dart' show showTorNotice; +export 'tor/tor_web_view.dart' show TorNav, TorViewHost, torAddress, torBrowserArguments, torEnvironment, torWebRtcBlockScript; /// Tor 브라우저 화면 (사용자 요청 10/10): 웹 브라우저 옆. Tor 로만 나가고 쿠키 · 기록 · 캐시는 일반 브라우저와 따로. /// Tor 연결 100% · 프록시 적용 · 전용 프로필이 모두 된 뒤에야 웹 뷰를 만든다 ([torViewMayLoad]). @@ -176,24 +67,6 @@ class TorPage extends StatefulWidget { State createState() => _TorPageState(); } -/// 처음 열 때 · 환경 설정에서: 공식 Tor Browser 가 아니라는 안내 -Future showTorNotice(BuildContext context) => showDialog( - context: context, - builder: (ctx) => AlertDialog( - scrollable: true, - title: Text(tr('Tor 브라우저 안내')), - content: SizedBox( - width: 480, - child: Text(tr('이 화면은 앱 안 웹 뷰의 연결을 Tor 로 보내는 것이며, 공식 Tor Browser 가 아닙니다.\n\n' - '· 연결 (이름 찾기 포함) 은 모두 Tor 로 나가고, 쿠키 · 기록 · 캐시는 일반 브라우저와 따로 두며 닫으면 지웁니다 (환경 설정에서 남기기 가능).\n' - '· 하지만 브라우저 지문 (화면 크기 · 글꼴 · 기기 정보 등) 을 감추는 보호는 공식 Tor Browser 보다 약합니다. ' - '사이트가 이 기기를 알아볼 수 있습니다.\n' - '· 강한 익명성이 필요하면 공식 Tor Browser (https://www.torproject.org) 를 쓰세요.')), - ), - actions: [FilledButton(onPressed: () => Navigator.pop(ctx), child: Text(tr('확인')))], - ), - ); - class _TorPageState extends State with WebProxyVisibility { final _tor = TorService.instance; late String _url = widget.c.settings.tor.homeUrl; @@ -605,136 +478,22 @@ class _TorPageState extends State with WebProxyVisibility { Widget _body(TorStatus status) { if (_missing != null) { - return _message(Icons.block, tr('이 기기의 웹 뷰로는 Tor 화면을 열 수 없습니다'), + return torMessage(Icons.block, tr('이 기기의 웹 뷰로는 Tor 화면을 열 수 없습니다'), trf('Android System WebView 에 필요한 기능이 없습니다 ({0}). Play 스토어에서 Android System WebView · Chrome 을 업데이트한 뒤 다시 여세요. ' 'Tor 를 거치지 않고 열릴 수 있으므로 이 화면은 페이지를 읽지 않습니다.', [_missing!.join(', ')])); } - if (_error != null) return _message(Icons.error_outline, tr('Tor 화면을 준비할 수 없습니다'), _error!); + if (_error != null) return torMessage(Icons.error_outline, tr('Tor 화면을 준비할 수 없습니다'), _error!); // 떠나 있는 동안 (다른 화면 · 앱이 뒤로): 웹 뷰를 두지 않는다 if (_away) { - return _message(Icons.vpn_lock_outlined, tr('Tor 화면을 떠나 페이지를 정리했습니다'), tr('돌아오면 홈부터 다시 엽니다.')); + return torMessage(Icons.vpn_lock_outlined, tr('Tor 화면을 떠나 페이지를 정리했습니다'), tr('돌아오면 홈부터 다시 엽니다.')); } if (!torViewMayLoad(status: status, proxyApplied: _proxyApplied, profileReady: _profileReady)) { - return _message(Icons.vpn_lock_outlined, tr('Tor 에 연결하는 중'), + return torMessage(Icons.vpn_lock_outlined, tr('Tor 에 연결하는 중'), tr('Tor 연결이 끝나고 웹 뷰가 Tor 로만 나가게 된 뒤에 페이지를 엽니다. 그 전에는 아무것도 읽지 않습니다.'), busy: status.phase == TorPhase.starting || status.ready); } final b = widget.viewBuilder; if (b != null) return b(_host, _url); - return _TorWebView(host: _host, url: _url, env: _env, javaScript: widget.c.settings.tor.javaScript, keepData: widget.c.settings.tor.keepData); - } - - Widget _message(IconData icon, String title, String body, {bool busy = false}) => Center( - child: SingleChildScrollView( - padding: const EdgeInsets.all(24), - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 520), - child: Column(mainAxisSize: MainAxisSize.min, children: [ - busy ? const SizedBox(width: 40, height: 40, child: CircularProgressIndicator()) : Icon(icon, size: 40, color: JjColors.textDim), - const SizedBox(height: 12), - Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), textAlign: TextAlign.center), - const SizedBox(height: 8), - Text(body, style: TextStyle(color: JjColors.textDim), textAlign: TextAlign.center), - ]), - ), - ), - ); -} - -/// Tor 화면의 주소창 입력 → 주소 (주소가 아니면 DuckDuckGo 검색 - Google 로 보내지 않음) -String torAddress(String input) { - final u = normalizeAddress(input); - if (u.startsWith('https://www.google.com/search?q=')) { - return 'https://duckduckgo.com/?q=${u.substring('https://www.google.com/search?q='.length)}'; - } - return u; -} - -class _TorWebView extends StatefulWidget { - final TorViewHost host; - final String url; - final WebViewEnvironment? env; - final bool javaScript; - final bool keepData; - const _TorWebView({required this.host, required this.url, required this.env, required this.javaScript, required this.keepData}); - - @override - State<_TorWebView> createState() => _TorWebViewState(); -} - -/// 웹 뷰가 살아 있는 동안 WebProxy 에 알린다 ([WebProxy.torViewCreated]) - 없어지면 프록시를 다시 걸고, 남기지 않으면 프로필을 지운다 -class _TorWebViewState extends State<_TorWebView> { - @override - void initState() { - super.initState(); - WebProxy.torViewCreated(); - WebProxy.onLastTorView = widget.keepData || !Platform.isAndroid ? null : _TorPageState._clearProfile; - } - - @override - void dispose() { - WebProxy.torViewDisposed(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final host = widget.host, url = widget.url, env = widget.env, javaScript = widget.javaScript, keepData = widget.keepData; - Future history(InAppWebViewController c) async => host.onHistory(await c.canGoBack(), await c.canGoForward()); - return InAppWebView( - webViewEnvironment: env, - initialUrlRequest: URLRequest(url: WebUri(torAddress(url))), - initialSettings: _TorWebSettings(javaScript: javaScript, keepData: keepData), - initialUserScripts: UnmodifiableListView([ - UserScript(source: torWebRtcBlockScript, injectionTime: UserScriptInjectionTime.AT_DOCUMENT_START, forMainFrameOnly: false), - ]), - onWebViewCreated: (c) => host.attach(_InAppTorNav(c)), - // 받지 않는다 (useOnDownloadStart: 웹 뷰도 스스로 받지 않음) - onDownloadStartRequest: (c, r) => host.onDownloadBlocked(r.url.toString()), - onLoadStart: (c, u) => host.onUrl(u?.toString() ?? ''), - onLoadStop: (c, u) async { - host.onUrl(u?.toString() ?? ''); - host.onShown?.call(u?.toString() ?? ''); - host.onProgress(1); - await history(c); - }, - onUpdateVisitedHistory: (c, u, _) async { - host.onUrl(u?.toString() ?? ''); - host.onShown?.call(u?.toString() ?? ''); - await history(c); - }, - onProgressChanged: (c, v) => host.onProgress(v / 100), - // 카메라 · 마이크 · 위치는 묻지 않고 거절 (WebRTC 와 함께 진짜 기기 정보가 나가지 않게) - onPermissionRequest: (c, r) async => PermissionResponse(resources: r.resources, action: PermissionResponseAction.DENY), - onGeolocationPermissionsShowPrompt: (c, o) async => GeolocationPermissionShowPromptResponse(origin: o, allow: false, retain: false), - ); - } -} - -class _InAppTorNav implements TorNav { - final InAppWebViewController c; - _InAppTorNav(this.c); - @override - Future load(String url) => c.loadUrl(urlRequest: URLRequest(url: WebUri(url))); - @override - Future back() => c.goBack(); - @override - Future forward() => c.goForward(); - @override - Future reload() => c.reload(); - @override - Future stop() async { - try { - await c.stopLoading(); - } catch (_) {} - } - - @override - Future evaluate(String js) async { - try { - return await c.evaluateJavascript(source: js); - } catch (_) { - return null; - } + return TorWebView(clearProfile: _clearProfile, host: _host, url: _url, env: _env, javaScript: widget.c.settings.tor.javaScript, keepData: widget.c.settings.tor.keepData); } }