请求编辑,多窗口支持
@@ -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">
|
||||
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 58 KiB |
@@ -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": {
|
||||
|
||||
|
Before Width: | Height: | Size: 509 KiB After Width: | Height: | Size: 302 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-38@2x.png
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-38@3x.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 41 KiB |
BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-64@2x.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-64@3x.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-68@2x.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 36 KiB |
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -93,7 +93,6 @@ class HttpChannelHandler extends ChannelHandler<HttpRequest> {
|
||||
|
||||
/// 转发请求
|
||||
Future<void> forward(Channel channel, HttpRequest httpRequest) async {
|
||||
|
||||
var remoteChannel = await _getRemoteChannel(channel, httpRequest);
|
||||
|
||||
//实现抓包代理转发
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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}';
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)));
|
||||
});
|
||||
}),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
257
lib/ui/desktop/left/request_editor.dart
Normal 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"))));
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ class Search extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: TextField(
|
||||
cursorHeight: 15,
|
||||
cursorHeight: 22,
|
||||
onChanged: (val) async {
|
||||
value = val;
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
)))
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ class _SocketLaunchState extends State<SocketLaunch> with WindowListener, Widget
|
||||
print("onWindowClose");
|
||||
await widget.proxyServer.stop();
|
||||
started = false;
|
||||
windowManager.destroy();
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -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));
|
||||
}));
|
||||
|
||||
@@ -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,
|
||||
|
||||
307
lib/ui/mobile/request/request_editor.dart
Normal 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,
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
desktop_multi_window
|
||||
proxy_manager
|
||||
screen_retriever
|
||||
url_launcher_linux
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
],
|
||||
|
||||
|
Before Width: | Height: | Size: 821 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 263 KiB |
|
Before Width: | Height: | Size: 10 KiB |
BIN
macos/Runner/Assets.xcassets/AppIcon.appiconset/icon_128x128.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 70 KiB |
BIN
macos/Runner/Assets.xcassets/AppIcon.appiconset/icon_16x16.png
Normal file
|
After Width: | Height: | Size: 896 B |
|
After Width: | Height: | Size: 2.5 KiB |
BIN
macos/Runner/Assets.xcassets/AppIcon.appiconset/icon_256x256.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 220 KiB |
BIN
macos/Runner/Assets.xcassets/AppIcon.appiconset/icon_32x32.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
BIN
macos/Runner/Assets.xcassets/AppIcon.appiconset/icon_512x512.png
Normal file
|
After Width: | Height: | Size: 220 KiB |
|
After Width: | Height: | Size: 657 KiB |
16
pubspec.lock
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
desktop_multi_window
|
||||
proxy_manager
|
||||
screen_retriever
|
||||
share_plus
|
||||
|
||||
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 272 KiB |