From b8525e75aedf74dfbb8e1665e31eed0a5db7b656 Mon Sep 17 00:00:00 2001 From: wanghongen Date: Fri, 8 Sep 2023 01:38:08 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AF=B7=E6=B1=82=E9=87=8D=E5=86=99=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E9=87=8D=E5=AE=9A=E5=90=91&=E8=AF=B7=E6=B1=82?= =?UTF-8?q?=E4=BD=93=E9=87=8D=E5=86=99=E5=A2=9E=E5=8A=A0URL=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E9=87=8D=E5=86=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +- .../com/network/proxy/ProxyVpnService.kt | 4 +- lib/network/handler.dart | 32 ++- lib/network/http/http.dart | 6 +- lib/network/http_client.dart | 10 +- lib/network/util/request_rewrite.dart | 58 ++++- lib/ui/content/panel.dart | 4 +- .../toolbar/setting/request_rewrite.dart | 216 +++++++++++------- 8 files changed, 224 insertions(+), 110 deletions(-) diff --git a/README.md b/README.md index 396f51c..e3dbdd7 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/android/app/src/main/kotlin/com/network/proxy/ProxyVpnService.kt b/android/app/src/main/kotlin/com/network/proxy/ProxyVpnService.kt index 9dfa71c..50af240 100644 --- a/android/app/src/main/kotlin/com/network/proxy/ProxyVpnService.kt +++ b/android/app/src/main/kotlin/com/network/proxy/ProxyVpnService.kt @@ -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" //允许的名单 diff --git a/lib/network/handler.dart b/lib/network/handler.dart index 5ef1996..30a0116 100644 --- a/lib/network/handler.dart +++ b/lib/network/handler.dart @@ -115,7 +115,6 @@ class HttpChannelHandler extends ChannelHandler { 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 { 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'; diff --git a/lib/network/http/http.dart b/lib/network/http/http.dart index b7e7af8..306afb2 100644 --- a/lib/network/http/http.dart +++ b/lib/network/http/http.dart @@ -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; } diff --git a/lib/network/http_client.dart b/lib/network/http_client.dart index aca3eb7..4039d74 100644 --- a/lib/network/http_client.dart +++ b/lib/network/http_client.dart @@ -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 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; diff --git a/lib/network/util/request_rewrite.dart b/lib/network/util/request_rewrite.dart index db50e80..028cfd6 100644 --- a/lib/network/util/request_rewrite.dart +++ b/lib/network/util/request_rewrite.dart @@ -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 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, }; } } diff --git a/lib/ui/content/panel.dart b/lib/ui/content/panel.dart index 3424d3e..7773a00 100644 --- a/lib/ui/content/panel.dart +++ b/lib/ui/content/panel.dart @@ -158,9 +158,9 @@ class NetworkTabState extends State 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()), ]); } diff --git a/lib/ui/desktop/toolbar/setting/request_rewrite.dart b/lib/ui/desktop/toolbar/setting/request_rewrite.dart index 184c9e6..6853f2a 100644 --- a/lib/ui/desktop/toolbar/setting/request_rewrite.dart +++ b/lib/ui/desktop/toolbar/setting/request_rewrite.dart @@ -110,67 +110,92 @@ class _RequestRewriteState extends State { } ///请求重写规则添加对话框 -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 createState() { + return _RuleAddDialogState(); + } +} + +class _RuleAddDialogState extends State { + late ValueNotifier 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(); - ValueNotifier 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: [ - 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: [ + 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( + 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 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 { dataRowMaxHeight: 100, border: TableBorder.symmetric(outside: BorderSide(width: 1, color: Theme.of(context).highlightColor)), columns: const [ + 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; + }); + }, + )), ))); } }