桌面端增加历史记录功能

This commit is contained in:
wanghongen
2023-09-18 01:41:43 +08:00
parent 36e32d298c
commit 69152ad974
26 changed files with 847 additions and 154 deletions

View File

@@ -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<EventListener> 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<EventListener> 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);
}
}
}

View File

@@ -103,7 +103,7 @@ class HttpChannelHandler extends ChannelHandler<HttpRequest> {
/// 转发请求
Future<void> 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;

View File

@@ -63,7 +63,7 @@ class HostAndPort {
List<String> 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);

View File

@@ -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 (_) {}
}

View File

@@ -29,11 +29,13 @@ class HostFilter {
}
}
///
abstract class HostList {
/// 列表
final List<RegExp> list = [];
bool enabled = false;
///加载配置
void load(Map<String, dynamic>? map) {
if (map == null) {
return;
@@ -62,6 +64,7 @@ abstract class HostList {
}
}
// json序列化
Map<String, dynamic> 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;

View File

@@ -1,5 +1,6 @@
/// @author wanghongen
/// 2023/7/26
/// 请求重写
class RequestRewrites {
bool enabled = true;
final List<RequestRewriteRule> rules = [];
@@ -11,6 +12,7 @@ class RequestRewrites {
static RequestRewrites get instance => _instance;
//加载配置
load(Map<String, dynamic>? 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<String, dynamic> map) {
return RequestRewriteRule(map['enabled'] == true, map['path'], map['domain'],
name: map['name'],

149
lib/storage/histories.dart Normal file
View File

@@ -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<String, HistoryItem> _histories = LinkedHashMap<String, HistoryItem>();
static final Map<String, List<HttpRequest>> _requests = {};
///单例
static Future<HistoryStorage> get instance async {
if (_instance == null) {
_instance = HistoryStorage._internal();
await _init();
}
return _instance!;
}
//初始化
static Future<void> _init() async {
var file = await _path;
if (await file.exists()) {
var content = await file.readAsString();
if (content.trim().isEmpty) {
return;
}
final Map<dynamic, dynamic> data = jsonDecode(content);
for (var entry in data.entries) {
_histories[entry.key] = HistoryItem.formJson(entry.value);
}
}
}
/// 获取历史记录
Map<String, HistoryItem> get histories {
return _histories;
}
//获取配置路径
static Future<File> 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<File> 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<List<HttpRequest>> 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<String, dynamic> map) {
return HistoryItem(map['path'], map['requestLength'], map['fileSize']);
}
//json序列化
Map<String, dynamic> 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";
}
}

View File

@@ -0,0 +1,96 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
typedef ContextMenuBuilder = List<ContextMenuButtonItem> 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<ContextMenuRegion> createState() => _ContextMenuRegionState();
}
class _ContextMenuRegionState extends State<ContextMenuRegion> {
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,
);
}
}

View File

@@ -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,

View File

@@ -96,3 +96,32 @@ void unSelect(EditableTextState editableTextState) {
SelectionChangedCause.tap,
);
}
Widget futureWidget<T>(Future<T> future, Widget Function(T data) toWidget) {
return FutureBuilder<T>(
future: future,
builder: (BuildContext context, AsyncSnapshot<T> 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<PopupMenuEntry> 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);
}

View File

@@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
class CustomPopupMenuItem<T> extends PopupMenuItem<T> {
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<T, CustomPopupMenuItem<T>> createState() => _CustomPopupMenuItemState<T>();
}
class _CustomPopupMenuItemState<T> extends PopupMenuItemState<T, CustomPopupMenuItem<T>> {
@override
Widget build(BuildContext context) {
return Theme(
data: Theme.of(context).copyWith(
hoverColor: Theme.of(context).focusColor,
),
child: super.build(context),
);
}
}

View File

@@ -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<DesktopHomePage> implements EventL
final PageController pageController = PageController();
final ValueNotifier<int> _selectIndex = ValueNotifier(0);
late ProxyServer proxyServer;
late ProxyServer proxyServer = ProxyServer(widget.configuration);
late NetworkTabController panel;
final List<NavigationRailDestination> 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<DesktopHomePage> 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<DesktopHomePage> 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<DesktopHomePage> 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<DesktopHomePage> 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)));
});
}

View File

@@ -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<HttpRequest>? 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<StatefulWidget> createState() {
@@ -28,7 +31,8 @@ class DomainWidget extends StatefulWidget {
}
}
class DomainWidgetState extends State<DomainWidget> with AutomaticKeepAliveClientMixin{
class DomainWidgetState extends State<DomainWidget> with AutomaticKeepAliveClientMixin {
List<HttpRequest> container = [];
LinkedHashMap<HostAndPort, HeaderBody> containerMap = LinkedHashMap<HostAndPort, HeaderBody>();
LinkedHashMap<HostAndPort, HeaderBody> searchView = LinkedHashMap<HostAndPort, HeaderBody>();
@@ -47,6 +51,22 @@ class DomainWidgetState extends State<DomainWidget> 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<DomainWidget> 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<DomainWidget> 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<DomainWidget> 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<DomainWidget> with AutomaticKeepAliveClien
remove(HostAndPort hostAndPort) {
setState(() {
containerMap.remove(hostAndPort);
container.removeWhere((element) => element.hostAndPort == hostAndPort);
});
}
@@ -140,6 +167,7 @@ class DomainWidgetState extends State<DomainWidget> 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<PathRow> _body = Queue();
final Queue<PathRow> 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<PathRow> 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<HeaderBody> {
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<HeaderBody> {
}
//域名右键菜单
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: <PopupMenuEntry>[
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();
}
}

View File

@@ -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: <PopupMenuEntry>[
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)));
}
///请求编辑

