diff --git a/lib/network/bin/server.dart b/lib/network/bin/server.dart index b17d2dc..8776e5b 100644 --- a/lib/network/bin/server.dart +++ b/lib/network/bin/server.dart @@ -19,8 +19,8 @@ import 'dart:io'; import 'package:network_proxy/network/bin/configuration.dart'; import 'package:network_proxy/network/channel.dart'; +import 'package:network_proxy/network/components/request_rewrite_component.dart'; import 'package:network_proxy/network/http/http.dart'; -import 'package:network_proxy/network/components/request_rewrite_manager.dart'; import 'package:network_proxy/network/http/websocket.dart'; import 'package:network_proxy/network/util/crts.dart'; import 'package:network_proxy/utils/platform.dart'; @@ -73,11 +73,14 @@ class ProxyServer { /// 启动代理服务 Future start() async { Server server = Server(configuration, listener: CombinedEventListener(listeners)); - var requestRewrites = await RequestRewrites.instance; + var requestRewriteComponent = RequestRewriteComponent.instance; server.initChannel((channel) { - channel.pipeline.handle(HttpRequestCodec(), HttpResponseCodec(), - HttpProxyChannelHandler(listener: CombinedEventListener(listeners), requestRewrites: requestRewrites)); + channel.pipeline.handle( + HttpRequestCodec(), + HttpResponseCodec(), + HttpProxyChannelHandler( + listener: CombinedEventListener(listeners), requestRewriteComponent: requestRewriteComponent)); }); return server.bind(port).then((serverSocket) { diff --git a/lib/network/components/interceptor.dart b/lib/network/components/interceptor.dart new file mode 100644 index 0000000..b0cebfc --- /dev/null +++ b/lib/network/components/interceptor.dart @@ -0,0 +1,12 @@ +import 'package:network_proxy/network/http/http.dart'; + +/// A Interceptor that can intercept and modify the request and response. +/// @author Hongen Wang +abstract class Interceptor { + + /// Called before the request is sent to the server. + HttpRequest? onRequest(HttpRequest request); + + /// Called after the response is received from the server. + HttpResponse? onResponse(HttpRequest request, HttpResponse response); +} diff --git a/lib/network/components/request_rewrite_component.dart b/lib/network/components/request_rewrite_component.dart new file mode 100644 index 0000000..0bc01d4 --- /dev/null +++ b/lib/network/components/request_rewrite_component.dart @@ -0,0 +1,256 @@ +/* + * Copyright 2024 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:collection'; +import 'dart:convert'; + +import 'package:network_proxy/network/components/rewrite/request_rewrite_manager.dart'; +import 'package:network_proxy/network/http/constants.dart'; +import 'package:network_proxy/network/http/http.dart'; +import 'package:network_proxy/network/http/http_headers.dart'; +import 'package:network_proxy/network/util/file_read.dart'; +import 'package:network_proxy/utils/lang.dart'; + +import 'rewrite/rewrite_rule.dart'; + +/// RequestRewriteComponent is a component that can rewrite the request before sending it to the server. +/// @author Hongen Wang +class RequestRewriteComponent { + static RequestRewriteComponent instance = RequestRewriteComponent._(); + + final requestRewriteManager = RequestRewriteManager.instance; + + RequestRewriteComponent._(); + + ///获取重定向 + Future getRedirectRule(String? url) async { + var manager = await requestRewriteManager; + var rewriteRule = manager.getRewriteRule(url, [RuleType.redirect]); + if (rewriteRule == null) { + return null; + } + + var rewriteItems = await manager.getRewriteItems(rewriteRule); + var redirectUrl = rewriteItems?.firstWhereOrNull((element) => element.enabled)?.redirectUrl; + if (rewriteRule.url.contains("*") && redirectUrl?.contains("*") == true) { + String ruleUrl = rewriteRule.url.replaceAll("*", ""); + redirectUrl = redirectUrl?.replaceAll("*", url!.replaceAll(ruleUrl, "")); + } + return redirectUrl; + } + + /// 重写请求 + Future requestRewrite(HttpRequest request) async { + var url = request.requestUrl; + var manager = await RequestRewriteManager.instance; + var rewriteRule = manager.getRewriteRule(url, [RuleType.requestReplace, RuleType.requestUpdate]); + + if (rewriteRule?.type == RuleType.requestReplace) { + var rewriteItems = await manager.getRewriteItems(rewriteRule!); + for (var item in rewriteItems!) { + if (item.enabled) { + await _replaceRequest(request, item); + } + } + } + + if (rewriteRule?.type == RuleType.requestUpdate) { + var rewriteItems = await manager.getRewriteItems(rewriteRule!); + rewriteItems?.where((item) => item.enabled).forEach((item) => _updateRequest(request, item)); + } + } + + /// 重写响应 + Future responseRewrite(String? url, HttpResponse response) async { + var manager = await RequestRewriteManager.instance; + + var rewriteRule = manager.getRewriteRule(url, [RuleType.responseReplace, RuleType.responseUpdate]); + if (rewriteRule == null) { + return; + } + + if (rewriteRule.type == RuleType.responseReplace) { + var rewriteItems = await manager.getRewriteItems(rewriteRule); + for (var item in rewriteItems!) { + if (item.enabled) { + await _replaceResponse(response, item); + } + } + } + + if (rewriteRule.type == RuleType.responseUpdate) { + var rewriteItems = await manager.getRewriteItems(rewriteRule); + rewriteItems?.where((item) => item.enabled).forEach((item) => _updateMessage(response, item)); + } + } + + _updateRequest(HttpRequest request, RewriteItem item) { + var paramTypes = [RewriteType.addQueryParam, RewriteType.removeQueryParam, RewriteType.updateQueryParam]; + + if (paramTypes.contains(item.type)) { + var requestUri = request.requestUri; + Map queryParameters = LinkedHashMap.from(requestUri!.queryParameters); + + switch (item.type) { + case RewriteType.addQueryParam: + queryParameters[item.key!] = item.value; + break; + case RewriteType.removeQueryParam: + if (item.value?.isNotEmpty == true) { + var val = queryParameters[item.key!]; + if (val != null && item.value != null && RegExp(item.value!).hasMatch(val)) { + return; + } + } + queryParameters.remove(item.value); + break; + case RewriteType.updateQueryParam: + var itemKey = item.key; + if (itemKey == null || itemKey.trim().isEmpty) return; + + var entries = queryParameters.entries; + var regExp = RegExp(item.key!); + + for (var entry in entries) { + var line = "${entry.key}=${entry.value}"; + + if (regExp.hasMatch(line)) { + line = line.replaceAll(regExp, item.value ?? ''); + var pair = line.splitFirst(HttpConstants.colon); + if (pair.first != entry.key) queryParameters.remove(entry.key); + + queryParameters[pair.first] = pair.length > 1 ? pair.last : ''; + } + } + break; + default: + break; + } + requestUri = requestUri.replace(queryParameters: queryParameters); + + if (requestUri.isScheme('https')) { + request.uri = requestUri.path + (requestUri.hasQuery ? "?${requestUri.query}" : ""); + } else { + request.uri = requestUri.toString(); + } + return; + } + + _updateMessage(request, item); + } + + //修改消息 + _updateMessage(HttpMessage message, RewriteItem item) { + if (item.type == RewriteType.updateBody && message.body != null) { + String body = message.bodyAsString.replaceAllMapped(RegExp(item.key!), (match) { + if (match.groupCount > 0 && item.value?.contains("\$1") == true) { + return item.value!.replaceAll("\$1", match.group(1)!); + } + return item.value ?? ''; + }); + + message.body = message.charset == 'utf-8' || message.charset == 'utf8' ? utf8.encode(body) : body.codeUnits; + + message.headers.remove(HttpHeaders.CONTENT_ENCODING); + message.headers.contentLength = message.body!.length; + return; + } + + if (item.type == RewriteType.addHeader) { + message.headers.set(item.key!, item.value ?? ''); + return; + } + + if (item.type == RewriteType.removeHeader) { + if (item.value?.isNotEmpty == true) { + var val = message.headers.get(item.key!); + if (val != null && item.value != null && RegExp(item.value!).hasMatch(val)) { + return; + } + } + message.headers.remove(item.key!); + return; + } + + if (item.type == RewriteType.updateHeader) { + if (item.key == null || item.key?.trim().isEmpty == true) return; + + var entries = message.headers.entries; + var regExp = RegExp(item.key!, caseSensitive: false); + + for (var entry in entries) { + var line = "${entry.key}: ${entry.value}"; + + if (regExp.hasMatch(line)) { + line = line.replaceAll(regExp, item.value ?? ''); + var pair = line.splitFirst(HttpConstants.colon); + if (pair.first != entry.key) message.headers.remove(entry.key); + + message.headers.set(pair.first, pair.length > 1 ? pair.last : ''); + } + } + return; + } + } + + //替换请求 + Future _replaceRequest(HttpRequest request, RewriteItem item) async { + if (item.type == RewriteType.replaceRequestLine) { + request.method = item.method ?? request.method; + Uri uri = Uri.parse(request.requestUrl).replace(path: item.path, query: item.queryParam); + if (uri.isScheme('https')) { + request.uri = uri.path + (uri.hasQuery ? "?${uri.query}" : ""); + } else { + request.uri = uri.toString(); + } + return; + } + await _replaceHttpMessage(request, item); + } + + //替换相应 + Future _replaceResponse(HttpResponse response, RewriteItem item) async { + if (item.type == RewriteType.replaceResponseStatus && item.statusCode != null) { + response.status = HttpStatus.valueOf(item.statusCode!); + return; + } + await _replaceHttpMessage(response, item); + } + + Future _replaceHttpMessage(HttpMessage message, RewriteItem item) async { + if (item.type == RewriteType.replaceResponseHeader && item.headers != null) { + item.headers?.forEach((key, value) => message.headers.set(key, value)); + return; + } + + if (item.type == RewriteType.replaceResponseBody || item.type == RewriteType.replaceRequestBody) { + if (item.bodyType == ReplaceBodyType.file.name) { + if (item.bodyFile == null) return; + + message.body = await FileRead.readFile(item.bodyFile!); + message.headers.contentLength = message.body!.length; + return; + } + + if (item.body != null) { + message.body = + message.charset == 'utf-8' || message.charset == 'utf8' ? utf8.encode(item.body!) : item.body?.codeUnits; + message.headers.contentLength = message.body!.length; + } + return; + } + } +} diff --git a/lib/network/components/request_rewrite_manager.dart b/lib/network/components/request_rewrite_manager.dart deleted file mode 100644 index 2d2f5cb..0000000 --- a/lib/network/components/request_rewrite_manager.dart +++ /dev/null @@ -1,710 +0,0 @@ -/* - * 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:collection'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:network_proxy/network/http/http.dart'; -import 'package:network_proxy/network/http/http_headers.dart'; -import 'package:network_proxy/network/util/file_read.dart'; -import 'package:network_proxy/network/util/logger.dart'; -import 'package:network_proxy/network/util/random.dart'; -import 'package:network_proxy/utils/lang.dart'; - -/// @author wanghongen -/// 2023/7/26 -/// 请求重写 -class RequestRewrites { - static String separator = Platform.pathSeparator; - - //重写规则 - final Map> rewriteItemsCache = {}; - - //单例 - static RequestRewrites? _instance; - - RequestRewrites._(); - - static Future get instance async { - if (_instance == null) { - var config = await _loadRequestRewriteConfig(); - _instance = RequestRewrites._(); - await _instance!.reload(config); - } - return _instance!; - } - - bool enabled = true; - List rules = []; - - //重新加载配置 - Future reload(Map? map) async { - rewriteItemsCache.clear(); - if (map == null) { - return; - } - - enabled = map['enabled'] == true; - List list = map['rules'] ?? []; - rules.clear(); - bool flush = false; - for (var element in list) { - try { - bool oldVersion = false; - // body("重写消息体"), 兼容旧版本 - if (element['requestBody']?.isNotEmpty == true || element['queryParam']?.isNotEmpty == true) { - element['type'] = RuleType.requestReplace.name; - - List items = []; - if (element['requestBody']?.isNotEmpty == true) { - RewriteItem item = RewriteItem(RewriteType.replaceRequestBody, true); - item.body = element['requestBody']; - items.add(item); - } - if (element['queryParam']?.isNotEmpty == true) { - RewriteItem item = RewriteItem(RewriteType.replaceRequestLine, true); - item.queryParam = element['queryParam']; - items.add(item); - } - var rule = RequestRewriteRule.formJson(element); - await addRule(rule, items); - oldVersion = true; - } - - if (element['responseBody']?.isNotEmpty == true) { - element['type'] = RuleType.responseReplace.name; - RewriteItem item = RewriteItem(RewriteType.replaceResponseBody, true); - item.body = element['responseBody']; - var rule = RequestRewriteRule.formJson(element); - await addRule(rule, [item]); - - oldVersion = true; - continue; - } - - if (element['redirectUrl']?.isNotEmpty == true) { - RewriteItem item = RewriteItem(RewriteType.redirect, true); - item.redirectUrl = element['redirectUrl']; - var rule = RequestRewriteRule.formJson(element); - await addRule(rule, [item]); - oldVersion = true; - continue; - } - - if (oldVersion) { - flush = true; - continue; - } - rules.add(RequestRewriteRule.formJson(element)); - } catch (e) { - logger.e('加载请求重写配置失败 $element', error: e); - } - } - - if (flush) { - await flushRequestRewriteConfig(); - } - } - - ///重新加载请求重写 - Future reloadRequestRewrite() async { - var config = await _loadRequestRewriteConfig(); - reload(config); - } - - ///同步配置 - Future syncConfig(Map? config) async { - if (config == null) { - return; - } - - rewriteItemsCache.clear(); - enabled = config['enabled'] == true; - List list = config['rules'] ?? []; - rules.clear(); - for (var element in list) { - try { - var rule = RequestRewriteRule.formJson(element); - List list = element['items'] as List; - List items = list.map((e) => RewriteItem.fromJson(e)).toList(); - await addRule(rule, items); - } catch (e) { - logger.e('加载请求重写配置失败 $element', error: e); - } - } - flushRequestRewriteConfig(); - } - - /// 加载请求重写配置文件 - static Future?> _loadRequestRewriteConfig() async { - var home = await FileRead.homeDir(); - var file = File('${home.path}${Platform.pathSeparator}request_rewrite.json'); - var exits = await file.exists(); - if (!exits) { - return null; - } - - Map config = jsonDecode(await file.readAsString()); - logger.i('加载请求重写配置文件 [$file]'); - return config; - } - - /// 保存请求重写配置文件 - Future flushRequestRewriteConfig() async { - var home = await FileRead.homeDir(); - var file = File('${home.path}${Platform.pathSeparator}request_rewrite.json'); - bool exists = await file.exists(); - if (!exists) { - await file.create(recursive: true); - } - var json = jsonEncode(toJson()); - logger.i('刷新请求重写配置文件 ${file.path}'); - await file.writeAsString(json); - } - - ///添加规则 - Future addRule(RequestRewriteRule rule, List items) async { - final home = await FileRead.homeDir(); - - String rewritePath = "${separator}rewrite$separator${RandomUtil.randomString(16)}.json"; - var file = File(home.path + rewritePath); - await file.create(recursive: true); - file.writeAsString(jsonEncode(items.map((e) => e.toJson()).toList())); - rule.rewritePath = rewritePath; - - rules.add(rule); - rewriteItemsCache[rule] = items; - } - - ///更新规则 - Future updateRule(int index, RequestRewriteRule rule, List? items) async { - rewriteItemsCache.remove(rules[index]); - final home = await FileRead.homeDir(); - rule._updatePathReg(); - rules[index] = rule; - - if (items == null) { - return; - } - bool isExist = rule.rewritePath != null; - if (rule.rewritePath == null) { - String rewritePath = "${separator}rewrite$separator${RandomUtil.randomString(16)}.json"; - rule.rewritePath = rewritePath; - } - - File file = File(home.path + rule.rewritePath!); - if (!isExist) { - await file.create(recursive: true); - } - - await file.writeAsString(jsonEncode(items.map((e) => e.toJson()).toList())); - rewriteItemsCache[rule] = items; - } - - removeIndex(List indexes) async { - for (var i in indexes) { - var rule = rules.removeAt(i); - rewriteItemsCache.remove(rule); //删除缓存 - if (rule.rewritePath != null) { - File home = await FileRead.homeDir(); - try { - await File(home.path + rule.rewritePath!).delete(); - } catch (e) { - logger.e('删除请求重写配置文件失败 ${home.path + rule.rewritePath!}', error: e); - } - rule.rewritePath = null; - } - } - } - - ///获取重定向 - Future getRedirectRule(String? url) async { - var rewriteRule = getRewriteRule(url, [RuleType.redirect]); - if (rewriteRule == null) { - return null; - } - - var rewriteItems = await getRewriteItems(rewriteRule); - var redirectUrl = rewriteItems.firstWhereOrNull((element) => element.enabled)?.redirectUrl; - if (rewriteRule.url.contains("*") && redirectUrl?.contains("*") == true) { - String ruleUrl = rewriteRule.url.replaceAll("*", ""); - redirectUrl = redirectUrl?.replaceAll("*", url!.replaceAll(ruleUrl, "")); - } - return redirectUrl; - } - - RequestRewriteRule? getRewriteRule(String? url, List types) { - if (url == null || !enabled) { - return null; - } - for (var rule in rules) { - if (rule.match(url) && types.contains(rule.type)) { - return rule; - } - } - return null; - } - - /// 获取重写规则 - Future> getRewriteItems(RequestRewriteRule rule) async { - if (rewriteItemsCache.containsKey(rule)) { - return rewriteItemsCache[rule]!; - } - if (rule.rewritePath == null) { - return []; - } - - final home = await FileRead.homeDir(); - List items = []; - try { - var json = await File(home.path + rule.rewritePath!).readAsString(); - List? list = jsonDecode(json); - list?.forEach((element) => items.add(RewriteItem.fromJson(element))); - rewriteItemsCache[rule] = items; - } catch (e) { - logger.e('加载请求重写配置文件失败 ${home.path + rule.rewritePath!}', error: e); - } - return items; - } - - /// 查找重写规则 - Future requestRewrite(HttpRequest request) async { - var url = request.requestUrl; - var rewriteRule = getRewriteRule(url, [RuleType.requestReplace, RuleType.requestUpdate]); - - if (rewriteRule?.type == RuleType.requestReplace) { - var rewriteItems = await getRewriteItems(rewriteRule!); - for (var item in rewriteItems) { - if (item.enabled) { - await _replaceRequest(request, item); - } - } - return; - } - - if (rewriteRule?.type == RuleType.requestUpdate) { - var rewriteItems = await getRewriteItems(rewriteRule!); - rewriteItems.where((item) => item.enabled).forEach((item) => _updateRequest(request, item)); - return; - } - } - - _updateRequest(HttpRequest request, RewriteItem item) { - var paramTypes = [RewriteType.addQueryParam, RewriteType.removeQueryParam, RewriteType.updateQueryParam]; - - if (paramTypes.contains(item.type)) { - var requestUri = request.requestUri; - Map queryParameters = LinkedHashMap.from(requestUri!.queryParameters); - - switch (item.type) { - case RewriteType.addQueryParam: - queryParameters[item.key!] = item.value; - break; - case RewriteType.removeQueryParam: - if (item.value?.isNotEmpty == true) { - var val = queryParameters[item.key!]; - if (val != null && item.value != null && RegExp(item.value!).hasMatch(val)) { - return; - } - } - queryParameters.remove(item.value); - break; - case RewriteType.updateQueryParam: - var itemKey = item.key; - var itemValue = item.value; - if (itemKey == null || itemValue == null) { - break; - } - - var itemKeySplitIdx = itemKey.indexOf('='); //key=value - itemKeySplitIdx = itemKeySplitIdx == -1 ? itemKey.length : itemKeySplitIdx; - var itemKeyK = itemKey.substring(0, itemKeySplitIdx); - var itemKeyV = itemKeySplitIdx >= itemKey.length ? null : itemKey.substring(itemKeySplitIdx + 1); - - var val = queryParameters[itemKeyK]; - //not match - if (val == null) { - return; - } - - if (itemKeyV == null || RegExp(itemKeyV).hasMatch(val)) { - var itemValueSplitIdx = itemValue.indexOf('='); - itemValueSplitIdx = itemValueSplitIdx == -1 ? itemValue.length : itemValueSplitIdx; - var itemValueK = itemValue.substring(0, itemValueSplitIdx); - var itemValueV = itemValueSplitIdx >= itemValue.length ? '' : itemValue.substring(itemValueSplitIdx + 1); - - queryParameters.remove(itemKeyK); - queryParameters[itemValueK] = itemValueV; - } - break; - default: - break; - } - requestUri = requestUri.replace(queryParameters: queryParameters); - - if (requestUri.isScheme('https')) { - request.uri = requestUri.path + (requestUri.hasQuery ? "?${requestUri.query}" : ""); - } else { - request.uri = requestUri.toString(); - } - return; - } - - _updateMessage(request, item); - } - - //替换请求 - Future _replaceRequest(HttpRequest request, RewriteItem item) async { - if (item.type == RewriteType.replaceRequestLine) { - request.method = item.method ?? request.method; - Uri uri = Uri.parse(request.requestUrl).replace(path: item.path, query: item.queryParam); - if (uri.isScheme('https')) { - request.uri = uri.path + (uri.hasQuery ? "?${uri.query}" : ""); - } else { - request.uri = uri.toString(); - } - return; - } - await _replaceHttpMessage(request, item); - } - - /// 查找重写规则 - Future responseRewrite(String? url, HttpResponse response) async { - var rewriteRule = getRewriteRule(url, [RuleType.responseReplace, RuleType.responseUpdate]); - if (rewriteRule == null) { - return; - } - - if (rewriteRule.type == RuleType.responseReplace) { - var rewriteItems = await getRewriteItems(rewriteRule); - for (var item in rewriteItems) { - if (item.enabled) { - await _replaceResponse(response, item); - } - } - // logger.d('rewrite response $response'); - return; - } - - if (rewriteRule.type == RuleType.responseUpdate) { - var rewriteItems = await getRewriteItems(rewriteRule); - rewriteItems.where((item) => item.enabled).forEach((item) => _updateMessage(response, item)); - } - } - - //修改消息 - _updateMessage(HttpMessage message, RewriteItem item) { - if (item.type == RewriteType.updateBody && message.body != null) { - String body = message.bodyAsString.replaceAllMapped(RegExp(item.key!), (match) { - if (match.groupCount > 0 && item.value?.contains("\$1") == true) { - return item.value!.replaceAll("\$1", match.group(1)!); - } - return item.value ?? ''; - }); - message.body = message.charset == 'utf-8' || message.charset == 'utf8' ? utf8.encode(body) : body.codeUnits; - - message.headers.remove(HttpHeaders.CONTENT_ENCODING); - message.headers.contentLength = message.body!.length; - return; - } - - if (item.type == RewriteType.addHeader) { - message.headers.set(item.key!, item.value ?? ''); - return; - } - - if (item.type == RewriteType.removeHeader) { - if (item.value?.isNotEmpty == true) { - var val = message.headers.get(item.key!); - if (val != null && item.value != null && RegExp(item.value!).hasMatch(val)) { - return; - } - } - message.headers.remove(item.key!); - return; - } - - if (item.type == RewriteType.updateHeader) { - var pair = item.key?.split(":"); - var val = message.headers.get(pair!.first); - if (val != null && RegExp(pair.last.trim()).hasMatch(val)) { - var split = item.value?.split(":"); - message.headers.remove(pair.first); - message.headers.set(split!.first, split.last.trim()); - } - return; - } - } - - //替换相应 - Future _replaceResponse(HttpResponse response, RewriteItem item) async { - if (item.type == RewriteType.replaceResponseStatus && item.statusCode != null) { - response.status = HttpStatus.valueOf(item.statusCode!); - return; - } - await _replaceHttpMessage(response, item); - } - - Future _replaceHttpMessage(HttpMessage message, RewriteItem item) async { - if (item.type == RewriteType.replaceResponseHeader && item.headers != null) { - item.headers?.forEach((key, value) => message.headers.set(key, value)); - return; - } - - if (item.type == RewriteType.replaceResponseBody || item.type == RewriteType.replaceRequestBody) { - if (item.bodyType == ReplaceBodyType.file.name) { - if (item.bodyFile == null) return; - - message.body = await FileRead.readFile(item.bodyFile!); - message.headers.contentLength = message.body!.length; - return; - } - - if (item.body != null) { - message.body = - message.charset == 'utf-8' || message.charset == 'utf8' ? utf8.encode(item.body!) : item.body?.codeUnits; - message.headers.contentLength = message.body!.length; - } - return; - } - } - - toJson() { - return { - 'enabled': enabled, - 'rules': rules.map((e) => e.toJson()).toList(), - }; - } - - Future> toFullJson() async { - var rulesJson = []; - for (var rule in rules) { - var json = rule.toJson(); - json['items'] = await getRewriteItems(rule); - rulesJson.add(json); - } - - return { - 'enabled': enabled, - 'rules': rulesJson, - }; - } -} - -enum RuleType { - // body("重写消息体"), //OLD VERSION - - requestReplace("替换请求"), - responseReplace("替换响应"), - requestUpdate("修改请求"), - responseUpdate("修改响应"), - redirect("重定向"); - - //名称 - final String label; - - const RuleType(this.label); - - static RuleType fromName(String name) { - return values.firstWhere((element) => element.name == name || element.label == name); - } -} - -class RequestRewriteRule { - bool enabled; - RuleType type; - - String? name; - String url; - RegExp _urlReg; - String? rewritePath; - - RequestRewriteRule({this.enabled = true, this.name, required this.url, required this.type, this.rewritePath}) - : _urlReg = RegExp(url.replaceAll("*", ".*").replaceAll('?', '\\?')); - - bool match(String url, {RuleType? type}) { - return enabled && (type == null || this.type == type) && _urlReg.hasMatch(url); - } - - bool matchUrl(String url, RuleType type) { - return this.type == type && _urlReg.hasMatch(url); - } - - /// 从json中创建 - factory RequestRewriteRule.formJson(Map map) { - return RequestRewriteRule( - enabled: map['enabled'] == true, - name: map['name'], - url: map['url'] ?? map['domain'] + map['path'], - type: RuleType.fromName(map['type']), - rewritePath: map['rewritePath']); - } - - void _updatePathReg() { - _urlReg = RegExp(url.replaceAll("*", ".*")); - } - - toJson() { - return { - 'name': name, - 'enabled': enabled, - 'url': url, - 'type': type.name, - 'rewritePath': rewritePath, - }; - } -} - -enum ReplaceBodyType { - text("文本"), - file("文件"); - - final String label; - - const ReplaceBodyType(this.label); -} - -class RewriteItem { - bool enabled; - RewriteType type; - - //key redirectUrl, method, path, queryParam, headers, body, statusCode - final Map values = {}; - - RewriteItem(this.type, this.enabled, {Map? values}) { - if (values != null) { - this.values.addAll(Map.from(values)); - } - } - - factory RewriteItem.fromJson(Map map) { - return RewriteItem(RewriteType.fromName(map['type']), map['enabled'], values: map['values']); - } - - //key - String? get key => values['key']; - - set key(String? key) => values['key'] = key; - - String? get value => values['value']; - - set value(String? value) => values['value'] = value; - - //redirectUrl - String? get redirectUrl => values['redirectUrl']; - - set redirectUrl(String? redirectUrl) => values['redirectUrl'] = redirectUrl; - - //method - HttpMethod? get method => values['method'] == null - ? null - : HttpMethod.values.firstWhereOrNull((element) => element.name == values['method']); - - String? get path => values['path']; - - //queryParam - String? get queryParam => values['queryParam']; - - set queryParam(String? queryParam) => values['queryParam'] = queryParam; - - //statusCode - int? get statusCode => values['statusCode']; - - set statusCode(int? statusCode) => values['statusCode'] = statusCode; - - //headers - Map? get headers => values['headers'] == null ? null : Map.from(values['headers']); - - set headers(Map? headers) => values['headers'] = headers; - - //body - String? get body => values['body']; - - set body(String? body) => values['body'] = body; - - String? get bodyType => values['bodyType']; - - set bodyType(String? bodyType) => values['bodyType'] = bodyType; - - String? get bodyFile => values['bodyFile']; - - set bodyFile(String? bodyFile) => values['bodyFile'] = bodyFile; - - Map toJson() { - return { - 'enabled': enabled, - 'type': type.name, - 'values': values, - }; - } - - @override - String toString() { - return toJson().toString(); - } -} - -enum RewriteType { - //重定向 - redirect("重定向"), - - //替换请求 - replaceRequestLine("请求行"), - replaceRequestHeader("请求头"), - replaceRequestBody("请求体"), - replaceResponseStatus("状态码"), - replaceResponseHeader("响应头"), - replaceResponseBody("响应体"), - - //修改请求 - updateBody("修改Body"), - addQueryParam("添加参数"), - removeQueryParam("删除参数"), - updateQueryParam("修改参数"), - addHeader("添加头部"), - removeHeader("删除头部"), - updateHeader("修改头部"), - ; - - static List updateRequest = [ - updateBody, - addQueryParam, - updateQueryParam, - removeQueryParam, - addHeader, - updateHeader, - removeHeader - ]; - - static List updateResponse = [updateBody, addHeader, updateHeader, removeHeader]; - - final String label; - - const RewriteType(this.label); - - static RewriteType fromName(String name) { - return values.firstWhere((element) => element.name == name); - } - - String getDescribe(bool isCN) { - if (isCN) { - return label; - } - - return name.replaceFirst("replace", "").replaceFirst("Query", ""); - } -} diff --git a/lib/network/components/rewrite/request_rewrite_manager.dart b/lib/network/components/rewrite/request_rewrite_manager.dart new file mode 100644 index 0000000..4665ecd --- /dev/null +++ b/lib/network/components/rewrite/request_rewrite_manager.dart @@ -0,0 +1,297 @@ +/* + * 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:network_proxy/network/components/rewrite/rewrite_rule.dart'; +import 'package:network_proxy/network/http/http.dart'; +import 'package:network_proxy/network/util/file_read.dart'; +import 'package:network_proxy/network/util/logger.dart'; +import 'package:network_proxy/network/util/random.dart'; + +/// @author wanghongen +/// 2023/7/26 +/// 请求重写 +class RequestRewriteManager { + static String separator = Platform.pathSeparator; + + //重写规则 + final Map> rewriteItemsCache = {}; + + //单例 + static RequestRewriteManager? _instance; + + RequestRewriteManager._(); + + static Future get instance async { + if (_instance == null) { + var config = await _loadRequestRewriteConfig(); + _instance = RequestRewriteManager._(); + await _instance!.reload(config); + } + return _instance!; + } + + bool enabled = true; + List rules = []; + + //重新加载配置 + Future reload(Map? map) async { + rewriteItemsCache.clear(); + if (map == null) { + return; + } + + enabled = map['enabled'] == true; + List list = map['rules'] ?? []; + rules.clear(); + bool flush = false; + for (var element in list) { + try { + bool oldVersion = false; + // body("重写消息体"), 兼容旧版本 + if (element['requestBody']?.isNotEmpty == true || element['queryParam']?.isNotEmpty == true) { + element['type'] = RuleType.requestReplace.name; + + List items = []; + if (element['requestBody']?.isNotEmpty == true) { + RewriteItem item = RewriteItem(RewriteType.replaceRequestBody, true); + item.body = element['requestBody']; + items.add(item); + } + if (element['queryParam']?.isNotEmpty == true) { + RewriteItem item = RewriteItem(RewriteType.replaceRequestLine, true); + item.queryParam = element['queryParam']; + items.add(item); + } + var rule = RequestRewriteRule.formJson(element); + await addRule(rule, items); + oldVersion = true; + } + + if (element['responseBody']?.isNotEmpty == true) { + element['type'] = RuleType.responseReplace.name; + RewriteItem item = RewriteItem(RewriteType.replaceResponseBody, true); + item.body = element['responseBody']; + var rule = RequestRewriteRule.formJson(element); + await addRule(rule, [item]); + + oldVersion = true; + continue; + } + + if (element['redirectUrl']?.isNotEmpty == true) { + RewriteItem item = RewriteItem(RewriteType.redirect, true); + item.redirectUrl = element['redirectUrl']; + var rule = RequestRewriteRule.formJson(element); + await addRule(rule, [item]); + oldVersion = true; + continue; + } + + if (oldVersion) { + flush = true; + continue; + } + rules.add(RequestRewriteRule.formJson(element)); + } catch (e) { + logger.e('加载请求重写配置失败 $element', error: e); + } + } + + if (flush) { + await flushRequestRewriteConfig(); + } + } + + ///重新加载请求重写 + Future reloadRequestRewrite() async { + var config = await _loadRequestRewriteConfig(); + reload(config); + } + + ///同步配置 + Future syncConfig(Map? config) async { + if (config == null) { + return; + } + + rewriteItemsCache.clear(); + enabled = config['enabled'] == true; + List list = config['rules'] ?? []; + rules.clear(); + for (var element in list) { + try { + var rule = RequestRewriteRule.formJson(element); + List list = element['items'] as List; + List items = list.map((e) => RewriteItem.fromJson(e)).toList(); + await addRule(rule, items); + } catch (e) { + logger.e('加载请求重写配置失败 $element', error: e); + } + } + flushRequestRewriteConfig(); + } + + /// 加载请求重写配置文件 + static Future?> _loadRequestRewriteConfig() async { + var home = await FileRead.homeDir(); + var file = File('${home.path}${Platform.pathSeparator}request_rewrite.json'); + var exits = await file.exists(); + if (!exits) { + return null; + } + + Map config = jsonDecode(await file.readAsString()); + logger.i('加载请求重写配置文件 [$file]'); + return config; + } + + /// 保存请求重写配置文件 + Future flushRequestRewriteConfig() async { + var home = await FileRead.homeDir(); + var file = File('${home.path}${Platform.pathSeparator}request_rewrite.json'); + bool exists = await file.exists(); + if (!exists) { + await file.create(recursive: true); + } + var json = jsonEncode(toJson()); + logger.i('刷新请求重写配置文件 ${file.path}'); + await file.writeAsString(json); + } + + ///添加规则 + Future addRule(RequestRewriteRule rule, List items) async { + final home = await FileRead.homeDir(); + + String rewritePath = "${separator}rewrite$separator${RandomUtil.randomString(16)}.json"; + var file = File(home.path + rewritePath); + await file.create(recursive: true); + file.writeAsString(jsonEncode(items.map((e) => e.toJson()).toList())); + rule.rewritePath = rewritePath; + + rules.add(rule); + rewriteItemsCache[rule] = items; + } + + ///更新规则 + Future updateRule(int index, RequestRewriteRule rule, List? items) async { + rewriteItemsCache.remove(rules[index]); + final home = await FileRead.homeDir(); + rule.updatePathReg(); + rules[index] = rule; + + if (items == null) { + return; + } + bool isExist = rule.rewritePath != null; + if (rule.rewritePath == null) { + String rewritePath = "${separator}rewrite$separator${RandomUtil.randomString(16)}.json"; + rule.rewritePath = rewritePath; + } + + File file = File(home.path + rule.rewritePath!); + if (!isExist) { + await file.create(recursive: true); + } + + await file.writeAsString(jsonEncode(items.map((e) => e.toJson()).toList())); + rewriteItemsCache[rule] = items; + } + + removeIndex(List indexes) async { + for (var i in indexes) { + var rule = rules.removeAt(i); + rewriteItemsCache.remove(rule); //删除缓存 + if (rule.rewritePath != null) { + File home = await FileRead.homeDir(); + try { + await File(home.path + rule.rewritePath!).delete(); + } catch (e) { + logger.e('删除请求重写配置文件失败 ${home.path + rule.rewritePath!}', error: e); + } + rule.rewritePath = null; + } + } + } + + RequestRewriteRule getRequestRewriteRule(HttpRequest request, RuleType type) { + var url = '${request.remoteDomain()}${request.path()}'; + for (var rule in rules) { + if (rule.match(url) && rule.type == type) { + return rule; + } + } + + return RequestRewriteRule(type: type, url: url); + } + + RequestRewriteRule? getRewriteRule(String? url, List types) { + if (url == null || !enabled) { + return null; + } + for (var rule in rules) { + if (rule.match(url) && types.contains(rule.type)) { + return rule; + } + } + return null; + } + + /// 获取重写规则 + Future?> getRewriteItems(RequestRewriteRule rule) async { + if (rewriteItemsCache.containsKey(rule)) { + return rewriteItemsCache[rule]!; + } + if (rule.rewritePath == null) { + return null; + } + + final home = await FileRead.homeDir(); + List items = []; + try { + var json = await File(home.path + rule.rewritePath!).readAsString(); + List? list = jsonDecode(json); + list?.forEach((element) => items.add(RewriteItem.fromJson(element))); + rewriteItemsCache[rule] = items; + } catch (e) { + logger.e('加载请求重写配置文件失败 ${home.path + rule.rewritePath!}', error: e); + } + return items; + } + + toJson() { + return { + 'enabled': enabled, + 'rules': rules.map((e) => e.toJson()).toList(), + }; + } + + Future> toFullJson() async { + var rulesJson = []; + for (var rule in rules) { + var json = rule.toJson(); + json['items'] = await getRewriteItems(rule); + rulesJson.add(json); + } + + return { + 'enabled': enabled, + 'rules': rulesJson, + }; + } +} diff --git a/lib/network/components/rewrite/rewrite_rule.dart b/lib/network/components/rewrite/rewrite_rule.dart new file mode 100644 index 0000000..813c9f9 --- /dev/null +++ b/lib/network/components/rewrite/rewrite_rule.dart @@ -0,0 +1,246 @@ +/* + * Copyright 2024 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 'package:network_proxy/network/http/http.dart'; +import 'package:network_proxy/utils/lang.dart'; + +///重写规则 +///@author: wanghongen +enum RuleType { + // body("重写消息体"), //OLD VERSION + + requestReplace("替换请求"), + responseReplace("替换响应"), + requestUpdate("修改请求"), + responseUpdate("修改响应"), + redirect("重定向"); + + //名称 + final String label; + + const RuleType(this.label); + + static RuleType fromName(String name) { + return values.firstWhere((element) => element.name == name || element.label == name); + } +} + +class RequestRewriteRule { + bool enabled; + RuleType type; + + String? name; + String url; + RegExp _urlReg; + String? rewritePath; + + RequestRewriteRule({this.enabled = true, this.name, required this.url, required this.type, this.rewritePath}) + : _urlReg = RegExp(url.replaceAll("*", ".*").replaceAll('?', '\\?')); + + bool match(String url, {RuleType? type}) { + return enabled && (type == null || this.type == type) && _urlReg.hasMatch(url); + } + + bool matchUrl(String url, RuleType type) { + return this.type == type && _urlReg.hasMatch(url); + } + + /// 从json中创建 + factory RequestRewriteRule.formJson(Map map) { + return RequestRewriteRule( + enabled: map['enabled'] == true, + name: map['name'], + url: map['url'] ?? map['domain'] + map['path'], + type: RuleType.fromName(map['type']), + rewritePath: map['rewritePath']); + } + + void updatePathReg() { + _urlReg = RegExp(url.replaceAll("*", ".*")); + } + + toJson() { + return { + 'name': name, + 'enabled': enabled, + 'url': url, + 'type': type.name, + 'rewritePath': rewritePath, + }; + } +} + +enum ReplaceBodyType { + text("文本"), + file("文件"); + + final String label; + + const ReplaceBodyType(this.label); +} + +class RewriteItem { + bool enabled; + RewriteType type; + + //key redirectUrl, method, path, queryParam, headers, body, statusCode + final Map values = {}; + + RewriteItem(this.type, this.enabled, {Map? values}) { + if (values != null) { + this.values.addAll(Map.from(values)); + } + } + + factory RewriteItem.fromJson(Map map) { + return RewriteItem(RewriteType.fromName(map['type']), map['enabled'], values: map['values']); + } + + static List fromRequest(HttpRequest request) { + List items = []; + items.add(RewriteItem(RewriteType.replaceRequestLine, false)..path = request.requestUri?.path); + items.add(RewriteItem(RewriteType.replaceRequestHeader, false)..headers = request.headers.toMap()); + items.add(RewriteItem(RewriteType.replaceRequestBody, true)..body = request.bodyAsString); + + return items; + } + + static List fromResponse(HttpResponse response) { + List items = []; + items.add(RewriteItem(RewriteType.replaceResponseStatus, false)..statusCode = response.status.code); + items.add(RewriteItem(RewriteType.replaceResponseHeader, false)..headers = response.headers.toMap()); + items.add(RewriteItem(RewriteType.replaceResponseBody, true)..body = response.bodyAsString); + + return items; + } + + //key + String? get key => values['key']; + + set key(String? key) => values['key'] = key; + + String? get value => values['value']; + + set value(String? value) => values['value'] = value; + + //redirectUrl + String? get redirectUrl => values['redirectUrl']; + + set redirectUrl(String? redirectUrl) => values['redirectUrl'] = redirectUrl; + + //method + HttpMethod? get method => values['method'] == null + ? null + : HttpMethod.values.firstWhereOrNull((element) => element.name == values['method']); + + set method(HttpMethod? method) => values['method'] = method?.name; + + String? get path => values['path']; + + set path(String? path) => values['path'] = path; + + //queryParam + String? get queryParam => values['queryParam']; + + set queryParam(String? queryParam) => values['queryParam'] = queryParam; + + //statusCode + int? get statusCode => values['statusCode']; + + set statusCode(int? statusCode) => values['statusCode'] = statusCode; + + //headers + Map? get headers => values['headers'] == null ? null : Map.from(values['headers']); + + set headers(Map? headers) => values['headers'] = headers; + + //body + String? get body => values['body']; + + set body(String? body) => values['body'] = body; + + String? get bodyType => values['bodyType']; + + set bodyType(String? bodyType) => values['bodyType'] = bodyType; + + String? get bodyFile => values['bodyFile']; + + set bodyFile(String? bodyFile) => values['bodyFile'] = bodyFile; + + Map toJson() { + return { + 'enabled': enabled, + 'type': type.name, + 'values': values, + }; + } + + @override + String toString() { + return toJson().toString(); + } +} + +enum RewriteType { + //重定向 + redirect("重定向"), + + //替换请求 + replaceRequestLine("请求行"), + replaceRequestHeader("请求头"), + replaceRequestBody("请求体"), + replaceResponseStatus("状态码"), + replaceResponseHeader("响应头"), + replaceResponseBody("响应体"), + + //修改请求 + updateBody("修改Body"), + addQueryParam("添加参数"), + removeQueryParam("删除参数"), + updateQueryParam("修改参数"), + addHeader("添加头部"), + removeHeader("删除头部"), + updateHeader("修改头部"), + ; + + static List updateRequest = [ + updateBody, + addQueryParam, + updateQueryParam, + removeQueryParam, + addHeader, + updateHeader, + removeHeader + ]; + + static List updateResponse = [updateBody, addHeader, updateHeader, removeHeader]; + + final String label; + + const RewriteType(this.label); + + static RewriteType fromName(String name) { + return values.firstWhere((element) => element.name == name); + } + + String getDescribe(bool isCN) { + if (isCN) { + return label; + } + + return name.replaceFirst("replace", "").replaceFirst("Query", ""); + } +} diff --git a/lib/network/handler.dart b/lib/network/handler.dart index 3e4ca58..842076c 100644 --- a/lib/network/handler.dart +++ b/lib/network/handler.dart @@ -19,7 +19,7 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:network_proxy/network/components/host_filter.dart'; -import 'package:network_proxy/network/components/request_rewrite_manager.dart'; +import 'package:network_proxy/network/components/request_rewrite_component.dart'; import 'package:network_proxy/network/components/script_manager.dart'; import 'package:network_proxy/network/host_port.dart'; import 'package:network_proxy/network/http/http.dart'; @@ -47,9 +47,9 @@ abstract class EventListener { /// http请求处理器 class HttpProxyChannelHandler extends ChannelHandler { EventListener? listener; - RequestRewrites? requestRewrites; + RequestRewriteComponent? requestRewriteComponent; - HttpProxyChannelHandler({this.listener, this.requestRewrites}); + HttpProxyChannelHandler({this.listener, this.requestRewriteComponent}); @override void channelRead(ChannelContext channelContext, Channel channel, HttpRequest msg) async { @@ -132,7 +132,7 @@ class HttpProxyChannelHandler extends ChannelHandler { httpRequest = request; //重写请求 - await requestRewrites?.requestRewrite(httpRequest); + await requestRewriteComponent?.requestRewrite(httpRequest); listener?.onRequest(channel, httpRequest); @@ -146,7 +146,7 @@ class HttpProxyChannelHandler extends ChannelHandler { } //重定向 - String? redirectUrl = await requestRewrites?.getRedirectRule(uri); + String? redirectUrl = await requestRewriteComponent?.getRedirectRule(uri); if (redirectUrl?.isNotEmpty == true) { await redirect(channelContext, channel, httpRequest, redirectUrl!); return; @@ -158,7 +158,7 @@ class HttpProxyChannelHandler extends ChannelHandler { //重定向 Future redirect( ChannelContext channelContext, Channel channel, HttpRequest httpRequest, String redirectUrl) async { - var proxyHandler = HttpResponseProxyHandler(channel, listener: listener, requestRewrites: requestRewrites); + var proxyHandler = HttpResponseProxyHandler(channel, listener: listener, requestRewriteComponent: requestRewriteComponent); var redirectUri = UriBuild.build(redirectUrl, params: httpRequest.queries.isEmpty ? null : httpRequest.queries); log.d("[${channel.id}] 重定向 $redirectUri"); @@ -229,7 +229,7 @@ class HttpProxyChannelHandler extends ChannelHandler { /// 连接远程 Future connectRemote(ChannelContext channelContext, Channel clientChannel, HostAndPort connectHost) async { - var proxyHandler = HttpResponseProxyHandler(clientChannel, listener: listener, requestRewrites: requestRewrites); + var proxyHandler = HttpResponseProxyHandler(clientChannel, listener: listener, requestRewriteComponent: requestRewriteComponent); var proxyChannel = await channelContext.connectServerChannel(connectHost, proxyHandler); return proxyChannel; } @@ -241,9 +241,9 @@ class HttpResponseProxyHandler extends ChannelHandler { final Channel clientChannel; EventListener? listener; - RequestRewrites? requestRewrites; + RequestRewriteComponent? requestRewriteComponent; - HttpResponseProxyHandler(this.clientChannel, {this.listener, this.requestRewrites}); + HttpResponseProxyHandler(this.clientChannel, {this.listener, this.requestRewriteComponent}); @override void channelRead(ChannelContext channelContext, Channel channel, HttpResponse msg) async { @@ -274,7 +274,7 @@ class HttpResponseProxyHandler extends ChannelHandler { //重写响应 try { - await requestRewrites?.responseRewrite(msg.request?.requestUrl, msg); + await requestRewriteComponent?.responseRewrite(msg.request?.requestUrl, msg); } catch (e, t) { msg.body = "$e".codeUnits; log.e('[${clientChannel.id}] 响应重写异常 ', error: e, stackTrace: t); diff --git a/lib/network/http/http_headers.dart b/lib/network/http/http_headers.dart index bcf4c6b..947af3d 100644 --- a/lib/network/http/http_headers.dart +++ b/lib/network/http/http_headers.dart @@ -193,6 +193,18 @@ class HttpHeaders { return headers; } + ///原始header文本 + String toRawHeaders() { + StringBuffer sb = StringBuffer(); + forEach((name, values) { + for (var value in values) { + sb.writeln("$name: $value"); + } + }); + + return sb.toString(); + } + @override String toString() { return 'HttpHeaders{$_headers}'; diff --git a/lib/network/proxy_helper.dart b/lib/network/proxy_helper.dart index 4e5a48b..3f99ac3 100644 --- a/lib/network/proxy_helper.dart +++ b/lib/network/proxy_helper.dart @@ -18,7 +18,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:network_proxy/network/channel.dart'; -import 'package:network_proxy/network/components/request_rewrite_manager.dart'; +import 'package:network_proxy/network/components/rewrite/request_rewrite_manager.dart'; import 'package:network_proxy/network/components/script_manager.dart'; import 'package:network_proxy/network/handler.dart'; import 'package:network_proxy/network/host_port.dart'; @@ -35,7 +35,7 @@ class ProxyHelper { static localRequest(HttpRequest msg, Channel channel) async { //获取配置 if (msg.path() == '/config') { - final requestRewrites = await RequestRewrites.instance; + final requestRewrites = await RequestRewriteManager.instance; var response = HttpResponse(HttpStatus.ok, protocolVersion: msg.protocolVersion); var body = { "requestRewrites": await requestRewrites.toFullJson(), diff --git a/lib/ui/component/multi_window.dart b/lib/ui/component/multi_window.dart index 154421f..930d413 100644 --- a/lib/ui/component/multi_window.dart +++ b/lib/ui/component/multi_window.dart @@ -23,7 +23,8 @@ import 'package:file_selector/file_selector.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:network_proxy/network/bin/server.dart'; -import 'package:network_proxy/network/components/request_rewrite_manager.dart'; +import 'package:network_proxy/network/components/rewrite/request_rewrite_manager.dart'; +import 'package:network_proxy/network/components/rewrite/rewrite_rule.dart'; import 'package:network_proxy/network/components/script_manager.dart'; import 'package:network_proxy/network/http/http.dart'; import 'package:network_proxy/network/util/lists.dart'; @@ -76,7 +77,7 @@ Widget multiWindow(int windowId, Map argument) { //请求重写 if (argument['name'] == 'RequestRewriteWidget') { return futureWidget( - RequestRewrites.instance, (data) => RequestRewriteWidget(windowId: windowId, requestRewrites: data)); + RequestRewriteManager.instance, (data) => RequestRewriteWidget(windowId: windowId, requestRewrites: data)); } if (argument['name'] == 'QrCodePage') { @@ -146,7 +147,7 @@ class MultiWindow { static bool _refreshRewrite = false; static Future _handleRefreshRewrite(Operation operation, Map arguments) async { - RequestRewrites requestRewrites = await RequestRewrites.instance; + RequestRewriteManager requestRewrites = await RequestRewriteManager.instance; switch (operation) { case Operation.add: @@ -294,10 +295,9 @@ openScriptWindow() async { {'name': 'ScriptWidget'}, )); - // window.setTitle('script'); window.setTitle('Script'); window - ..setFrame(const Offset(30, 0) & Size(800 * ratio, 690 * ratio)) + ..setFrame(const Offset(30, 0) & Size(800 * ratio, 700 * ratio)) ..center() ..show(); } diff --git a/lib/ui/component/text_field.dart b/lib/ui/component/text_field.dart new file mode 100644 index 0000000..d36e7a3 --- /dev/null +++ b/lib/ui/component/text_field.dart @@ -0,0 +1,62 @@ +/* + * Copyright 2024 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 'package:flutter/material.dart'; + +/// 匹配文本高亮 +/// @author: Hongen Wang +class HighlightTextEditingController extends TextEditingController { + RegExp? highlightPattern; + + // + bool highlightEnabled = true; + + HighlightTextEditingController({super.text}); + + bool highlight(String? value, {bool caseSensitive = true}) { + highlightPattern = value == null ? null : RegExp(value, caseSensitive: caseSensitive); + return highlightPattern?.hasMatch(text) ?? false; + } + + @override + TextSpan buildTextSpan({required BuildContext context, TextStyle? style, required bool withComposing}) { + final text = this.text; + + if (!highlightEnabled || highlightPattern == null || !highlightPattern!.hasMatch(text)) { + return super.buildTextSpan(context: context, style: style, withComposing: withComposing); + } + + Color color = Theme.of(context).colorScheme.primary; + final highlightStyle = style?.copyWith(color: color); + final normalStyle = style; + List spans = []; + int start = 0; + + for (final match in highlightPattern!.allMatches(text)) { + if (match.start > start) { + spans.add(TextSpan(text: text.substring(start, match.start), style: normalStyle)); + } + spans.add(TextSpan(text: match.group(0), style: highlightStyle)); + start = match.end; + } + + if (start < text.length) { + spans.add(TextSpan(text: text.substring(start), style: normalStyle)); + } + + return TextSpan(children: spans, style: style); + } +} diff --git a/lib/ui/component/utils.dart b/lib/ui/component/utils.dart index e3bab11..6d0c24e 100644 --- a/lib/ui/component/utils.dart +++ b/lib/ui/component/utils.dart @@ -115,7 +115,7 @@ Widget contextMenu(BuildContext context, EditableTextState editableTextState) { onPressed: () { unSelect(editableTextState); Clipboard.setData(ClipboardData(text: editableTextState.textEditingValue.text)).then((value) { - FlutterToastr.show(AppLocalizations.of(context)!.copied, context); + if (context.mounted) FlutterToastr.show(AppLocalizations.of(context)!.copied, context); editableTextState.hideToolbar(); }); }, diff --git a/lib/ui/content/body.dart b/lib/ui/content/body.dart index 6e91d14..9e09ddf 100644 --- a/lib/ui/content/body.dart +++ b/lib/ui/content/body.dart @@ -21,7 +21,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_toastr/flutter_toastr.dart'; -import 'package:network_proxy/network/components/request_rewrite_manager.dart'; +import 'package:network_proxy/network/components/rewrite/request_rewrite_manager.dart'; +import 'package:network_proxy/network/components/rewrite/rewrite_rule.dart'; import 'package:network_proxy/network/http/content_type.dart'; import 'package:network_proxy/network/http/http.dart'; import 'package:network_proxy/network/util/logger.dart'; @@ -205,26 +206,22 @@ class HttpBodyState extends State { //展示请求重写 showRequestRewrite() async { HttpRequest? request; + if (widget.httpMessage == null) { + return; + } + bool isRequest = widget.httpMessage is HttpRequest; if (widget.httpMessage is HttpRequest) { request = widget.httpMessage as HttpRequest; } else { request = (widget.httpMessage as HttpResponse).request; } - var requestRewrites = await RequestRewrites.instance; + var requestRewrites = await RequestRewriteManager.instance; var ruleType = isRequest ? RuleType.requestReplace : RuleType.responseReplace; - var url = '${request?.remoteDomain()}${request?.path()}'; - var rule = requestRewrites.rules - .firstWhere((it) => it.matchUrl(url, ruleType), orElse: () => RequestRewriteRule(type: ruleType, url: url)); - - var body = bodyKey.currentState?.body; + var rule = requestRewrites.getRequestRewriteRule(request!, ruleType); var rewriteItems = await requestRewrites.getRewriteItems(rule); - RewriteType rewriteType = isRequest ? RewriteType.replaceRequestBody : RewriteType.replaceResponseBody; - if (!rewriteItems.any((element) => element.type == rewriteType)) { - rewriteItems.add(RewriteItem(rewriteType, true, values: {'body': body})); - } if (!mounted) return; @@ -234,10 +231,9 @@ class HttpBodyState extends State { showDialog( context: context, barrierDismissible: false, - builder: (BuildContext context) => - RuleAddDialog(rule: rule, items: rewriteItems, windowId: widget.windowController?.windowId)) + builder: (BuildContext context) => RewriteRuleEdit(rule: rule, items: rewriteItems, request: request)) .then((value) { - if (value is RequestRewriteRule) { + if (value is RequestRewriteRule && mounted) { FlutterToastr.show(localizations.saveSuccess, context); } }); diff --git a/lib/ui/desktop/common.dart b/lib/ui/desktop/common.dart new file mode 100644 index 0000000..1abb8bc --- /dev/null +++ b/lib/ui/desktop/common.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:network_proxy/network/components/rewrite/request_rewrite_manager.dart'; +import 'package:network_proxy/network/components/rewrite/rewrite_rule.dart'; +import 'package:network_proxy/network/http/http.dart'; + +import 'toolbar/setting/request_rewrite.dart'; + +/// 显示请求重写对话框 +showRequestRewriteDialog(BuildContext context, HttpRequest request) async { + bool isRequest = request.response == null; + var requestRewrites = await RequestRewriteManager.instance; + + var ruleType = isRequest ? RuleType.requestReplace : RuleType.responseReplace; + var rule = requestRewrites.getRequestRewriteRule(request, ruleType); + var rewriteItems = await requestRewrites.getRewriteItems(rule); + if (!context.mounted) return; + + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => RewriteRuleEdit(rule: rule, items: rewriteItems, request: request)); +} diff --git a/lib/ui/desktop/left_menus/favorite.dart b/lib/ui/desktop/left_menus/favorite.dart index 3f33293..5907f81 100644 --- a/lib/ui/desktop/left_menus/favorite.dart +++ b/lib/ui/desktop/left_menus/favorite.dart @@ -25,7 +25,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_toastr/flutter_toastr.dart'; -import 'package:network_proxy/network/components/script_manager.dart'; import 'package:network_proxy/network/host_port.dart'; import 'package:network_proxy/network/http/http.dart'; import 'package:network_proxy/network/http_client.dart'; @@ -34,13 +33,14 @@ import 'package:network_proxy/ui/component/utils.dart'; import 'package:network_proxy/ui/component/widgets.dart'; import 'package:network_proxy/ui/content/panel.dart'; import 'package:network_proxy/ui/desktop/request/repeat.dart'; -import 'package:network_proxy/ui/desktop/toolbar/setting/script.dart'; import 'package:network_proxy/utils/curl.dart'; import 'package:network_proxy/utils/lang.dart'; import 'package:network_proxy/utils/python.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:window_manager/window_manager.dart'; +import '../common.dart'; + /// @author wanghongen /// 2023/10/8 class Favorites extends StatefulWidget { @@ -172,7 +172,6 @@ class _FavoriteItemState extends State<_FavoriteItem> { .then((value) => FlutterToastr.show(localizations.copied, context)); }), const PopupMenuDivider(height: 0.3), - popupItem(localizations.rename, onTap: () => rename(widget.favorite)), popupItem(localizations.repeat, onTap: () => onRepeat(request)), popupItem(localizations.customRepeat, onTap: () => showCustomRepeat(request)), popupItem(localizations.editRequest, onTap: () { @@ -180,17 +179,9 @@ class _FavoriteItemState extends State<_FavoriteItem> { requestEdit(request); }); }), - popupItem(localizations.script, onTap: () async { - var scriptManager = await ScriptManager.instance; - var url = '${request.remoteDomain()}${request.path()}'; - var scriptItem = (scriptManager).list.firstWhereOrNull((it) => it.url == url); - - String? script = scriptItem == null ? null : await scriptManager.getScript(scriptItem); - if (!mounted) return; - showDialog( - context: context, builder: (context) => ScriptEdit(scriptItem: scriptItem, script: script, url: url)); - }), + popupItem(localizations.requestRewrite, onTap: () => showRequestRewriteDialog(context, request)), const PopupMenuDivider(height: 0.3), + popupItem(localizations.rename, onTap: () => rename(widget.favorite)), popupItem(localizations.deleteFavorite, onTap: () { widget.onRemove?.call(widget.favorite); }) diff --git a/lib/ui/desktop/request/request.dart b/lib/ui/desktop/request/request.dart index 130bd3c..80b2c26 100644 --- a/lib/ui/desktop/request/request.dart +++ b/lib/ui/desktop/request/request.dart @@ -41,6 +41,8 @@ import 'package:network_proxy/utils/python.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:window_manager/window_manager.dart'; +import '../common.dart'; + /// 请求 URI /// @author wanghongen /// 2023/10/8 @@ -178,6 +180,8 @@ class _RequestWidgetState extends State { requestEdit(); }); }), + MenuItem.separator(), + MenuItem(label: localizations.requestRewrite, onClick: (_) => showRequestRewriteDialog(context, widget.request)), MenuItem( label: localizations.script, onClick: (_) async { diff --git a/lib/ui/desktop/toolbar/setting/request_rewrite.dart b/lib/ui/desktop/toolbar/setting/request_rewrite.dart index ef3e0a4..67779f3 100644 --- a/lib/ui/desktop/toolbar/setting/request_rewrite.dart +++ b/lib/ui/desktop/toolbar/setting/request_rewrite.dart @@ -23,7 +23,9 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_toastr/flutter_toastr.dart'; -import 'package:network_proxy/network/components/request_rewrite_manager.dart'; +import 'package:network_proxy/network/components/rewrite/request_rewrite_manager.dart'; +import 'package:network_proxy/network/components/rewrite/rewrite_rule.dart'; +import 'package:network_proxy/network/http/http.dart'; import 'package:network_proxy/network/util/logger.dart'; import 'package:network_proxy/ui/component/multi_window.dart'; import 'package:network_proxy/ui/component/utils.dart'; @@ -35,7 +37,7 @@ import 'package:network_proxy/ui/desktop/toolbar/setting/rewrite/rewrite_update. /// 2023/10/8 class RequestRewriteWidget extends StatefulWidget { final int windowId; - final RequestRewrites requestRewrites; + final RequestRewriteManager requestRewrites; const RequestRewriteWidget({super.key, required this.windowId, required this.requestRewrites}); @@ -183,7 +185,7 @@ class RequestRewriteState extends State { showDialog( context: context, barrierDismissible: false, - builder: (BuildContext context) => RuleAddDialog(windowId: widget.windowId)).then((value) { + builder: (BuildContext context) => RewriteRuleEdit(windowId: widget.windowId)).then((value) { if (value != null) setState(() {}); }); } @@ -192,7 +194,7 @@ class RequestRewriteState extends State { ///请求重写规则列表 class RequestRuleList extends StatefulWidget { final int windowId; - final RequestRewrites requestRewrites; + final RequestRewriteManager requestRewrites; const RequestRuleList(this.requestRewrites, {super.key, required this.windowId}); @@ -399,7 +401,7 @@ class _RequestRuleListState extends State { context: context, barrierDismissible: false, builder: (BuildContext context) { - return RuleAddDialog(rule: rule, items: rewriteItems, windowId: widget.windowId); + return RewriteRuleEdit(rule: rule, items: rewriteItems, windowId: widget.windowId); }).then((value) { if (value != null) { setState(() {}); @@ -443,20 +445,21 @@ class _RequestRuleListState extends State { } ///请求重写规则添加对话框 -class RuleAddDialog extends StatefulWidget { +class RewriteRuleEdit extends StatefulWidget { final RequestRewriteRule? rule; final List? items; + final HttpRequest? request; final int? windowId; - const RuleAddDialog({super.key, this.rule, this.items, required this.windowId}); + const RewriteRuleEdit({super.key, this.rule, this.items, this.windowId, this.request}); @override State createState() { - return _RuleAddDialogState(); + return _RewriteRuleEditState(); } } -class _RuleAddDialogState extends State { +class _RewriteRuleEditState extends State { late RequestRewriteRule rule; List? items; @@ -477,6 +480,11 @@ class _RuleAddDialogState extends State { ruleType = rule.type; nameInput = TextEditingController(text: rule.name); urlInput = TextEditingController(text: rule.url); + + if (items == null && widget.request != null) { + print('items == null && widget.request != null'); + items = fromRequestItems(widget.request!, ruleType); + } } @override @@ -512,8 +520,8 @@ class _RuleAddDialogState extends State { ]), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10.0)), content: Container( - width: 500, - constraints: const BoxConstraints(minHeight: 200), + width: 550, + constraints: const BoxConstraints(minHeight: 200,maxHeight: 550), child: Form( key: formKey, child: Column( @@ -547,14 +555,7 @@ class _RuleAddDialogState extends State { value: e, child: Text(isCN ? e.label : e.name, style: const TextStyle(fontSize: 13)))) .toList(), - onChanged: (val) { - setState(() { - ruleType = val!; - items = ruleType == widget.rule?.type ? widget.items : []; - rewriteReplaceKey.currentState?.initItems(ruleType, items); - rewriteUpdateKey.currentState?.initItems(ruleType, items); - }); - }, + onChanged: onChangeType, )), const SizedBox(width: 10), ]), @@ -576,7 +577,7 @@ class _RuleAddDialogState extends State { rule.url = urlInput.text; items = rewriteReplaceKey.currentState?.getItems() ?? rewriteUpdateKey.currentState?.getItems(); - var requestRewrites = await RequestRewrites.instance; + var requestRewrites = await RequestRewriteManager.instance; requestRewrites.rewriteItemsCache[rule] = items!; var index = requestRewrites.rules.indexOf(rule); @@ -598,9 +599,38 @@ class _RuleAddDialogState extends State { ]); } + void onChangeType(RuleType? val) async { + if (ruleType == val) return; + + ruleType = val!; + items = []; + + if (ruleType == widget.rule?.type) { + items = widget.items; + } else if (widget.request != null) { + items?.addAll(fromRequestItems(widget.request!, ruleType)); + } + + setState(() { + rewriteReplaceKey.currentState?.initItems(ruleType, items); + rewriteUpdateKey.currentState?.initItems(ruleType, items); + }); + } + + static List fromRequestItems(HttpRequest request, RuleType ruleType) { + if (ruleType == RuleType.requestReplace) { + //请求替换 + return RewriteItem.fromRequest(request); + } else if (ruleType == RuleType.responseReplace && request.response != null) { + //响应替换 + return RewriteItem.fromResponse(request.response!); + } + return []; + } + Widget rewriteRule() { if (ruleType == RuleType.requestUpdate || ruleType == RuleType.responseUpdate) { - return DesktopRewriteUpdate(key: rewriteUpdateKey, items: items, ruleType: ruleType); + return DesktopRewriteUpdate(key: rewriteUpdateKey, items: items, ruleType: ruleType, request: widget.request); } return DesktopRewriteReplace(key: rewriteReplaceKey, items: items, ruleType: ruleType); diff --git a/lib/ui/desktop/toolbar/setting/rewrite/rewrite_replace.dart b/lib/ui/desktop/toolbar/setting/rewrite/rewrite_replace.dart index 7bb1d46..3565d90 100644 --- a/lib/ui/desktop/toolbar/setting/rewrite/rewrite_replace.dart +++ b/lib/ui/desktop/toolbar/setting/rewrite/rewrite_replace.dart @@ -18,7 +18,7 @@ import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:network_proxy/network/components/request_rewrite_manager.dart'; +import 'package:network_proxy/network/components/rewrite/rewrite_rule.dart'; import 'package:network_proxy/ui/component/state_component.dart'; import 'package:network_proxy/ui/component/widgets.dart'; import 'package:network_proxy/utils/lang.dart'; @@ -113,9 +113,7 @@ class RewriteReplaceState extends State { Widget build(BuildContext context) { if (ruleType == RuleType.redirect) { return SizedBox( - width: 500, - height: 120, - child: Padding(padding: EdgeInsets.symmetric(vertical: 15), child: redirectEdit(items.first))); + height: 120, child: Padding(padding: EdgeInsets.symmetric(vertical: 15), child: redirectEdit(items.first))); } if (ruleType == RuleType.responseReplace || ruleType == RuleType.requestReplace) { @@ -125,8 +123,7 @@ class RewriteReplaceState extends State { : [localizations.statusCode, localizations.responseHeader, localizations.responseBody]; return Container( - width: 500, - constraints: const BoxConstraints(maxHeight: 340), + constraints: const BoxConstraints(maxHeight: 360), child: DefaultTabController( length: tabs.length, initialIndex: tabs.length - 1, @@ -270,7 +267,7 @@ class RewriteReplaceState extends State { })) ])) ]), - Headers(headers: rewriteItem.headers, key: _headerKey) + Expanded(child: Headers(headers: rewriteItem.headers, key: _headerKey)) ]); } @@ -315,7 +312,7 @@ class RewriteReplaceState extends State { ]), const SizedBox(height: 15), textField("Path", rewriteItem.path, "${localizations.example} /api/v1/user", - onChanged: (val) => rewriteItem.values['path'] = val), + onChanged: (val) => rewriteItem.path = val), const SizedBox(height: 15), textField("URL${localizations.param}", rewriteItem.queryParam, "${localizations.example} id=1&name=2", onChanged: (val) => rewriteItem.queryParam = val), @@ -481,27 +478,27 @@ class HeadersState extends State with AutomaticKeepAliveClientMixin { Widget build(BuildContext context) { super.build(context); - var list = [ - ..._buildRows(), - ]; + var list = _buildRows(); - list.add(TextButton( - child: Text("${localizations.add}Header", textAlign: TextAlign.center), - onPressed: () { - setState(() { - _headers[TextEditingController()] = TextEditingController(); - }); - }, - )); - - return Padding( - padding: const EdgeInsets.only(top: 10), - child: ListView.separated( - shrinkWrap: true, - separatorBuilder: (context, index) => - index == list.length ? const SizedBox() : const Divider(thickness: 0.2), - itemBuilder: (context, index) => list[index], - itemCount: list.length)); + return Column(children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.only(top: 10, bottom: 10), + child: ListView.separated( + shrinkWrap: true, + separatorBuilder: (context, index) => + index == list.length ? const SizedBox() : const Divider(thickness: 0.2), + itemBuilder: (context, index) => list[index], + itemCount: list.length))), + TextButton( + child: Text("${localizations.add}Header", textAlign: TextAlign.center), + onPressed: () { + setState(() { + _headers[TextEditingController()] = TextEditingController(); + }); + }, + ), + ]); } List _buildRows() { @@ -533,7 +530,11 @@ class HeadersState extends State with AutomaticKeepAliveClientMixin { controller: val, minLines: 1, maxLines: 3, - decoration: InputDecoration(isDense: true, border: InputBorder.none, hintText: isKey ? "Key" : "Value"))); + decoration: InputDecoration( + isDense: true, + border: InputBorder.none, + hintStyle: TextStyle(fontSize: 12, color: Colors.grey), + hintText: isKey ? "Key" : "Value"))); } Widget _row(Widget key, Widget val, Widget? op) { diff --git a/lib/ui/desktop/toolbar/setting/rewrite/rewrite_update.dart b/lib/ui/desktop/toolbar/setting/rewrite/rewrite_update.dart index f17cf41..613f850 100644 --- a/lib/ui/desktop/toolbar/setting/rewrite/rewrite_update.dart +++ b/lib/ui/desktop/toolbar/setting/rewrite/rewrite_update.dart @@ -13,9 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import 'package:flutter/material.dart'; import 'package:flutter_toastr/flutter_toastr.dart'; -import 'package:network_proxy/network/components/request_rewrite_manager.dart'; +import 'package:network_proxy/network/components/rewrite/rewrite_rule.dart'; +import 'package:network_proxy/network/http/http.dart'; + +import 'package:network_proxy/ui/component/text_field.dart'; import 'package:network_proxy/ui/component/utils.dart'; import 'package:network_proxy/ui/component/widgets.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -26,8 +30,9 @@ import 'package:network_proxy/utils/lang.dart'; class DesktopRewriteUpdate extends StatefulWidget { final RuleType ruleType; final List? items; + final HttpRequest? request; - const DesktopRewriteUpdate({super.key, required this.ruleType, this.items}); + const DesktopRewriteUpdate({super.key, required this.ruleType, this.items, this.request}); @override State createState() => RewriteUpdateState(); @@ -65,28 +70,27 @@ class RewriteUpdateState extends State { @override Widget build(BuildContext context) { - return SizedBox( - height: 340, - width: 500, - child: Column( + return Column( + children: [ + Row( children: [ - Row( - children: [ - Text(localizations.requestRewriteRule, style: const TextStyle(fontSize: 13, color: Colors.grey)), - Expanded( - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [IconButton(onPressed: add, icon: const Icon(Icons.add)), const SizedBox(width: 10)], - )) - ], - ), - UpdateList(items: items, ruleType: ruleType), + Text(localizations.requestRewriteRule, style: const TextStyle(fontSize: 13, color: Colors.grey)), + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [IconButton(onPressed: add, icon: const Icon(Icons.add)), const SizedBox(width: 10)], + )) ], - )); + ), + UpdateList(items: items, ruleType: ruleType), + ], + ); } add() { - showDialog(context: context, builder: (context) => RewriteUpdateAddDialog(ruleType: ruleType)).then((value) { + showDialog( + context: context, + builder: (context) => RewriteUpdateAddDialog(ruleType: ruleType, request: widget.request)).then((value) { if (value != null) { setState(() { items.add(value); @@ -99,8 +103,9 @@ class RewriteUpdateState extends State { class RewriteUpdateAddDialog extends StatefulWidget { final RewriteItem? item; final RuleType ruleType; + final HttpRequest? request; - const RewriteUpdateAddDialog({super.key, this.item, required this.ruleType}); + const RewriteUpdateAddDialog({super.key, this.item, required this.ruleType, this.request}); @override State createState() => _RewriteUpdateAddState(); @@ -112,12 +117,29 @@ class _RewriteUpdateAddState extends State { late RewriteItem rewriteItem; AppLocalizations get localizations => AppLocalizations.of(context)!; + var keyController = TextEditingController(); + var valueController = TextEditingController(); + var dataController = HighlightTextEditingController(); @override void initState() { super.initState(); rewriteType = widget.item?.type ?? RewriteType.updateBody; rewriteItem = widget.item ?? RewriteItem(rewriteType, true); + keyController.text = rewriteItem.key ?? ''; + valueController.text = rewriteItem.value ?? ''; + + initTestData(); + keyController.addListener(onInputChangeMatch); + dataController.addListener(onInputChangeMatch); + } + + @override + void dispose() { + keyController.dispose(); + valueController.dispose(); + dataController.dispose(); + super.dispose(); } @override @@ -139,6 +161,9 @@ class _RewriteUpdateAddState extends State { var typeList = widget.ruleType == RuleType.requestUpdate ? RewriteType.updateRequest : RewriteType.updateResponse; return AlertDialog( + titlePadding: const EdgeInsets.only(top: 10, left: 20), + actionsPadding: const EdgeInsets.only(right: 15, bottom: 15), + contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 5), title: Text(localizations.add, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500), textAlign: TextAlign.center), actions: [ @@ -149,15 +174,16 @@ class _RewriteUpdateAddState extends State { FlutterToastr.show(localizations.cannotBeEmpty, context, position: FlutterToastr.center); return; } - (formKey.currentState as FormState).save(); + rewriteItem.key = keyController.text; + rewriteItem.value = valueController.text; rewriteItem.type = rewriteType; Navigator.of(context).pop(rewriteItem); }, child: Text(localizations.confirm)), ], - content: SizedBox( - width: 330, - height: 170, + content: Container( + width: 500, + constraints: const BoxConstraints(maxHeight: 400), child: Form( key: formKey, child: Column(children: [ @@ -182,41 +208,123 @@ class _RewriteUpdateAddState extends State { onChanged: (val) { setState(() { rewriteType = val!; + initTestData(); }); })), ], ), const SizedBox(height: 15), - textField(isUpdate ? localizations.match : localizations.name, rewriteItem.key, keyTips, - required: !isDelete, onSaved: (val) => rewriteItem.key = val), + textField(isUpdate ? localizations.match : localizations.name, keyTips, + controller: keyController, required: !isDelete), const SizedBox(height: 15), - textField(isUpdate ? localizations.replace : localizations.value, rewriteItem.value, valueTips, - onSaved: (val) => rewriteItem.value = val), + textField(isUpdate ? localizations.replace : localizations.value, valueTips, + controller: valueController), + const SizedBox(height: 10), + Row(children: [ + Align(alignment: Alignment.centerLeft, child: Text('测试数据', style: const TextStyle(fontSize: 14))), + const SizedBox(width: 10), + if (!isMatch) Text('未检测到变更', style: TextStyle(color: Colors.red, fontSize: 14)) + ]), + const SizedBox(height: 5), + formField('输入待匹配的数据', lines: 10, required: false, controller: dataController), ])))); } - Widget textField(String label, String? val, String hint, {bool required = false, FormFieldSetter? onSaved}) { + initTestData() { + dataController.highlightEnabled = rewriteType != RewriteType.addQueryParam && rewriteType != RewriteType.addHeader; + bool isRemove = [RewriteType.removeHeader, RewriteType.removeQueryParam].contains(rewriteType); + + valueController.removeListener(onInputChangeMatch); + if (isRemove) { + valueController.addListener(onInputChangeMatch); + } + + if (widget.request == null) return; + + if (rewriteType == RewriteType.updateBody) { + dataController.text = (widget.ruleType == RuleType.requestUpdate + ? widget.request?.bodyAsString + : widget.request?.response?.bodyAsString) ?? + ''; + return; + } + + if (rewriteType == RewriteType.updateQueryParam || rewriteType == RewriteType.removeQueryParam) { + dataController.text = Uri.decodeQueryComponent(widget.request?.requestUri?.query ?? ''); + return; + } + + if (rewriteType == RewriteType.updateHeader || rewriteType == RewriteType.removeHeader) { + dataController.text = widget.request?.headers.toRawHeaders() ?? ''; + return; + } + + dataController.clear(); + } + + bool onMatch = false; //是否正在匹配 + bool isMatch = true; + + onInputChangeMatch() { + if (onMatch || dataController.highlightEnabled == false) { + return; + } + onMatch = true; + + //高亮显示 + Future.delayed(const Duration(milliseconds: 500), () { + onMatch = false; + if (dataController.text.isEmpty) { + if (isMatch) return; + setState(() { + isMatch = true; + }); + return; + } + + setState(() { + bool isRemove = [RewriteType.removeHeader, RewriteType.removeQueryParam].contains(rewriteType); + String key = keyController.text; + if (isRemove && key.isNotEmpty) { + if (rewriteType == RewriteType.removeHeader) { + key = '$key: ${valueController.text}'; + } else { + key = '$key=${valueController.text}'; + } + } + + var match = dataController.highlight(key, caseSensitive: rewriteType != RewriteType.updateHeader); + print('onChangeMatch $match'); + isMatch = match; + }); + }); + } + + Widget textField(String label, String hint, {bool required = false, int? lines, TextEditingController? controller}) { return Row(children: [ SizedBox(width: 60, child: Text(label)), - Expanded( - child: TextFormField( - initialValue: val, - style: const TextStyle(fontSize: 14), - maxLines: 2, - validator: (val) => val?.isNotEmpty == true || !required ? null : "", - onSaved: onSaved, - decoration: InputDecoration( - hintText: hint, - hintStyle: TextStyle(color: Colors.grey.shade500, fontSize: 14), - contentPadding: const EdgeInsets.all(10), - errorStyle: const TextStyle(height: 0, fontSize: 0), - focusedBorder: focusedBorder(), - isDense: true, - border: const OutlineInputBorder()), - )) + Expanded(child: formField(hint, required: required, lines: lines, controller: controller)) ]); } + Widget formField(String hint, {bool required = false, int? lines, TextEditingController? controller}) { + return TextFormField( + controller: controller, + style: const TextStyle(fontSize: 14), + minLines: lines ?? 1, + maxLines: lines ?? 2, + validator: (val) => val?.isNotEmpty == true || !required ? null : "", + decoration: InputDecoration( + hintText: hint, + hintStyle: TextStyle(color: Colors.grey.shade500, fontSize: 14), + contentPadding: const EdgeInsets.all(10), + errorStyle: const TextStyle(height: 0, fontSize: 0), + focusedBorder: focusedBorder(), + isDense: true, + border: const OutlineInputBorder()), + ); + } + InputBorder focusedBorder() { return OutlineInputBorder(borderSide: BorderSide(color: Theme.of(context).colorScheme.primary, width: 2)); } @@ -244,7 +352,7 @@ class _UpdateListState extends State { Widget build(BuildContext context) { return Container( padding: const EdgeInsets.only(top: 10), - height: 290, + constraints: const BoxConstraints(minHeight: 330), decoration: BoxDecoration(border: Border.all(color: Colors.grey.withOpacity(0.2))), child: SingleChildScrollView( child: Column(children: [ diff --git a/lib/ui/desktop/toolbar/setting/script.dart b/lib/ui/desktop/toolbar/setting/script.dart index 14dcc94..9592e5b 100644 --- a/lib/ui/desktop/toolbar/setting/script.dart +++ b/lib/ui/desktop/toolbar/setting/script.dart @@ -398,7 +398,7 @@ class _ScriptEditState extends State { const SizedBox(height: 5), SizedBox( width: 850, - height: 360, + height: 380, child: CodeTheme( data: CodeThemeData(styles: monokaiSublimeTheme), child: SingleChildScrollView( diff --git a/lib/ui/mobile/menu/drawer.dart b/lib/ui/mobile/menu/drawer.dart index e41076f..b12c0af 100644 --- a/lib/ui/mobile/menu/drawer.dart +++ b/lib/ui/mobile/menu/drawer.dart @@ -20,7 +20,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:network_proxy/network/bin/server.dart'; import 'package:network_proxy/network/components/host_filter.dart'; import 'package:network_proxy/network/components/request_block_manager.dart'; -import 'package:network_proxy/network/components/request_rewrite_manager.dart'; +import 'package:network_proxy/network/components/rewrite/request_rewrite_manager.dart'; import 'package:network_proxy/network/http/http.dart'; import 'package:network_proxy/storage/histories.dart'; import 'package:network_proxy/ui/component/toolbox.dart'; @@ -94,7 +94,7 @@ class DrawerWidget extends StatelessWidget { title: Text(localizations.requestRewrite), leading: const Icon(Icons.replay_outlined), onTap: () async { - var requestRewrites = await RequestRewrites.instance; + var requestRewrites = await RequestRewriteManager.instance; if (context.mounted) { navigator(context, MobileRequestRewrite(requestRewrites: requestRewrites)); } diff --git a/lib/ui/mobile/menu/me.dart b/lib/ui/mobile/menu/me.dart index 2079a30..131fa26 100644 --- a/lib/ui/mobile/menu/me.dart +++ b/lib/ui/mobile/menu/me.dart @@ -18,7 +18,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:network_proxy/network/bin/server.dart'; import 'package:network_proxy/network/components/request_block_manager.dart'; -import 'package:network_proxy/network/components/request_rewrite_manager.dart'; +import 'package:network_proxy/network/components/rewrite/request_rewrite_manager.dart'; import 'package:network_proxy/storage/histories.dart'; import 'package:network_proxy/ui/component/utils.dart'; import 'package:network_proxy/ui/configuration.dart'; @@ -97,7 +97,7 @@ class _MePageState extends State { leading: Icon(Icons.replay_outlined, color: color), trailing: const Icon(Icons.arrow_forward_ios, size: 16), onTap: () async { - var requestRewrites = await RequestRewrites.instance; + var requestRewrites = await RequestRewriteManager.instance; if (context.mounted) { navigator(context, MobileRequestRewrite(requestRewrites: requestRewrites)); } diff --git a/lib/ui/mobile/request/favorite.dart b/lib/ui/mobile/request/favorite.dart index 71178b9..f6f7161 100644 --- a/lib/ui/mobile/request/favorite.dart +++ b/lib/ui/mobile/request/favorite.dart @@ -23,7 +23,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_toastr/flutter_toastr.dart'; import 'package:network_proxy/network/bin/server.dart'; -import 'package:network_proxy/network/components/request_rewrite_manager.dart'; +import 'package:network_proxy/network/components/rewrite/request_rewrite_manager.dart'; import 'package:network_proxy/network/components/script_manager.dart'; import 'package:network_proxy/network/host_port.dart'; import 'package:network_proxy/network/http/http.dart'; @@ -274,22 +274,9 @@ class _FavoriteItemState extends State<_FavoriteItem> { onPressed: () async { Navigator.maybePop(context); bool isRequest = request.response == null; - var requestRewrites = await RequestRewrites.instance; + var requestRewrites = await RequestRewriteManager.instance; - var ruleType = isRequest ? RuleType.requestReplace : RuleType.responseReplace; - var url = '${request.remoteDomain()}${request.path()}'; - var rule = requestRewrites.rules.firstWhere((it) => it.matchUrl(url, ruleType), - orElse: () => RequestRewriteRule(type: ruleType, url: url)); - - var rewriteItems = await requestRewrites.getRewriteItems(rule); - RewriteType rewriteType = - isRequest ? RewriteType.replaceRequestBody : RewriteType.replaceResponseBody; - if (!rewriteItems.any((element) => element.type == rewriteType)) { - rewriteItems.add(RewriteItem(rewriteType, true, - values: {'body': isRequest ? request.bodyAsString : request.response?.bodyAsString})); - } - - var pageRoute = MaterialPageRoute(builder: (_) => RewriteRule(rule: rule, items: rewriteItems)); + var pageRoute = MaterialPageRoute(builder: (_) => RewriteRule()); if (mounted) Navigator.push(context, pageRoute); }, label: localizations.requestRewrite, diff --git a/lib/ui/mobile/request/request.dart b/lib/ui/mobile/request/request.dart index 64810d9..20377c8 100644 --- a/lib/ui/mobile/request/request.dart +++ b/lib/ui/mobile/request/request.dart @@ -21,7 +21,8 @@ import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_toastr/flutter_toastr.dart'; import 'package:network_proxy/network/bin/server.dart'; -import 'package:network_proxy/network/components/request_rewrite_manager.dart'; +import 'package:network_proxy/network/components/rewrite/request_rewrite_manager.dart'; +import 'package:network_proxy/network/components/rewrite/rewrite_rule.dart'; import 'package:network_proxy/network/components/script_manager.dart'; import 'package:network_proxy/network/host_port.dart'; import 'package:network_proxy/network/http/http.dart'; @@ -274,7 +275,7 @@ class RequestRowState extends State { onPressed: () async { Navigator.maybePop(availableContext); bool isRequest = response == null; - var requestRewrites = await RequestRewrites.instance; + var requestRewrites = await RequestRewriteManager.instance; var ruleType = isRequest ? RuleType.requestReplace : RuleType.responseReplace; var url = '${request.remoteDomain()}${request.path()}'; @@ -284,10 +285,10 @@ class RequestRowState extends State { var rewriteItems = await requestRewrites.getRewriteItems(rule); RewriteType rewriteType = isRequest ? RewriteType.replaceRequestBody : RewriteType.replaceResponseBody; - if (!rewriteItems.any((element) => element.type == rewriteType)) { - rewriteItems.add(RewriteItem(rewriteType, true, - values: {'body': isRequest ? request.bodyAsString : response?.bodyAsString})); - } + // if (!rewriteItems?.any((element) => element.type == rewriteType)) { + // rewriteItems.add(RewriteItem(rewriteType, true, + // values: {'body': isRequest ? request.bodyAsString : response?.bodyAsString})); + // } var pageRoute = MaterialPageRoute(builder: (_) => RewriteRule(rule: rule, items: rewriteItems)); var context = availableContext; diff --git a/lib/ui/mobile/setting/request_rewrite.dart b/lib/ui/mobile/setting/request_rewrite.dart index c04889b..d511285 100644 --- a/lib/ui/mobile/setting/request_rewrite.dart +++ b/lib/ui/mobile/setting/request_rewrite.dart @@ -21,7 +21,9 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_toastr/flutter_toastr.dart'; -import 'package:network_proxy/network/components/request_rewrite_manager.dart'; +import 'package:network_proxy/network/components/rewrite/request_rewrite_manager.dart'; +import 'package:network_proxy/network/components/rewrite/rewrite_rule.dart'; +import 'package:network_proxy/network/http/http.dart'; import 'package:network_proxy/network/util/logger.dart'; import 'package:network_proxy/ui/component/utils.dart'; import 'package:network_proxy/ui/component/widgets.dart'; @@ -32,7 +34,7 @@ import 'package:url_launcher/url_launcher.dart'; import 'rewrite/rewrite_replace.dart'; class MobileRequestRewrite extends StatefulWidget { - final RequestRewrites requestRewrites; + final RequestRewriteManager requestRewrites; const MobileRequestRewrite({super.key, required this.requestRewrites}); @@ -133,7 +135,7 @@ class _MobileRequestRewriteState extends State { ///请求重写规则列表 class RequestRuleList extends StatefulWidget { - final RequestRewrites requestRewrites; + final RequestRewriteManager requestRewrites; RequestRuleList(this.requestRewrites) : super(key: GlobalKey<_RequestRuleListState>()); @@ -471,7 +473,7 @@ class _RewriteRuleState extends State { rule.url = urlInput.text; items = rewriteReplaceKey.currentState?.getItems() ?? rewriteUpdateKey.currentState?.getItems(); - var requestRewrites = await RequestRewrites.instance; + var requestRewrites = await RequestRewriteManager.instance; var index = requestRewrites.rules.indexOf(rule); if (index >= 0) { @@ -535,6 +537,17 @@ class _RewriteRuleState extends State { )); } + static List fromRequestItems(HttpRequest request, RuleType ruleType) { + if (ruleType == RuleType.requestReplace) { + //请求替换 + return RewriteItem.fromRequest(request); + } else if (ruleType == RuleType.responseReplace && request.response != null) { + //响应替换 + return RewriteItem.fromResponse(request.response!); + } + return []; + } + Widget rewriteRule() { if (ruleType == RuleType.requestUpdate || ruleType == RuleType.responseUpdate) { return MobileRewriteUpdate(key: rewriteUpdateKey, items: items, ruleType: ruleType); diff --git a/lib/ui/mobile/setting/rewrite/rewrite_replace.dart b/lib/ui/mobile/setting/rewrite/rewrite_replace.dart index 4070dce..1c739dd 100644 --- a/lib/ui/mobile/setting/rewrite/rewrite_replace.dart +++ b/lib/ui/mobile/setting/rewrite/rewrite_replace.dart @@ -18,7 +18,7 @@ import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:network_proxy/network/components/request_rewrite_manager.dart'; +import 'package:network_proxy/network/components/rewrite/rewrite_rule.dart'; import 'package:network_proxy/ui/component/state_component.dart'; import 'package:network_proxy/ui/component/widgets.dart'; import 'package:network_proxy/utils/lang.dart'; diff --git a/lib/ui/mobile/setting/rewrite/rewrite_update.dart b/lib/ui/mobile/setting/rewrite/rewrite_update.dart index 171cb97..0fdf50d 100644 --- a/lib/ui/mobile/setting/rewrite/rewrite_update.dart +++ b/lib/ui/mobile/setting/rewrite/rewrite_update.dart @@ -17,7 +17,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_toastr/flutter_toastr.dart'; -import 'package:network_proxy/network/components/request_rewrite_manager.dart'; +import 'package:network_proxy/network/components/rewrite/rewrite_rule.dart'; import 'package:network_proxy/ui/component/widgets.dart'; import 'package:network_proxy/utils/lang.dart'; diff --git a/lib/ui/mobile/widgets/remote_device.dart b/lib/ui/mobile/widgets/remote_device.dart index 9bc668e..6372f9c 100644 --- a/lib/ui/mobile/widgets/remote_device.dart +++ b/lib/ui/mobile/widgets/remote_device.dart @@ -24,7 +24,7 @@ import 'package:network_proxy/native/vpn.dart'; import 'package:network_proxy/network/bin/configuration.dart'; import 'package:network_proxy/network/bin/server.dart'; import 'package:network_proxy/network/components/host_filter.dart'; -import 'package:network_proxy/network/components/request_rewrite_manager.dart'; +import 'package:network_proxy/network/components/rewrite/request_rewrite_manager.dart'; import 'package:network_proxy/network/components/script_manager.dart'; import 'package:network_proxy/network/http_client.dart'; import 'package:network_proxy/network/util/logger.dart'; @@ -573,7 +573,7 @@ class ConfigSyncState extends State { widget.configuration.flushConfig(); if (syncRewrite) { - var requestRewrites = await RequestRewrites.instance; + var requestRewrites = await RequestRewriteManager.instance; await requestRewrites.syncConfig(widget.config['requestRewrites']); } diff --git a/pubspec.yaml b/pubspec.yaml index ec18af0..d013838 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: flutter_localizations: sdk: flutter intl: any - crypto: ^3.0.5 + crypto: ^3.0.6 cupertino_icons: ^1.0.2 basic_utils: ^5.7.0 logger: ^2.4.0