Files
proxypin/lib/network/components/manager/script_manager.dart

432 lines
13 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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/RemoveQueries、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<ScriptItem> list = [];
final ExpiringCache<ScriptItem, String> _scriptMap = ExpiringCache<ScriptItem, String>(Duration(minutes: 15));
static late JavascriptRuntime flutterJs;
static String? deviceId;
static final List<LogHandler> _logHandlers = [];
ScriptManager._();
///单例
static Future<ScriptManager> 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<void> reloadScript() async {
List<ScriptItem> 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<String> 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<File> get _path async {
final path = await homePath();
var file = File('$path${separator}script.json');
if (!await file.exists()) {
await file.create();
}
return file;
}
Future<String?> 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<String?> _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<void> 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<void> 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<void> 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<void> 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<void> flushConfig() async {
await _path.then((value) => value.writeAsString(jsonEncode({'enabled': enabled, 'list': list})));
}
Map<dynamic, dynamic> scriptSession = {};
///脚本上下文
Map<String, dynamic> scriptContext(ScriptItem item) {
return {'scriptName': item.name, 'os': Platform.operatingSystem, 'session': scriptSession, "deviceId": deviceId};
}
///运行脚本
Future<HttpRequest?> 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<HttpResponse?> 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<String, dynamic> json) {
return LogInfo(json['level'], json['output'], time: DateTime.fromMillisecondsSinceEpoch(json['time']));
}
Map<String, dynamic> 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<String> urls;
String? scriptPath;
List<RegExp?>? 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<String> ? urls : <String>[]);
// 匹配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<dynamic, dynamic> json) {
final urlField = json['url'];
List<String> urls;
if (urlField is List) {
urls = urlField.cast<String>();
} else if (urlField is String) {
urls = urlField.contains(',') ? urlField.split(',').map((e) => e.trim()).toList() : [urlField];
} else {
urls = <String>[];
}
return ScriptItem(
json['enabled'],
json['name'],
urls,
scriptPath: json['scriptPath'],
remoteUrl: json['remoteUrl'],
);
}
Map<String, dynamic> 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}';
}
}