JJ_MKVMaker/lib/ui/tor/tor_web_view.dart

229 lines
9.8 KiB
Dart

/// 약속: 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<void> load(String url);
Future<void> back();
Future<void> forward();
Future<void> reload();
Future<void> stop();
/// 페이지에서 스크립트를 실행하고 결과를 받는다 (실패하면 null)
Future<Object?> 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<names.length;i++){ try{ Object.defineProperty(w,names[i],{value:undefined,writable:false,configurable:false}); }catch(e){ try{ delete w[names[i]]; w[names[i]]=undefined; }catch(_){} } } }catch(e){}
try{ if(w.navigator && w.navigator.mediaDevices){ Object.defineProperty(w.navigator,'mediaDevices',{value:undefined,configurable:false}); } }catch(e){}
}
strip(window);
// 스크립트가 먼저 돌지 않는 빈 틀 (about:blank) 에서 꺼내 쓰지 못하게: 틀의 창을 건네기 전에 지운다
try{
var d=Object.getOwnPropertyDescriptor(HTMLIFrameElement.prototype,'contentWindow');
Object.defineProperty(HTMLIFrameElement.prototype,'contentWindow',{configurable:false,get:function(){ var w=d.get.call(this); if(w){ try{ strip(w); }catch(e){} } return w; }});
var dd=Object.getOwnPropertyDescriptor(HTMLIFrameElement.prototype,'contentDocument');
Object.defineProperty(HTMLIFrameElement.prototype,'contentDocument',{configurable:false,get:function(){ var doc=dd.get.call(this); if(doc && doc.defaultView){ try{ strip(doc.defaultView); }catch(e){} } return doc; }});
}catch(e){}
})();
''';
/// Windows: Tor 화면 전용 WebView2 환경 (데이터 폴더를 일반 브라우저와 따로, 모든 연결을 Tor 다리로)
Future<WebViewEnvironment>? _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<WebViewEnvironment> 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<String, dynamic> 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<void> 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<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 : 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<void> 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<void> load(String url) => c.loadUrl(urlRequest: URLRequest(url: WebUri(url)));
@override
Future<void> back() => c.goBack();
@override
Future<void> forward() => c.goForward();
@override
Future<void> reload() => c.reload();
@override
Future<void> stop() async {
try {
await c.stopLoading();
} catch (_) {}
}
@override
Future<Object?> evaluate(String js) async {
try {
return await c.evaluateJavascript(source: js);
} catch (_) {
return null;
}
}
}