请求编辑,多窗口支持

This commit is contained in:
wanghongen
2023-07-21 18:47:12 +08:00
parent 3d980fb879
commit b2995efe37
77 changed files with 1018 additions and 164 deletions

View File

@@ -13,6 +13,6 @@ ios下载地址(Safari浏览器打开) https://testflight.apple.com/join/gURG
- [ ] 支持安卓微信小程序抓包安卓分为系统证书和用户证书下载的自签名根证书安装都是用户证书微信不信任用户证书不Root导致Https抓不了了, 目前市场上所有抓包软件抓不了微信的包,后面单独做个运行空间插件,动态反编译修改配置,信任用户证书来解决。
- [ ] WebSocket协议支持。
<img alt="image" width="600px" height="400px" src="https://github.com/wanghongenpin/network-proxy-flutter/assets/24794200/67a2feb1-f1c3-4c0c-8737-5abe62c34794">. <img alt="image" height="500px" src="https://github.com/wanghongenpin/network_proxy_flutter/assets/24794200/1bb4b1ec-ec5c-44a7-add7-f0f94c8765b9">
<img alt="image" width="500px" height="400px" src="https://github.com/wanghongenpin/network-proxy-flutter/assets/24794200/67a2feb1-f1c3-4c0c-8737-5abe62c34794">. <img alt="image" height="500px" src="https://github.com/wanghongenpin/network_proxy_flutter/assets/24794200/1bb4b1ec-ec5c-44a7-add7-f0f94c8765b9">

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 58 KiB

View File

