/* * Copyright 2023 Hongen Wang All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import 'dart:convert'; import 'dart:io'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter_js/flutter_js.dart'; import 'package:proxypin/network/http/http.dart'; import 'package:proxypin/network/util/cache.dart'; import 'package:proxypin/network/util/logger.dart'; import 'package:proxypin/network/util/random.dart'; import 'package:proxypin/ui/component/device.dart'; import 'package:path_provider/path_provider.dart'; import 'package:http/http.dart' as http; import '../js/script_engine.dart'; /// @author wanghongen /// 2023/10/06 /// js脚本 class ScriptManager { static String template = """ // e.g. Add/Update/Remove:Queries、Headers、Body async function onRequest(context, request) { console.log(request.url); //Update or add Header //request.headers["X-New-Headers"] = "My-Value"; // Update Body use fetch API request,具体文档可网上搜索fetch API //request.body = await fetch('https://www.baidu.com/').then(response => response.text()); return request; } //You can modify the Response Data here before it goes to the client async function onResponse(context, request, response) { // 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 ExpiringCache _scriptMap = ExpiringCache(Duration(minutes: 15)); static late JavascriptRuntime flutterJs; static String? deviceId; static final List _logHandlers = []; ScriptManager._(); ///单例 static Future get instance async { if (_instance == null) { _instance = ScriptManager._(); await _instance?.reloadScript(); flutterJs = await JavaScriptEngine.getJavaScript(consoleLog: consoleLog); deviceId = await DeviceUtils.deviceId(); logger.d('init script manager $deviceId'); } return _instance!; } static void registerConsoleLog(int fromWindowId) { LogHandler logHandler = LogHandler( channelId: fromWindowId, handle: (logInfo) { DesktopMultiWindow.invokeMethod(fromWindowId, "consoleLog", logInfo.toJson()).onError((e, t) { logger.e("consoleLog error: $e"); removeLogHandler(fromWindowId); }); }); registerLogHandler(logHandler); } static void registerLogHandler(LogHandler logHandler) { if (_logHandlers.any((it) => it.channelId == logHandler.channelId)) { _logHandlers.removeWhere((it) => it.channelId == logHandler.channelId); } _logHandlers.add(logHandler); } static void removeLogHandler(int channelId) { _logHandlers.removeWhere((element) => channelId == element.channelId); } static dynamic consoleLog(dynamic args) async { if (_logHandlers.isEmpty) { return; } var level = args.removeAt(0); String output = args.join(' '); if (level == 'info') level = 'warn'; LogInfo logInfo = LogInfo(level, output); for (int i = 0; i < _logHandlers.length; i++) { _logHandlers[i].handle.call(logInfo); } } ///重新加载脚本 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 { // Local script (existing behavior) if (_scriptMap.containsKey(item)) { return _scriptMap[item]!; } // Remote script if (item.remoteUrl != null && item.remoteUrl!.trim().isNotEmpty) { var script = await _fetchRemoteScript(item); if (script != null) { _scriptMap[item] = script; } return script; } final home = await homePath(); var script = await File(home + item.scriptPath!).readAsString(); _scriptMap[item] = script; return script; } Future _fetchRemoteScript(ScriptItem item) async { final url = item.remoteUrl!.trim(); if (!_isHttpUrl(url)) { return null; } final resp = await http.get(Uri.parse(url)); final bytes = resp.bodyBytes; final content = utf8.decode(bytes); _scriptMap[item] = content; return content; } bool _isHttpUrl(String url) { final uri = Uri.tryParse(url); if (uri == null) return false; return uri.scheme == 'http' || uri.scheme == 'https'; } ///添加脚本 Future addScript(ScriptItem item, String? script) async { // Remote script: script is treated as initial cache (optional) if (item.remoteUrl != null && item.remoteUrl!.trim().isNotEmpty) { list.add(item); return; } script ??= template; final path = await homePath(); String scriptPath = "${separator}scripts$separator${RandomUtil.randomString(16)}.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 { // Remote scripts: update cache file (treat as local override of cache) if (item.remoteUrl != null && item.remoteUrl!.trim().isNotEmpty) { _scriptMap[item] = script; return; } 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); _scriptMap.remove(item); if (item.scriptPath != null) { final home = await homePath(); File(home + item.scriptPath!).delete(); } } Future clean() async { _scriptMap.clear(); while (list.isNotEmpty) { var item = list.removeLast(); if (item.scriptPath != null) { final home = await homePath(); File(home + item.scriptPath!).delete(); } } await flushConfig(); } ///刷新配置 Future flushConfig() async { await _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, "deviceId": deviceId}; } ///运行脚本 Future runScript(HttpRequest request) async { if (!enabled) { return request; } var url = request.domainPath; for (var item in list) { if (item.enabled && item.match(url)) { var context = jsonEncode(scriptContext(item)); var jsRequest = jsonEncode(await JavaScriptEngine.convertJsRequest(request)); String? script = await getScript(item); if (script == null) { continue; } var jsResult = await flutterJs.evaluateAsync( """var request = $jsRequest, context = $context; request['scriptContext'] = context; $script\n onRequest(context, request)"""); var result = await JavaScriptEngine.jsResultResolve(flutterJs, jsResult); if (result == null) { return null; } request.attributes['scriptContext'] = result['scriptContext']; scriptSession = result['scriptContext']['session'] ?? {}; request = JavaScriptEngine.convertHttpRequest(request, result); } } return request; } ///运行脚本 Future runResponseScript(HttpResponse response) async { if (!enabled || response.request == null) { return response; } var request = response.request!; var url = request.domainPath; for (var item in list) { if (item.enabled && item.match(url)) { var context = jsonEncode(request.attributes['scriptContext'] ?? scriptContext(item)); var jsRequest = jsonEncode(await JavaScriptEngine.convertJsRequest(request)); var jsResponse = jsonEncode(await JavaScriptEngine.convertJsResponse(response)); String? script = await getScript(item); if (script == null) { continue; } 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 JavaScriptEngine.jsResultResolve(flutterJs, jsResult); if (result == null) { return null; } scriptSession = result['scriptContext']['session'] ?? {}; response = JavaScriptEngine.convertHttpResponse(response, result); } } return response; } } class LogHandler { final int channelId; final Function(LogInfo logInfo) handle; LogHandler({required this.channelId, required this.handle}); } class LogInfo { final DateTime time; final String level; final String output; LogInfo(this.level, this.output, {DateTime? time}) : time = time ?? DateTime.now(); factory LogInfo.fromJson(Map json) { return LogInfo(json['level'], json['output'], time: DateTime.fromMillisecondsSinceEpoch(json['time'])); } Map toJson() { return {'time': time.millisecondsSinceEpoch, 'level': level, 'output': output}; } @override String toString() { return '{time: $time, level: $level, output: $output}'; } } class ScriptItem { bool enabled = true; String? name; List urls; String? scriptPath; List? urlRegs; String? remoteUrl; ScriptItem(this.enabled, this.name, dynamic urls, {this.scriptPath, this.remoteUrl}) : urls = urls is String ? (urls.contains(',') ? urls.split(',').map((e) => e.trim()).toList() : [urls]) : (urls is List ? urls : []); // 匹配url,任意一个规则匹配即可 bool match(String url) { urlRegs ??= urls.map((u) => RegExp(u.replaceAll("*", ".*"))).toList(); for (final reg in urlRegs!) { if (reg!.hasMatch(url)) return true; } return false; } factory ScriptItem.fromJson(Map json) { final urlField = json['url']; List urls; if (urlField is List) { urls = urlField.cast(); } else if (urlField is String) { urls = urlField.contains(',') ? urlField.split(',').map((e) => e.trim()).toList() : [urlField]; } else { urls = []; } return ScriptItem( json['enabled'], json['name'], urls, scriptPath: json['scriptPath'], remoteUrl: json['remoteUrl'], ); } Map toJson() { return { 'enabled': enabled, 'name': name, 'url': urls.length == 1 ? urls[0] : urls, 'scriptPath': scriptPath, if (remoteUrl != null) 'remoteUrl': remoteUrl, }; } @override String toString() { return 'ScriptItem{enabled: $enabled, name: $name, url: $urls, scriptPath: $scriptPath, remoteUrl: $remoteUrl}'; } }