请求重写增加重定向&请求体重写增加URL参数重写

This commit is contained in:
wanghongen
2023-09-08 01:38:08 +08:00
parent 51cadcd39b
commit b8525e75ae
8 changed files with 224 additions and 110 deletions

View File

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

View File

@@ -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" //允许的名单

View File

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

View File

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

View File

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

View File

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

View File

@@ -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()),
]);
}

View File

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