From 26511025cd882f6179d198dad4151a7e88f2bdf6 Mon Sep 17 00:00:00 2001 From: wanghongen Date: Mon, 7 Aug 2023 02:31:59 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A1=8C=E9=9D=A2=E7=AB=AF=E5=A4=8D=E6=9D=82?= =?UTF-8?q?=E6=90=9C=E7=B4=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/network/handler.dart | 6 +- lib/network/http/http.dart | 2 + lib/network/http/http_headers.dart | 2 + lib/ui/component/json/json_text.dart | 23 +- lib/ui/component/json/json_viewer.dart | 7 +- lib/ui/content/panel.dart | 11 +- lib/ui/desktop/left/domain.dart | 139 +++++++++--- lib/ui/desktop/left/model/search.dart | 61 ++++- lib/ui/desktop/left/search.dart | 57 ++++- lib/ui/desktop/left/search_condition.dart | 209 ++++++++++++++++++ .../toolbar/setting/request_rewrite.dart | 6 +- lib/ui/mobile/setting/request_rewrite.dart | 4 + lib/utils/lang.dart | 11 + pubspec.yaml | 2 +- 14 files changed, 480 insertions(+), 60 deletions(-) create mode 100644 lib/ui/desktop/left/search_condition.dart diff --git a/lib/network/handler.dart b/lib/network/handler.dart index de4fbd5..481fe73 100644 --- a/lib/network/handler.dart +++ b/lib/network/handler.dart @@ -70,6 +70,7 @@ class HttpChannelHandler extends ChannelHandler { void exceptionCaught(Channel channel, error, {StackTrace? trace}) { super.exceptionCaught(channel, error, trace: trace); HostAndPort? hostAndPort = channel.getAttribute(AttributeKeys.host); + hostAndPort ??= HostAndPort.host(channel.remoteAddress.host, channel.remotePort); String message = error.toString(); HttpStatus status = HttpStatus(-1, message); if (error is HandshakeException) { @@ -79,8 +80,9 @@ class HttpChannelHandler extends ChannelHandler { } else if (error is SocketException) { status = HttpStatus(-4, error.message); } - HttpRequest request = HttpRequest(HttpMethod.connect, hostAndPort?.domain ?? channel.remoteAddress.host) - ..body = message.codeUnits; + HttpRequest request = HttpRequest(HttpMethod.connect, hostAndPort.domain) + ..body = message.codeUnits + ..hostAndPort = hostAndPort; request.response = HttpResponse(status)..body = message.codeUnits; listener?.onRequest(channel, request); listener?.onResponse(channel, request.response!); diff --git a/lib/network/http/http.dart b/lib/network/http/http.dart index 19d66dc..090d98a 100644 --- a/lib/network/http/http.dart +++ b/lib/network/http/http.dart @@ -31,6 +31,7 @@ abstract class HttpMessage { "text/html": ContentType.html, "text/plain": ContentType.text, "application/x-www-form-urlencoded": ContentType.formUrl, + "form-data": ContentType.formData, "image": ContentType.image, "application/json": ContentType.json }; @@ -137,6 +138,7 @@ class HttpRequest extends HttpMessage { enum ContentType { json, formUrl, + formData, js, html, text, diff --git a/lib/network/http/http_headers.dart b/lib/network/http/http_headers.dart index 7098892..2b82772 100644 --- a/lib/network/http/http_headers.dart +++ b/lib/network/http/http_headers.dart @@ -114,6 +114,8 @@ class HttpHeaders { _originalHeaderNames.forEach(f); } + Iterable>> get entries => _originalHeaderNames.entries; + set contentType(String contentType) => set(CONTENT_TYPE, contentType); String get contentType => get(CONTENT_TYPE) ?? ""; diff --git a/lib/ui/component/json/json_text.dart b/lib/ui/component/json/json_text.dart index a0bd820..33322f6 100644 --- a/lib/ui/component/json/json_text.dart +++ b/lib/ui/component/json/json_text.dart @@ -1,3 +1,4 @@ +import 'dart:io'; import 'dart:math'; import 'package:flutter/foundation.dart'; @@ -16,7 +17,7 @@ class JsonText extends StatelessWidget { Widget build(BuildContext context) { var jsnParser = JsnParser(json, colorTheme, indent); var future = - compute((message) => message.getJsonTree(), jsnParser).catchError((error) => [Text(error.toString())]); + compute((message) => message.getJsonTree(), jsnParser).catchError((error) => [Text(error.toString())]); return FutureBuilder( future: future, @@ -25,15 +26,12 @@ class JsonText extends StatelessWidget { var textList = snapshot.data as List; Widget widget; - if (textList.length < 1000) { + if (textList.length < 2000) { widget = Column(crossAxisAlignment: CrossAxisAlignment.start, children: textList); } else { widget = SizedBox( width: double.infinity, - height: MediaQuery - .of(context) - .size - .height - 160, + height: MediaQuery.of(context).size.height - 160, child: ListView.builder( physics: const BouncingScrollPhysics(), controller: trackingScroll(), @@ -60,6 +58,17 @@ class JsonText extends StatelessWidget { } offset = trackingScroll.offset; }); + + if (Platform.isIOS) { + scrollController?.addListener(() { + if (scrollController!.offset >= scrollController!.position.maxScrollExtent) { + scrollController?.jumpTo(scrollController!.position.maxScrollExtent); + trackingScroll + .jumpTo(trackingScroll.offset + (scrollController!.offset - scrollController!.position.maxScrollExtent)); + } + }); + } + return trackingScroll; } } @@ -80,7 +89,7 @@ class JsnParser { textList.add(const Text('[')); textList.addAll(getArrayText(json)); } else { - textList.add(Text(json.toString())); + textList.add(Text(json == null ? '' : json.toString())); } return textList; } diff --git a/lib/ui/component/json/json_viewer.dart b/lib/ui/component/json/json_viewer.dart index e9b9bf7..312a6a9 100644 --- a/lib/ui/component/json/json_viewer.dart +++ b/lib/ui/component/json/json_viewer.dart @@ -68,6 +68,10 @@ class JsonObjectViewerState extends State { _getList() { List list = []; for (MapEntry entry in widget.jsonObj.entries) { + if (openFlag[entry.key] == null) { + openFlag[entry.key] = widget.notRoot == false && _isExtensible(entry.value); + } + list.add(Row( children: [ getKeyWidget(entry), @@ -78,9 +82,6 @@ class JsonObjectViewerState extends State { )); list.add(const SizedBox(height: 4)); - if (openFlag[entry.key] == null) { - openFlag[entry.key] = widget.notRoot == false && _isExtensible(entry.value); - } if ((openFlag[entry.key] ?? false) && entry.value != null) { list.add(getContentWidget(entry.value, widget.colorTheme)); } diff --git a/lib/ui/content/panel.dart b/lib/ui/content/panel.dart index 4c2a576..c027c65 100644 --- a/lib/ui/content/panel.dart +++ b/lib/ui/content/panel.dart @@ -129,10 +129,13 @@ class NetworkTabState extends State with SingleTickerProvi return const SizedBox(); } var scrollController = ScrollController(); - return ListView(controller: scrollController, children: [ - rowWidget("URI", Uri.decodeFull(widget.request.get()?.path() ?? '')), - ...message(widget.request.get(), "Request", scrollController) - ]); + var path = widget.request.get()?.path() ?? ''; + try { + path = Uri.decodeFull(widget.request.get()?.path() ?? ''); + } catch (_) {} + return ListView( + controller: scrollController, + children: [rowWidget("URI", path), ...message(widget.request.get(), "Request", scrollController)]); } Widget response() { diff --git a/lib/ui/desktop/left/domain.dart b/lib/ui/desktop/left/domain.dart index bc19d37..98f84a4 100644 --- a/lib/ui/desktop/left/domain.dart +++ b/lib/ui/desktop/left/domain.dart @@ -30,6 +30,7 @@ class DomainWidget extends StatefulWidget { class DomainWidgetState extends State { LinkedHashMap containerMap = LinkedHashMap(); + LinkedHashMap searchView = LinkedHashMap(); //搜索的内容 SearchModel? searchModel; @@ -49,9 +50,13 @@ class DomainWidgetState extends State { @override Widget build(BuildContext context) { var list = containerMap.values; + //根究搜素文本过滤 if (searchModel?.isNotEmpty == true) { - list = searchFilter(searchModel!); + searchView = searchFilter(searchModel!); + list = searchView.values; + } else { + searchView.clear(); } return Scaffold( @@ -64,14 +69,16 @@ class DomainWidgetState extends State { } ///搜索过滤 - List searchFilter(SearchModel searchModel) { - var result = []; + LinkedHashMap searchFilter(SearchModel searchModel) { + LinkedHashMap result = LinkedHashMap(); + containerMap.forEach((key, headerBody) { - var body = headerBody.filter(searchModel.keyword?.toLowerCase(), searchModel.contentType); + var body = headerBody.search(searchModel); if (body.isNotEmpty) { - result.add(headerBody.copy(body: body, selected: true)); + result[key] = headerBody.copy(body: body, selected: searchView[key]?.currentSelected); } }); + return result; } @@ -84,9 +91,9 @@ class DomainWidgetState extends State { if (headerBody != null) { headerBody.addBody(channel.id, listURI); - //搜索状态,刷新数据 - if (searchModel?.isNotEmpty == true) { - changeState(); + //搜索视图 + if (searchModel?.isNotEmpty == true && headerBody.filter(listURI, searchModel!)) { + searchView[hostAndPort]?.addBody(channel.id, listURI); } return; } @@ -108,7 +115,20 @@ class DomainWidgetState extends State { addResponse(Channel channel, HttpResponse response) { HostAndPort hostAndPort = channel.getAttribute(AttributeKeys.host); HeaderBody? headerBody = containerMap[hostAndPort]; - headerBody?.getBody(channel.id)?.add(response); + var pathRow = headerBody?.getBody(channel.id); + pathRow?.add(response); + if (pathRow == null) { + return; + } + + //搜索视图 + if (searchModel?.isNotEmpty == true && headerBody?.filter(pathRow, searchModel!) == true) { + var header = searchView[hostAndPort]; + if (header?.getBody(channel.id) == null) { + header?.addBody(channel.id, pathRow); + } + headerBody?.getBody(channel.id)?.add(response); + } } ///清理 @@ -122,6 +142,8 @@ class DomainWidgetState extends State { ///标题和内容布局 标题是域名 内容是域名下请求 class HeaderBody extends StatefulWidget { + final stateKey = GlobalKey<_HeaderBodyState>(); + //请求ID和请求的映射 final Map channelIdPathMap = HashMap(); @@ -144,8 +166,7 @@ class HeaderBody extends StatefulWidget { void addBody(String key, PathRow widget) { _body.addFirst(widget); channelIdPathMap[key] = widget; - var state = super.key as GlobalKey<_HeaderBodyState>; - state.currentState?.changeState(); + changeState(); } PathRow? getBody(String key) { @@ -153,39 +174,97 @@ class HeaderBody extends StatefulWidget { } ///根据文本过滤 - Iterable filter(String? keyword, ContentType? contentType) { - return _body.where((element) { - if (contentType != null && element.response.get()?.contentType != contentType) { - return false; - } - - if (keyword == null) { - return true; - } - if (element.request.method.name.toLowerCase() == keyword) { - return true; - } - if (element.request.requestUrl.toLowerCase().contains(keyword)) { - return true; - } - return element.response.get()?.contentType.name.toLowerCase().contains(keyword) == true; - }); + Iterable search(SearchModel searchModel) { + return _body.where((element) => filter(element, searchModel)); } ///复制 HeaderBody copy({Iterable? body, bool? selected}) { - var headerBody = - HeaderBody(header, selected: selected ?? this.selected, onRemove: onRemove, proxyServer: proxyServer); + var state = key as GlobalKey<_HeaderBodyState>; + var headerBody = HeaderBody(header, + selected: selected ?? state.currentState?.selected == true, onRemove: onRemove, proxyServer: proxyServer); if (body != null) { headerBody._body.addAll(body); } return headerBody; } + bool get currentSelected { + var state = key as GlobalKey<_HeaderBodyState>; + return state.currentState?.selected == true; + } + + changeState() { + var state = key as GlobalKey<_HeaderBodyState>; + state.currentState?.changeState(); + } + @override State createState() { return _HeaderBodyState(); } + + bool filter(PathRow element, SearchModel searchModel) { + var request = element.request; + var response = element.response.get(); + + if (searchModel.requestMethod != null && searchModel.requestMethod != request.method) { + return false; + } + if (searchModel.requestContentType != null && request.contentType != searchModel.requestContentType) { + return false; + } + + if (searchModel.responseContentType != null && response?.contentType != searchModel.responseContentType) { + return false; + } + if (searchModel.statusCode != null && response?.status.code != searchModel.statusCode) { + return false; + } + + if (searchModel.keyword == null || searchModel.keyword?.isEmpty == true || searchModel.searchOptions.isEmpty) { + return true; + } + + for (var option in searchModel.searchOptions) { + if (keywordFilter(searchModel.keyword!, option, request, response)) { + return true; + } + } + + return false; + } + + bool keywordFilter(String keyword, Option option, HttpRequest request, HttpResponse? response) { + if (option == Option.url && request.uri.toString().toLowerCase().contains(keyword.toLowerCase())) { + return true; + } + + if (option == Option.requestBody && request.bodyAsString.contains(keyword) == true) { + return true; + } + if (option == Option.responseBody && response?.bodyAsString.contains(keyword) == true) { + return true; + } + if (option == Option.method && request.method.name.toLowerCase() == keyword.toLowerCase()) { + return true; + } + if (option == Option.responseContentType && response?.headers.contentType.contains(keyword) == true) { + return true; + } + + if (option == Option.requestHeader || option == Option.responseHeader) { + print(response?.headers.entries); + var entries = option == Option.requestHeader ? request.headers.entries : response?.headers.entries ?? []; + + for (var entry in entries) { + if (entry.value.any((element) => element.contains(keyword))) { + return true; + } + } + } + return false; + } } class _HeaderBodyState extends State { diff --git a/lib/ui/desktop/left/model/search.dart b/lib/ui/desktop/left/model/search.dart index 8c75358..e9d8acf 100644 --- a/lib/ui/desktop/left/model/search.dart +++ b/lib/ui/desktop/left/model/search.dart @@ -1,17 +1,70 @@ import 'package:network_proxy/network/http/http.dart'; +/// @author wanghongen +/// 2023/8/4 class SearchModel { String? keyword; - ContentType? contentType; - SearchModel(this.keyword, this.contentType); + //搜索范围 + Set