/// 약속: 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; } } }