diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 3a01820..ea74563 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -332,5 +332,8 @@ "appUpdateNewVersionLbl": "New Version", "appUpdateUpdateNowBtnTxt": "Update Now", "appUpdateLaterBtnTxt": "Later", - "appUpdateIgnoreBtnTxt": "Ignore" + "appUpdateIgnoreBtnTxt": "Ignore", + + "requestMap": "Request Map", + "requestMapDescribe": "Do not request remote services, use local configuration or script for response" } \ No newline at end of file diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 02f437c..6866d22 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1961,6 +1961,18 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Ignore'** String get appUpdateIgnoreBtnTxt; + + /// No description provided for @requestMap. + /// + /// In en, this message translates to: + /// **'Request Map'** + String get requestMap; + + /// No description provided for @requestMapDescribe. + /// + /// In en, this message translates to: + /// **'Do not request remote services, use local configuration or script for response'** + String get requestMapDescribe; } class _AppLocalizationsDelegate extends LocalizationsDelegate { diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 2e5c19a..7042def 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -969,4 +969,10 @@ class AppLocalizationsEn extends AppLocalizations { @override String get appUpdateIgnoreBtnTxt => 'Ignore'; + + @override + String get requestMap => 'Request Map'; + + @override + String get requestMapDescribe => 'Do not request remote services, use local configuration or script for response'; } diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index e6581fa..b62650a 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -957,6 +957,12 @@ class AppLocalizationsZh extends AppLocalizations { @override String get appUpdateIgnoreBtnTxt => '忽略'; + + @override + String get requestMap => '请求映射'; + + @override + String get requestMapDescribe => '不请求远程服务,使用本地配置或脚本进行响应'; } /// The translations for Chinese, using the Han script (`zh_Hant`). @@ -1913,4 +1919,10 @@ class AppLocalizationsZhHant extends AppLocalizationsZh { @override String get appUpdateIgnoreBtnTxt => '忽略'; + + @override + String get requestMap => '請求映射'; + + @override + String get requestMapDescribe => '不請求遠端服務,使用本地配置或腳本進行回應'; } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 629837c..8e10063 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -331,5 +331,8 @@ "appUpdateNewVersionLbl": "新版本", "appUpdateUpdateNowBtnTxt": "现在更新", "appUpdateLaterBtnTxt": "以后再说", - "appUpdateIgnoreBtnTxt": "忽略" + "appUpdateIgnoreBtnTxt": "忽略", + + "requestMap": "请求映射", + "requestMapDescribe": "不请求远程服务,使用本地配置或脚本进行响应" } \ No newline at end of file diff --git a/lib/l10n/app_zh_Hant.arb b/lib/l10n/app_zh_Hant.arb index 4631438..e191047 100644 --- a/lib/l10n/app_zh_Hant.arb +++ b/lib/l10n/app_zh_Hant.arb @@ -309,5 +309,8 @@ "appUpdateNewVersionLbl": "新版本", "appUpdateUpdateNowBtnTxt": "現在更新", "appUpdateLaterBtnTxt": "稍後再說", - "appUpdateIgnoreBtnTxt": "忽略" + "appUpdateIgnoreBtnTxt": "忽略", + + "requestMap": "請求映射", + "requestMapDescribe": "不請求遠端服務,使用本地配置或腳本進行回應" } \ No newline at end of file diff --git a/lib/network/bin/server.dart b/lib/network/bin/server.dart index d5a4981..63b7e75 100644 --- a/lib/network/bin/server.dart +++ b/lib/network/bin/server.dart @@ -27,6 +27,7 @@ import 'package:proxypin/network/handle/http_proxy_handle.dart'; import 'package:proxypin/network/util/crts.dart'; import 'package:proxypin/utils/platform.dart'; +import '../components/request_map.dart'; import '../http/codec.dart'; import '../channel/network.dart'; import '../util/logger.dart'; @@ -80,6 +81,7 @@ class ProxyServer { List interceptors = [ Hosts(), + RequestMapInterceptor.instance, RequestRewriteInterceptor.instance, ScriptInterceptor(), RequestBlockInterceptor(), diff --git a/lib/network/channel/channel_dispatcher.dart b/lib/network/channel/channel_dispatcher.dart index 7c520bb..c208a5a 100644 --- a/lib/network/channel/channel_dispatcher.dart +++ b/lib/network/channel/channel_dispatcher.dart @@ -168,7 +168,7 @@ class ChannelDispatcher extends ChannelHandler { } } - onError(ChannelContext channelContext, Channel channel, dynamic error, {StackTrace? trace}) { + void onError(ChannelContext channelContext, Channel channel, dynamic error, {StackTrace? trace}) { logger.e( "[${channelContext.clientChannel?.id}] channelRead error isSsl:${channel.isSsl} client: ${channelContext.clientChannel?.selectedProtocol} server: ${channelContext.serverChannel?.selectedProtocol} ${String.fromCharCodes(buffer.bytes)}", error: error, diff --git a/lib/network/components/js/script_engine.dart b/lib/network/components/js/script_engine.dart new file mode 100644 index 0000000..9e7a356 --- /dev/null +++ b/lib/network/components/js/script_engine.dart @@ -0,0 +1,156 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter_js/flutter_js.dart'; +import 'package:proxypin/network/components/js/xhr.dart'; + +import '../../http/http.dart'; +import '../../http/http.dart' as http; +import '../../http/http_headers.dart'; +import '../../util/lang.dart'; +import '../../util/logger.dart'; +import '../../util/uri.dart'; +import 'file.dart'; +import 'md5.dart'; + +class JavaScriptEngine { + static Future getJavaScript({Function(dynamic args)? consoleLog}) async { + final JavascriptRuntime flutterJs = getJavascriptRuntime(xhr: false); + + // register channel callback + if (consoleLog != null) { + final channelCallbacks = JavascriptRuntime.channelFunctionsRegistered[flutterJs.getEngineInstanceId()]; + channelCallbacks!["ConsoleLog"] = consoleLog; + } + Md5Bridge.registerMd5(flutterJs); + FileBridge.registerFile(flutterJs); + + flutterJs.enableFetch2(); + return flutterJs; + } + + /// js结果转换 + static Future jsResultResolve(JavascriptRuntime flutterJs, JsEvalResult jsResult) async { + try { + if (jsResult.isPromise || jsResult.rawResult is Future) { + jsResult = await flutterJs.handlePromise(jsResult); + } + + if (jsResult.isPromise || jsResult.rawResult is Future) { + jsResult = await flutterJs.handlePromise(jsResult); + } + } catch (e) { + throw SignalException(jsResult.stringResult); + } + + var result = jsResult.rawResult; + if (Platform.isMacOS || Platform.isIOS) { + result = flutterJs.convertValue(jsResult); + } + if (result is String) { + result = jsonDecode(result); + } + if (jsResult.isError) { + logger.e('jsResultResolve error: ${jsResult.stringResult}'); + throw SignalException(jsResult.stringResult); + } + return result; + } + + //转换js request + static Future> convertJsRequest(HttpRequest request) async { + var requestUri = request.requestUri; + return { + 'host': requestUri?.host, + 'url': request.requestUrl, + 'path': requestUri?.path, + 'queries': requestUri?.queryParameters, + 'headers': request.headers.toMap(), + 'method': request.method.name, + 'body': await request.decodeBodyString(), + 'rawBody': request.body + }; + } + + //转换js response + static Future> convertJsResponse(HttpResponse response) async { + dynamic body = await response.decodeBodyString(); + if (response.contentType.isBinary) { + body = response.body; + } + + return { + 'headers': response.headers.toMap(), + 'statusCode': response.status.code, + 'body': body, + 'rawBody': response.body + }; + } + + //http request + static HttpRequest convertHttpRequest(HttpRequest request, Map map) { + request.headers.clear(); + request.method = http.HttpMethod.values.firstWhere((element) => element.name == map['method']); + String query = UriUtils.mapToQuery(map['queries']); + + var requestUri = request.requestUri!.replace(path: map['path'], query: query); + if (requestUri.isScheme('https')) { + var query = requestUri.query; + request.uri = requestUri.path + (query.isNotEmpty ? '?${requestUri.query}' : ''); + } else { + request.uri = requestUri.toString(); + } + + map['headers'].forEach((key, value) { + if (value is List) { + request.headers.addValues(key, value.map((e) => e.toString()).toList()); + return; + } + request.headers.set(key, value); + }); + + request.headers.remove(HttpHeaders.CONTENT_ENCODING); + + //判断是否是二进制 + if (Lists.getElementType(map['body']) == int) { + request.body = Lists.convertList(map['body']); + return request; + } + + request.body = map['body']?.toString().codeUnits; + + if (request.body != null && (request.charset == 'utf-8' || request.charset == 'utf8')) { + request.body = utf8.encode(map['body'].toString()); + } + return request; + } + + //http response + static HttpResponse convertHttpResponse(HttpResponse response, Map map) { + response.headers.clear(); + response.status = HttpStatus.valueOf(map['statusCode']); + map['headers'].forEach((key, value) { + if (value is List) { + response.headers.addValues(key, value.map((e) => e.toString()).toList()); + return; + } + + response.headers.set(key, value); + }); + + response.headers.remove(HttpHeaders.CONTENT_ENCODING); + + //判断是否是二进制 + if (Lists.getElementType(map['body']) == int) { + response.body = Lists.convertList(map['body']); + return response; + } + + response.body = map['body']?.toString().codeUnits; + if (response.body != null && (response.charset == 'utf-8' || response.charset == 'utf8')) { + response.body = utf8.encode(map['body'].toString()); + } + + return response; + } +} diff --git a/lib/network/components/manager/script_manager.dart b/lib/network/components/manager/script_manager.dart index 65dc4c3..38438a0 100644 --- a/lib/network/components/manager/script_manager.dart +++ b/lib/network/components/manager/script_manager.dart @@ -19,19 +19,14 @@ import 'dart:io'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter_js/flutter_js.dart'; -import 'package:proxypin/network/components/js/file.dart'; -import 'package:proxypin/network/components/js/md5.dart'; -import 'package:proxypin/network/components/js/xhr.dart'; import 'package:proxypin/network/http/http.dart'; -import 'package:proxypin/network/http/http.dart' as http; -import 'package:proxypin/network/http/http_headers.dart'; -import 'package:proxypin/network/util/lang.dart'; import 'package:proxypin/network/util/logger.dart'; import 'package:proxypin/network/util/random.dart'; -import 'package:proxypin/network/util/uri.dart'; import 'package:proxypin/ui/component/device.dart'; import 'package:path_provider/path_provider.dart'; +import '../js/script_engine.dart'; + /// @author wanghongen /// 2023/10/06 /// js脚本 @@ -71,7 +66,7 @@ async function onResponse(context, request, response) { final Map _scriptMap = {}; - static JavascriptRuntime flutterJs = getJavascriptRuntime(xhr: false); + static late JavascriptRuntime flutterJs; static String? deviceId; @@ -84,15 +79,8 @@ async function onResponse(context, request, response) { if (_instance == null) { _instance = ScriptManager._(); await _instance?.reloadScript(); - - // register channel callback - final channelCallbacks = JavascriptRuntime.channelFunctionsRegistered[flutterJs.getEngineInstanceId()]; - channelCallbacks!["ConsoleLog"] = _instance!.consoleLog; + flutterJs = await JavaScriptEngine.getJavaScript(consoleLog: consoleLog); deviceId = await DeviceUtils.deviceId(); - Md5Bridge.registerMd5(flutterJs); - FileBridge.registerFile(flutterJs); - - flutterJs.enableFetch2(); logger.d('init script manager $deviceId'); } @@ -119,7 +107,7 @@ async function onResponse(context, request, response) { _logHandlers.removeWhere((element) => channelId == element.channelId); } - dynamic consoleLog(dynamic args) async { + static dynamic consoleLog(dynamic args) async { if (_logHandlers.isEmpty) { return; } @@ -247,17 +235,17 @@ async function onResponse(context, request, response) { for (var item in list) { if (item.enabled && item.match(url)) { var context = jsonEncode(scriptContext(item)); - var jsRequest = jsonEncode(await convertJsRequest(request)); + var jsRequest = jsonEncode(await JavaScriptEngine.convertJsRequest(request)); String script = await getScript(item); var jsResult = await flutterJs.evaluateAsync( """var request = $jsRequest, context = $context; request['scriptContext'] = context; $script\n onRequest(context, request)"""); - var result = await jsResultResolve(jsResult); + var result = await JavaScriptEngine.jsResultResolve(flutterJs, jsResult); if (result == null) { return null; } request.attributes['scriptContext'] = result['scriptContext']; scriptSession = result['scriptContext']['session'] ?? {}; - request = convertHttpRequest(request, result); + request = JavaScriptEngine.convertHttpRequest(request, result); } } return request; @@ -274,148 +262,23 @@ async function onResponse(context, request, response) { for (var item in list) { if (item.enabled && item.match(url)) { var context = jsonEncode(request.attributes['scriptContext'] ?? scriptContext(item)); - var jsRequest = jsonEncode(await convertJsRequest(request)); - var jsResponse = jsonEncode(await convertJsResponse(response)); + var jsRequest = jsonEncode(await JavaScriptEngine.convertJsRequest(request)); + var jsResponse = jsonEncode(await JavaScriptEngine.convertJsResponse(response)); String script = await getScript(item); var jsResult = await flutterJs.evaluateAsync( """var response = $jsResponse, context = $context; response['scriptContext'] = context; $script \n onResponse(context, $jsRequest, response);"""); // print("response: ${jsResult.isPromise} ${jsResult.isError} ${jsResult.rawResult}"); - var result = await jsResultResolve(jsResult); + var result = await JavaScriptEngine.jsResultResolve(flutterJs, jsResult); if (result == null) { return null; } scriptSession = result['scriptContext']['session'] ?? {}; - response = convertHttpResponse(response, result); + response = JavaScriptEngine.convertHttpResponse(response, result); } } return response; } - - /// js结果转换 - static Future jsResultResolve(JsEvalResult jsResult) async { - try { - if (jsResult.isPromise || jsResult.rawResult is Future) { - jsResult = await flutterJs.handlePromise(jsResult); - } - - if (jsResult.isPromise || jsResult.rawResult is Future) { - jsResult = await flutterJs.handlePromise(jsResult); - } - } catch (e) { - throw SignalException(jsResult.stringResult); - } - - var result = jsResult.rawResult; - if (Platform.isMacOS || Platform.isIOS) { - result = flutterJs.convertValue(jsResult); - } - if (result is String) { - result = jsonDecode(result); - } - if (jsResult.isError) { - logger.e('jsResultResolve error: ${jsResult.stringResult}'); - throw SignalException(jsResult.stringResult); - } - return result; - } - - //转换js request - Future> convertJsRequest(HttpRequest request) async { - var requestUri = request.requestUri; - return { - 'host': requestUri?.host, - 'url': request.requestUrl, - 'path': requestUri?.path, - 'queries': requestUri?.queryParameters, - 'headers': request.headers.toMap(), - 'method': request.method.name, - 'body': await request.decodeBodyString(), - 'rawBody': request.body - }; - } - - //转换js response - Future> convertJsResponse(HttpResponse response) async { - dynamic body = await response.decodeBodyString(); - if (response.contentType.isBinary) { - body = response.body; - } - - return { - 'headers': response.headers.toMap(), - 'statusCode': response.status.code, - 'body': body, - 'rawBody': response.body - }; - } - - //http request - HttpRequest convertHttpRequest(HttpRequest request, Map map) { - request.headers.clear(); - request.method = http.HttpMethod.values.firstWhere((element) => element.name == map['method']); - String query = UriUtils.mapToQuery(map['queries']); - - var requestUri = request.requestUri!.replace(path: map['path'], query: query); - if (requestUri.isScheme('https')) { - var query = requestUri.query; - request.uri = requestUri.path + (query.isNotEmpty ? '?${requestUri.query}' : ''); - } else { - request.uri = requestUri.toString(); - } - - map['headers'].forEach((key, value) { - if (value is List) { - request.headers.addValues(key, value.map((e) => e.toString()).toList()); - return; - } - request.headers.set(key, value); - }); - - request.headers.remove(HttpHeaders.CONTENT_ENCODING); - - //判断是否是二进制 - if (Lists.getElementType(map['body']) == int) { - request.body = Lists.convertList(map['body']); - return request; - } - - request.body = map['body']?.toString().codeUnits; - - if (request.body != null && (request.charset == 'utf-8' || request.charset == 'utf8')) { - request.body = utf8.encode(map['body'].toString()); - } - return request; - } - - //http response - HttpResponse convertHttpResponse(HttpResponse response, Map map) { - response.headers.clear(); - response.status = HttpStatus.valueOf(map['statusCode']); - map['headers'].forEach((key, value) { - if (value is List) { - response.headers.addValues(key, value.map((e) => e.toString()).toList()); - return; - } - - response.headers.set(key, value); - }); - - response.headers.remove(HttpHeaders.CONTENT_ENCODING); - - //判断是否是二进制 - if (Lists.getElementType(map['body']) == int) { - response.body = Lists.convertList(map['body']); - return response; - } - - response.body = map['body']?.toString().codeUnits; - if (response.body != null && (response.charset == 'utf-8' || response.charset == 'utf8')) { - response.body = utf8.encode(map['body'].toString()); - } - - return response; - } } class LogHandler { diff --git a/lib/network/components/request_map.dart b/lib/network/components/request_map.dart index a6c5120..dc1ddf9 100644 --- a/lib/network/components/request_map.dart +++ b/lib/network/components/request_map.dart @@ -15,58 +15,67 @@ */ import 'dart:convert'; +import 'dart:io'; +import 'package:flutter_js/flutter_js.dart'; import 'package:proxypin/network/components/interceptor.dart'; import 'package:proxypin/network/http/http.dart'; import 'package:proxypin/network/util/file_read.dart'; +import 'js/script_engine.dart'; import 'manager/request_map_manager.dart'; +import 'manager/script_manager.dart'; /// RequestRewriteComponent is a component that can rewrite the request before sending it to the server. /// @author Hongen Wang class RequestMapInterceptor extends Interceptor { static RequestMapInterceptor instance = RequestMapInterceptor._(); + static JavascriptRuntime? flutterJs; + static Map scriptSession = {}; final managerInstance = RequestMapManager.instance; RequestMapInterceptor._(); + ///脚本上下文 + Map scriptContext(RequestMapRule rule) { + return {'scriptName': rule.name, 'os': Platform.operatingSystem, 'session': scriptSession}; + } + @override Future execute(HttpRequest request) async { final manager = await managerInstance; if (!manager.enabled) { return null; } + RequestMapRule? mapRule = manager.findMatch(request.requestUrl); + if (mapRule == null) { + return null; + } + var item = await manager.getMapItem(mapRule); + if (item == null) { + return null; + } - return null; + HttpResponse? response; + if (mapRule.type == RequestMapType.local) { + // 本地映射 + response = await mapLocalResponse(mapRule, item); + } else if (mapRule.type == RequestMapType.script && item.script != null) { + response = await executeScript(request, mapRule, item.script!); + } + + response?.request = request; + request.response = response; + return response; } /// 重写响应 - Future mapLocalResponse(String url, HttpResponse response) async { - final manager = await RequestMapManager.instance; - var mapRule = manager.findMatch(url); - if (mapRule == null) { - return; - } - - if (mapRule.type == RequestMapType.script) { - // var rewriteItems = await manager.getMapItem(rewriteRule); - // await _replaceResponse(response, item); - } - } - - //替换相应 - Future _replaceResponse(RequestMapRule rule, RequestMapItem item) async { - // if (rule.type == RequestMapType.script) { - // response.status = HttpStatus.valueOf(item.statusCode!); - // return; - // } - + Future mapLocalResponse(RequestMapRule rule, RequestMapItem item) async { HttpResponse response = HttpResponse(HttpStatus.valueOf(item.statusCode ?? 200)); item.headers?.forEach((key, value) { response.headers.set(key, value); }); - if (item.bodyType == MapBodyType.file.name) { if (item.bodyFile == null) return response; response.body = await FileRead.readFile(item.bodyFile!); @@ -76,4 +85,26 @@ class RequestMapInterceptor extends Interceptor { } return response; } + + /// script执行 + Future executeScript(HttpRequest request, RequestMapRule rule, String script) async { + flutterJs ??= await JavaScriptEngine.getJavaScript(consoleLog: ScriptManager.consoleLog); + var context = jsonEncode(scriptContext(rule)); + var jsRequest = jsonEncode(await JavaScriptEngine.convertJsRequest(request)); + + var jsResult = await flutterJs!.evaluateAsync( + """var request = $jsRequest, context = $context; request['scriptContext'] = context; $script\n onRequest(context, request)"""); + // print("response: ${jsResult.isPromise} ${jsResult.isError} ${jsResult.rawResult}"); + var result = await JavaScriptEngine.jsResultResolve(flutterJs!, jsResult); + if (result == null) { + return null; + } + + if (result['scriptContext']?['session'] != null) { + scriptSession = result['scriptContext']['session']; + } + HttpResponse response = HttpResponse(HttpStatus.valueOf(200)); + response = JavaScriptEngine.convertHttpResponse(response, result); + return response; + } } diff --git a/lib/network/handle/http_proxy_handle.dart b/lib/network/handle/http_proxy_handle.dart index 41218c0..28a1968 100644 --- a/lib/network/handle/http_proxy_handle.dart +++ b/lib/network/handle/http_proxy_handle.dart @@ -109,6 +109,14 @@ class HttpProxyChannelHandler extends ChannelHandler { listener?.onRequest(channel, request!); + for (var interceptor in interceptors) { + var response = await interceptor.execute(request!); + if (response != null) { + channel.writeAndClose(channelContext, response); + return; + } + } + //重定向 var uri = request!.domainPath; String? redirectUrl = await (RequestRewriteInterceptor.instance).getRedirectRule(uri); diff --git a/lib/network/http/http_client.dart b/lib/network/http/http_client.dart index 3d789e9..e940970 100644 --- a/lib/network/http/http_client.dart +++ b/lib/network/http/http_client.dart @@ -231,7 +231,7 @@ class Http2ClientHandler { await channel.writeBytes(buffer); } - onData(ChannelContext channelContext, Channel channel, Uint8List data) { + void onData(ChannelContext channelContext, Channel channel, Uint8List data) { byteBuf.add(data); var decodeResult = decoder.decode(channelContext, byteBuf); diff --git a/lib/ui/component/multi_window.dart b/lib/ui/component/multi_window.dart index 80021f6..2038a86 100644 --- a/lib/ui/component/multi_window.dart +++ b/lib/ui/component/multi_window.dart @@ -40,6 +40,7 @@ import 'package:proxypin/utils/platform.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:window_manager/window_manager.dart'; +import '../desktop/toolbar/setting/request_map.dart'; import '../toolbox/cert_hash.dart'; import '../toolbox/encoder.dart'; import '../toolbox/js_run.dart'; @@ -82,6 +83,9 @@ Widget multiWindow(int windowId, Map argument) { return futureWidget( RequestRewriteManager.instance, (data) => RequestRewriteWidget(windowId: windowId, requestRewrites: data)); } + if (argument['name'] == 'RequestMapPage') { + return RequestMapPage(windowId: windowId); + } if (argument['name'] == 'QrCodePage') { return QrCodePage(windowId: windowId); diff --git a/lib/ui/desktop/toolbar/setting/request_map.dart b/lib/ui/desktop/toolbar/setting/request_map.dart index 0eb36b6..2ab2a4d 100644 --- a/lib/ui/desktop/toolbar/setting/request_map.dart +++ b/lib/ui/desktop/toolbar/setting/request_map.dart @@ -9,12 +9,12 @@ import 'package:flutter/services.dart'; import 'package:flutter_toastr/flutter_toastr.dart'; import 'package:proxypin/l10n/app_localizations.dart'; import 'package:proxypin/network/components/manager/request_map_manager.dart'; -import 'package:proxypin/network/components/manager/script_manager.dart'; import 'package:proxypin/ui/component/app_dialog.dart'; import 'package:proxypin/ui/component/utils.dart'; import 'package:proxypin/ui/component/widgets.dart'; import 'package:proxypin/ui/desktop/toolbar/setting/request_map/map_local.dart'; import 'package:proxypin/ui/desktop/toolbar/setting/request_map/map_scipt.dart'; +import 'package:proxypin/utils/lang.dart'; import '../../../../network/util/logger.dart'; @@ -76,7 +76,7 @@ class _RequestMapPageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text("请求映射", style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)), + title: Text(localizations.requestMap, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)), toolbarHeight: 36, centerTitle: true), body: Padding( @@ -92,14 +92,14 @@ class _RequestMapPageState extends State { SizedBox( width: 350, child: ListTile( - title: Text("启用请求映射"), - subtitle: Text("不请求远程服务,使用本地配置或脚本进行响应"), + title: Text("${localizations.enable} ${localizations.requestMap}"), + subtitle: Text(localizations.requestMapDescribe, style: const TextStyle(fontSize: 12)), trailing: SwitchWidget( value: data.enabled, scale: 0.8, onChanged: (value) { data.enabled = value; - // _refreshScript(); + _refreshConfig(); }))), Expanded( child: Row( @@ -122,7 +122,7 @@ class _RequestMapPageState extends State { const SizedBox(width: 15) ]), const SizedBox(height: 5), - RequestMapList(list: data.rules, windowId: widget.windowId!), + RequestMapList(list: data.rules, windowId: widget.windowId), ])))); } @@ -145,24 +145,21 @@ class _RequestMapPageState extends State { } try { var json = jsonDecode(await File(path).readAsString()); - var scriptManager = (await ScriptManager.instance); + var manager = (await RequestMapManager.instance); if (json is List) { for (var item in json) { - var scriptItem = ScriptItem.fromJson(item); - await scriptManager.addScript(scriptItem, item['script']); + var mapRule = RequestMapRule.fromJson(item); + var requestMapItem = RequestMapItem.fromJson(item['item']); + await manager.addRule(mapRule, requestMapItem); } - } else { - var scriptItem = ScriptItem.fromJson(json); - await scriptManager.addScript(scriptItem, json['script']); } - // _refreshScript(); if (mounted) { CustomToast.success(localizations.importSuccess).show(context); } setState(() {}); } catch (e, t) { - logger.e('导入失败 $path', error: e, stackTrace: t); + logger.e('[RequestMap] import fail $path', error: e, stackTrace: t); if (mounted) { CustomToast.error("${localizations.importFailed} $e").show(context); } @@ -181,7 +178,7 @@ class _RequestMapPageState extends State { /// 脚本列表 class RequestMapList extends StatefulWidget { - final int windowId; + final int? windowId; final List list; const RequestMapList({super.key, required this.list, required this.windowId}); @@ -200,7 +197,12 @@ class _RequestMapListState extends State { @override Widget build(BuildContext context) { return GestureDetector( - onSecondaryTapDown: (details) => showGlobalMenu(details.globalPosition), + onSecondaryTap: () { + if (lastPressPosition == null) { + return; + } + showGlobalMenu(lastPressPosition!); + }, onTapDown: (details) { if (selected.isEmpty) { return; @@ -226,12 +228,16 @@ class _RequestMapListState extends State { decoration: BoxDecoration(border: Border.all(color: Colors.grey.withOpacity(0.2))), child: SingleChildScrollView( child: Column(children: [ - Row(mainAxisAlignment: MainAxisAlignment.start, children: [ - Container(width: 200, padding: const EdgeInsets.only(left: 10), child: Text(localizations.name)), - SizedBox(width: 50, child: Text(localizations.enable, textAlign: TextAlign.center)), - const VerticalDivider(), - const Expanded(child: Text("URL")), - ]), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Container(width: 130, padding: const EdgeInsets.only(left: 10), child: Text(localizations.name)), + SizedBox(width: 50, child: Text(localizations.enable, textAlign: TextAlign.center)), + const VerticalDivider(), + const Expanded(child: Text("URL")), + SizedBox(width: 100, child: Text(localizations.action, textAlign: TextAlign.center)), + ], + ), const Divider(thickness: 0.5), Column(children: rows(widget.list)) ]))))); @@ -239,18 +245,15 @@ class _RequestMapListState extends State { List rows(List list) { var primaryColor = Theme.of(context).colorScheme.primary; + bool isEN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'en'); return List.generate(list.length, (index) { return InkWell( - // onTap: () { - // selected[index] = !(selected[index] ?? false); - // setState(() {}); - // }, highlightColor: Colors.transparent, splashColor: Colors.transparent, hoverColor: primaryColor.withOpacity(0.3), - onDoubleTap: () => showEdit(index), onSecondaryTapDown: (details) => showMenus(details, index), + onDoubleTap: () => showEdit(index), onHover: (hover) { if (isPressed && !selected.contains(index)) { setState(() { @@ -282,7 +285,7 @@ class _RequestMapListState extends State { padding: const EdgeInsets.all(5), child: Row( children: [ - SizedBox(width: 200, child: Text(list[index].name!, style: const TextStyle(fontSize: 13))), + SizedBox(width: 130, child: Text(list[index].name ?? '', style: const TextStyle(fontSize: 13))), SizedBox( width: 40, child: Transform.scale( @@ -294,7 +297,13 @@ class _RequestMapListState extends State { _refreshConfig(); }))), const SizedBox(width: 20), - Expanded(child: Text(list[index].url, style: const TextStyle(fontSize: 13))), + Expanded( + child: + Text(list[index].url, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 13))), + SizedBox( + width: 100, + child: Text(isEN ? list[index].type.name.camelCaseToSpaced() : list[index].type.label, + textAlign: TextAlign.center, style: const TextStyle(fontSize: 13))), ], ))); }); @@ -337,8 +346,8 @@ class _RequestMapListState extends State { height: 35, child: Text(localizations.delete), onTap: () async { - var scriptManager = await ScriptManager.instance; - await scriptManager.removeScript(index); + var manager = await RequestMapManager.instance; + await manager.deleteRule(index); _refreshConfig(); }), ]).then((value) { @@ -374,7 +383,8 @@ class _RequestMapListState extends State { String? path; if (Platform.isMacOS) { path = await DesktopMultiWindow.invokeMethod(0, "saveFile", {"fileName": fileName}); - WindowController.fromWindowId(widget.windowId).show(); + + if (widget.windowId != null) WindowController.fromWindowId(widget.windowId!).show(); } else { path = await FilePicker.platform.saveFile(fileName: fileName); } @@ -388,7 +398,7 @@ class _RequestMapListState extends State { var item = widget.list[idx]; var map = item.toJson(); map.remove("itemPath"); - map['item'] = await manager.getMapItem(item); + map['item'] = (await manager.getMapItem(item))?.toJson(); json.add(map); } @@ -548,7 +558,7 @@ class _RequestMapEditState extends State { var requestMapManager = await RequestMapManager.instance; var index = requestMapManager.rules.indexOf(rule); if (index >= 0) { - requestMapManager.updateRule(rule, item); + await requestMapManager.updateRule(rule, item); } else { await requestMapManager.addRule(rule, item); } @@ -564,10 +574,7 @@ class _RequestMapEditState extends State { void onChangeType(RequestMapType? val) async { if (mapType == val) return; mapType = val!; - setState(() { - // rewriteReplaceKey.currentState?.initItems(ruleType, items); - // rewriteUpdateKey.currentState?.initItems(ruleType, items); - }); + setState(() {}); } Widget mapRule() { diff --git a/lib/ui/desktop/toolbar/setting/request_rewrite.dart b/lib/ui/desktop/toolbar/setting/request_rewrite.dart index 9155544..43da71e 100644 --- a/lib/ui/desktop/toolbar/setting/request_rewrite.dart +++ b/lib/ui/desktop/toolbar/setting/request_rewrite.dart @@ -275,7 +275,7 @@ class _RequestRuleListState extends State { ]))))); } - enableStatus(bool enable) { + void enableStatus(bool enable) { if (selected.isEmpty) return; selected.forEach((key, value) { if (rules[key].enabled == enable) return; @@ -287,7 +287,7 @@ class _RequestRuleListState extends State { setState(() {}); } - showGlobalMenu(Offset offset) { + void showGlobalMenu(Offset offset) { showContextMenu(context, offset, items: [ PopupMenuItem(height: 35, child: Text(localizations.newBuilt), onTap: () => showEdit()), PopupMenuItem(height: 35, child: Text(localizations.export), onTap: () => export(selected.keys.toList())), @@ -302,7 +302,7 @@ class _RequestRuleListState extends State { List rows(List list) { var primaryColor = Theme.of(context).colorScheme.primary; - bool isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh'); + bool isEN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'en'); return List.generate(list.length, (index) { return InkWell( @@ -358,7 +358,7 @@ class _RequestRuleListState extends State { Text(list[index].url, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 13))), SizedBox( width: 100, - child: Text(isCN ? list[index].type.label : list[index].type.name.camelCaseToSpaced(), + child: Text(isEN ? list[index].type.name.camelCaseToSpaced() : list[index].type.label, textAlign: TextAlign.center, style: const TextStyle(fontSize: 13))), ], ))); @@ -366,7 +366,7 @@ class _RequestRuleListState extends State { } //导出 - export(List indexes) async { + Future export(List indexes) async { if (indexes.isEmpty) return; String fileName = 'proxypin-rewrites.config'; @@ -415,7 +415,7 @@ class _RequestRuleListState extends State { }); } - showEdit([int? index]) async { + Future showEdit([int? index]) async { RequestRewriteRule? rule; List? rewriteItems; @@ -437,7 +437,7 @@ class _RequestRuleListState extends State { } //点击菜单 - showMenus(TapDownDetails details, int index) { + void showMenus(TapDownDetails details, int index) { if (selected.length > 1) { showGlobalMenu(details.globalPosition); return; diff --git a/lib/ui/desktop/toolbar/setting/script.dart b/lib/ui/desktop/toolbar/setting/script.dart index e027cab..bdb30e2 100644 --- a/lib/ui/desktop/toolbar/setting/script.dart +++ b/lib/ui/desktop/toolbar/setting/script.dart @@ -155,7 +155,7 @@ class _ScriptWidgetState extends State { } //导入js - import() async { + Future import() async { String? path; if (Platform.isMacOS) { path = await DesktopMultiWindow.invokeMethod(0, "pickFiles", { @@ -464,7 +464,12 @@ class _ScriptListState extends State { @override Widget build(BuildContext context) { return GestureDetector( - onSecondaryTapDown: (details) => showGlobalMenu(details.globalPosition), + onSecondaryTap: () { + if (lastPressPosition == null) { + return; + } + showGlobalMenu(lastPressPosition!); + }, onTapDown: (details) { if (selected.isEmpty) { return; diff --git a/lib/ui/desktop/toolbar/setting/setting.dart b/lib/ui/desktop/toolbar/setting/setting.dart index 3575e42..b312ea0 100644 --- a/lib/ui/desktop/toolbar/setting/setting.dart +++ b/lib/ui/desktop/toolbar/setting/setting.dart @@ -76,7 +76,7 @@ class _SettingState extends State { item(localizations.hosts, onPressed: hosts), item(localizations.requestBlock, onPressed: showRequestBlock), item(localizations.requestRewrite, onPressed: requestRewrite), - item("请求映射", onPressed: requestMapLocal), + item(localizations.requestMap, onPressed: requestMap), item(localizations.script, onPressed: () => MultiWindow.openWindow(localizations.script, 'ScriptWidget', size: const Size(800, 700))), item(localizations.externalProxy, onPressed: setExternalProxy), @@ -115,15 +115,9 @@ class _SettingState extends State { } ///请求本地映射 - void requestMapLocal() async { + void requestMap() async { if (!mounted) return; - // MultiWindow.openWindow(localizations.requestRewrite, 'RequestRewriteWidget', size: const Size(800, 750)); - showDialog( - barrierDismissible: false, - context: context, - builder: (context) { - return RequestMapPage(); - }); + MultiWindow.openWindow(localizations.requestMap, 'RequestMapPage', size: const Size(800, 720)); } ///show域名过滤Dialog @@ -198,7 +192,6 @@ class _ProxyMenuState extends State<_ProxyMenu> { const Divider(thickness: 0.3, height: 8), setSystemProxy(), const Divider(thickness: 0.3, height: 8), - Row(children: [ Expanded( child: Padding( @@ -214,7 +207,6 @@ class _ProxyMenuState extends State<_ProxyMenu> { SizedBox(width: 10) ]), const Divider(thickness: 0.3, height: 8), - Row(children: [ Expanded( child: Padding( diff --git a/test/base64_test.dart b/test/base64_test.dart new file mode 100644 index 0000000..0d10077 --- /dev/null +++ b/test/base64_test.dart @@ -0,0 +1,25 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +void main() { + print(base64Decode("CiRjNjJlOTc0ZC1j")); + print(utf8.decode(base64Decode("CiRjNjJlOTc0ZC1j"))); + // 输入的十六进制字符串 + String hex = "1F 8B 08 00 00 00 00 00 00 FF DD 58 CF"; + // 转换为Base64 + String base64Str = hexToBase64(hex); + print("转换后的Base64: $base64Str"); +} + +String hexToBase64(String hex) { + // 移除十六进制字符串中的空格 + var arr = hex.split(' '); + // 将十六进制字符串转换为字节数组 + List bytes = []; + for (int i = 0; i < arr.length; i ++) { + bytes.add(int.parse(arr[i], radix: 16)); + } + print(bytes); + // 将字节数组编码为 Base64 + return base64Encode(Uint8List.fromList(bytes)); +} \ No newline at end of file