From 69152ad974a1349163c5b3edbb413c8dcc4f06cc Mon Sep 17 00:00:00 2001 From: wanghongen Date: Mon, 18 Sep 2023 01:41:43 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A1=8C=E9=9D=A2=E7=AB=AF=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E5=8E=86=E5=8F=B2=E8=AE=B0=E5=BD=95=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/network/bin/server.dart | 38 ++- lib/network/handler.dart | 2 +- lib/network/host_port.dart | 2 +- lib/network/http_client.dart | 2 +- lib/network/util/host_filter.dart | 5 + lib/network/util/request_rewrite.dart | 5 + lib/storage/histories.dart | 149 +++++++++++ lib/ui/component/context_menu_region.dart | 96 +++++++ lib/ui/component/encoder.dart | 1 + lib/ui/component/utils.dart | 29 +++ lib/ui/component/widgets.dart | 30 +++ lib/ui/desktop/desktop.dart | 32 ++- lib/ui/desktop/left/domain.dart | 100 +++++--- lib/ui/desktop/left/favorite.dart | 14 +- lib/ui/desktop/left/history.dart | 234 ++++++++++++++++++ lib/ui/desktop/left/path.dart | 25 +- .../toolbar/setting/request_rewrite.dart | 42 +++- lib/ui/desktop/toolbar/setting/setting.dart | 12 +- lib/ui/desktop/toolbar/ssl/ssl.dart | 7 +- lib/ui/mobile/mobile.dart | 5 +- lib/ui/mobile/request/request_editor.dart | 4 +- lib/ui/mobile/setting/request_rewrite.dart | 31 ++- lib/utils/har.dart | 108 ++++++-- pubspec.lock | 24 +- pubspec.yaml | 1 - test/tests.dart | 3 + 26 files changed, 847 insertions(+), 154 deletions(-) create mode 100644 lib/storage/histories.dart create mode 100644 lib/ui/component/context_menu_region.dart create mode 100644 lib/ui/component/widgets.dart create mode 100644 lib/ui/desktop/left/history.dart diff --git a/lib/network/bin/server.dart b/lib/network/bin/server.dart index dca8c98..c0ce0d4 100644 --- a/lib/network/bin/server.dart +++ b/lib/network/bin/server.dart @@ -18,6 +18,8 @@ import 'dart:async'; import 'dart:io'; import 'package:network_proxy/network/bin/configuration.dart'; +import 'package:network_proxy/network/channel.dart'; +import 'package:network_proxy/network/http/http.dart'; import '../handler.dart'; import '../http/codec.dart'; @@ -36,12 +38,12 @@ class ProxyServer { Server? server; //请求事件监听 - EventListener? listener; + List listeners = []; //配置 final Configuration configuration; - ProxyServer(this.configuration, {this.listener}); + ProxyServer(this.configuration); //是否启动 bool get isRunning => server?.isRunning ?? false; @@ -67,8 +69,11 @@ class ProxyServer { Server server = Server(configuration); server.initChannel((channel) { - channel.pipeline.handle(HttpRequestCodec(), HttpResponseCodec(), - HttpChannelHandler(listener: listener, requestRewrites: configuration.requestRewrites)); + channel.pipeline.handle( + HttpRequestCodec(), + HttpResponseCodec(), + HttpChannelHandler( + listener: CombinedEventListener(listeners), requestRewrites: configuration.requestRewrites)); }); return server.bind(port).then((serverSocket) { @@ -106,4 +111,29 @@ class ProxyServer { restart() { stop().then((value) => start()); } + + ///添加监听器 + addListener(EventListener listener) { + listeners.add(listener); + } +} + +class CombinedEventListener extends EventListener { + final List listeners; + + CombinedEventListener(this.listeners); + + @override + void onRequest(Channel channel, HttpRequest request) { + for (var element in listeners) { + element.onRequest(channel, request); + } + } + + @override + void onResponse(Channel channel, HttpResponse response) { + for (var element in listeners) { + element.onResponse(channel, response); + } + } } diff --git a/lib/network/handler.dart b/lib/network/handler.dart index 30a0116..f4af301 100644 --- a/lib/network/handler.dart +++ b/lib/network/handler.dart @@ -103,7 +103,7 @@ class HttpChannelHandler extends ChannelHandler { /// 转发请求 Future forward(Channel channel, HttpRequest httpRequest) async { - log.i("[${channel.id}] ${httpRequest.method.name} ${httpRequest.requestUrl}"); + // log.i("[${channel.id}] ${httpRequest.method.name} ${httpRequest.requestUrl}"); if (channel.error != null) { _exceptionHandler(channel, httpRequest, channel.error); return; diff --git a/lib/network/host_port.dart b/lib/network/host_port.dart index c978e66..c86f30f 100644 --- a/lib/network/host_port.dart +++ b/lib/network/host_port.dart @@ -63,7 +63,7 @@ class HostAndPort { List hostAndPort = domain.split(":"); if (hostAndPort.length == 2) { bool isSsl = ssl ?? hostAndPort[1] == "443"; - scheme = isSsl ? httpsScheme : httpScheme; + scheme ??= isSsl ? httpsScheme : httpScheme; return HostAndPort(scheme, hostAndPort[0], int.parse(hostAndPort[1])); } scheme ??= (ssl == true ? httpsScheme : httpScheme); diff --git a/lib/network/http_client.dart b/lib/network/http_client.dart index 4039d74..f0298fb 100644 --- a/lib/network/http_client.dart +++ b/lib/network/http_client.dart @@ -109,7 +109,7 @@ class HttpClients { {ProxyInfo? proxyInfo, Duration timeout = const Duration(seconds: 3)}) async { if (request.headers.host == null || request.headers.host?.trim().isEmpty == true) { try { - request.headers.host = Uri.parse(request.uri).host; + request.headers.host = '${Uri.parse(request.uri).host}:${Uri.parse(request.uri).port}'; } catch (_) {} } diff --git a/lib/network/util/host_filter.dart b/lib/network/util/host_filter.dart index f2d17ac..40f5c44 100644 --- a/lib/network/util/host_filter.dart +++ b/lib/network/util/host_filter.dart @@ -29,11 +29,13 @@ class HostFilter { } } +/// abstract class HostList { /// 列表 final List list = []; bool enabled = false; + ///加载配置 void load(Map? map) { if (map == null) { return; @@ -62,6 +64,7 @@ abstract class HostList { } } + // json序列化 Map toJson() { return { 'list': list.map((e) => e.pattern).toList(), @@ -70,8 +73,10 @@ abstract class HostList { } } +///白名单 class Whites extends HostList {} +///黑名单 class Blacks extends HostList { Blacks() { enabled = true; diff --git a/lib/network/util/request_rewrite.dart b/lib/network/util/request_rewrite.dart index 7a5a5a3..97f7703 100644 --- a/lib/network/util/request_rewrite.dart +++ b/lib/network/util/request_rewrite.dart @@ -1,5 +1,6 @@ /// @author wanghongen /// 2023/7/26 +/// 请求重写 class RequestRewrites { bool enabled = true; final List rules = []; @@ -11,6 +12,7 @@ class RequestRewrites { static RequestRewrites get instance => _instance; + //加载配置 load(Map? map) { if (map == null) { return; @@ -23,6 +25,7 @@ class RequestRewrites { }); } + /// RequestRewriteRule? findRequestRewrite(String? domain, String? url, RuleType type) { if (!enabled || url == null) { return null; @@ -54,6 +57,7 @@ class RequestRewrites { return null; } + // void addRule(RequestRewriteRule rule) { rules.removeWhere((it) => it.path == rule.path && it.domain == rule.domain); rules.add(rule); @@ -110,6 +114,7 @@ class RequestRewriteRule { {this.name, this.type = RuleType.body, this.queryParam, this.requestBody, this.responseBody, this.redirectUrl}) : urlReg = RegExp(path.replaceAll("*", ".*")); + /// factory RequestRewriteRule.formJson(Map map) { return RequestRewriteRule(map['enabled'] == true, map['path'], map['domain'], name: map['name'], diff --git a/lib/storage/histories.dart b/lib/storage/histories.dart new file mode 100644 index 0000000..6edafbb --- /dev/null +++ b/lib/storage/histories.dart @@ -0,0 +1,149 @@ +import 'dart:collection'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:network_proxy/network/http/http.dart'; +import 'package:network_proxy/utils/har.dart'; +import 'package:path_provider/path_provider.dart'; + +///历史存储 +class HistoryStorage { + static HistoryStorage? _instance; + + HistoryStorage._internal(); + + static final LinkedHashMap _histories = LinkedHashMap(); + + static final Map> _requests = {}; + + ///单例 + static Future get instance async { + if (_instance == null) { + _instance = HistoryStorage._internal(); + await _init(); + } + return _instance!; + } + + //初始化 + static Future _init() async { + var file = await _path; + if (await file.exists()) { + var content = await file.readAsString(); + if (content.trim().isEmpty) { + return; + } + final Map data = jsonDecode(content); + for (var entry in data.entries) { + _histories[entry.key] = HistoryItem.formJson(entry.value); + } + } + } + + /// 获取历史记录 + Map get histories { + return _histories; + } + + //获取配置路径 + static Future get _path async { + final directory = await getApplicationSupportDirectory(); + var file = File('${directory.path}${Platform.pathSeparator}histories.json'); + if (!await file.exists()) { + await file.create(recursive: true); + } + return file; + } + + ///打开文件 + static Future openFile(String name) async { + final directory = await getApplicationSupportDirectory(); + var file = File('${directory.path}${Platform.pathSeparator}history${Platform.pathSeparator}$name'); + return file.create(recursive: true); + } + + /// 添加历史记录 + void addHistory(String name, File file, int requestLength) async { + var size = await file.length(); + _histories[name] = HistoryItem(file.path, requestLength, size); + (await _path).writeAsString(jsonEncode(_histories)); + } + + //更新 + updateHistory(String name, HistoryItem item) async { + _histories[name] = item; + (await _path).writeAsString(jsonEncode(_histories)); + } + + //获取 + HistoryItem getHistory(String name) { + return _histories[name]!; + } + + ///删除 + void removeHistory(String name) async { + var history = _histories.remove(name); + if (history == null) { + return; + } + var file = File(history.path); + if (await file.exists()) { + await file.delete(); + } + (await _path).writeAsString(jsonEncode(_histories)); + } + + //获取请求列表 + Future> getRequests(String name) async { + var request = _requests[name]; + if (request == null) { + HistoryItem history = _histories[name]!; + var file = File(history.path); + _requests[name] = await Har.readFile(file); + histories[name]?.requestLength = _requests[name]!.length; + file.length().then((size) => histories[name]?.fileSize = size); + } + return _requests[name]!; + } + + void removeCache(String name) { + _requests.remove(name); + } +} + +/// 历史记录 +class HistoryItem { + final String path; // 文件路径 + int requestLength = 0; // 请求数量 + int? fileSize; // 文件大小 + + HistoryItem(this.path, this.requestLength, this.fileSize); + + //json反序列化 + factory HistoryItem.formJson(Map map) { + return HistoryItem(map['path'], map['requestLength'], map['fileSize']); + } + + //json序列化 + Map toJson() { + return { + 'path': path, + 'requestLength': requestLength, + 'fileSize': fileSize, + }; + } + + //获取文件大小 + String get size { + if (this.fileSize == null) { + return ""; + } + + int fileSize = this.fileSize!; + if (fileSize > 1024 * 1024) { + return "${(fileSize / 1024 / 1024).toStringAsFixed(1)}MB"; + } + + return "${(fileSize / 1024).toStringAsFixed(1)}KB"; + } +} diff --git a/lib/ui/component/context_menu_region.dart b/lib/ui/component/context_menu_region.dart new file mode 100644 index 0000000..72690c4 --- /dev/null +++ b/lib/ui/component/context_menu_region.dart @@ -0,0 +1,96 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +typedef ContextMenuBuilder = List Function(); + +/// 根据用户手势显示和隐藏上下文菜单。 +/// 默认情况下,在右键单击和长按时显示菜单。 +class ContextMenuRegion extends StatefulWidget { + const ContextMenuRegion({ + super.key, + required this.child, + required this.contextMenuBuilder, + }); + + /// Builds the context menu. + final ContextMenuBuilder contextMenuBuilder; + + /// The child widget that will be listened to for gestures. + final Widget child; + + @override + State createState() => _ContextMenuRegionState(); +} + +class _ContextMenuRegionState extends State { + Offset? _longPressOffset; + + final ContextMenuController _contextMenuController = ContextMenuController(); + + static bool get _longPressEnabled { + switch (defaultTargetPlatform) { + case TargetPlatform.android: + case TargetPlatform.iOS: + return true; + case TargetPlatform.macOS: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + return false; + } + } + + void _onSecondaryTapUp(TapUpDetails details) { + _show(details.globalPosition); + } + + void _onTap() { + if (!_contextMenuController.isShown) { + return; + } + _hide(); + } + + void _onLongPressStart(LongPressStartDetails details) { + _longPressOffset = details.globalPosition; + } + + void _onLongPress() { + assert(_longPressOffset != null); + _show(_longPressOffset!); + _longPressOffset = null; + } + + void _show(Offset position) { + _contextMenuController.show( + context: context, + contextMenuBuilder: (context) { + return AdaptiveTextSelectionToolbar.buttonItems( + buttonItems: widget.contextMenuBuilder.call(), + anchors: TextSelectionToolbarAnchors(primaryAnchor: position)); + }, + ); + } + + void _hide() { + _contextMenuController.remove(); + } + + @override + void dispose() { + _hide(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onSecondaryTapUp: _onSecondaryTapUp, + onTap: _onTap, + onLongPress: _longPressEnabled ? _onLongPress : null, + onLongPressStart: _longPressEnabled ? _onLongPressStart : null, + child: widget.child, + ); + } +} diff --git a/lib/ui/component/encoder.dart b/lib/ui/component/encoder.dart index adb0eec..7915a2c 100644 --- a/lib/ui/component/encoder.dart +++ b/lib/ui/component/encoder.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_toastr/flutter_toastr.dart'; +///编码类型 enum EncoderType { url, base64, diff --git a/lib/ui/component/utils.dart b/lib/ui/component/utils.dart index 542792d..5a9dade 100644 --- a/lib/ui/component/utils.dart +++ b/lib/ui/component/utils.dart @@ -96,3 +96,32 @@ void unSelect(EditableTextState editableTextState) { SelectionChangedCause.tap, ); } + +Widget futureWidget(Future future, Widget Function(T data) toWidget) { + return FutureBuilder( + future: future, + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + if (snapshot.hasError) { + print(snapshot.error); + } + return toWidget(snapshot.requireData); + } + return const SizedBox(); + }, + ); +} + +showContextMenu(BuildContext context, Offset offset, {required List items}) { + showMenu( + context: context, + surfaceTintColor: + Brightness.dark == Theme.of(context).brightness ? null : Theme.of(context).colorScheme.primaryContainer, + position: RelativeRect.fromLTRB( + offset.dx, + offset.dy - 50, + offset.dx, + offset.dy - 50, + ), + items: items); +} diff --git a/lib/ui/component/widgets.dart b/lib/ui/component/widgets.dart new file mode 100644 index 0000000..cd5e4a1 --- /dev/null +++ b/lib/ui/component/widgets.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; + +class CustomPopupMenuItem extends PopupMenuItem { + final Color? color; + + const CustomPopupMenuItem({ + super.key, + super.onTap, + super.height, + T? value, + bool enabled = true, + required Widget child, + this.color, + }) : super(value: value, enabled: enabled, child: child); + + @override + PopupMenuItemState> createState() => _CustomPopupMenuItemState(); +} + +class _CustomPopupMenuItemState extends PopupMenuItemState> { + @override + Widget build(BuildContext context) { + return Theme( + data: Theme.of(context).copyWith( + hoverColor: Theme.of(context).focusColor, + ), + child: super.build(context), + ); + } +} diff --git a/lib/ui/desktop/desktop.dart b/lib/ui/desktop/desktop.dart index bb80a21..564b480 100644 --- a/lib/ui/desktop/desktop.dart +++ b/lib/ui/desktop/desktop.dart @@ -4,10 +4,12 @@ import 'package:network_proxy/network/bin/server.dart'; import 'package:network_proxy/network/channel.dart'; import 'package:network_proxy/network/handler.dart'; import 'package:network_proxy/network/http/http.dart'; +import 'package:network_proxy/ui/component/state_component.dart'; +import 'package:network_proxy/ui/component/toolbox.dart'; import 'package:network_proxy/ui/content/panel.dart'; import 'package:network_proxy/ui/desktop/left/domain.dart'; import 'package:network_proxy/ui/desktop/left/favorite.dart'; -import 'package:network_proxy/ui/component/toolbox.dart'; +import 'package:network_proxy/ui/desktop/left/history.dart'; import 'package:network_proxy/ui/desktop/toolbar/toolbar.dart'; import '../component/split_view.dart'; @@ -26,13 +28,13 @@ class _DesktopHomePagePageState extends State implements EventL final PageController pageController = PageController(); final ValueNotifier _selectIndex = ValueNotifier(0); - late ProxyServer proxyServer; + late ProxyServer proxyServer = ProxyServer(widget.configuration); late NetworkTabController panel; final List destinations = const [ NavigationRailDestination(icon: Icon(Icons.workspaces), label: Text("抓包", style: TextStyle(fontSize: 12))), - // NavigationRailDestination(icon: Icon(Icons.history), label: Text("历史", style: TextStyle(fontSize: 12))), NavigationRailDestination(icon: Icon(Icons.favorite), label: Text("收藏", style: TextStyle(fontSize: 12))), + NavigationRailDestination(icon: Icon(Icons.history), label: Text("历史", style: TextStyle(fontSize: 12))), NavigationRailDestination(icon: Icon(Icons.construction), label: Text("工具箱", style: TextStyle(fontSize: 12))), ]; @@ -49,7 +51,7 @@ class _DesktopHomePagePageState extends State implements EventL @override void initState() { super.initState(); - proxyServer = ProxyServer(widget.configuration, listener: this); + proxyServer.addListener(this); panel = NetworkTabController(tabStyle: const TextStyle(fontSize: 16), proxyServer: proxyServer); if (widget.configuration.upgradeNoticeV2) { @@ -62,7 +64,6 @@ class _DesktopHomePagePageState extends State implements EventL @override Widget build(BuildContext context) { final domainWidget = DomainWidget(key: domainStateKey, proxyServer: proxyServer, panel: panel); - return Scaffold( appBar: Tab(child: Toolbar(proxyServer, domainStateKey, sideNotifier: _selectIndex)), body: Row( @@ -86,8 +87,14 @@ class _DesktopHomePagePageState extends State implements EventL ratio: 0.3, minRatio: 0.15, maxRatio: 0.9, - left: PageView( - controller: pageController, children: [domainWidget, Favorites(panel: panel), const Toolbox()]), + left: PageView(controller: pageController, physics: const NeverScrollableScrollPhysics(), children: [ + domainWidget, + Favorites(panel: panel), + KeepAliveWrapper( + child: HistoryPageWidget( + proxyServer: proxyServer, domainWidgetState: domainStateKey, panel: panel)), + const Toolbox() + ]), right: panel), ) ], @@ -130,12 +137,11 @@ class _DesktopHomePagePageState extends State implements EventL '提示:默认不会开启HTTPS抓包,请安装证书后再开启HTTPS抓包。\n' '点击的HTTPS抓包(加锁图标),选择安装根证书,按照提示操作即可。\n\n' '新增更新:\n' - '1. 增加工具箱,提供常用编解码,增加HTTP请求,可粘贴cURL格式解析发起请求;\n' - '2. 请求编辑发送可直接查看响应体,发送请求无需开启代理;\n' - '3. 详情增加快速请求重写, 复制cURL格式可直接导入Postman;\n' - '4. 增加请求收藏功能;\n' - '5. 主题增加Material3切换;\n' - '6. 请求删除&大响应体直接转发;', + '1. 增加历史记录功能;\n' + '2. 请求重写增加名称&URL参数重写;\n' + '3. 请求重写增加重定向;\n' + '4. 建立连接异常显示请求体;\n' + '5. 请求编辑重发响应体查看增加多种格式,详情Body体增加快速解码入口;', style: TextStyle(fontSize: 14))); }); } diff --git a/lib/ui/desktop/left/domain.dart b/lib/ui/desktop/left/domain.dart index 282fa2f..44140af 100644 --- a/lib/ui/desktop/left/domain.dart +++ b/lib/ui/desktop/left/domain.dart @@ -1,6 +1,5 @@ import 'dart:collection'; -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:network_proxy/network/bin/configuration.dart'; import 'package:network_proxy/network/bin/server.dart'; @@ -10,17 +9,21 @@ import 'package:network_proxy/network/http/http.dart'; import 'package:network_proxy/network/util/attribute_keys.dart'; import 'package:network_proxy/network/util/host_filter.dart'; import 'package:network_proxy/ui/component/transition.dart'; +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/left/model/search_model.dart'; import 'package:network_proxy/ui/desktop/left/path.dart'; -import 'package:network_proxy/ui/content/panel.dart'; import 'package:network_proxy/ui/desktop/left/search.dart'; ///左侧域名 class DomainWidget extends StatefulWidget { final NetworkTabController panel; final ProxyServer proxyServer; + final List? list; + final bool shrinkWrap; - const DomainWidget({super.key, required this.panel, required this.proxyServer}); + const DomainWidget({super.key, required this.panel, required this.proxyServer, this.list, this.shrinkWrap = true}); @override State createState() { @@ -28,7 +31,8 @@ class DomainWidget extends StatefulWidget { } } -class DomainWidgetState extends State with AutomaticKeepAliveClientMixin{ +class DomainWidgetState extends State with AutomaticKeepAliveClientMixin { + List container = []; LinkedHashMap containerMap = LinkedHashMap(); LinkedHashMap searchView = LinkedHashMap(); @@ -47,6 +51,22 @@ class DomainWidgetState extends State with AutomaticKeepAliveClien } } + @override + void initState() { + super.initState(); + widget.list?.forEach((request) { + var host = HostAndPort.of(request.requestUrl); + HeaderBody? headerBody = containerMap[host]; + if (headerBody == null) { + headerBody = HeaderBody(host, proxyServer: widget.panel.proxyServer, onRemove: () => remove(host)); + containerMap[host] = headerBody; + } + var listURI = + PathRow(request, widget.panel, proxyServer: widget.panel.proxyServer, remove: (it) => headerBody!.remove(it)); + headerBody.addBody(null, listURI); + }); + } + @override bool get wantKeepAlive => true; @@ -63,8 +83,11 @@ class DomainWidgetState extends State with AutomaticKeepAliveClien searchView.clear(); } + Widget body = widget.shrinkWrap + ? SingleChildScrollView(child: Column(children: list.toList())) + : ListView.builder(itemCount: list.length, cacheExtent: 1000, itemBuilder: (_, index) => list.elementAt(index)); return Scaffold( - body: SingleChildScrollView(child: Column(children: list.toList())), + body: body, bottomNavigationBar: Search(onSearch: (val) { setState(() { searchModel = val; @@ -88,11 +111,13 @@ class DomainWidgetState extends State with AutomaticKeepAliveClien ///添加请求 add(Channel channel, HttpRequest request) { + container.add(request); HostAndPort hostAndPort = channel.getAttribute(AttributeKeys.host); //按照域名分类 HeaderBody? headerBody = containerMap[hostAndPort]; if (headerBody != null) { - var listURI = PathRow(request, widget.panel, proxyServer: widget.proxyServer, remove: (it) => headerBody!.remove(it)); + var listURI = + PathRow(request, widget.panel, proxyServer: widget.proxyServer, remove: (it) => headerBody!.remove(it)); headerBody.addBody(channel.id, listURI); //搜索视图 @@ -103,7 +128,8 @@ class DomainWidgetState extends State with AutomaticKeepAliveClien } headerBody = HeaderBody(hostAndPort, proxyServer: widget.proxyServer, onRemove: () => remove(hostAndPort)); - var listURI = PathRow(request, widget.panel, proxyServer: widget.proxyServer, remove: (it) => headerBody!.remove(it)); + var listURI = + PathRow(request, widget.panel, proxyServer: widget.proxyServer, remove: (it) => headerBody!.remove(it)); headerBody.addBody(channel.id, listURI); setState(() { containerMap[hostAndPort] = headerBody!; @@ -113,6 +139,7 @@ class DomainWidgetState extends State with AutomaticKeepAliveClien remove(HostAndPort hostAndPort) { setState(() { containerMap.remove(hostAndPort); + container.removeWhere((element) => element.hostAndPort == hostAndPort); }); } @@ -140,6 +167,7 @@ class DomainWidgetState extends State with AutomaticKeepAliveClien clean() { widget.panel.change(null, null); setState(() { + container.clear(); containerMap.clear(); }); } @@ -156,7 +184,7 @@ class HeaderBody extends StatefulWidget { final ProxyServer proxyServer; //请求列表 - final Queue _body = Queue(); + final Queue body = Queue(); //是否选中 final bool selected; @@ -168,8 +196,11 @@ class HeaderBody extends StatefulWidget { : super(key: GlobalKey<_HeaderBodyState>()); ///添加请求 - void addBody(String key, PathRow widget) { - _body.addFirst(widget); + void addBody(String? key, PathRow widget) { + body.addFirst(widget); + if (key == null) { + return; + } channelIdPathMap[key] = widget; changeState(); } @@ -179,14 +210,15 @@ class HeaderBody extends StatefulWidget { } remove(PathRow pathRow) { - if (_body.remove(pathRow)) { + if (body.remove(pathRow)) { changeState(); } } ///根据文本过滤 Iterable search(SearchModel searchModel) { - return _body.where((element) => searchModel.filter(element.request, element.response.get())); + return body + .where((element) => searchModel.filter(element.request, element.response.get() ?? element.request.response)); } ///复制 @@ -195,7 +227,7 @@ class HeaderBody extends StatefulWidget { var headerBody = HeaderBody(header, selected: selected ?? state.currentState?.selected == true, onRemove: onRemove, proxyServer: proxyServer); if (body != null) { - headerBody._body.addAll(body); + headerBody.body.addAll(body); } return headerBody; } @@ -237,13 +269,13 @@ class _HeaderBodyState extends State { Widget build(BuildContext context) { return Column(children: [ _hostWidget(widget.header.domain), - Offstage(offstage: !selected, child: Column(children: widget._body.toList())) + Offstage(offstage: !selected, child: Column(children: widget.body.toList())) ]); } Widget _hostWidget(String title) { var host = GestureDetector( - onSecondaryLongPressDown: menu, + onSecondaryTapDown: menu, child: ListTile( minLeadingWidth: 25, leading: Icon(selected ? Icons.arrow_drop_down : Icons.arrow_right, size: 18), @@ -270,46 +302,42 @@ class _HeaderBodyState extends State { } //域名右键菜单 - menu(LongPressDownDetails details) { - showMenu( - context: context, - position: RelativeRect.fromLTRB( - details.globalPosition.dx, - details.globalPosition.dy, - details.globalPosition.dx, - details.globalPosition.dy, - ), + menu(TapDownDetails details) { + showContextMenu( + context, + details.globalPosition, items: [ - PopupMenuItem( - height: 38, - child: const Text("添加黑名单", style: TextStyle(fontSize: 14)), + CustomPopupMenuItem( + height: 35, + child: const Text("添加黑名单", style: TextStyle(fontSize: 13)), onTap: () { HostFilter.blacklist.add(widget.header.host); configuration.flushConfig(); }), - PopupMenuItem( - height: 38, - child: const Text("添加白名单", style: TextStyle(fontSize: 14)), + CustomPopupMenuItem( + height: 35, + child: const Text("添加白名单", style: TextStyle(fontSize: 13)), onTap: () { HostFilter.whitelist.add(widget.header.host); configuration.flushConfig(); }), - PopupMenuItem( - height: 38, - child: const Text("删除白名单", style: TextStyle(fontSize: 14)), + CustomPopupMenuItem( + height: 35, + child: const Text("删除白名单", style: TextStyle(fontSize: 13)), onTap: () { HostFilter.whitelist.remove(widget.header.host); configuration.flushConfig(); }), - PopupMenuItem(height: 38, child: const Text("删除", style: TextStyle(fontSize: 14)), onTap: () => _delete()), + const PopupMenuDivider(height: 0.3), + CustomPopupMenuItem( + height: 35, child: const Text("删除", style: TextStyle(fontSize: 13)), onTap: () => _delete()), ], ); } _delete() { widget.channelIdPathMap.clear(); - widget._body.clear(); + widget.body.clear(); widget.onRemove?.call(); } - } diff --git a/lib/ui/desktop/left/favorite.dart b/lib/ui/desktop/left/favorite.dart index 2c7f163..dcf7084 100644 --- a/lib/ui/desktop/left/favorite.dart +++ b/lib/ui/desktop/left/favorite.dart @@ -12,6 +12,7 @@ import 'package:network_proxy/network/http/http.dart'; import 'package:network_proxy/network/http_client.dart'; import 'package:network_proxy/storage/favorites.dart'; 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/utils/curl.dart'; import 'package:window_manager/window_manager.dart'; @@ -101,14 +102,9 @@ class _FavoriteItemState extends State<_FavoriteItem> { ///右键菜单 menu(LongPressDownDetails details) { - showMenu( - context: context, - position: RelativeRect.fromLTRB( - details.globalPosition.dx, - details.globalPosition.dy, - details.globalPosition.dx, - details.globalPosition.dy, - ), + showContextMenu( + context, + details.globalPosition, items: [ popupItem("复制请求链接", onTap: () { var requestUrl = widget.request.requestUrl; @@ -141,7 +137,7 @@ class _FavoriteItemState extends State<_FavoriteItem> { } PopupMenuItem popupItem(String text, {VoidCallback? onTap}) { - return PopupMenuItem(height: 38, onTap: onTap, child: Text(text, style: const TextStyle(fontSize: 14))); + return CustomPopupMenuItem(height: 35, onTap: onTap, child: Text(text, style: const TextStyle(fontSize: 13))); } ///请求编辑 diff --git a/lib/ui/desktop/left/history.dart b/lib/ui/desktop/left/history.dart new file mode 100644 index 0000000..72cbd16 --- /dev/null +++ b/lib/ui/desktop/left/history.dart @@ -0,0 +1,234 @@ +import 'dart:async'; +import 'dart:collection'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:date_format/date_format.dart'; +import 'package:flutter/material.dart'; +import 'package:network_proxy/network/bin/server.dart'; +import 'package:network_proxy/network/channel.dart'; +import 'package:network_proxy/network/handler.dart'; +import 'package:network_proxy/network/http/http.dart'; +import 'package:network_proxy/storage/histories.dart'; +import 'package:network_proxy/ui/component/utils.dart'; +import 'package:network_proxy/ui/component/widgets.dart'; +import 'package:network_proxy/utils/har.dart'; + +import '../../content/panel.dart'; +import 'domain.dart'; + +///历史记录 +class HistoryPageWidget extends StatelessWidget { + final ProxyServer proxyServer; + final GlobalKey domainWidgetState; + final NetworkTabController panel; + + const HistoryPageWidget({super.key, required this.proxyServer, required this.domainWidgetState, required this.panel}); + + @override + Widget build(BuildContext context) { + return Navigator( + onGenerateRoute: (settings) { + switch (settings.name) { + case "/domain": + return MaterialPageRoute(builder: (_) => domainWidget(settings.arguments as Map)); + default: + return MaterialPageRoute( + builder: (_) => futureWidget( + HistoryStorage.instance, + (storage) => _HistoryWidget(storage, + container: domainWidgetState.currentState!.container, proxyServer: proxyServer), + )); + } + }, + ); + } + + Widget domainWidget(Map arguments) { + return Scaffold( + appBar: PreferredSize( + preferredSize: const Size.fromHeight(40), + child: AppBar( + leading: BackButton(style: ButtonStyle(iconSize: MaterialStateProperty.all(15))), + centerTitle: false, + title: Text(arguments['title'], style: const TextStyle(fontSize: 14)), + )), + body: futureWidget(HistoryStorage.instance.then((value) => value.getRequests(arguments['name'])), (data) { + return DomainWidget(panel: panel, proxyServer: proxyServer, list: data, shrinkWrap: false); + })); + } +} + +class _HistoryWidget extends StatefulWidget { + // 存储 + final HistoryStorage storage; + final List container; + final ProxyServer proxyServer; + + const _HistoryWidget(this.storage, {required this.container, required this.proxyServer}); + + @override + State createState() { + return _HistoryState(); + } +} + +class _HistoryState extends State<_HistoryWidget> implements EventListener { + ///是否保存会话 + static bool _sessionSaved = false; + static WriteTask? writeTask; + + // 存储 + late HistoryStorage storage; + + late List container; + late ProxyServer proxyServer; + + @override + void initState() { + super.initState(); + storage = widget.storage; + container = widget.container; + proxyServer = widget.proxyServer; + } + + @override + Widget build(BuildContext context) { + print("_HistoryState build"); + List children = []; + if (!_sessionSaved) { + //当前会话未保存,是否保存当前会话 + children.add(buildSaveSession(container)); + } + + var entries = storage.histories.entries; + for (int i = entries.length - 1; i >= 0; i--) { + var entry = entries.elementAt(i); + children.add(buildItem(context, entry.key, entry.value)); + } + + return ListView.separated( + itemCount: children.length, + itemBuilder: (_, index) => children[index], + separatorBuilder: (_, index) => const Divider(thickness: 0.3, height: 0), + ); + } + + //构建保存会话 + Widget buildSaveSession(List container) { + var name = formatDate(DateTime.now(), [mm, '-', d, ' ', HH, ':', nn, ':', ss]); + + return ListTile( + dense: true, + title: Text(name), + subtitle: Text("当前会话未保存 记录数 ${container.length}"), + trailing: TextButton.icon( + icon: const Icon(Icons.save), + label: const Text("保存"), + onPressed: () async { + await _writeHarFile(container, name); + setState(() { + _sessionSaved = true; + }); + }, + ), + onTap: () => ContextMenuController.removeAny()); + } + + //构建历史记录 + Widget buildItem(BuildContext context, String name, HistoryItem item) { + return GestureDetector( + onSecondaryTapDown: (details) => { + showContextMenu(context, details.globalPosition, items: [ + CustomPopupMenuItem( + height: 35, + child: const Text('删除', style: TextStyle(fontSize: 13)), + onTap: () { + setState(() { + if (name == writeTask?.name) { + writeTask?.timer?.cancel(); + writeTask?.open.close(); + } + storage.removeHistory(name); + }); + }) + ]) + }, + child: ListTile( + dense: true, + title: Text(name), + subtitle: Text("记录数 ${item.requestLength} 文件 ${item.size}"), + onTap: () { + ContextMenuController.removeAny(); + Navigator.pushNamed(context, '/domain', + arguments: {'title': '$name 记录数 ${item.requestLength}', 'name': name}) + .then((value) => Future.delayed(const Duration(seconds: 60), () => storage.removeCache(name))); + })); + } + + //写入文件 + _writeHarFile(List container, String name) async { + var file = await HistoryStorage.openFile("${DateTime.now().millisecondsSinceEpoch}.txt"); + print(file); + RandomAccessFile open = await file.open(mode: FileMode.append); + storage.addHistory(name, file, 0); + + writeTask = WriteTask(name, open, storage, callback: () => setState(() {})); + writeTask?.writeList.addAll(container); + writeTask?.startTask(); + + proxyServer.addListener(this); + } + + @override + void onRequest(Channel channel, HttpRequest request) {} + + @override + void onResponse(Channel channel, HttpResponse response) async { + if (response.request == null) { + return; + } + writeTask?.writeList.add(response.request!); + } +} + +class WriteTask { + final HistoryStorage historyStorage; + final RandomAccessFile open; + Queue writeList = Queue(); + Timer? timer; + final Function? callback; + final String name; + + WriteTask(this.name, this.open, this.historyStorage, {this.callback}); + + //写入任务 + startTask() { + timer = Timer.periodic(const Duration(seconds: 15), (it) => writeTask()); + } + + //写入任务 + writeTask() async { + if (writeList.isEmpty) { + return; + } + var history = historyStorage.getHistory(name); + int length = history.requestLength; + + while (writeList.isNotEmpty) { + var request = writeList.removeFirst(); + var har = Har.toHar(request); + + await open.writeString(jsonEncode(har)); + await open.writeString(",\n"); + length++; + } + + await open.flush(); //刷新 + + history.requestLength = length; + history.fileSize = await open.length(); + historyStorage.updateHistory(name, history); + callback?.call(); + } +} diff --git a/lib/ui/desktop/left/path.dart b/lib/ui/desktop/left/path.dart index 5eca772..ced1933 100644 --- a/lib/ui/desktop/left/path.dart +++ b/lib/ui/desktop/left/path.dart @@ -12,6 +12,7 @@ import 'package:network_proxy/network/http/http.dart'; import 'package:network_proxy/network/http_client.dart'; import 'package:network_proxy/storage/favorites.dart'; 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/utils/curl.dart'; import 'package:network_proxy/utils/lang.dart'; @@ -55,11 +56,12 @@ class _PathRowState extends State { title = '${request.method.name} ${Uri.parse(request.uri).path}'; } catch (_) {} var time = formatDate(request.requestTime, [HH, ':', nn, ':', ss]); + return GestureDetector( - onSecondaryLongPressDown: menu, + onSecondaryTapDown: menu, child: ListTile( minLeadingWidth: 25, - leading: getIcon(widget.response.get()), + leading: getIcon(widget.response.get() ?? widget.request.response), title: Text(title, overflow: TextOverflow.ellipsis, maxLines: 1), subtitle: Text( '$time - [${response?.status.code ?? ''}] ${response?.contentType.name.toUpperCase() ?? ''} ${response?.costTime() ?? ''} ', @@ -75,15 +77,10 @@ class _PathRowState extends State { } ///右键菜单 - menu(LongPressDownDetails details) { - showMenu( - context: context, - position: RelativeRect.fromLTRB( - details.globalPosition.dx, - details.globalPosition.dy, - details.globalPosition.dx, - details.globalPosition.dy, - ), + menu(TapDownDetails details) { + showContextMenu( + context, + details.globalPosition, items: [ popupItem("复制请求链接", onTap: () { var requestUrl = widget.request.requestUrl; @@ -97,6 +94,7 @@ class _PathRowState extends State { Clipboard.setData(ClipboardData(text: curlRequest(widget.request))) .then((value) => FlutterToastr.show('已复制到剪切板', context)); }), + const PopupMenuDivider(height: 0.3), popupItem("重放请求", onTap: () { var request = widget.request.copy(uri: widget.request.requestUrl); HttpClients.proxyRequest(request); @@ -112,6 +110,7 @@ class _PathRowState extends State { FavoriteStorage.addFavorite(widget.request); FlutterToastr.show('收藏成功', context); }), + const PopupMenuDivider(height: 0.3), popupItem("删除", onTap: () { widget.remove?.call(widget); }), @@ -120,7 +119,7 @@ class _PathRowState extends State { } PopupMenuItem popupItem(String text, {VoidCallback? onTap}) { - return PopupMenuItem(height: 38, onTap: onTap, child: Text(text, style: const TextStyle(fontSize: 14))); + return CustomPopupMenuItem(height: 32, onTap: onTap, child: Text(text, style: const TextStyle(fontSize: 13))); } ///请求编辑 @@ -157,6 +156,6 @@ class _PathRowState extends State { }); } selectedState = this; - widget.panel.change(widget.request, widget.response.get()); + widget.panel.change(widget.request, widget.response.get() ?? widget.request.response); } } diff --git a/lib/ui/desktop/toolbar/setting/request_rewrite.dart b/lib/ui/desktop/toolbar/setting/request_rewrite.dart index 6853f2a..c3a800e 100644 --- a/lib/ui/desktop/toolbar/setting/request_rewrite.dart +++ b/lib/ui/desktop/toolbar/setting/request_rewrite.dart @@ -147,7 +147,7 @@ class _RuleAddDialogState extends State { title: const Text("添加请求重写规则", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600)), scrollable: true, content: Container( - constraints: const BoxConstraints(minWidth: 320), + constraints: const BoxConstraints(minWidth: 350, minHeight: 460), child: Form( key: formKey, child: Column( @@ -158,22 +158,25 @@ class _RuleAddDialogState extends State { valueListenable: enableNotifier, builder: (_, bool enable, __) { return SwitchListTile( + dense: true, contentPadding: const EdgeInsets.only(left: 0), title: const Text('是否启用', textAlign: TextAlign.start), value: enable, onChanged: (value) => enableNotifier.value = value); }), TextFormField( - decoration: const InputDecoration(labelText: '名称'), + decoration: decoration('名称'), initialValue: rule.name, onSaved: (val) => rule.name = val, ), + const SizedBox(height: 5), TextFormField( - decoration: const InputDecoration(labelText: '域名(可选)', hintText: 'baidu.com 不需要填写HTTP'), + decoration: decoration('域名(可选)', hintText: 'baidu.com 不需要填写HTTP'), initialValue: rule.domain, onSaved: (val) => rule.domain = val?.trim()), + const SizedBox(height: 5), TextFormField( - decoration: const InputDecoration(labelText: 'Path', hintText: '/api/v1/*'), + decoration: decoration('Path', hintText: '/api/v1/*'), validator: (val) { if (val == null || val.isEmpty) { return 'Path不能为空'; @@ -182,9 +185,11 @@ class _RuleAddDialogState extends State { }, initialValue: rule.path, onSaved: (val) => rule.path = val!.trim()), + const SizedBox(height: 5), DropdownButtonFormField( - decoration: const InputDecoration(labelText: '行为'), value: rule.type, + isDense: true, + decoration: decoration('行为'), items: RuleType.values .map((e) => DropdownMenuItem(value: e, child: Text(e.name, style: const TextStyle(fontSize: 14)))) @@ -194,6 +199,7 @@ class _RuleAddDialogState extends State { rule.type = val!; }); }), + const SizedBox(height: 5), ...rewriteWidgets() ]))), actions: [ @@ -222,11 +228,25 @@ class _RuleAddDialogState extends State { ]); } + InputDecoration decoration(String label, {String? hintText}) { + Color color = Theme.of(context).colorScheme.primary; + // Color color = Colors.blueAccent; + + return InputDecoration( + labelText: label, + hintText: hintText, + isDense: true, + border: UnderlineInputBorder(borderSide: BorderSide(width: 0.3, color: color)), + enabledBorder: UnderlineInputBorder(borderSide: BorderSide(width: 0.3, color: color)), + focusedBorder: UnderlineInputBorder(borderSide: BorderSide(width: 1.5, color: color))); + } + List rewriteWidgets() { if (rule.type == RuleType.redirect) { return [ TextFormField( - decoration: const InputDecoration(labelText: '重定向到:', hintText: 'http://www.example.com/api'), + decoration: decoration('重定向到:', hintText: 'http://www.example.com/api'), + maxLines: 3, initialValue: rule.redirectUrl, onSaved: (val) => rule.redirectUrl = val, validator: (val) { @@ -241,20 +261,22 @@ class _RuleAddDialogState extends State { return [ TextFormField( initialValue: rule.queryParam, - decoration: const InputDecoration(labelText: 'URL参数替换为:'), + decoration: decoration('URL参数替换为:'), maxLines: 1, onSaved: (val) => rule.queryParam = val), + const SizedBox(height: 5), TextFormField( initialValue: rule.requestBody, - decoration: const InputDecoration(labelText: '请求体替换为:'), + decoration: decoration('请求体替换为:'), minLines: 1, maxLines: 5, onSaved: (val) => rule.requestBody = val), + const SizedBox(height: 5), TextFormField( initialValue: rule.responseBody, minLines: 3, - maxLines: 15, - decoration: const InputDecoration(labelText: '响应体替换为:', hintText: '{"code":"200","data":{}}'), + maxLines: 10, + decoration: decoration('响应体替换为:', hintText: '{"code":"200","data":{}}'), onSaved: (val) => rule.responseBody = val) ]; } diff --git a/lib/ui/desktop/toolbar/setting/setting.dart b/lib/ui/desktop/toolbar/setting/setting.dart index a93bf94..ef2475c 100644 --- a/lib/ui/desktop/toolbar/setting/setting.dart +++ b/lib/ui/desktop/toolbar/setting/setting.dart @@ -46,15 +46,13 @@ class _SettingState extends State { itemBuilder: (context) { return [ PopupMenuItem( - padding: const EdgeInsets.all(0), child: PortWidget(proxyServer: widget.proxyServer, textStyle: const TextStyle(fontSize: 13))), PopupMenuItem( - padding: const EdgeInsets.all(0), child: ValueListenableBuilder( - valueListenable: enableDesktopListenable, - builder: (_, val, __) => setSystemProxy(), - )), - const PopupMenuItem(padding: EdgeInsets.all(0), child: ThemeSetting(dense: true)), + valueListenable: enableDesktopListenable, + builder: (_, val, __) => setSystemProxy(), + )), + const PopupMenuItem(child: ThemeSetting(dense: true)), menuItem("域名过滤", onTap: hostFilter), menuItem("请求重写", onTap: requestRewrite), menuItem("外部代理设置", onTap: setExternalProxy), @@ -71,7 +69,6 @@ class _SettingState extends State { PopupMenuItem menuItem(String title, {GestureTapCallback? onTap}) { return PopupMenuItem( - padding: const EdgeInsets.all(0), child: ListTile( title: Text(title), dense: true, @@ -185,7 +182,6 @@ class _PortState extends State { @override Widget build(BuildContext context) { return Row(children: [ - const Padding(padding: EdgeInsets.only(left: 16)), Text("端口号:", style: widget.textStyle), SizedBox( width: 80, diff --git a/lib/ui/desktop/toolbar/ssl/ssl.dart b/lib/ui/desktop/toolbar/ssl/ssl.dart index e2971b8..725d451 100644 --- a/lib/ui/desktop/toolbar/ssl/ssl.dart +++ b/lib/ui/desktop/toolbar/ssl/ssl.dart @@ -29,10 +29,8 @@ class _SslState extends State { itemBuilder: (context) { return [ PopupMenuItem( - padding: const EdgeInsets.all(0), child: _Switch(proxyServer: widget.proxyServer, onEnableChange: (val) => setState(() {}))), PopupMenuItem( - padding: const EdgeInsets.all(0), child: ListTile( dense: true, hoverColor: Colors.transparent, @@ -44,7 +42,6 @@ class _SslState extends State { }, )), PopupMenuItem( - padding: const EdgeInsets.all(0), child: ListTile( title: const Text("安装根证书到 iOS"), dense: true, @@ -56,7 +53,6 @@ class _SslState extends State { }), ), PopupMenuItem( - padding: const EdgeInsets.all(0), child: ListTile( title: const Text("安装根证书到 Android"), dense: true, @@ -68,7 +64,6 @@ class _SslState extends State { }), ), PopupMenuItem( - padding: const EdgeInsets.all(0), child: ListTile( title: const Text("下载根证书"), dense: true, @@ -288,4 +283,4 @@ class _SwitchState extends State<_Switch> { widget.proxyServer.configuration.flushConfig(); } } -} \ No newline at end of file +} diff --git a/lib/ui/mobile/mobile.dart b/lib/ui/mobile/mobile.dart index 8b885fe..b5aea9a 100644 --- a/lib/ui/mobile/mobile.dart +++ b/lib/ui/mobile/mobile.dart @@ -43,7 +43,10 @@ class MobileHomeState extends State implements EventListener { @override void initState() { - proxyServer = ProxyServer(widget.configuration, listener: this); + proxyServer = ProxyServer(widget.configuration); + proxyServer.addListener(this); + + // 远程连接 desktop.addListener(() { if (desktop.value.connect) { proxyServer.configuration.remoteHost = "http://${desktop.value.host}:${desktop.value.port}"; diff --git a/lib/ui/mobile/request/request_editor.dart b/lib/ui/mobile/request/request_editor.dart index 6d73460..62e38d9 100644 --- a/lib/ui/mobile/request/request_editor.dart +++ b/lib/ui/mobile/request/request_editor.dart @@ -1,5 +1,3 @@ -import 'dart:convert'; - import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_toastr/flutter_toastr.dart'; @@ -135,7 +133,7 @@ class RequestEditorState extends State with SingleTickerPro tabController.animateTo(1); responseChange.value = !responseChange.value; }).catchError((e) { - FlutterToastr.show('请求失败', context); + FlutterToastr.show('请求失败$e', context); }); } } diff --git a/lib/ui/mobile/setting/request_rewrite.dart b/lib/ui/mobile/setting/request_rewrite.dart index e4ab80b..36626f4 100644 --- a/lib/ui/mobile/setting/request_rewrite.dart +++ b/lib/ui/mobile/setting/request_rewrite.dart @@ -167,7 +167,7 @@ class _RewriteRuleState extends State { ], ), body: Padding( - padding: const EdgeInsets.all(10), + padding: const EdgeInsets.all(15), child: Form( key: formKey, child: ListView(children: [ @@ -181,16 +181,16 @@ class _RewriteRuleState extends State { onChanged: (value) => enableNotifier.value = value); }), TextFormField( - decoration: const InputDecoration(labelText: '名称'), + decoration: decoration('名称'), initialValue: rule.name, onSaved: (val) => rule.name = val, ), TextFormField( - decoration: const InputDecoration(labelText: '域名(可选)', hintText: 'baidu.com 不需要填写HTTP'), + decoration: decoration('域名(可选)', hintText: 'baidu.com 不需要填写HTTP'), initialValue: rule.domain, onSaved: (val) => rule.domain = val?.trim()), TextFormField( - decoration: const InputDecoration(labelText: 'Path', hintText: '/api/v1/*'), + decoration: decoration('Path', hintText: '/api/v1/*'), validator: (val) { if (val == null || val.isEmpty) { return 'Path不能为空'; @@ -200,7 +200,7 @@ class _RewriteRuleState extends State { initialValue: rule.path, onSaved: (val) => rule.path = val!.trim()), DropdownButtonFormField( - decoration: const InputDecoration(labelText: '行为'), + decoration: decoration('行为'), value: rule.type, items: RuleType.values .map((e) => @@ -216,11 +216,24 @@ class _RewriteRuleState extends State { ); } + InputDecoration decoration(String label, {String? hintText}) { + Color color = Theme.of(context).colorScheme.primary; + // Color color = Colors.blueAccent; + + return InputDecoration( + labelText: label, + hintText: hintText, + isDense: true, + border: UnderlineInputBorder(borderSide: BorderSide(width: 0.3, color: color)), + enabledBorder: UnderlineInputBorder(borderSide: BorderSide(width: 0.3, color: color)), + focusedBorder: UnderlineInputBorder(borderSide: BorderSide(width: 1.5, color: color))); + } + List rewriteWidgets() { if (rule.type == RuleType.redirect) { return [ TextFormField( - decoration: const InputDecoration(labelText: '重定向到:', hintText: 'http://www.example.com/api'), + decoration: decoration('重定向到:', hintText: 'http://www.example.com/api'), initialValue: rule.redirectUrl, onSaved: (val) => rule.redirectUrl = val, validator: (val) { @@ -235,12 +248,12 @@ class _RewriteRuleState extends State { return [ TextFormField( initialValue: rule.queryParam, - decoration: const InputDecoration(labelText: 'URL参数替换为:'), + decoration: decoration('URL参数替换为:'), maxLines: 1, onSaved: (val) => rule.queryParam = val), TextFormField( initialValue: rule.requestBody, - decoration: const InputDecoration(labelText: '请求体替换为:'), + decoration: decoration('请求体替换为:'), minLines: 1, maxLines: 10, onSaved: (val) => rule.requestBody = val), @@ -248,7 +261,7 @@ class _RewriteRuleState extends State { initialValue: rule.responseBody, minLines: 3, maxLines: 15, - decoration: const InputDecoration(labelText: '响应体替换为:', hintText: '{"code":"200","data":{}}'), + decoration: decoration('响应体替换为:', hintText: '{"code":"200","data":{}}'), onSaved: (val) => rule.responseBody = val) ]; } diff --git a/lib/utils/har.dart b/lib/utils/har.dart index a528735..d44a2b3 100644 --- a/lib/utils/har.dart +++ b/lib/utils/har.dart @@ -1,12 +1,20 @@ +import 'dart:convert'; +import 'dart:io'; + import 'package:network_proxy/network/http/http.dart'; -class Hae { - List entries = []; +class Har { + static int maxBodyLength = 1024 * 1024 * 4; - void addEntry(HttpRequest request) { - entries.add({ - "startedDateTime": request.requestTime, // 请求发出的时间(ISO 8601) - "time": request.response?.responseTime.difference(request.requestTime).inMilliseconds, // 请求耗时(ms) + static List _entries(List list) { + return list.map((e) => toHar(e)).toList(); + } + + static Map toHar(HttpRequest request) { + bool isImage = request.response?.contentType == ContentType.image; + Map har = { + "startedDateTime": request.requestTime.toIso8601String(), // 请求发出的时间(ISO 8601) + "time": request.response?.responseTime.difference(request.requestTime).inMilliseconds, "request": { "method": request.method.name, // 请求方法 "url": request.requestUrl, // 请求地址 @@ -15,27 +23,13 @@ class Hae { "headers": _headers(request), // 请求头 "queryString": [], // 请求参数 "postData": { - "mimeType": request.contentType, // 请求体类型 + "mimeType": request.headers.contentType, // 请求体类型 "text": request.bodyAsString, // 请求体内容 }, "headersSize": -1, // 请求头大小 "bodySize": request.body?.length ?? -1, // 请求体大小 }, - 'response': { - "status": request.response?.status.code, // 响应状态码 - "statusText": request.response?.status.reasonPhrase, // 响应状态码描述 - "httpVersion": request.response?.protocolVersion, // HTTP协议版本 - "cookies": [], // 响应携带的cookie - "headers": _headers(request.response), // 响应头 - "content": { - "size": request.response?.body?.length, // 响应体大小 - "mimeType": request.response?.contentType, // 响应体类型 - "text": request.response?.bodyAsString, // 响应体内容 - }, - "redirectURL": '', // 重定向地址 - "headersSize": -1, // 响应头大小 - "bodySize": request.response?.body?.length ?? -1, // 响应体大小 - }, + "cache": {}, 'timings': { 'send': 0, @@ -43,19 +37,54 @@ class Hae { 'receive': 0, }, 'serverIPAddress': request.response?.remoteAddress - }); + }; + + if (request.response != null) { + har['response'] = { + "status": request.response?.status.code, // 响应状态码 + "statusText": request.response?.status.reasonPhrase, // 响应状态码描述 + "httpVersion": request.response?.protocolVersion, // HTTP协议版本 + "cookies": [], // 响应携带的cookie + "headers": _headers(request.response), // 响应头 + "content": { + "size": isImage ? 0 : request.response?.body?.length, // 响应体大小 + "mimeType": request.response?.headers.contentType, // 响应体类型 + "text": isImage ? '' : request.response?.bodyAsString, // 响应体内容 + }, + "redirectURL": '', // 重定向地址 + "headersSize": -1, // 响应头大小 + "bodySize": request.response?.body?.length ?? -1, // 响应体大小 + }; + } + return har; } - void toFile() { + static Future writeFile(List list, File file) async { + var entries = _entries(list); Map har = {}; har["log"] = { "version": "1.2", - "creator": {"name": "ProxyPin", "version": "1.0.1"}, + "creator": {"name": "ProxyPin", "version": "1.0.2"}, "entries": entries, }; + var json = jsonEncode(har); + return file.writeAsString(json); } - List _headers(HttpMessage? message) { + //读取文件 + static Future> readFile(File file) async { + var lines = await file.readAsLines(); + List list = []; + + for (var value in lines) { + var har = jsonDecode(value.substring(0, value.length - 1)); + var request = _toRequest(har); + list.add(request); + } + return list; + } + + static List _headers(HttpMessage? message) { var headers = >[]; message?.headers.forEach((name, values) { for (var element in values) { @@ -64,4 +93,31 @@ class Hae { }); return headers; } + + static HttpRequest _toRequest(Map har) { + var request = har['request']; + var method = request['method']; + List headers = request['headers']; + + var httpRequest = HttpRequest(HttpMethod.valueOf(method), request['url'], protocolVersion: request['httpVersion']); + httpRequest.body = request['postData']['text']?.toString().codeUnits; + for (var element in headers) { + httpRequest.headers.add(element['name'], element['value']); + } + var response = har['response']; + HttpResponse? httpResponse; + if (response != null && response['status'] != null) { + httpResponse = HttpResponse(HttpStatus.newStatus(response['status'], response['statusText']), + protocolVersion: response['httpVersion']); + httpResponse.body = response['content']['text']?.toString().codeUnits; + List responseHeaders = response['headers']; + for (var element in responseHeaders) { + httpResponse.headers.add(element['name'], element['value']); + } + } + + httpRequest.response = httpResponse; + httpResponse?.request = httpRequest; + return httpRequest; + } } diff --git a/pubspec.lock b/pubspec.lock index d9fe6b7..938804b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -85,10 +85,10 @@ packages: dependency: "direct main" description: name: cupertino_icons - sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be + sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d url: "https://pub.flutter-io.cn" source: hosted - version: "1.0.5" + version: "1.0.6" date_format: dependency: "direct main" description: @@ -498,26 +498,26 @@ packages: dependency: transitive description: name: url_launcher_ios - sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2" + sha256: "7c65021d5dee51813d652357bc65b8dd4a6177082a9966bc8ba6ee477baa795f" url: "https://pub.flutter-io.cn" source: hosted - version: "6.1.4" + version: "6.1.5" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: "207f4ddda99b95b4d4868320a352d374b0b7e05eefad95a4a26f57da413443f5" + sha256: b651aad005e0cb06a01dbd84b428a301916dc75f0e7ea6165f80057fee2d8e8e url: "https://pub.flutter-io.cn" source: hosted - version: "3.0.5" + version: "3.0.6" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "1c4fdc0bfea61a70792ce97157e5cc17260f61abbe4f39354513f39ec6fd73b1" + sha256: b55486791f666e62e0e8ff825e58a023fd6b1f71c49926483f1128d3bbd8fe88 url: "https://pub.flutter-io.cn" source: hosted - version: "3.0.6" + version: "3.0.7" url_launcher_platform_interface: dependency: transitive description: @@ -538,10 +538,10 @@ packages: dependency: transitive description: name: url_launcher_windows - sha256: "7967065dd2b5fccc18c653b97958fdf839c5478c28e767c61ee879f4e7882422" + sha256: "95fef3129dc7cfaba2bc3d5ba2e16063bb561fc6d78e63eee16162bc70029069" url: "https://pub.flutter-io.cn" source: hosted - version: "3.0.7" + version: "3.0.8" uuid: dependency: transitive description: @@ -586,10 +586,10 @@ packages: dependency: transitive description: name: xdg_directories - sha256: f0c26453a2d47aa4c2570c6a033246a3fc62da2fe23c7ffdd0a7495086dc0247 + sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2" url: "https://pub.flutter-io.cn" source: hosted - version: "1.0.2" + version: "1.0.3" sdks: dart: ">=3.1.0 <4.0.0" flutter: ">=3.13.0" diff --git a/pubspec.yaml b/pubspec.yaml index 9ab21f5..0901249 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -31,7 +31,6 @@ dependencies: share_plus: ^7.1.0 brotli: ^0.6.0 installed_apps: ^1.3.1 - dev_dependencies: flutter_test: sdk: flutter diff --git a/test/tests.dart b/test/tests.dart index fdfe25d..9da7384 100644 --- a/test/tests.dart +++ b/test/tests.dart @@ -1,6 +1,9 @@ import 'dart:io'; void main() { + print(DateTime.now().toIso8601String()); + print(DateTime.now().toString()); + print(DateTime.now().toUtc().toString()); print(Platform.version); print(Platform.localHostname); print(Platform.operatingSystem);