mirror of
https://github.com/wanghongenpin/proxypin.git
synced 2026-03-19 05:19:47 +08:00
请求重写增加重定向&请求体重写增加URL参数重写
This commit is contained in:
@@ -5,9 +5,9 @@
|
||||
|
||||
国内下载地址: https://gitee.com/wanghongenpin/network-proxy-flutter/releases
|
||||
|
||||
iOS美版香港AppStore下载地址或直接搜ProxyPin(推荐): https://apps.apple.com/app/proxypin/id6450932949
|
||||
iOS美版香港AppStore下载地址或直接搜ProxyPin: https://apps.apple.com/app/proxypin/id6450932949
|
||||
|
||||
iOS国内下载地址(有1万名额限制,满了会清理不使用的用户): https://testflight.apple.com/join/gURGH6B4
|
||||
iOS国内TF下载地址(有1万名额限制,满了会清理不使用的用户): https://testflight.apple.com/join/gURGH6B4
|
||||
|
||||
TG: https://t.me/proxypin_tg
|
||||
|
||||
|
||||
@@ -15,10 +15,12 @@ import androidx.core.app.NotificationCompat
|
||||
* VPN服务
|
||||
* @author wanghongen
|
||||
*/
|
||||
class ProxyVpnService : VpnService(), IProtectSocket {
|
||||
class ProxyVpnService : VpnService() {
|
||||
private var vpnInterface: ParcelFileDescriptor? = null
|
||||
|
||||
companion object {
|
||||
const val MAX_PACKET_LEN = 1500
|
||||
|
||||
const val ProxyHost = "ProxyHost"
|
||||
const val ProxyPort = "ProxyPort"
|
||||
const val AllowApps = "AllowApps" //允许的名单
|
||||
|
||||
@@ -115,7 +115,6 @@ class HttpChannelHandler extends ChannelHandler<HttpRequest> {
|
||||
remoteChannel = await _getRemoteChannel(channel, httpRequest);
|
||||
remoteChannel.putAttribute(remoteChannel.id, channel);
|
||||
} catch (error) {
|
||||
|
||||
channel.error = error; //记录异常
|
||||
//https代理新建连接请求
|
||||
if (httpRequest.method == HttpMethod.connect) {
|
||||
@@ -129,18 +128,41 @@ class HttpChannelHandler extends ChannelHandler<HttpRequest> {
|
||||
if (httpRequest.method != HttpMethod.connect) {
|
||||
log.i("[${channel.id}] ${httpRequest.method.name} ${httpRequest.requestUrl}");
|
||||
|
||||
var replaceBody = requestRewrites?.findRequestReplaceWith(httpRequest.hostAndPort?.host, httpRequest.path());
|
||||
if (replaceBody?.isNotEmpty == true) {
|
||||
httpRequest.body = utf8.encode(replaceBody!);
|
||||
}
|
||||
//替换请求体
|
||||
_rewriteBody(httpRequest);
|
||||
|
||||
if (!HostFilter.filter(httpRequest.hostAndPort?.host)) {
|
||||
listener?.onRequest(channel, httpRequest);
|
||||
}
|
||||
|
||||
//重定向
|
||||
var redirectRewrite =
|
||||
requestRewrites?.findRequestRewrite(httpRequest.hostAndPort?.host, httpRequest.path(), RuleType.redirect);
|
||||
if (redirectRewrite?.redirectUrl?.isNotEmpty == true) {
|
||||
var proxyHandler = HttpResponseProxyHandler(channel, listener: listener, requestRewrites: requestRewrites);
|
||||
httpRequest.uri = redirectRewrite!.redirectUrl!;
|
||||
httpRequest.headers.host = Uri.parse(redirectRewrite.redirectUrl!).host;
|
||||
var redirectChannel = await HttpClients.connect(Uri.parse(redirectRewrite.redirectUrl!), proxyHandler);
|
||||
await redirectChannel.write(httpRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
await remoteChannel.write(httpRequest);
|
||||
}
|
||||
}
|
||||
|
||||
//替换请求体
|
||||
_rewriteBody(HttpRequest httpRequest) {
|
||||
var rewrite = requestRewrites?.findRequestRewrite(httpRequest.hostAndPort?.host, httpRequest.path(), RuleType.body);
|
||||
|
||||
if (rewrite?.requestBody?.isNotEmpty == true) {
|
||||
httpRequest.body = utf8.encode(rewrite!.requestBody!);
|
||||
}
|
||||
if (rewrite?.queryParam?.isNotEmpty == true) {
|
||||
httpRequest.uri = httpRequest.requestUri?.replace(query: rewrite!.queryParam!).toString() ?? httpRequest.uri;
|
||||
}
|
||||
}
|
||||
|
||||
/// 下载证书
|
||||
void _crtDownload(Channel channel, HttpRequest request) async {
|
||||
const String fileMimeType = 'application/x-x509-ca-cert';
|
||||
|
||||
@@ -81,7 +81,7 @@ abstract class HttpMessage {
|
||||
|
||||
///HTTP请求。
|
||||
class HttpRequest extends HttpMessage {
|
||||
final String uri;
|
||||
String uri;
|
||||
late HttpMethod method;
|
||||
|
||||
HostAndPort? hostAndPort;
|
||||
@@ -90,10 +90,10 @@ class HttpRequest extends HttpMessage {
|
||||
|
||||
HttpRequest(this.method, this.uri, {String protocolVersion = "HTTP/1.1"}) : super(protocolVersion);
|
||||
|
||||
String? remoteDomain() {
|
||||
String? remoteDomain() {
|
||||
if (hostAndPort == null) {
|
||||
try {
|
||||
return Uri.parse(uri).host;
|
||||
return Uri.parse(uri).host;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -40,6 +40,11 @@ class HttpClients {
|
||||
var client = Client()
|
||||
..initChannel((channel) => channel.pipeline.handle(HttpResponseCodec(), HttpRequestCodec(), handler));
|
||||
|
||||
if (proxyInfo == null) {
|
||||
var proxyTypes = hostAndPort.isSsl() ? ProxyTypes.https : ProxyTypes.http;
|
||||
proxyInfo = await SystemProxy.getSystemProxy(proxyTypes);
|
||||
}
|
||||
|
||||
HostAndPort connectHost = proxyInfo == null ? hostAndPort : HostAndPort.host(proxyInfo.host, proxyInfo.port!);
|
||||
var channel = await client.connect(connectHost);
|
||||
|
||||
@@ -102,11 +107,6 @@ class HttpClients {
|
||||
/// 发送代理请求
|
||||
static Future<HttpResponse> proxyRequest(HttpRequest request,
|
||||
{ProxyInfo? proxyInfo, Duration timeout = const Duration(seconds: 3)}) async {
|
||||
if (proxyInfo == null) {
|
||||
var proxyTypes = request.uri.startsWith("https://") ? ProxyTypes.https : ProxyTypes.http;
|
||||
proxyInfo = await SystemProxy.getSystemProxy(proxyTypes);
|
||||
}
|
||||
|
||||
if (request.headers.host == null || request.headers.host?.trim().isEmpty == true) {
|
||||
try {
|
||||
request.headers.host = Uri.parse(request.uri).host;
|
||||
|
||||
@@ -23,16 +23,17 @@ class RequestRewrites {
|
||||
});
|
||||
}
|
||||
|
||||
String? findRequestReplaceWith(String? domain, String? url) {
|
||||
RequestRewriteRule? findRequestRewrite(String? domain, String? url, RuleType type) {
|
||||
if (!enabled || url == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (var rule in rules) {
|
||||
if (rule.enabled && rule.urlReg.hasMatch(url)) {
|
||||
if (rule.enabled && rule.urlReg.hasMatch(url) && type == rule.type) {
|
||||
if (rule.domain?.isNotEmpty == true && rule.domain != domain) {
|
||||
continue;
|
||||
}
|
||||
return rule.requestBody;
|
||||
return rule;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -72,29 +73,68 @@ class RequestRewrites {
|
||||
}
|
||||
}
|
||||
|
||||
enum RuleType {
|
||||
body("重写消息体"),
|
||||
// header("重写Header"),
|
||||
redirect("重定向");
|
||||
|
||||
//名称
|
||||
final String name;
|
||||
|
||||
const RuleType(this.name);
|
||||
|
||||
static RuleType fromName(String name) {
|
||||
return values.firstWhere((element) => element.name == name);
|
||||
}
|
||||
}
|
||||
|
||||
class RequestRewriteRule {
|
||||
bool enabled = false;
|
||||
final String path;
|
||||
final String? domain;
|
||||
final RegExp urlReg;
|
||||
String path;
|
||||
String? domain;
|
||||
RuleType type;
|
||||
|
||||
String? name;
|
||||
|
||||
//消息体
|
||||
String? queryParam;
|
||||
String? requestBody;
|
||||
String? responseBody;
|
||||
|
||||
RequestRewriteRule(this.enabled, this.path, this.domain, {this.requestBody, this.responseBody})
|
||||
//重定向
|
||||
String? redirectUrl;
|
||||
|
||||
RegExp urlReg;
|
||||
|
||||
RequestRewriteRule(this.enabled, this.path, this.domain,
|
||||
{this.name, this.type = RuleType.body, this.queryParam, this.requestBody, this.responseBody, this.redirectUrl})
|
||||
: urlReg = RegExp(path.replaceAll("*", ".*"));
|
||||
|
||||
factory RequestRewriteRule.formJson(Map<String, dynamic> map) {
|
||||
return RequestRewriteRule(map['enabled'] == true, map['path'] ?? map['url'], map['domain'],
|
||||
requestBody: map['requestBody'], responseBody: map['responseBody']);
|
||||
return RequestRewriteRule(map['enabled'] == true, map['path'], map['domain'],
|
||||
name: map['name'],
|
||||
type: RuleType.fromName(map['type']),
|
||||
queryParam: map['queryParam'],
|
||||
requestBody: map['requestBody'],
|
||||
responseBody: map['responseBody'],
|
||||
redirectUrl: map['redirectUrl']);
|
||||
}
|
||||
|
||||
void updatePathReg() {
|
||||
urlReg = RegExp(path.replaceAll("*", ".*"));
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
'name': name,
|
||||
'enabled': enabled,
|
||||
'domain': domain,
|
||||
'path': path,
|
||||
'type': type.name,
|
||||
'queryParam': queryParam,
|
||||
'requestBody': requestBody,
|
||||
'responseBody': responseBody,
|
||||
'redirectUrl': redirectUrl,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,9 +158,9 @@ class NetworkTabState extends State<NetworkTabController> with SingleTickerProvi
|
||||
|
||||
var responseCookie = widget.response.get()?.headers.getList("Set-Cookie")?.expand((e) => _cookieWidget(e)!);
|
||||
return ListView(children: [
|
||||
expansionTile("Request Cookies", requestCookie?.toList() ?? []),
|
||||
requestCookie == null ? const SizedBox() : expansionTile("Request Cookies", requestCookie.toList()),
|
||||
const SizedBox(height: 20),
|
||||
expansionTile("Response Cookies", responseCookie?.toList() ?? []),
|
||||
responseCookie == null ? const SizedBox() : expansionTile("Response Cookies", responseCookie.toList()),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -110,67 +110,92 @@ class _RequestRewriteState extends State<RequestRewrite> {
|
||||
}
|
||||
|
||||
///请求重写规则添加对话框
|
||||
class RuleAddDialog extends StatelessWidget {
|
||||
class RuleAddDialog extends StatefulWidget {
|
||||
final int currentIndex;
|
||||
final RequestRewriteRule? rule;
|
||||
|
||||
const RuleAddDialog({super.key, this.currentIndex = -1, this.rule});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _RuleAddDialogState();
|
||||
}
|
||||
}
|
||||
|
||||
class _RuleAddDialogState extends State<RuleAddDialog> {
|
||||
late ValueNotifier<bool> enableNotifier;
|
||||
late RequestRewriteRule rule;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
rule = widget.rule ?? RequestRewriteRule(true, "", null);
|
||||
enableNotifier = ValueNotifier(rule.enabled == true);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
enableNotifier.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
GlobalKey formKey = GlobalKey<FormState>();
|
||||
|
||||
ValueNotifier<bool> enableNotifier = ValueNotifier(rule == null || rule?.enabled == true);
|
||||
String? domain = rule?.domain;
|
||||
String? path = rule?.path;
|
||||
String? requestBody = rule?.requestBody;
|
||||
String? responseBody = rule?.responseBody;
|
||||
|
||||
return AlertDialog(
|
||||
title: const Text("添加请求重写规则", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600)),
|
||||
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: '请求体替换为:'),
|
||||
minLines: 1,
|
||||
maxLines: 5,
|
||||
onSaved: (val) => requestBody = val),
|
||||
TextFormField(
|
||||
initialValue: responseBody,
|
||||
minLines: 3,
|
||||
maxLines: 15,
|
||||
decoration: const InputDecoration(labelText: '响应体替换为:', hintText: '{"code":"200","data":{}}'),
|
||||
onSaved: (val) => responseBody = val)
|
||||
])),
|
||||
content: Container(
|
||||
constraints: const BoxConstraints(minWidth: 320),
|
||||
child: 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: '名称'),
|
||||
initialValue: rule.name,
|
||||
onSaved: (val) => rule.name = val,
|
||||
),
|
||||
TextFormField(
|
||||
decoration: const InputDecoration(labelText: '域名(可选)', hintText: 'baidu.com 不需要填写HTTP'),
|
||||
initialValue: rule.domain,
|
||||
onSaved: (val) => rule.domain = val?.trim()),
|
||||
TextFormField(
|
||||
decoration: const InputDecoration(labelText: 'Path', hintText: '/api/v1/*'),
|
||||
validator: (val) {
|
||||
if (val == null || val.isEmpty) {
|
||||
return 'Path不能为空';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
initialValue: rule.path,
|
||||
onSaved: (val) => rule.path = val!.trim()),
|
||||
DropdownButtonFormField<RuleType>(
|
||||
decoration: const InputDecoration(labelText: '行为'),
|
||||
value: rule.type,
|
||||
items: RuleType.values
|
||||
.map((e) =>
|
||||
DropdownMenuItem(value: e, child: Text(e.name, style: const TextStyle(fontSize: 14))))
|
||||
.toList(),
|
||||
onChanged: (val) {
|
||||
setState(() {
|
||||
rule.type = val!;
|
||||
});
|
||||
}),
|
||||
...rewriteWidgets()
|
||||
]))),
|
||||
actions: [
|
||||
FilledButton(
|
||||
child: const Text("保存"),
|
||||
@@ -178,12 +203,9 @@ 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.instance.rules[currentIndex] = rule;
|
||||
rule.updatePathReg();
|
||||
if (widget.currentIndex >= 0) {
|
||||
RequestRewrites.instance.rules[widget.currentIndex] = rule;
|
||||
} else {
|
||||
RequestRewrites.instance.addRule(rule);
|
||||
}
|
||||
@@ -199,6 +221,43 @@ class RuleAddDialog extends StatelessWidget {
|
||||
})
|
||||
]);
|
||||
}
|
||||
|
||||
List<Widget> rewriteWidgets() {
|
||||
if (rule.type == RuleType.redirect) {
|
||||
return [
|
||||
TextFormField(
|
||||
decoration: const InputDecoration(labelText: '重定向到:', hintText: 'http://www.example.com/api'),
|
||||
initialValue: rule.redirectUrl,
|
||||
onSaved: (val) => rule.redirectUrl = val,
|
||||
validator: (val) {
|
||||
if (val == null || val.trim().isEmpty) {
|
||||
return '重定向URL不能为空';
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
TextFormField(
|
||||
initialValue: rule.queryParam,
|
||||
decoration: const InputDecoration(labelText: 'URL参数替换为:'),
|
||||
maxLines: 1,
|
||||
onSaved: (val) => rule.queryParam = val),
|
||||
TextFormField(
|
||||
initialValue: rule.requestBody,
|
||||
decoration: const InputDecoration(labelText: '请求体替换为:'),
|
||||
minLines: 1,
|
||||
maxLines: 5,
|
||||
onSaved: (val) => rule.requestBody = val),
|
||||
TextFormField(
|
||||
initialValue: rule.responseBody,
|
||||
minLines: 3,
|
||||
maxLines: 15,
|
||||
decoration: const InputDecoration(labelText: '响应体替换为:', hintText: '{"code":"200","data":{}}'),
|
||||
onSaved: (val) => rule.responseBody = val)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class RequestRuleList extends StatefulWidget {
|
||||
@@ -245,40 +304,31 @@ class _RequestRuleListState extends State<RequestRuleList> {
|
||||
dataRowMaxHeight: 100,
|
||||
border: TableBorder.symmetric(outside: BorderSide(width: 1, color: Theme.of(context).highlightColor)),
|
||||
columns: const <DataColumn>[
|
||||
DataColumn(label: Text('名称')),
|
||||
DataColumn(label: Text('启用')),
|
||||
DataColumn(label: Text('URL')),
|
||||
DataColumn(label: Text('请求体')),
|
||||
DataColumn(label: Text('响应体')),
|
||||
DataColumn(label: Text('行为')),
|
||||
],
|
||||
rows: List.generate(
|
||||
widget.requestRewrites.rules.length,
|
||||
(index) => DataRow(
|
||||
cells: [
|
||||
DataCell(Text(widget.requestRewrites.rules[index].enabled ? "是" : "否")),
|
||||
DataCell(ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 60, maxWidth: 280),
|
||||
child: Text(
|
||||
'${widget.requestRewrites.rules[index].domain ?? ''}${widget.requestRewrites.rules[index].path}'))),
|
||||
DataCell(Container(
|
||||
constraints: const BoxConstraints(maxWidth: 120),
|
||||
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: currentSelectedIndex == index,
|
||||
onSelectChanged: (value) {
|
||||
setState(() {
|
||||
currentSelectedIndex = value == true ? index : -1;
|
||||
});
|
||||
},
|
||||
)),
|
||||
cells: [
|
||||
DataCell(Text(widget.requestRewrites.rules[index].name ?? "")),
|
||||
DataCell(Text(widget.requestRewrites.rules[index].enabled ? "是" : "否")),
|
||||
DataCell(ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 60, maxWidth: 280),
|
||||
child: Text(
|
||||
'${widget.requestRewrites.rules[index].domain ?? ''}${widget.requestRewrites.rules[index].path}'),
|
||||
)),
|
||||
DataCell(Text(widget.requestRewrites.rules[index].type.name)),
|
||||
],
|
||||
selected: currentSelectedIndex == index,
|
||||
onSelectChanged: (value) {
|
||||
setState(() {
|
||||
currentSelectedIndex = value == true ? index : -1;
|
||||
});
|
||||
},
|
||||
)),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user