@@ -2,117 +2,115 @@
"images": [
{
"size": "20x20",
"idiom": "iphone",
"idiom": "universal",
"filename": "icon-20@2x.png",
"scale": "2x"
"scale": "2x",
"platform": "ios"
},
{
"size": "20x20",
"idiom": "iphone",
"idiom": "universal",
"filename": "icon-20@3x.png",
"scale": "3x"
"scale": "3x",
"platform": "ios"
},
{
"size": "29x29",
"idiom": "iphone",
"filename": "icon-29.png",
"scale": "1x"
},
{
"size": "29x29",
"idiom": "iphone",
"idiom": "universal",
"filename": "icon-29@2x.png",
"scale": "2x"
"scale": "2x",
"platform": "ios"
},
{
"size": "29x29",
"idiom": "iphone",
"idiom": "universal",
"filename": "icon-29@3x.png",
"scale": "3x"
"scale": "3x",
"platform": "ios"
},
{
"size": "38x38",
"idiom": "universal",
"filename": "icon-38@2x.png",
"scale": "2x",
"platform": "ios"
},
{
"size": "38x38",
"idiom": "universal",
"filename": "icon-38@3x.png",
"scale": "3x",
"platform": "ios"
},
{
"size": "40x40",
"idiom": "iphone",
"idiom": "universal",
"filename": "icon-40@2x.png",
"scale": "2x"
"scale": "2x",
"platform": "ios"
},
{
"size": "40x40",
"idiom": "iphone",
"idiom": "universal",
"filename": "icon-40@3x.png",
"scale": "3x"
"scale": "3x",
"platform": "ios"
},
{
"size": "60x60",
"idiom": "iphone",
"idiom": "universal",
"filename": "icon-60@2x.png",
"scale": "2x"
"scale": "2x",
"platform": "ios"
},
{
"size": "60x60",
"idiom": "iphone",
"idiom": "universal",
"filename": "icon-60@3x.png",
"scale": "3x"
"scale": "3x",
"platform": "ios"
},
{
"size": "20x20",
"idiom": "ipad",
"filename": "icon-20-ipad.png",
"scale": "1x"
"size": "64x64",
"idiom": "universal",
"filename": "icon-64@2x.png",
"scale": "2x",
"platform": "ios"
},
{
"size": "20x20",
"idiom": "ipad",
"filename": "icon-20@2x-ipad.png",
"scale": "2x"
"size": "64x64",
"idiom": "universal",
"filename": "icon-64@3x.png",
"scale": "3x",
"platform": "ios"
},
{
"size": "29x29",
"idiom": "ipad",
"filename": "icon-29-ipad.png",
"scale": "1x"
},
{
"size": "29x29",
"idiom": "ipad",
"filename": "icon-29@2x-ipad.png",
"scale": "2x"
},
{
"size": "40x40",
"idiom": "ipad",
"filename": "icon-40.png",
"scale": "1x"
},
{
"size": "40x40",
"idiom": "ipad",
"filename": "icon-40@2x.png",
"scale": "2x"
"size": "68x68",
"idiom": "universal",
"filename": "icon-68@2x.png",
"scale": "2x",
"platform": "ios"
},
{
"size": "76x76",
"idiom": "ipad",
"filename": "icon-76.png",
"scale": "1x"
},
{
"size": "76x76",
"idiom": "ipad",
"idiom": "universal",
"filename": "icon-76@2x.png",
"scale": "2x"
"scale": "2x",
"platform": "ios"
},
{
"size": "83.5x83.5",
"idiom": "ipad",
"idiom": "universal",
"filename": "icon-83.5@2x.png",
"scale": "2x"
"scale": "2x",
"platform": "ios"
},
{
"size": "1024x1024",
"idiom": "ios-marketing",
"idiom": "universal",
"filename": "icon-1024.png",
"scale": "1x"
"scale": "1x",
"platform": "ios"
}
],
"info": {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 509 KiB

After

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

@@ -1,11 +1,15 @@
import 'dart:convert';
import 'dart:io';
import 'package:chinese_font_library/chinese_font_library.dart';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart';
import 'package:network_proxy/network/bin/server.dart';
import 'package:network_proxy/ui/component/split_view.dart';
import 'package:network_proxy/ui/content/body.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/request_editor.dart';
import 'package:network_proxy/ui/desktop/toolbar/toolbar.dart';
import 'package:network_proxy/ui/mobile/mobile.dart';
import 'package:network_proxy/utils/platform.dart';
@@ -15,31 +19,65 @@ import 'network/channel.dart';
import 'network/handler.dart';
import 'network/http/http.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
if (Platforms.isDesktop()) {
//设置窗口大小
await windowManager.ensureInitialized();
WindowOptions windowOptions = WindowOptions(
minimumSize: const Size(980, 600),
size: Platform.isMacOS ? const Size(1200, 750) : const Size(1080, 650),
center: true,
titleBarStyle: Platform.isMacOS ? TitleBarStyle.hidden : TitleBarStyle.normal);
windowManager.waitUntilReadyToShow(windowOptions, () async {
await windowManager.show();
await windowManager.focus();
});
void main(List<String> args) async {
if (Platforms.isMobile()) {
runApp(const FluentApp(MobileHomePage()));
return;
}
runApp(const FluentApp());
//多窗口
if (args.firstOrNull == 'multi_window') {
final windowId = int.parse(args[1]);
final argument = args[2].isEmpty ? const {} : jsonDecode(args[2]) as Map<String, dynamic>;
runApp(FluentApp(multiWindow(windowId, argument)));
return;
}
WidgetsFlutterBinding.ensureInitialized();
await windowManager.ensureInitialized();
//设置窗口大小
WindowOptions windowOptions = WindowOptions(
minimumSize: const Size(980, 600),
size: Platform.isMacOS ? const Size(1200, 750) : const Size(1080, 650),
center: true,
titleBarStyle: Platform.isMacOS ? TitleBarStyle.hidden : TitleBarStyle.normal);
windowManager.waitUntilReadyToShow(windowOptions, () async {
await windowManager.show();
await windowManager.focus();
});
runApp(const FluentApp(DesktopHomePage()));
}
///多窗口
Widget multiWindow(int windowId, Map<dynamic, dynamic> argument) {
if (argument['name'] == 'RequestEditor') {
return RequestEditor(
windowController: WindowController.fromWindowId(windowId),
request: HttpRequest.fromJson(argument['request']),
proxyPort: argument['proxyPort']);
}
if (argument['name'] == 'HttpBodyWidget') {
return HttpBodyWidget(
windowController: WindowController.fromWindowId(windowId),
httpMessage: HttpMessage.fromJson(argument['httpMessage']),
inNewWindow: true);
}
return const SizedBox();
}
/// 主题
final ValueNotifier<ThemeMode> themeNotifier = ValueNotifier(ThemeMode.system);
class FluentApp extends StatelessWidget {
const FluentApp({super.key});
final Widget home;
const FluentApp(
this.home, {
super.key,
});
@override
Widget build(BuildContext context) {
@@ -59,7 +97,7 @@ class FluentApp extends StatelessWidget {
theme: lightTheme,
darkTheme: darkTheme,
themeMode: currentMode,
home: Platforms.isDesktop() ? const DesktopHomePage() : const MobileHomePage(),
home: home,
);
});
}
@@ -74,9 +112,9 @@ class DesktopHomePage extends StatefulWidget {
class _DesktopHomePagePageState extends State<DesktopHomePage> implements EventListener {
final domainStateKey = GlobalKey<DomainWidgetState>();
final NetworkTabController panel = NetworkTabController(tabStyle: const TextStyle(fontSize: 18));
late ProxyServer proxyServer;
late NetworkTabController panel;
@override
void onRequest(Channel channel, HttpRequest request) {
@@ -92,6 +130,8 @@ class _DesktopHomePagePageState extends State<DesktopHomePage> implements EventL
void initState() {
super.initState();
proxyServer = ProxyServer(listener: this);
panel = NetworkTabController(tabStyle: const TextStyle(fontSize: 18), proxyServer: proxyServer);
proxyServer.initializedListener(() {
if (!proxyServer.guide) {
return;

View File

@@ -22,8 +22,11 @@ class ProxyServer {
//是否初始化
bool init = false;
int port = 9099;
//是否启用https抓包
bool _enableSsl = false;
//是否启用桌面抓包
bool enableDesktop = true;
//是否引导
@@ -33,7 +36,11 @@ class ProxyServer {
bool get isRunning => server?.isRunning ?? false;
Server? server;
//请求事件监听
EventListener? listener;
//请求重写
RequestRewrites requestRewrites = RequestRewrites();
final List<Function> _initializedListeners = [];
@@ -48,7 +55,8 @@ class ProxyServer {
Future<File> homeDir() async {
String? userHome;
if (Platforms.isDesktop()) {
userHome = Platform.environment['HOME'] ?? Platform.environment['USERPROFILE'];
userHome =
Platform.environment['HOME'] ?? Platform.environment['USERPROFILE'];
} else {
userHome = (await getApplicationSupportDirectory()).path;
}
@@ -93,8 +101,11 @@ class ProxyServer {
server.enableSsl = _enableSsl;
server.initChannel((channel) {
channel.pipeline.handle(HttpRequestCodec(), HttpResponseCodec(),
HttpChannelHandler(listener: listener, requestRewrites: requestRewrites));
channel.pipeline.handle(
HttpRequestCodec(),
HttpResponseCodec(),
HttpChannelHandler(
listener: listener, requestRewrites: requestRewrites));
});
return server.bind(port).then((serverSocket) {
logger.i("listen on $port");
@@ -163,7 +174,8 @@ class ProxyServer {
/// 加载请求重写配置文件
Future<void> _loadRequestRewriteConfig() async {
var home = await homeDir();
var file = File('${home.path}${Platform.pathSeparator}request_rewrite.json');
var file =
File('${home.path}${Platform.pathSeparator}request_rewrite.json');
var exits = await file.exists();
if (!exits) {
return;
@@ -178,7 +190,8 @@ class ProxyServer {
/// 保存请求重写配置文件
flushRequestRewriteConfig() async {
var home = await homeDir();
var file = File('${home.path}${Platform.pathSeparator}request_rewrite.json');
var file =
File('${home.path}${Platform.pathSeparator}request_rewrite.json');
bool exists = await file.exists();
if (!exists) {
await file.create(recursive: true);

View File

@@ -93,7 +93,6 @@ class HttpChannelHandler extends ChannelHandler<HttpRequest> {
/// 转发请求
Future<void> forward(Channel channel, HttpRequest httpRequest) async {
var remoteChannel = await _getRemoteChannel(channel, httpRequest);
//实现抓包代理转发

View File

@@ -28,6 +28,17 @@ abstract class HttpMessage {
HttpMessage(this.protocolVersion);
//json序列化
factory HttpMessage.fromJson(Map<String, dynamic> json) {
if (json["_class"] == "HttpRequest") {
return HttpRequest.fromJson(json);
}
return HttpResponse.fromJson(json);
}
Map<String, dynamic> toJson();
ContentType get contentType => contentTypes.entries
.firstWhere((element) => headers.contentType.contains(element.key),
orElse: () => const MapEntry("unknown", ContentType.http))
@@ -79,6 +90,24 @@ class HttpRequest extends HttpMessage {
return request;
}
@override
Map<String, dynamic> toJson() {
return {
'_class': 'HttpRequest',
'uri': requestUrl,
'method': method.name,
'headers': headers.toJson(),
'body': bodyAsString,
};
}
factory HttpRequest.fromJson(Map<String, dynamic> json) {
var request = HttpRequest(HttpMethod.valueOf(json['method']), json['uri']);
request.headers.addAll(HttpHeaders.fromJson(json['headers']));
request.body = utf8.encode(json['body']);
return request;
}
@override
String toString() {
return 'HttpReqeust{version: $protocolVersion, url: $uri, method: ${method.name}, headers: $headers, contentLength: $contentLength, bodyLength: ${body?.length}}';
@@ -102,6 +131,26 @@ class HttpResponse extends HttpMessage {
return '${responseTime.difference(request!.requestTime).inMilliseconds}ms';
}
factory HttpResponse.fromJson(Map<String, dynamic> json) {
return HttpResponse(json['protocolVersion'], HttpStatus(json['status']['code'], json['status']['reasonPhrase']))
..headers.addAll(HttpHeaders.fromJson(json['headers']))
..body = utf8.encode(json['body']);
}
@override
Map<String, dynamic> toJson() {
return {
'_class': 'HttpResponse',
'protocolVersion': protocolVersion,
'status': {
'code': status.code,
'reasonPhrase': status.reasonPhrase,
},
'headers': headers.toJson(),
'body': bodyAsString,
};
}
@override
String toString() {
return 'HttpResponse{status: ${status.code}, headers: $headers, contentLength: $contentLength, bodyLength: ${body?.length}}';
@@ -110,13 +159,13 @@ class HttpResponse extends HttpMessage {
///HTTP请求方法。
enum HttpMethod {
options("OPTIONS"),
get("GET"),
head("HEAD"),
post("POST"),
put("PUT"),
patch("PATCH"),
delete("DELETE"),
options("OPTIONS"),
head("HEAD"),
trace("TRACE"),
connect("CONNECT"),
propfind("PROPFIND"),

View File

@@ -13,6 +13,8 @@ class HttpHeaders {
// 由小写标头名称键入的原始标头名称。
final Map<String, List<String>> _originalHeaderNames = {};
HttpHeaders();
///设置header。
void set(String name, String value) {
_headers[name.toLowerCase()] = [value];
@@ -21,22 +23,29 @@ class HttpHeaders {
///添加header。
void add(String name, String value) {
if (_headers.containsKey(name.toLowerCase())) {
_headers[name.toLowerCase()]!.add(value);
if (!_originalHeaderNames.containsKey(name)) {
_originalHeaderNames[name] = [];
}
_originalHeaderNames[name]!.add(value);
return;
if (!_headers.containsKey(name.toLowerCase())) {
_headers[name.toLowerCase()] = [];
_originalHeaderNames[name] = [];
}
_headers[name.toLowerCase()] = [value];
_originalHeaderNames[name] = [value];
_headers[name.toLowerCase()]?.add(value);
_originalHeaderNames[name]?.add(value);
}
//批量添加
addAll(HttpHeaders headers) {
headers.forEach((key, values) {
///添加header。
void addValues(String name, List<String> values) {
if (!_headers.containsKey(name.toLowerCase())) {
_headers[name.toLowerCase()] = [];
_originalHeaderNames[name] = [];
}
_headers[name.toLowerCase()]?.addAll(values);
_originalHeaderNames[name]?.addAll(values);
}
///从headers中添加
addAll(HttpHeaders? headers) {
headers?.forEach((key, values) {
for (var val in values) {
add(key, val);
}
@@ -103,6 +112,28 @@ class HttpHeaders {
return sb.toString();
}
///转换json
Map<String, dynamic> toJson() {
Map<String, dynamic> json = {};
forEach((name, values) {
json[name] = values;
});
return json;
}
///从json解析
factory HttpHeaders.fromJson(Map<String, dynamic> json) {
HttpHeaders headers = HttpHeaders();
json.forEach((key, values) {
for (var element in (values as List)) {
headers.add(key, element.toString());
}
});
return headers;
}
@override
String toString() {
return 'HttpHeaders{$_originalHeaderNames}';

View File

@@ -48,7 +48,7 @@ class HttpClients {
/// 发送代理请求
static Future<HttpResponse> proxyRequest(String proxyHost, int port, HttpRequest request,
{Duration duration = const Duration(seconds: 3)}) async {
{Duration timeout = const Duration(seconds: 3)}) async {
var httpResponseHandler = HttpResponseHandler();
bool isHttps = request.uri.startsWith("https://");
@@ -60,14 +60,13 @@ class HttpClients {
if (isHttps) {
HttpRequest proxyRequest = HttpRequest(HttpMethod.connect, request.uri);
await channel.write(proxyRequest);
var proxyResp = await httpResponseHandler.getResponse(duration);
print(proxyResp);
await httpResponseHandler.getResponse(timeout);
channel.secureSocket = await SecureSocket.secure(channel.socket, onBadCertificate: (certificate) => true);
}
httpResponseHandler.resetResponse();
await channel.write(request);
return httpResponseHandler.getResponse(duration).whenComplete(() => channel.close());
return httpResponseHandler.getResponse(timeout).whenComplete(() => channel.close());
}
}
@@ -87,4 +86,10 @@ class HttpResponseHandler extends ChannelHandler<HttpResponse> {
void resetResponse() {
_completer = Completer<HttpResponse>();
}
@override
void channelInactive(Channel channel) {
log.i("[${channel.id}] channelInactive");
_completer.completeError("channelInactive");
}
}

View File

@@ -3,17 +3,20 @@ import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_toastr/flutter_toastr.dart';
import 'package:network_proxy/network/bin/server.dart';
import 'package:network_proxy/network/http/http.dart';
import 'package:network_proxy/ui/component/utils.dart';
import 'package:network_proxy/ui/mobile/request/request_editor.dart';
import 'package:network_proxy/utils/curl.dart';
import 'package:share_plus/share_plus.dart';
///分享按钮
class ShareWidget extends StatelessWidget {
final ProxyServer proxyServer;
final HttpRequest? request;
final HttpResponse? response;
const ShareWidget({super.key, this.request, this.response});
const ShareWidget({super.key, required this.proxyServer, this.request, this.response});
@override
Widget build(BuildContext context) {
@@ -52,6 +55,14 @@ class ShareWidget extends StatelessWidget {
name: "cURL.txt", mimeType: "txt");
Share.shareXFiles([file], text: "ProxyPin全平台抓包软件");
}),
PopupMenuItem(
child: const Text('编辑请求重放'),
onTap: () {
WidgetsBinding.instance.addPostFrameCallback((_) {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => MobileRequestEditor(request: request, proxyServer: proxyServer)));
});
}),
]);
});
}

View File

@@ -1,17 +1,22 @@
import 'dart:convert';
import 'dart:io';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_json_viewer_new/flutter_json_viewer.dart';
import 'package:flutter_toastr/flutter_toastr.dart';
import 'package:network_proxy/network/http/http.dart';
import 'package:network_proxy/ui/component/utils.dart';
import 'package:network_proxy/utils/platform.dart';
import 'package:window_manager/window_manager.dart';
class HttpBodyWidget extends StatefulWidget {
final HttpMessage? httpMessage;
final bool inNewWindow; //是否在新窗口
final WindowController? windowController;
const HttpBodyWidget({super.key, required this.httpMessage, this.inNewWindow = false});
const HttpBodyWidget({super.key, required this.httpMessage, this.inNewWindow = false, this.windowController});
@override
State<StatefulWidget> createState() {
@@ -23,9 +28,24 @@ class HttpBodyState extends State<HttpBodyWidget> {
ValueNotifier<int> tabIndex = ValueNotifier(0);
String? body;
@override
void initState() {
super.initState();
RawKeyboard.instance.addListener(onKeyEvent);
}
void onKeyEvent(RawKeyEvent event) {
if (event.isKeyPressed(LogicalKeyboardKey.metaLeft) && event.isKeyPressed(LogicalKeyboardKey.keyW)) {
RawKeyboard.instance.removeListener(onKeyEvent);
widget.windowController?.close();
return;
}
}
@override
void dispose() {
tabIndex.dispose();
RawKeyboard.instance.removeListener(onKeyEvent);
super.dispose();
}
@@ -53,17 +73,25 @@ class HttpBodyState extends State<HttpBodyWidget> {
) //body
];
return DefaultTabController(
var tabController = DefaultTabController(
length: tabs.list.length,
child: widget.inNewWindow
? ListView(children: list)
: Column(crossAxisAlignment: CrossAxisAlignment.start, children: list));
if (widget.inNewWindow) {
return Scaffold(
appBar: AppBar(title: titleWidget(inNewWindow: true), toolbarHeight: Platform.isWindows ? 36 : null),
body: tabController);
}
return tabController;
}
Widget titleWidget({inNewWindow = false}) {
var type = widget.httpMessage is HttpRequest ? "Request" : "Response";
return Row(
mainAxisAlignment: widget.inNewWindow ? MainAxisAlignment.center : MainAxisAlignment.start,
children: [
Text('$type Body', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(width: 15),
@@ -84,13 +112,26 @@ class HttpBodyState extends State<HttpBodyWidget> {
);
}
void openNew() {
void openNew() async {
if (Platforms.isDesktop()) {
var size = MediaQuery.of(context).size;
var ratio = 1.0;
if (Platform.isWindows) {
WindowManager.instance.getDevicePixelRatio();
}
final window = await DesktopMultiWindow.createWindow(jsonEncode(
{'name': 'HttpBodyWidget', 'httpMessage': widget.httpMessage, 'inNewWindow': true},
));
window
..setTitle(widget.httpMessage is HttpRequest ? '请求体' : '响应体')
..setFrame(const Offset(100, 100) & Size(800 * ratio, size.height * ratio))
..center()
..show();
return;
}
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => Scaffold(
appBar: AppBar(title: titleWidget(inNewWindow: true)),
body: HttpBodyWidget(httpMessage: widget.httpMessage, inNewWindow: true))));
context, MaterialPageRoute(builder: (_) => HttpBodyWidget(httpMessage: widget.httpMessage, inNewWindow: true)));
}
Widget getBody(ViewType type) {

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:network_proxy/network/bin/server.dart';
import 'package:network_proxy/network/http/http.dart';
import 'package:network_proxy/ui/component/share.dart';
import 'package:network_proxy/ui/component/utils.dart';
@@ -14,12 +15,14 @@ class NetworkTabController extends StatefulWidget {
'Cookies',
];
final ProxyServer proxyServer;
final ValueWrap<HttpRequest> request = ValueWrap();
final ValueWrap<HttpResponse> response = ValueWrap();
final Widget? title;
final TextStyle? tabStyle;
NetworkTabController({HttpRequest? httpRequest, HttpResponse? httpResponse, this.title, this.tabStyle})
NetworkTabController(
{HttpRequest? httpRequest, HttpResponse? httpResponse, this.title, this.tabStyle, required this.proxyServer})
: super(key: GlobalKey<NetworkTabState>()) {
request.set(httpRequest);
response.set(httpResponse);
@@ -71,7 +74,10 @@ class NetworkTabState extends State<NetworkTabController> with SingleTickerProvi
: AppBar(
title: widget.title,
bottom: tabBar,
actions: [ShareWidget(request: widget.request.get(), response: widget.response.get())],
actions: [
ShareWidget(
proxyServer: widget.proxyServer, request: widget.request.get(), response: widget.response.get())
],
);
return Scaffold(
@@ -156,7 +162,7 @@ class NetworkTabState extends State<NetworkTabController> with SingleTickerProvi
headers.add(Row(children: [
SelectableText('$name: ',
style: const TextStyle(fontWeight: FontWeight.w500, color: Colors.deepOrangeAccent)),
Expanded( child: SelectableText(v, contextMenuBuilder: contextMenu, maxLines: 5, minLines: 1)),
Expanded(child: SelectableText(v, contextMenuBuilder: contextMenu, maxLines: 5, minLines: 1)),
]));
headers.add(const Divider(thickness: 0.1));
}

View File

@@ -1,4 +1,8 @@
import 'dart:convert';
import 'dart:io';
import 'package:date_format/date_format.dart';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -10,6 +14,7 @@ import 'package:network_proxy/ui/component/utils.dart';
import 'package:network_proxy/ui/content/panel.dart';
import 'package:network_proxy/utils/curl.dart';
import 'package:network_proxy/utils/lang.dart';
import 'package:window_manager/window_manager.dart';
///请求 URI
class PathRow extends StatefulWidget {
@@ -101,15 +106,44 @@ class _PathRowState extends State<PathRow> {
height: 38,
child: const Text("重放请求", style: TextStyle(fontSize: 14)),
onTap: () {
if (!widget.proxyServer.isRunning) {
FlutterToastr.show('代理服务未启动', context);
return;
}
var request = widget.request.copy(uri: widget.request.requestUrl);
HttpClients.proxyRequest("127.0.0.1", widget.proxyServer.port, request);
FlutterToastr.show('已重新发送请求', context);
}),
PopupMenuItem(
height: 38,
child: const Text("编辑重放请求", style: TextStyle(fontSize: 14)),
onTap: () {
WidgetsBinding.instance.addPostFrameCallback((_) {
requestEdit();
});
}),
],
);
}
requestEdit() async {
var size = MediaQuery.of(context).size;
var ratio = 1.0;
if (Platform.isWindows) {
WindowManager.instance.getDevicePixelRatio();
}
final window = await DesktopMultiWindow.createWindow(jsonEncode(
{'name': 'RequestEditor', 'request': widget.request, 'proxyPort': widget.proxyServer.port},
));
window.setTitle('请求编辑');
window
..setFrame(const Offset(100, 100) & Size(860 * ratio, size.height * ratio))
..center()
..show();
}
//点击事件
void onClick() {
if (selected) {

View File

@@ -0,0 +1,257 @@
import 'dart:io';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_toastr/flutter_toastr.dart';
import 'package:network_proxy/network/http/http.dart';
import 'package:network_proxy/network/http/http_headers.dart';
import 'package:network_proxy/network/http_client.dart';
class RequestEditor extends StatefulWidget {
final WindowController? windowController;
final HttpRequest? request;
final int proxyPort;
const RequestEditor({super.key, this.request, this.windowController, required this.proxyPort});
@override
State<StatefulWidget> createState() {
return RequestEditorState();
}
}
class RequestEditorState extends State<RequestEditor> {
final requestLineKey = GlobalKey<_RequestLineState>();
final headerKey = GlobalKey<HeadersState>();
String requestBody = "";
@override
void initState() {
super.initState();
RawKeyboard.instance.addListener(onKeyEvent);
requestBody = widget.request?.bodyAsString ?? '';
}
void onKeyEvent(RawKeyEvent event) {
if (event.isKeyPressed(LogicalKeyboardKey.metaLeft) && event.isKeyPressed(LogicalKeyboardKey.keyW)) {
RawKeyboard.instance.removeListener(onKeyEvent);
widget.windowController?.close();
return;
}
}
@override
void dispose() {
RawKeyboard.instance.removeListener(onKeyEvent);
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("请求编辑", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
toolbarHeight: Platform.isWindows ? 36 : null,
centerTitle: true,
actions: [
TextButton.icon(onPressed: () async => sendRequest(), icon: const Icon(Icons.send), label: const Text("发送"))
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(15),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
_RequestLine(request: widget.request, key: requestLineKey), // 请求行
Headers(headers: widget.request?.headers, key: headerKey), // 请求头
const Text("Body", style: TextStyle(fontWeight: FontWeight.w500, color: Colors.blue)),
body() // 请求体
])));
}
///发送请求
sendRequest() async {
var currentState = requestLineKey.currentState!;
HttpRequest request = HttpRequest(HttpMethod.valueOf(currentState.requestMethod), currentState.requestUrl);
var headers = headerKey.currentState?.getHeaders();
request.headers.addAll(headers);
request.body = requestBody.codeUnits;
HttpClients.proxyRequest("127.0.0.1", widget.proxyPort, request);
FlutterToastr.show('已重新发送请求', context);
RawKeyboard.instance.removeListener(onKeyEvent);
await Future.delayed(const Duration(milliseconds: 500), () => widget.windowController?.close());
}
Widget body() {
return TextField(
controller: TextEditingController(text: requestBody),
onChanged: (value) {
requestBody = value;
},
minLines: 3,
maxLines: 10);
}
}
///请求行
class _RequestLine extends StatefulWidget {
final HttpRequest? request;
const _RequestLine({super.key, this.request});
@override
State<StatefulWidget> createState() {
return _RequestLineState();
}
}
class _RequestLineState extends State<_RequestLine> {
String requestUrl = "";
String requestMethod = HttpMethod.get.name;
@override
void initState() {
super.initState();
if (widget.request == null) {
return;
}
var request = widget.request!;
requestUrl = request.requestUrl;
requestMethod = request.method.name;
}
@override
Widget build(BuildContext context) {
return TextField(
decoration: InputDecoration(
prefix: DropdownButton(
padding: const EdgeInsets.only(right: 10),
underline: const SizedBox(),
isDense: true,
focusColor: Colors.transparent,
value: requestMethod,
items: HttpMethod.values.map((it) => DropdownMenuItem(value: it.name, child: Text(it.name))).toList(),
onChanged: (String? value) {
setState(() {
requestMethod = value!;
});
},
),
isDense: true,
border: const OutlineInputBorder(borderSide: BorderSide()),
enabledBorder: const OutlineInputBorder(borderSide: BorderSide(color: Colors.grey, width: 0.3))),
controller: TextEditingController(text: requestUrl),
onChanged: (value) {
requestUrl = value;
});
}
}
///请求头
class Headers extends StatefulWidget {
final HttpHeaders? headers;
const Headers({super.key, this.headers});
@override
State<StatefulWidget> createState() {
return HeadersState();
}
}
class HeadersState extends State<Headers> {
Map<TextEditingController, List<TextEditingController>> headers = {};
@override
void initState() {
super.initState();
if (widget.headers == null) {
return;
}
widget.headers?.forEach((name, values) {
headers[TextEditingController(text: name)] = values.map((it) => TextEditingController(text: it)).toList();
});
}
///获取所有请求头
HttpHeaders getHeaders() {
var headers = HttpHeaders();
this.headers.forEach((name, values) {
if (name.text.isEmpty) {
return;
}
for (var element in values) {
if (element.text.isNotEmpty) {
headers.add(name.text, element.text);
}
}
});
return headers;
}
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.only(top: 15),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
const SizedBox(
width: double.infinity,
child: Text("Headers", style: TextStyle(fontWeight: FontWeight.w500, color: Colors.blue))),
const SizedBox(height: 10),
DataTable(
dataRowMaxHeight: 38,
dataRowMinHeight: 38,
dividerThickness: 0.2,
border: TableBorder.all(color: Theme.of(context).highlightColor),
columns: const [
DataColumn(label: Text('Key')),
DataColumn(label: Text('Value')),
DataColumn(label: Text(''))
],
rows: buildRows()),
const SizedBox(height: 10),
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
TextButton(
onPressed: () {
setState(() {
headers[TextEditingController()] = [TextEditingController()];
});
},
child: const Text("添加Header", textAlign: TextAlign.center))
]),
]));
}
List<DataRow> buildRows() {
var width = MediaQuery.of(context).size.width;
List<DataRow> list = [];
headers.forEach((key, values) {
for (var val in values) {
list.add(DataRow(cells: [
cell(key, width: 200, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
cell(val, width: width - 410),
DataCell(InkWell(
onTap: () {
setState(() {
headers.remove(key);
});
},
child: const Icon(Icons.remove_circle, size: 16)))
]));
}
});
return list;
}
DataCell cell(TextEditingController val, {TextStyle? style = const TextStyle(fontSize: 14), double? width}) {
return DataCell(SizedBox(
width: width,
child: TextFormField(
style: style,
controller: val,
decoration: const InputDecoration(isDense: true, border: InputBorder.none, hintText: "Header"))));
}
}

View File

@@ -17,7 +17,7 @@ class Search extends StatelessWidget {
borderRadius: BorderRadius.circular(20),
),
child: TextField(
cursorHeight: 15,
cursorHeight: 22,
onChanged: (val) async {
value = val;

View File

@@ -23,7 +23,8 @@ class _SettingState extends State<Setting> {
@override
void initState() {
enableDesktopListenable = ValueNotifier<bool>(widget.proxyServer.enableDesktop);
enableDesktopListenable =
ValueNotifier<bool>(widget.proxyServer.enableDesktop);
super.initState();
}
@@ -44,7 +45,9 @@ class _SettingState extends State<Setting> {
return [
PopupMenuItem<String>(
padding: const EdgeInsets.all(0),
child: PortWidget(proxyServer: widget.proxyServer, textStyle: const TextStyle(fontSize: 13))),
child: PortWidget(
proxyServer: widget.proxyServer,
textStyle: const TextStyle(fontSize: 13))),
PopupMenuItem<String>(
padding: const EdgeInsets.all(0),
child: ValueListenableBuilder(
@@ -145,7 +148,8 @@ class _PortState extends State<PortWidget> {
textController.text = widget.proxyServer.port.toString();
portFocus.addListener(() async {
//失去焦点
if (!portFocus.hasFocus && textController.text != widget.proxyServer.port.toString()) {
if (!portFocus.hasFocus &&
textController.text != widget.proxyServer.port.toString()) {
widget.proxyServer.port = int.parse(textController.text);
if (widget.proxyServer.isRunning) {
widget.proxyServer.restart();

View File

@@ -2,6 +2,7 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_toastr/flutter_toastr.dart';
import 'package:network_proxy/network/bin/server.dart';
import 'package:network_proxy/network/util/crts.dart';
import 'package:network_proxy/utils/ip.dart';
@@ -91,6 +92,10 @@ class _SslState extends State<SslWidget> {
focusColor: Colors.transparent,
trailing: const Icon(Icons.arrow_right),
onTap: () async {
if (!widget.proxyServer.isRunning) {
FlutterToastr.show("请先启动抓包", context);
return;
}
launchUrl(Uri.parse("http://127.0.0.1:${widget.proxyServer.port}/ssl"));
}),
)
@@ -220,7 +225,14 @@ class _SslState extends State<SslWidget> {
const SizedBox(height: 10),
const Text("2. 打开设置 -> 安全 -> 加密和凭据 -> 安装证书 -> CA 证书"),
const SizedBox(height: 10),
Image.network("https://foruda.gitee.com/images/1689352695624941051/74e3bed6_1073801.png", height: 600)
ClipRRect(
child: Align(
alignment: Alignment.topCenter,
heightFactor: .7,
child: Image.network(
"https://foruda.gitee.com/images/1689352695624941051/74e3bed6_1073801.png",
height: 550,
)))
]);
});
}

View File

@@ -49,6 +49,7 @@ class _SocketLaunchState extends State<SocketLaunch> with WindowListener, Widget
print("onWindowClose");
await widget.proxyServer.stop();
started = false;
windowManager.destroy();
}
@override

View File

@@ -85,8 +85,9 @@ class RequestListState extends State<RequestListWidget> {
class RequestSequence extends StatefulWidget {
final List<HttpRequest> list;
final ProxyServer proxyServer;
final bool displayDomain;
const RequestSequence({super.key, required this.list, required this.proxyServer});
const RequestSequence({super.key, required this.list, required this.proxyServer, this.displayDomain = true});
@override
State<StatefulWidget> createState() {
@@ -186,12 +187,16 @@ class RequestSequenceState extends State<RequestSequence> with AutomaticKeepAliv
return ListView.separated(
cacheExtent: 1000,
separatorBuilder: (context, index) => Divider(height: 0.5, color: Theme.of(context).focusColor),
separatorBuilder: (context, index) => Divider(thickness: 0.2, color: Theme.of(context).dividerColor),
itemCount: view.length,
itemBuilder: (context, index) {
GlobalKey<RequestRowState> key = GlobalKey();
indexes[view.elementAt(index)] = key;
return RequestRow(key: key, request: view.elementAt(index), proxyServer: widget.proxyServer);
return RequestRow(
key: key,
request: view.elementAt(index),
proxyServer: widget.proxyServer,
displayDomain: widget.displayDomain);
});
}
}
@@ -316,7 +321,8 @@ class DomainListState extends State<DomainList> with AutomaticKeepAliveClientMix
Widget build(BuildContext context) {
super.build(context);
return ListView.separated(
separatorBuilder: (context, index) => Divider(height: 0.5, color: Theme.of(context).focusColor),
padding: EdgeInsets.zero,
separatorBuilder: (context, index) => Divider(thickness: 0.2, color: Theme.of(context).dividerColor),
cacheExtent: 1000,
itemCount: list.length,
itemBuilder: (ctx, index) => title(index));
@@ -326,6 +332,7 @@ class DomainListState extends State<DomainList> with AutomaticKeepAliveClientMix
var time =
formatDate(containerMap[list.elementAt(index)]!.last.requestTime, [m, '/', d, ' ', HH, ':', nn, ':', ss]);
return ListTile(
visualDensity: const VisualDensity( vertical: -4),
title: Text(list.elementAt(index).domain, maxLines: 1, overflow: TextOverflow.ellipsis),
trailing: const Icon(Icons.chevron_right),
subtitle: Text("最后请求时间: $time, 次数: ${containerMap[list.elementAt(index)]!.length}",
@@ -335,9 +342,10 @@ class DomainListState extends State<DomainList> with AutomaticKeepAliveClientMix
Navigator.push(context, MaterialPageRoute(builder: (context) {
showHostAndPort = list.elementAt(index);
return Scaffold(
appBar: AppBar(title: const Text("请求列表")),
appBar: AppBar(title: Text(list.elementAt(index).domain, style: const TextStyle(fontSize: 16))),
body: RequestSequence(
key: requestSequenceKey,
displayDomain: false,
list: containerMap[list.elementAt(index)]!,
proxyServer: widget.proxyServer));
}));

View File

@@ -7,14 +7,16 @@ import 'package:network_proxy/network/http/http.dart';
import 'package:network_proxy/network/http_client.dart';
import 'package:network_proxy/ui/component/utils.dart';
import 'package:network_proxy/ui/content/panel.dart';
import 'package:network_proxy/ui/mobile/request/request_editor.dart';
import 'package:network_proxy/utils/curl.dart';
///请求行
class RequestRow extends StatefulWidget {
final HttpRequest request;
final ProxyServer proxyServer;
final bool displayDomain;
const RequestRow({super.key, required this.request, required this.proxyServer});
const RequestRow({super.key, required this.request, required this.proxyServer, this.displayDomain = true});
@override
State<StatefulWidget> createState() {
@@ -41,20 +43,22 @@ class RequestRowState extends State<RequestRow> {
@override
Widget build(BuildContext context) {
var title = '${request.method.name} ${request.requestUrl}';
var title = '${request.method.name} ${widget.displayDomain ? request.requestUrl : request.path()}';
var time = formatDate(request.requestTime, [HH, ':', nn, ':', ss]);
var subTitle =
'$time - [${response?.status.code ?? ''}] ${response?.contentType.name.toUpperCase() ?? ''} ${response?.costTime() ?? ''}';
return ListTile(
leading: Icon(getIcon(response), size: 16, color: Colors.green),
title: Text(title, overflow: TextOverflow.ellipsis, maxLines: 1),
subtitle: Text(subTitle, maxLines: 1),
visualDensity: const VisualDensity(vertical: -4),
leading: widget.displayDomain ? null : Icon(getIcon(response), size: 16, color: Colors.green),
title: Text(title, overflow: TextOverflow.ellipsis, maxLines: 1, style: const TextStyle(fontSize: 14)),
subtitle: Text(subTitle, maxLines: 1, style: const TextStyle(fontSize: 12)),
trailing: const Icon(Icons.chevron_right),
onLongPress: () => menu(menuPosition(context)),
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (context) {
return NetworkTabController(
proxyServer: widget.proxyServer,
httpRequest: request,
httpResponse: response,
title: const Text("抓包详情", style: TextStyle(fontSize: 16)));
@@ -62,7 +66,7 @@ class RequestRowState extends State<RequestRow> {
});
}
///右键菜单
///菜单
menu(RelativeRect position) {
showModalBottomSheet(
context: context,
@@ -76,13 +80,26 @@ class RequestRowState extends State<RequestRow> {
menuItem("复制 cURL 请求", () => curlRequest(widget.request)),
const Divider(thickness: 0.5),
TextButton(
child: const SizedBox(width: double.infinity, child: Text("重放请求", textAlign: TextAlign.center)),
child: const SizedBox(width: double.infinity, child: Text("请求重放", textAlign: TextAlign.center)),
onPressed: () {
var request = widget.request.copy(uri: widget.request.requestUrl);
if (!widget.proxyServer.isRunning) {
FlutterToastr.show('代理服务未启动', context);
return;
}
HttpClients.proxyRequest("127.0.0.1", widget.proxyServer.port, request);
FlutterToastr.show('已重新发送请求', context);
Navigator.of(context).pop();
}),
const Divider(thickness: 0.5),
TextButton(
child: const SizedBox(width: double.infinity, child: Text("编辑请求重放", textAlign: TextAlign.center)),
onPressed: () {
Navigator.of(context).pop();
Navigator.of(context).push(MaterialPageRoute(
builder: (context) =>
MobileRequestEditor(request: widget.request, proxyServer: widget.proxyServer)));
}),
Container(
color: Theme.of(context).hoverColor,
height: 8,

View File

@@ -0,0 +1,307 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_toastr/flutter_toastr.dart';
import 'package:network_proxy/network/bin/server.dart';
import 'package:network_proxy/network/http/http.dart';
import 'package:network_proxy/network/http/http_headers.dart';
import 'package:network_proxy/network/http_client.dart';
class MobileRequestEditor extends StatefulWidget {
final HttpRequest? request;
final ProxyServer proxyServer;
const MobileRequestEditor({super.key, this.request, required this.proxyServer});
@override
State<StatefulWidget> createState() {
return RequestEditorState();
}
}
class RequestEditorState extends State<MobileRequestEditor> {
final requestLineKey = GlobalKey<_RequestLineState>();
final headerKey = GlobalKey<HeadersState>();
String requestBody = "";
@override
void initState() {
super.initState();
requestBody = widget.request?.bodyAsString ?? "";
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("编辑请求", style: TextStyle(fontSize: 16)),
centerTitle: true,
leading: TextButton(
onPressed: () => Navigator.pop(context),
child: Text("取消", style: Theme.of(context).textTheme.bodyMedium)),
actions: [TextButton.icon(icon: const Icon(Icons.send), label: const Text("发送"), onPressed: sendRequest)],
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(15),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
_RequestLine(request: widget.request, key: requestLineKey), // 请求行
Headers(headers: widget.request?.headers, key: headerKey), // 请求头
const SizedBox(height: 10),
const Text("Body", style: TextStyle(fontWeight: FontWeight.w500, color: Colors.blue)),
body()
])));
}
///发送请求
sendRequest() {
if (!widget.proxyServer.isRunning) {
FlutterToastr.show('代理服务未启动', context);
return;
}
var currentState = requestLineKey.currentState!;
HttpRequest request = HttpRequest(HttpMethod.valueOf(currentState.requestMethod), currentState.requestUrl);
var headers = headerKey.currentState?.getHeaders();
request.headers.addAll(headers);
request.body = requestBody.codeUnits;
HttpClients.proxyRequest("127.0.0.1", widget.proxyServer.port, request);
FlutterToastr.show('已重新发送请求', context);
Navigator.pop(context, request);
}
///请求体
Widget body() {
return TextField(
controller: TextEditingController(text: requestBody),
onChanged: (value) {
requestBody = value;
},
minLines: 3,
maxLines: 10);
}
}
class _RequestLine extends StatefulWidget {
final HttpRequest? request;
const _RequestLine({this.request, super.key});
@override
State<StatefulWidget> createState() {
return _RequestLineState();
}
}
class _RequestLineState extends State<_RequestLine> {
String requestUrl = "";
String requestMethod = HttpMethod.get.name;
@override
void initState() {
super.initState();
if (widget.request == null) {
return;
}
var request = widget.request!;
requestUrl = request.requestUrl;
requestMethod = request.method.name;
}
@override
Widget build(BuildContext context) {
return TextField(
style: const TextStyle(fontSize: 14),
minLines: 1,
maxLines: 5,
decoration: InputDecoration(
prefix: DropdownButton(
padding: const EdgeInsets.only(right: 10),
underline: const SizedBox(),
isDense: true,
focusColor: Colors.transparent,
value: requestMethod,
items: HttpMethod.values
.map((it) =>
DropdownMenuItem(value: it.name, child: Text(it.name, style: const TextStyle(fontSize: 12))))
.toList(),
onChanged: (String? value) {
setState(() {
requestMethod = value!;
});
},
),
isDense: true,
border: const OutlineInputBorder(borderSide: BorderSide()),
enabledBorder: const OutlineInputBorder(borderSide: BorderSide(color: Colors.grey, width: 0.3))),
controller: TextEditingController(text: requestUrl),
onChanged: (value) {
requestUrl = value;
});
}
}
class Headers extends StatefulWidget {
final HttpHeaders? headers;
const Headers({super.key, this.headers});
@override
State<StatefulWidget> createState() {
return HeadersState();
}
}
class HeadersState extends State<Headers> {
Map<String, List<String>> headers = {};
@override
void initState() {
super.initState();
if (widget.headers == null) {
return;
}
widget.headers?.forEach((name, values) {
headers[name] = values;
});
}
HttpHeaders getHeaders() {
var headers = HttpHeaders();
this.headers.forEach((key, values) {
if (key.isNotEmpty) {
headers.addValues(key, values);
}
});
return headers;
}
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
const SizedBox(
width: double.infinity,
child: Text("Headers", style: TextStyle(fontWeight: FontWeight.w500, color: Colors.blue))),
const SizedBox(height: 10),
...buildHeaders(),
Container(
alignment: Alignment.center,
child: TextButton(
onPressed: () {
modifyHeader("", "");
},
child: const Text("添加Header", textAlign: TextAlign.center))) //添加按钮
]));
}
List<Widget> buildHeaders() {
List<Widget> list = [];
headers.forEach((key, values) {
for (var val in values) {
var header = row(Text(key, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500)),
Text(val, style: const TextStyle(fontSize: 12), maxLines: 5, overflow: TextOverflow.ellipsis));
var ink = InkWell(
onTap: () => modifyHeader(key, val),
onLongPress: () => deleteHeader(key),
child: Padding(padding: const EdgeInsets.only(top: 5, bottom: 5), child: header));
list.add(ink);
list.add(const Divider(thickness: 0.2));
}
});
return list;
}
/// 修改请求头
modifyHeader(String key, String val) {
String headerName = key;
showDialog(
context: context,
builder: (ctx) {
return AlertDialog(
titlePadding: const EdgeInsets.only(left: 25, top: 10),
actionsPadding: const EdgeInsets.only(right: 10, bottom: 10),
title: const Text("修改请求头", style: TextStyle(fontSize: 18)),
content: Wrap(
children: [
TextField(
minLines: 1,
maxLines: 3,
controller: TextEditingController(text: headerName),
decoration: const InputDecoration(labelText: "请求头名称"),
onChanged: (value) {
headerName = value;
},
),
TextField(
minLines: 1,
maxLines: 8,
controller: TextEditingController(text: val),
decoration: const InputDecoration(labelText: "请求头值"),
onChanged: (value) {
val = value;
},
)
],
),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text("取消")),
TextButton(
onPressed: () {
setState(() {
if (headerName != key) {
headers.remove(key);
}
headers[headerName] = [val];
});
Navigator.pop(context);
},
child: const Text("修改")),
],
);
});
}
//删除
deleteHeader(String key) {
HapticFeedback.heavyImpact();
showDialog(
context: context,
builder: (ctx) {
return AlertDialog(
title: const Text("是否删除该请求头?", style: TextStyle(fontSize: 18)),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text("取消")),
TextButton(
onPressed: () {
setState(() {
headers.remove(key);
});
Navigator.pop(context);
},
child: const Text("删除")),
],
);
});
}
Widget row(Widget title, Widget child) {
return Row(children: [
Expanded(flex: 3, child: title),
const SizedBox(width: 10, child: Text(":", style: TextStyle(color: Colors.orange, fontWeight: FontWeight.w600))),
Expanded(
flex: 6,
child: child,
),
]);
}
}

View File

@@ -79,7 +79,14 @@ class _MobileSslState extends State<MobileSslWidget> {
List<Widget> android() {
return [
TextButton(onPressed: () {}, child: const Text("2. 打开设置 -> 安全 -> 加密和凭据 -> 安装证书 -> CA 证书")),
Image.network("https://foruda.gitee.com/images/1689352695624941051/74e3bed6_1073801.png"),
ClipRRect(
child: Align(
alignment: Alignment.topCenter,
heightFactor: .7,
child: Image.network(
"https://foruda.gitee.com/images/1689352695624941051/74e3bed6_1073801.png",
height: 680,
)))
];
}
@@ -88,7 +95,7 @@ class _MobileSslState extends State<MobileSslWidget> {
showDialog(
context: context,
builder: (context) {
return const Text("请先启动代理服务器");
return const Text("请先启动抓包");
});
return;
}

View File

@@ -6,12 +6,16 @@
#include "generated_plugin_registrant.h"
#include <desktop_multi_window/desktop_multi_window_plugin.h>
#include <proxy_manager/proxy_manager_plugin.h>
#include <screen_retriever/screen_retriever_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
#include <window_manager/window_manager_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) desktop_multi_window_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopMultiWindowPlugin");
desktop_multi_window_plugin_register_with_registrar(desktop_multi_window_registrar);
g_autoptr(FlPluginRegistrar) proxy_manager_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "ProxyManagerPlugin");
proxy_manager_plugin_register_with_registrar(proxy_manager_registrar);

View File

@@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
desktop_multi_window
proxy_manager
screen_retriever
url_launcher_linux

View File

@@ -5,6 +5,7 @@
import FlutterMacOS
import Foundation
import desktop_multi_window
import path_provider_foundation
import proxy_manager
import screen_retriever
@@ -14,6 +15,7 @@ import url_launcher_macos
import window_manager
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterMultiWindowPlugin.register(with: registry.registrar(forPlugin: "FlutterMultiWindowPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
ProxyManagerPlugin.register(with: registry.registrar(forPlugin: "ProxyManagerPlugin"))
ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin"))

View File

@@ -3,61 +3,55 @@
{
"size" : "16x16",
"idiom" : "mac",
"filename" : "app_icon_16.png",
"scale" : "1x"
},
{
"size" : "16x16",
"idiom" : "mac",
"filename" : "app_icon_32.png",
"scale" : "2x"
},
{
"size" : "32x32",
"idiom" : "mac",
"filename" : "app_icon_32.png",
"filename" : "icon_16x16.png",
"scale" : "1x"
},
{
"size" : "32x32",
"idiom" : "mac",
"filename" : "app_icon_64.png",
"filename" : "icon_32x32.png",
"scale" : "1x"
},
{
"size" : "32x32",
"idiom" : "mac",
"filename" : "icon_32x32@2x.png",
"scale" : "2x"
},
{
"size" : "128x128",
"idiom" : "mac",
"filename" : "app_icon_128.png",
"filename" : "icon_128x128.png",
"scale" : "1x"
},
{
"size" : "128x128",
"idiom" : "mac",
"filename" : "app_icon_256.png",
"filename" : "icon_128x128@2x.png",
"scale" : "2x"
},
{
"size" : "256x256",
"idiom" : "mac",
"filename" : "app_icon_256.png",
"filename" : "icon_256x256.png",
"scale" : "1x"
},
{
"size" : "256x256",
"idiom" : "mac",
"filename" : "app_icon_512.png",
"filename" : "icon_256x256@2x.png",
"scale" : "2x"
},
{
"size" : "512x512",
"idiom" : "mac",
"filename" : "app_icon_512.png",
"filename" : "icon_512x512.png",
"scale" : "1x"
},
{
"size" : "512x512",
"idiom" : "mac",
"filename" : "app_icon_1024.png",
"filename" : "icon_512x512@2x.png",
"scale" : "2x"
}
],

Binary file not shown.

Before

Width:  |  Height:  |  Size: 821 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 263 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 896 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 657 KiB

View File

@@ -121,6 +121,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.7"
desktop_multi_window:
dependency: "direct main"
description:
name: desktop_multi_window
sha256: "29971186ae0790e32b156f127f9c22c5ee77bdb94b14f7cea23f2356d0c76cfc"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
easy_permission:
dependency: "direct main"
description:
@@ -420,10 +428,10 @@ packages:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc"
sha256: "43798d895c929056255600343db8f049921cbec94d31ec87f1dc5c16c01935dd"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
version: "2.1.5"
pointycastle:
dependency: transitive
description:
@@ -601,10 +609,10 @@ packages:
dependency: transitive
description:
name: url_launcher_android
sha256: "15f5acbf0dce90146a0f5a2c4a002b1814a6303c4c5c075aa2623b2d16156f03"
sha256: "78cb6dea3e93148615109e58e42c35d1ffbf5ef66c44add673d0ab75f12ff3af"
url: "https://pub.dev"
source: hosted
version: "6.0.36"
version: "6.0.37"
url_launcher_ios:
dependency: transitive
description:

View File

@@ -15,6 +15,7 @@ dependencies:
logger: ^1.4.0
date_format: ^2.0.7
window_manager: ^0.3.5
desktop_multi_window: ^0.2.0
path_provider: ^2.0.15
url_launcher: ^6.1.11
proxy_manager: ^0.0.3

View File

@@ -13,7 +13,7 @@ import 'package:network_proxy/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const FluentApp());
await tester.pumpWidget(const FluentApp(DesktopHomePage()));
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);

View File

@@ -6,6 +6,7 @@
#include "generated_plugin_registrant.h"
#include <desktop_multi_window/desktop_multi_window_plugin.h>
#include <proxy_manager/proxy_manager_plugin.h>
#include <screen_retriever/screen_retriever_plugin.h>
#include <share_plus/share_plus_windows_plugin_c_api.h>
@@ -13,6 +14,8 @@
#include <window_manager/window_manager_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
DesktopMultiWindowPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("DesktopMultiWindowPlugin"));
ProxyManagerPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ProxyManagerPlugin"));
ScreenRetrieverPluginRegisterWithRegistrar(

View File

@@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
desktop_multi_window
proxy_manager
screen_retriever
share_plus

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 272 KiB