详情增加快速请求重写

This commit is contained in:
wanghongen
2023-08-26 22:04:17 +08:00
parent 8cc1b83f99
commit cfd23cd059
11 changed files with 239 additions and 196 deletions

View File

@@ -292,7 +292,7 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1420;
LastUpgradeCheck = 1300;
LastUpgradeCheck = 1430;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1300"
LastUpgradeVersion = "1430"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@@ -114,7 +114,7 @@ class FluentApp extends StatelessWidget {
builder: (_, current, __) {
uiConfiguration.theme = current;
uiConfiguration.flushConfig();
print(current.mode.name);
return MaterialApp(
title: 'ProxyPin',
debugShowCheckedModeBanner: false,

View File

@@ -40,7 +40,7 @@ class Configuration {
bool upgradeNotice = true;
//请求重写
RequestRewrites requestRewrites = RequestRewrites();
RequestRewrites requestRewrites = RequestRewrites.instance;
//外部代理
ProxyInfo? externalProxy;

View File

@@ -1,10 +1,16 @@
/// @author wanghongen
/// 2023/7/26
class RequestRewrites {
bool enabled = true;
final List<RequestRewriteRule> rules = [];
RequestRewrites._();
//单例
static final RequestRewrites _instance = RequestRewrites._();
static RequestRewrites get instance => _instance;
load(Map<String, dynamic>? map) {
if (map == null) {
return;
@@ -47,7 +53,8 @@ class RequestRewrites {
return null;
}
addRule(RequestRewriteRule rule) {
void addRule(RequestRewriteRule rule) {
rules.removeWhere((it) => it.path == rule.path && it.domain == rule.domain);
rules.add(rule);
}

View File

@@ -69,6 +69,7 @@ class _Item {
_Item(this.request, [this.response]) {
response ??= request.response;
request.response = response;
response?.request = request;
}
factory _Item.fromJson(Map<String, dynamic> json) {

View File

@@ -5,10 +5,14 @@ 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/bin/configuration.dart';
import 'package:network_proxy/network/http/http.dart';
import 'package:network_proxy/network/util/request_rewrite.dart';
import 'package:network_proxy/ui/component/json/json_viewer.dart';
import 'package:network_proxy/ui/component/json/theme.dart';
import 'package:network_proxy/ui/component/utils.dart';
import 'package:network_proxy/ui/desktop/toolbar/setting/request_rewrite.dart';
import 'package:network_proxy/ui/mobile/setting/request_rewrite.dart';
import 'package:network_proxy/utils/num.dart';
import 'package:network_proxy/utils/platform.dart';
import 'package:window_manager/window_manager.dart';
@@ -99,30 +103,71 @@ class HttpBodyState extends State<HttpBodyWidget> {
return tabController;
}
/// 标题
Widget titleWidget({inNewWindow = false}) {
var type = widget.httpMessage is HttpRequest ? "Request" : "Response";
var list = [
Text('$type Body', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
const SizedBox(width: 15),
IconButton(
icon: const Icon(Icons.copy),
tooltip: '复制',
onPressed: () {
var body = bodyKey.currentState?.body;
if (body == null) {
return;
}
Clipboard.setData(ClipboardData(text: body)).then((value) => FlutterToastr.show("已复制到剪切板", context));
}),
];
if (!inNewWindow || Platforms.isMobile()) {
list.add(const SizedBox(width: 5));
list.add(IconButton(
icon: const Icon(Icons.edit_document),
tooltip: '请求重写',
onPressed: () {
HttpRequest? request;
if (widget.httpMessage is HttpRequest) {
request = widget.httpMessage as HttpRequest;
} else {
request = (widget.httpMessage as HttpResponse).request;
}
var body = bodyKey.currentState?.body;
var rule = RequestRewriteRule(true, request?.path() ?? '', request?.remoteDomain(),
requestBody: widget.httpMessage is HttpRequest ? body : null,
responseBody: widget.httpMessage is HttpResponse ? body : null);
if (Platforms.isMobile()) {
Navigator.push(context, MaterialPageRoute(builder: (_) => RewriteRule(rule: rule))).then((value) {
if (value is RequestRewriteRule) {
Configuration.instance.then((it) => it.flushRequestRewriteConfig());
}
});
} else {
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) => RuleAddDialog(rule: rule)).then((value) {
if (value != null) {
Configuration.instance.then((it) => it.flushRequestRewriteConfig());
FlutterToastr.show("添加请求重写规则成功", context);
}
});
}
}));
}
if (!inNewWindow) {
list.add(const SizedBox(width: 5));
list.add(IconButton(icon: const Icon(Icons.open_in_new), tooltip: '新窗口打开', onPressed: () => openNew()));
}
return Row(
mainAxisAlignment: widget.inNewWindow ? MainAxisAlignment.center : MainAxisAlignment.start,
children: [
Text('$type Body', style: const TextStyle(fontWeight: FontWeight.w500)),
const SizedBox(width: 15),
IconButton(
icon: const Icon(Icons.copy),
tooltip: '复制',
onPressed: () {
var body = bodyKey.currentState?.body;
if (body == null) {
return;
}
Clipboard.setData(ClipboardData(text: body)).then((value) => FlutterToastr.show("已复制到剪切板", context));
}),
const SizedBox(width: 5),
inNewWindow
? const SizedBox()
: IconButton(icon: const Icon(Icons.open_in_new), tooltip: '新窗口打开', onPressed: () => openNew())
],
children: list,
);
}
@@ -193,14 +238,16 @@ class _BodyState extends State<_Body> {
if (viewType == ViewType.hex) {
return message!.body!.map(intToHex).join(" ");
}
if (viewType == ViewType.formUrl) {
return Uri.decodeFull(message!.bodyAsString);
}
if (viewType == ViewType.jsonText || viewType == ViewType.json) {
//json格式化
var jsonObject = json.decode(message!.bodyAsString);
return const JsonEncoder.withIndent(" ").convert(jsonObject);
}
try {
if (viewType == ViewType.formUrl) {
return Uri.decodeFull(message!.bodyAsString);
}
if (viewType == ViewType.jsonText || viewType == ViewType.json) {
//json格式化
var jsonObject = json.decode(message!.bodyAsString);
return const JsonEncoder.withIndent(" ").convert(jsonObject);
}
} catch (_) {}
return message!.bodyAsString;
}

View File

@@ -97,33 +97,29 @@ class _RequestRewriteState extends State<RequestRewrite> {
barrierDismissible: false,
builder: (BuildContext context) {
return RuleAddDialog(
requestRewrites: widget.configuration.requestRewrites,
currentIndex: currentIndex,
onChange: () {
changed = true;
requestRuleList.changeState();
});
});
rule: currentIndex >= 0 ? widget.configuration.requestRewrites.rules[currentIndex] : null);
}).then((value) {
if (value != null) {
changed = true;
requestRuleList.changeState();
}
});
}
}
///请求重写规则添加对话框
class RuleAddDialog extends StatelessWidget {
final RequestRewrites requestRewrites;
final int currentIndex;
final Function onChange;
final RequestRewriteRule? rule;
const RuleAddDialog({super.key, required this.currentIndex, required this.onChange, required this.requestRewrites});
const RuleAddDialog({super.key, this.currentIndex = -1, this.rule});
@override
Widget build(BuildContext context) {
GlobalKey formKey = GlobalKey<FormState>();
RequestRewriteRule? rule;
if (currentIndex >= 0) {
rule = requestRewrites.rules[currentIndex];
}
ValueNotifier<bool> enableNotifier = ValueNotifier(rule == null || rule.enabled);
ValueNotifier<bool> enableNotifier = ValueNotifier(rule == null || rule?.enabled == true);
String? domain = rule?.domain;
String? path = rule?.path;
String? requestBody = rule?.requestBody;
@@ -179,17 +175,18 @@ class RuleAddDialog extends StatelessWidget {
if ((formKey.currentState as FormState).validate()) {
(formKey.currentState as FormState).save();
var rule = RequestRewriteRule(
enableNotifier.value, path!, domain?.trim().isEmpty == true ? null : domain?.trim(),
requestBody: requestBody, responseBody: responseBody);
if (currentIndex >= 0) {
requestRewrites.rules[currentIndex] = RequestRewriteRule(enableNotifier.value, path!, domain,
requestBody: requestBody, responseBody: responseBody);
RequestRewrites.instance.rules[currentIndex] = rule;
} else {
requestRewrites.addRule(RequestRewriteRule(enableNotifier.value, path!, domain,
requestBody: requestBody, responseBody: responseBody));
RequestRewrites.instance.addRule(rule);
}
enableNotifier.dispose();
onChange.call();
Navigator.of(context).pop();
Navigator.of(context).pop(rule);
}
}),
ElevatedButton(
@@ -210,19 +207,10 @@ class RequestRuleList extends StatefulWidget {
State<RequestRuleList> createState() => _RequestRuleListState();
List<int> removeSelected() {
var index = currentSelectedIndex();
var state = (key as GlobalKey<_RequestRuleListState>).currentState;
List<int> list = [];
var selectedIndex = state?.currentSelectedIndex;
state?.selected.forEach((key, value) {
if (value == true) {
list.add(key);
if (selectedIndex == key) {
state.currentSelectedIndex = -1;
}
}
});
state?.selected.clear();
return list;
state?.currentSelectedIndex = -1;
return index >= 0 ? [index] : [];
}
int currentSelectedIndex() {
@@ -237,7 +225,6 @@ class RequestRuleList extends StatefulWidget {
}
class _RequestRuleListState extends State<RequestRuleList> {
final Map<int, bool> selected = {};
int currentSelectedIndex = -1;
changeState() {
@@ -248,8 +235,7 @@ class _RequestRuleListState extends State<RequestRuleList> {
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.only(top: 10),
height: 300,
constraints: const BoxConstraints(minWidth: 500),
constraints: const BoxConstraints(minWidth: 500, minHeight: 300),
child: SingleChildScrollView(
child: DataTable(
columnSpacing: 36,
@@ -283,10 +269,9 @@ class _RequestRuleListState extends State<RequestRuleList> {
style: const TextStyle(fontSize: 12)),
))
],
selected: selected[index] == true,
selected: currentSelectedIndex == index,
onSelectChanged: (value) {
setState(() {
selected[index] = value!;
currentSelectedIndex = index;
});
})),

View File

@@ -86,17 +86,17 @@ class MobileHomeState extends State<MobileHomePage> implements EventListener {
]),
drawer: DrawerWidget(proxyServer: proxyServer),
floatingActionButton: FloatingActionButton(
onPressed: () {},
child: SocketLaunch(
proxyServer: proxyServer,
startup: false,
size: 38,
onStart: () => Vpn.startVpn("127.0.0.1", proxyServer.port, proxyServer.configuration.appWhitelist),
onStop: () => Vpn.stopVpn())),
onPressed: null,
child: Center(
child: SocketLaunch(
proxyServer: proxyServer,
size: 36,
startup: false,
onStart: () => Vpn.startVpn("127.0.0.1", proxyServer.port, proxyServer.configuration.appWhitelist),
onStop: () => Vpn.stopVpn()))),
body: ValueListenableBuilder(
valueListenable: desktop,
builder: (context, value, _) {
return Column(children: [
value.connect ? remoteConnect(value) : const SizedBox(),
Expanded(child: RequestListWidget(key: requestStateKey, proxyServer: proxyServer))

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_toastr/flutter_toastr.dart';
import 'package:network_proxy/network/bin/configuration.dart';
import 'package:network_proxy/network/util/request_rewrite.dart';
@@ -87,117 +88,118 @@ class _MobileRequestRewriteState extends State<MobileRequestRewrite> {
})
]),
const SizedBox(height: 10),
requestRuleList,
Expanded(child: requestRuleList),
],
)));
}
void add([int currentIndex = -1]) {
showDialog(
context: context,
builder: (BuildContext context) {
return RuleAddDialog(
requestRewrites: widget.configuration.requestRewrites,
currentIndex: currentIndex,
onChange: () {
changed = true;
requestRuleList.changeState();
});
var rewriteRule =
RewriteRule(rule: currentIndex == -1 ? null : widget.configuration.requestRewrites.rules[currentIndex]);
Navigator.push(context, MaterialPageRoute(builder: (_) => rewriteRule)).then((rule) {
if (rule != null) {
changed = true;
setState(() {
if (currentIndex == -1) {
widget.configuration.requestRewrites.addRule(rule);
} else {
widget.configuration.requestRewrites.rules[currentIndex] = rule;
}
requestRuleList.changeState();
});
}
});
}
}
///请求重写规则添加对话框
class RuleAddDialog extends StatelessWidget {
final RequestRewrites requestRewrites;
class RewriteRule extends StatelessWidget {
final int currentIndex;
final Function onChange;
final RequestRewriteRule? rule;
const RuleAddDialog({super.key, required this.currentIndex, required this.onChange, required this.requestRewrites});
const RewriteRule({super.key, required this.rule, this.currentIndex = -1});
@override
Widget build(BuildContext context) {
GlobalKey formKey = GlobalKey<FormState>();
RequestRewriteRule? rule;
if (currentIndex >= 0) {
rule = requestRewrites.rules[currentIndex];
}
ValueNotifier<bool> enableNotifier = ValueNotifier(rule == null || rule.enabled);
ValueNotifier<bool> enableNotifier = ValueNotifier(rule == null || rule!.enabled);
String? domain = rule?.domain;
String? path = rule?.path;
String? requestBody = rule?.requestBody;
String? responseBody = rule?.responseBody;
return AlertDialog(
title: const Text("添加请求重写规则", style: TextStyle(fontSize: 16)),
scrollable: true,
content: Form(
key: formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
ValueListenableBuilder(
valueListenable: enableNotifier,
builder: (_, bool enable, __) {
return SwitchListTile(
contentPadding: const EdgeInsets.only(left: 0),
title: const Text('是否启用', textAlign: TextAlign.start),
value: enable,
onChanged: (value) => enableNotifier.value = value);
}),
TextFormField(
decoration: const InputDecoration(labelText: '域名(可选)', hintText: 'baidu.com 不需要填写HTTP'),
initialValue: domain,
onSaved: (val) => domain = val),
TextFormField(
decoration: const InputDecoration(labelText: 'Path', hintText: '/api/v1/*'),
validator: (val) {
if (val == null || val.isEmpty) {
return 'Path不能为空';
}
return null;
},
initialValue: path,
onSaved: (val) => path = val),
TextFormField(
initialValue: requestBody,
decoration: const InputDecoration(labelText: '请求体替换为:'),
onSaved: (val) => requestBody = val),
TextFormField(
initialValue: responseBody,
minLines: 3,
maxLines: 10,
decoration: const InputDecoration(labelText: '响应体替换为:', hintText: '{"code":"200","data":{}}'),
onSaved: (val) => responseBody = val)
])),
return Scaffold(
appBar: AppBar(
title: const Text("请求重写规则", style: TextStyle(fontSize: 16)),
actions: [
FilledButton(
TextButton(
child: const Text("保存"),
onPressed: () {
if ((formKey.currentState as FormState).validate()) {
(formKey.currentState as FormState).save();
enableNotifier.dispose();
var requestRewriteRule = RequestRewriteRule(
enableNotifier.value, path!, domain?.trim().isEmpty == true ? null : domain?.trim(),
requestBody: requestBody, responseBody: responseBody);
if (currentIndex >= 0) {
requestRewrites.rules[currentIndex] = RequestRewriteRule(enableNotifier.value, path!, domain,
requestBody: requestBody, responseBody: responseBody);
RequestRewrites.instance.rules[currentIndex] = requestRewriteRule;
} else {
requestRewrites.addRule(RequestRewriteRule(enableNotifier.value, path!, domain,
requestBody: requestBody, responseBody: responseBody));
RequestRewrites.instance.addRule(requestRewriteRule);
}
enableNotifier.dispose();
onChange.call();
Navigator.of(context).pop();
FlutterToastr.show("添加请求重写规则成功", context);
Navigator.of(context).pop(requestRewriteRule);
}
}),
ElevatedButton(
child: const Text("关闭"),
onPressed: () {
Navigator.of(context).pop();
})
]);
],
),
body: Padding(
padding: const EdgeInsets.all(10),
child: Form(
key: formKey,
child: ListView(children: <Widget>[
ValueListenableBuilder(
valueListenable: enableNotifier,
builder: (_, bool enable, __) {
return SwitchListTile(
contentPadding: const EdgeInsets.only(left: 0),
title: const Text('是否启用', textAlign: TextAlign.start),
value: enable,
onChanged: (value) => enableNotifier.value = value);
}),
TextFormField(
decoration: const InputDecoration(labelText: '域名(可选)', hintText: 'baidu.com 不需要填写HTTP'),
initialValue: domain,
onSaved: (val) => domain = val),
TextFormField(
decoration: const InputDecoration(labelText: 'Path', hintText: '/api/v1/*'),
validator: (val) {
if (val == null || val.isEmpty) {
return 'Path不能为空';
}
return null;
},
minLines: 1,
maxLines: 3,
initialValue: path,
onSaved: (val) => path = val),
TextFormField(
initialValue: requestBody,
decoration: const InputDecoration(labelText: '请求体替换为:'),
minLines: 1,
maxLines: 3,
onSaved: (val) => requestBody = val),
TextFormField(
initialValue: responseBody,
minLines: 3,
maxLines: 10,
decoration: const InputDecoration(labelText: '响应体替换为:', hintText: '{"code":"200","data":{}}'),
onSaved: (val) => responseBody = val)
]))),
);
}
}
@@ -210,24 +212,15 @@ class RequestRuleList extends StatefulWidget {
State<RequestRuleList> createState() => _RequestRuleListState();
List<int> removeSelected() {
var index = currentSelectedIndex();
var state = (key as GlobalKey<_RequestRuleListState>).currentState;
List<int> list = [];
var selectedIndex = state?.currentSelectedIndex;
state?.selected.forEach((key, value) {
if (value == true) {
list.add(key);
if (selectedIndex == key) {
state.currentSelectedIndex = -1;
}
}
});
state?.selected.clear();
return list;
state?.selected = -1;
return index == -1 ? [] : [index];
}
int currentSelectedIndex() {
var state = (key as GlobalKey<_RequestRuleListState>).currentState;
return state?.currentSelectedIndex ?? -1;
return state?.selected ?? -1;
}
changeState() {
@@ -237,8 +230,7 @@ class RequestRuleList extends StatefulWidget {
}
class _RequestRuleListState extends State<RequestRuleList> {
final Map<int, bool> selected = {};
int currentSelectedIndex = -1;
int selected = -1;
changeState() {
setState(() {});
@@ -262,32 +254,43 @@ class _RequestRuleListState extends State<RequestRuleList> {
rows: List.generate(
widget.requestRewrites.rules.length,
(index) => DataRow(
cells: [
DataCell(Text(widget.requestRewrites.rules[index].enabled ? "" : "")),
DataCell(ConstrainedBox(
constraints: const BoxConstraints(minWidth: 60),
child: Text(
'${widget.requestRewrites.rules[index].domain ?? ''}${widget.requestRewrites.rules[index].path}'))),
DataCell(Container(
constraints: const BoxConstraints(maxWidth: 180),
cells: [
cell(Text(widget.requestRewrites.rules[index].enabled ? "" : ""), index),
cell(
ConstrainedBox(
constraints: const BoxConstraints(minWidth: 60, maxWidth: 150),
child: Text(
'${widget.requestRewrites.rules[index].domain ?? ''}${widget.requestRewrites.rules[index].path}')),
index),
cell(
Container(
constraints: const BoxConstraints(maxWidth: 150),
child: SelectableText.rich(
maxLines: 3,
TextSpan(text: widget.requestRewrites.rules[index].requestBody),
style: const TextStyle(fontSize: 12))),
index),
cell(
Container(
constraints: const BoxConstraints(maxWidth: 200),
padding: const EdgeInsetsDirectional.all(10),
child: SelectableText.rich(
TextSpan(text: widget.requestRewrites.rules[index].requestBody),
style: const TextStyle(fontSize: 12)))),
DataCell(Container(
constraints: const BoxConstraints(maxWidth: 300),
padding: const EdgeInsetsDirectional.all(10),
child: SelectableText.rich(
TextSpan(text: widget.requestRewrites.rules[index].responseBody),
style: const TextStyle(fontSize: 12)),
))
],
selected: selected[index] == true,
onSelectChanged: (value) {
setState(() {
selected[index] = value!;
currentSelectedIndex = index;
});
})),
maxLines: 5,
TextSpan(text: widget.requestRewrites.rules[index].responseBody),
style: const TextStyle(fontSize: 12)),
),
index)
],
selected: selected == index,
)),
)));
}
DataCell cell(Widget child, int index) {
return DataCell(child, onTap: () {
setState(() {
selected = index;
});
});
}
}

View File

@@ -13,6 +13,6 @@ String curlRequest(HttpRequest request) {
if (request.bodyAsString.isNotEmpty) {
body = " --data '${request.bodyAsString}' \\\n";
}
return "curl '${request.requestUrl}' \\\n"
return "curl -X ${request.method.name} '${request.requestUrl}' \\\n"
"${headers.join('\\\n')} $body \\\n --compressed";
}