import 'dart:convert'; import 'dart:io'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter_js/flutter_js.dart'; import 'package:network_proxy/network/http/http.dart'; import 'package:network_proxy/network/http/http_headers.dart'; import 'package:network_proxy/network/util/logger.dart'; import 'package:path_provider/path_provider.dart'; /// @author wanghongen /// 2023/10/06 /// js脚本 class ScriptManager { static String template = """ // 在请求到达服务器之前,调用此函数,您可以在此处修改请求数据 // 例如Add/Update/Remove:Queries、Headers、Body async function onRequest(context, request) { console.log(request.url); //URL参数 //request.queries["name"] = "value"; //Update or add Header //request.headers["X-New-Headers"] = "My-Value"; // Update Body 使用fetch API请求接口,具体文档可网上搜索fetch API //request.body = await fetch('https://www.baidu.com/').then(response => response.text()); return request; } // 在将响应数据发送到客户端之前,调用此函数,您可以在此处修改响应数据 async function onResponse(context, request, response) { //Update or add Header // response.headers["Name"] = "Value"; // response.statusCode = 200; //var body = JSON.parse(response.body); //body['key'] = "value"; //response.body = JSON.stringify(body); return response; } """; static String separator = Platform.pathSeparator; static ScriptManager? _instance; bool enabled = true; List list = []; final Map _scriptMap = {}; static JavascriptRuntime flutterJs = getJavascriptRuntime(); static final List _windowIds = []; ScriptManager._(); ///单例 static Future get instance async { if (_instance == null) { _instance = ScriptManager._(); await _instance?.reloadScript(); // register channel callback final channelCallbacks = JavascriptRuntime.channelFunctionsRegistered[flutterJs.getEngineInstanceId()]; channelCallbacks!["ConsoleLog"] = _instance!.consoleLog; logger.d('init script manager'); } return _instance!; } static void registerConsoleLog(int fromWindowId) { if (!_windowIds.contains(fromWindowId)) _windowIds.add(fromWindowId); } dynamic consoleLog(dynamic args) async { var level = args.removeAt(0); String output = args.join(' '); if (level == 'info') level = 'warn'; for (int i = 0; i < _windowIds.length; i++) { var winId = _windowIds.elementAt(i); DesktopMultiWindow.invokeMethod(winId, "consoleLog", {"level": level, "output": output}).onError((e, t) { logger.e("consoleLog error: $e"); _windowIds.remove(winId); }); } } ///重新加载脚本 Future reloadScript() async { List scripts = []; var file = await _path; logger.d("reloadScript ${file.path}"); if (await file.exists()) { var content = await file.readAsString(); if (content.isEmpty) { return; } var config = jsonDecode(content); enabled = config['enabled'] == true; for (var entry in config['list']) { scripts.add(ScriptItem.fromJson(entry)); } } list = scripts; _scriptMap.clear(); } static String? _homePath; static Future homePath() async { if (_homePath != null) { return _homePath!; } if (Platform.isMacOS) { _homePath = await DesktopMultiWindow.invokeMethod(0, "getApplicationSupportDirectory"); } else { _homePath = await getApplicationSupportDirectory().then((it) => it.path); } return _homePath!; } static Future get _path async { final path = await homePath(); var file = File('$path${separator}script.json'); if (!await file.exists()) { await file.create(); } return file; } Future getScript(ScriptItem item) async { if (_scriptMap.containsKey(item)) { return _scriptMap[item]!; } final home = await homePath(); var script = await File(home + item.scriptPath!).readAsString(); _scriptMap[item] = script; return script; } ///添加脚本 Future addScript(ScriptItem item, String script) async { final path = await homePath(); String scriptPath = "${separator}scripts$separator${DateTime.now().millisecondsSinceEpoch}.js"; var file = File(path + scriptPath); await file.create(recursive: true); file.writeAsString(script); item.scriptPath = scriptPath; list.add(item); _scriptMap[item] = script; } ///更新脚本 Future updateScript(ScriptItem item, String script) async { if (_scriptMap[item] == script) { return; } final home = await homePath(); File(home + item.scriptPath!).writeAsString(script); _scriptMap[item] = script; } ///删除脚本 Future removeScript(int index) async { var item = list.removeAt(index); final home = await homePath(); File(home + item.scriptPath!).delete(); } Future clean() async { while (list.isNotEmpty) { var item = list.removeLast(); final home = await homePath(); File(home + item.scriptPath!).delete(); } await flushConfig(); } ///刷新配置 Future flushConfig() async { _path.then((value) => value.writeAsString(jsonEncode({'enabled': enabled, 'list': list}))); } Map scriptSession = {}; ///脚本上下文 Map scriptContext(ScriptItem item) { return { 'scriptName': item.name, 'os': Platform.operatingSystem, 'session': scriptSession, }; } ///运行脚本 Future runScript(HttpRequest request) async { if (!enabled) { return request; } var url = '${request.remoteDomain()}${request.path()}'; for (var item in list) { if (item.enabled && item.match(url)) { var context = jsonEncode(scriptContext(item)); var jsRequest = jsonEncode(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); if (result == null) { return null; } request.attributes['scriptContext'] = result['scriptContext']; scriptSession = result['scriptContext']['session'] ?? {}; return convertHttpRequest(request, result); } } return request; } ///运行脚本 Future runResponseScript(HttpResponse response) async { if (!enabled || response.request == null) { return response; } var request = response.request!; var url = '${request.remoteDomain()}${request.path()}'; for (var item in list) { if (item.enabled && item.match(url)) { var context = jsonEncode(request.attributes['scriptContext'] ?? scriptContext(item)); var jsRequest = jsonEncode(convertJsRequest(request)); var jsResponse = jsonEncode(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); if (result == null) { return null; } scriptSession = result['scriptContext']['session'] ?? {}; return convertHttpResponse(response, result); } } return response; } /// js结果转换 static Future jsResultResolve(JsEvalResult jsResult) async { if (jsResult.isPromise || jsResult.rawResult is Future) { jsResult = await flutterJs.handlePromise(jsResult); } var result = jsResult.rawResult; if (Platform.isMacOS || Platform.isIOS) { result = flutterJs.convertValue(jsResult); } if (result is String) { result = jsonDecode(result); } if (jsResult.isError) { throw SignalException(jsResult.stringResult); } return result; } //转换js request Map convertJsRequest(HttpRequest request) { 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': request.bodyAsString }; } //转换js response Map convertJsResponse(HttpResponse response) { return {'headers': response.headers.toMap(), 'statusCode': response.status.code, 'body': response.bodyAsString}; } //http request HttpRequest convertHttpRequest(HttpRequest request, Map map) { request.headers.clear(); request.method = HttpMethod.values.firstWhere((element) => element.name == map['method']); String query = ''; map['queries']?.forEach((key, value) { query += '$key=$value&'; }); request.uri = Uri.parse('${request.remoteDomain()}${map['path']}?$query').toString(); map['headers'].forEach((key, value) { request.headers.add(key, value); }); request.body = map['body'] == null ? null : 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) { response.headers.add(key, value); }); response.headers.remove(HttpHeaders.CONTENT_ENCODING); response.body = map['body'] == null ? null : utf8.encode(map['body'].toString()); return response; } } class ScriptItem { bool enabled = true; String? name; String url; String? scriptPath; RegExp? urlReg; ScriptItem(this.enabled, this.name, this.url, {this.scriptPath}); //匹配url bool match(String url) { urlReg ??= RegExp(this.url.replaceAll("*", ".*")); return urlReg!.hasMatch(url); } factory ScriptItem.fromJson(Map json) { return ScriptItem(json['enabled'], json['name'], json['url'], scriptPath: json['scriptPath']); } Map toJson() { return {'enabled': enabled, 'name': name, 'url': url, 'scriptPath': scriptPath}; } @override String toString() { return 'ScriptItem{enabled: $enabled, name: $name, url: $url, scriptPath: $scriptPath}'; } }