View File

@@ -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> 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<HttpRequest> container;
final ProxyServer proxyServer;
const _HistoryWidget(this.storage, {required this.container, required this.proxyServer});
@override
State<StatefulWidget> createState() {
return _HistoryState();
}
}
class _HistoryState extends State<_HistoryWidget> implements EventListener {
///是否保存会话
static bool _sessionSaved = false;
static WriteTask? writeTask;
// 存储
late HistoryStorage storage;
late List<HttpRequest> 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<Widget> 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<HttpRequest> 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<HttpRequest> 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();
}
}

View File

@@ -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<PathRow> {
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<PathRow> {
}
///右键菜单
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: <PopupMenuEntry>[
popupItem("复制请求链接", onTap: () {
var requestUrl = widget.request.requestUrl;
@@ -97,6 +94,7 @@ class _PathRowState extends State<PathRow> {
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<PathRow> {
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<PathRow> {
}
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<PathRow> {
});
}
selectedState = this;
widget.panel.change(widget.request, widget.response.get());
widget.panel.change(widget.request, widget.response.get() ?? widget.request.response);
}
}

View File

@@ -147,7 +147,7 @@ class _RuleAddDialogState extends State<RuleAddDialog> {
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<RuleAddDialog> {
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<RuleAddDialog> {
},
initialValue: rule.path,
onSaved: (val) => rule.path = val!.trim()),
const SizedBox(height: 5),
DropdownButtonFormField<RuleType>(
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<RuleAddDialog> {
rule.type = val!;
});
}),
const SizedBox(height: 5),
...rewriteWidgets()
]))),
actions: [
@@ -222,11 +228,25 @@ class _RuleAddDialogState extends State<RuleAddDialog> {
]);
}
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<Widget> 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<RuleAddDialog> {
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)
];
}

View File

@@ -46,15 +46,13 @@ class _SettingState extends State<Setting> {
itemBuilder: (context) {
return [
PopupMenuItem<String>(
padding: const EdgeInsets.all(0),
child: PortWidget(proxyServer: widget.proxyServer, textStyle: const TextStyle(fontSize: 13))),
PopupMenuItem<String>(
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<Setting> {
PopupMenuItem<String> menuItem(String title, {GestureTapCallback? onTap}) {
return PopupMenuItem<String>(
padding: const EdgeInsets.all(0),
child: ListTile(
title: Text(title),
dense: true,
@@ -185,7 +182,6 @@ class _PortState extends State<PortWidget> {
@override
Widget build(BuildContext context) {
return Row(children: [
const Padding(padding: EdgeInsets.only(left: 16)),
Text("端口号:", style: widget.textStyle),
SizedBox(
width: 80,

View File

@@ -29,10 +29,8 @@ class _SslState extends State<SslWidget> {
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<SslWidget> {
},
)),
PopupMenuItem<String>(
padding: const EdgeInsets.all(0),
child: ListTile(
title: const Text("安装根证书到 iOS"),
dense: true,
@@ -56,7 +53,6 @@ class _SslState extends State<SslWidget> {
}),
),
PopupMenuItem<String>(
padding: const EdgeInsets.all(0),
child: ListTile(
title: const Text("安装根证书到 Android"),
dense: true,
@@ -68,7 +64,6 @@ class _SslState extends State<SslWidget> {
}),
),
PopupMenuItem<String>(
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();
}
}
}
}

View File

@@ -43,7 +43,10 @@ class MobileHomeState extends State<MobileHomePage> 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}";

View File

@@ -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<MobileRequestEditor> with SingleTickerPro
tabController.animateTo(1);
responseChange.value = !responseChange.value;
}).catchError((e) {
FlutterToastr.show('请求失败', context);
FlutterToastr.show('请求失败$e', context);
});
}
}

View File

@@ -167,7 +167,7 @@ class _RewriteRuleState extends State<RewriteRule> {
],
),
body: Padding(
padding: const EdgeInsets.all(10),
padding: const EdgeInsets.all(15),
child: Form(
key: formKey,
child: ListView(children: <Widget>[
@@ -181,16 +181,16 @@ class _RewriteRuleState extends State<RewriteRule> {
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<RewriteRule> {
initialValue: rule.path,
onSaved: (val) => rule.path = val!.trim()),
DropdownButtonFormField<RuleType>(
decoration: const InputDecoration(labelText: '行为'),
decoration: decoration('行为'),
value: rule.type,
items: RuleType.values
.map((e) =>
@@ -216,11 +216,24 @@ class _RewriteRuleState extends State<RewriteRule> {
);
}
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<Widget> 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<RewriteRule> {
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<RewriteRule> {
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)
];
}

View File

@@ -1,12 +1,20 @@
import 'dart:convert';
import 'dart:io';
import 'package:network_proxy/network/http/http.dart';
class Hae {
List<Map> 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<Map> _entries(List<HttpRequest> 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<File> writeFile(List<HttpRequest> 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<Map> _headers(HttpMessage? message) {
//读取文件
static Future<List<HttpRequest>> readFile(File file) async {
var lines = await file.readAsLines();
List<HttpRequest> 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<Map> _headers(HttpMessage? message) {
var headers = <Map<String, String>>[];
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;
}
}

View File

@@ -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"

View File

@@ -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

View File

@@ -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);