From 8e8123f3a8b076b84f3b74bc9c11187a6ec71f2c Mon Sep 17 00:00:00 2001 From: wanghongen Date: Mon, 31 Jul 2023 15:32:01 +0800 Subject: [PATCH] =?UTF-8?q?=E6=90=9C=E7=B4=A2=E5=8A=9F=E8=83=BD=E5=A2=9E?= =?UTF-8?q?=E5=BC=BA=EF=BC=8C=E5=8F=AF=E7=9B=B4=E6=8E=A5=E6=90=9C=E7=B4=A2?= =?UTF-8?q?=E5=93=8D=E5=BA=94=E7=B1=BB=E5=9E=8B=E5=92=8C=E8=AF=B7=E6=B1=82?= =?UTF-8?q?=E6=96=B9=E6=B3=95,=20=E6=94=AF=E6=8C=81brotli=E7=BC=96?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/network/bin/configuration.dart | 30 +++++ lib/network/bin/server.dart | 12 +- lib/network/channel.dart | 38 +++++-- lib/network/handler.dart | 16 ++- lib/network/http/http.dart | 30 ++++- lib/network/http/http_headers.dart | 4 +- lib/network/util/attribute_keys.dart | 1 + lib/ui/component/guide.dart | 0 lib/ui/desktop/left/domain.dart | 35 +++--- lib/ui/desktop/left/model/search.dart | 17 +++ lib/ui/desktop/left/search.dart | 104 ++++++++++++++---- .../toolbar/setting/external_proxy.dart | 89 +++++++++++++++ lib/ui/desktop/toolbar/setting/setting.dart | 48 +++++--- lib/ui/mobile/mobile.dart | 18 +-- lib/ui/mobile/request/list.dart | 6 +- lib/utils/compress.dart | 14 ++- lib/utils/lang.dart | 7 ++ pubspec.lock | 12 +- pubspec.yaml | 1 + test/web_test.dart | 90 +++++++++++++++ 20 files changed, 473 insertions(+), 99 deletions(-) delete mode 100644 lib/ui/component/guide.dart create mode 100644 lib/ui/desktop/left/model/search.dart create mode 100644 lib/ui/desktop/toolbar/setting/external_proxy.dart create mode 100644 test/web_test.dart diff --git a/lib/network/bin/configuration.dart b/lib/network/bin/configuration.dart index 9f21a41..5785087 100644 --- a/lib/network/bin/configuration.dart +++ b/lib/network/bin/configuration.dart @@ -25,6 +25,9 @@ class Configuration { //请求重写 RequestRewrites requestRewrites = RequestRewrites(); + //外部代理 + ProxyInfo? externalProxy; + Configuration._(); /// 单例 @@ -94,6 +97,9 @@ class Configuration { enableDesktop = config['enableDesktop'] ?? true; guide = config['guide'] ?? false; upgradeNotice = config['upgradeNotice'] ?? true; + if (config['externalProxy'] != null) { + externalProxy = ProxyInfo.fromJson(config['externalProxy']); + } HostFilter.whitelist.load(config['whitelist']); HostFilter.blacklist.load(config['blacklist']); @@ -135,8 +141,32 @@ class Configuration { 'port': port, 'enableSsl': enableSsl, 'enableDesktop': enableDesktop, + 'externalProxy': externalProxy?.toJson(), 'whitelist': HostFilter.whitelist.toJson(), 'blacklist': HostFilter.blacklist.toJson(), }; } } + +/// 代理信息 +class ProxyInfo { + bool enable = false; + String host = '127.0.0.1'; + int? port; + + ProxyInfo(); + + ProxyInfo.fromJson(Map json) { + enable = json['enable'] == true; + host = json['host']; + port = json['port']; + } + + Map toJson() { + return { + 'enable': enable, + 'host': host, + 'port': port, + }; + } +} diff --git a/lib/network/bin/server.dart b/lib/network/bin/server.dart index 3e4ffef..b885f7c 100644 --- a/lib/network/bin/server.dart +++ b/lib/network/bin/server.dart @@ -16,17 +16,20 @@ Future main() async { /// 代理服务器 class ProxyServer { - //是否启动 - bool get isRunning => server?.isRunning ?? false; + //socket服务 Server? server; //请求事件监听 EventListener? listener; + //配置 final Configuration configuration; ProxyServer(this.configuration, {this.listener}); + //是否启动 + bool get isRunning => server?.isRunning ?? false; + ///是否启用https抓包 bool get enableSsl => configuration.enableSsl; @@ -34,7 +37,6 @@ class ProxyServer { set enableSsl(bool enableSsl) { configuration.enableSsl = enableSsl; - server?.enableSsl = enableSsl; if (server == null || server?.isRunning == false) { return; } @@ -46,13 +48,13 @@ class ProxyServer { /// 启动代理服务 Future start() async { - Server server = Server(); + Server server = Server(configuration); - server.enableSsl = configuration.enableSsl; server.initChannel((channel) { channel.pipeline.handle(HttpRequestCodec(), HttpResponseCodec(), HttpChannelHandler(listener: listener, requestRewrites: configuration.requestRewrites)); }); + return server.bind(port).then((serverSocket) { logger.i("listen on $port"); this.server = server; diff --git a/lib/network/channel.dart b/lib/network/channel.dart index 8e7e719..99e46fe 100644 --- a/lib/network/channel.dart +++ b/lib/network/channel.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'dart:math'; import 'dart:typed_data'; +import 'package:network_proxy/network/bin/configuration.dart'; import 'package:network_proxy/network/http/http.dart'; import 'package:network_proxy/network/util/attribute_keys.dart'; import 'package:network_proxy/network/util/crts.dart'; @@ -35,6 +36,8 @@ class Channel { final ChannelPipeline pipeline = ChannelPipeline(); Socket _socket; final Map _attributes = {}; + + //是否打开 bool isOpen = true; //此通道连接到的远程地址 @@ -71,11 +74,13 @@ class Channel { } } + ///写入并关闭此channel Future writeAndClose(Object obj) async { await write(obj); close(); } + ///关闭此channel void close() async { if (isClosed) { return; @@ -90,6 +95,7 @@ class Channel { isOpen = false; } + ///返回此channel是否打开 bool get isClosed => !isOpen; T? getAttribute(String key) { @@ -205,7 +211,7 @@ class HostAndPort { } /// 根据url构建 - static HostAndPort of(String url) { + static HostAndPort of(String url, {bool? ssl}) { String domain = url; String? scheme; //域名格式 直接解析 @@ -221,11 +227,11 @@ class HostAndPort { //ip格式 host:port List hostAndPort = domain.split(":"); if (hostAndPort.length == 2) { - bool isSsl = hostAndPort[1] == "443"; + bool isSsl = ssl ?? hostAndPort[1] == "443"; scheme = isSsl ? httpsScheme : httpScheme; return HostAndPort(scheme, hostAndPort[0], int.parse(hostAndPort[1])); } - scheme ??= httpScheme; + scheme ??= (ssl == true ? httpsScheme : httpScheme); return HostAndPort(scheme, hostAndPort[0], scheme == httpScheme ? 80 : 443); } @@ -286,8 +292,8 @@ abstract interface class ChannelInitializer { class Network { late Function _channelInitializer; - bool enableSsl = false; String? remoteHost; + Configuration? configuration; Network initChannel(void Function(Channel channel) initializer) { _channelInitializer = initializer; @@ -305,14 +311,19 @@ class Network { } _onEvent(Uint8List data, Channel channel) async { - HostAndPort? hostAndPort = channel.getAttribute(AttributeKeys.host); - if (remoteHost != null) { channel.putAttribute(AttributeKeys.remote, HostAndPort.of(remoteHost!)); } - //黑名单 直接转发 - if (HostFilter.filter(hostAndPort?.host) || (hostAndPort?.isSsl() == true && !enableSsl)) { + //代理信息 + if (configuration?.externalProxy?.enable == true) { + channel.putAttribute(AttributeKeys.proxyInfo, configuration!.externalProxy!); + } + + HostAndPort? hostAndPort = channel.getAttribute(AttributeKeys.host); + + //黑名单 或 没开启https 直接转发 + if (HostFilter.filter(hostAndPort?.host) || (hostAndPort?.isSsl() == true && configuration?.enableSsl == false)) { relay(channel, channel.getAttribute(channel.id)); channel.pipeline.channelRead(channel, data); return; @@ -329,15 +340,16 @@ class Network { void ssl(Channel channel, HostAndPort hostAndPort, Uint8List data) async { try { - //客户端ssl + //客户端ssl握手 Channel remoteChannel = channel.getAttribute(channel.id); - remoteChannel.secureSocket = await SecureSocket.secure(remoteChannel.socket, onBadCertificate: (certificate) => true); + remoteChannel.pipeline.listen(remoteChannel); - //服务端ssl + //ssl自签证书 var certificate = await CertificateManager.getCertificateContext(hostAndPort.host); + SecureSocket secureSocket = await SecureSocket.secureServer(channel.socket, certificate, bufferedData: data); channel.secureSocket = secureSocket; channel.pipeline.listen(channel); @@ -358,6 +370,10 @@ class Server extends Network { late ServerSocket serverSocket; bool isRunning = false; + Server(Configuration configuration) { + super.configuration = configuration; + } + Future bind(int port) async { serverSocket = await ServerSocket.bind(InternetAddress.anyIPv4, port); serverSocket.listen((socket) { diff --git a/lib/network/handler.dart b/lib/network/handler.dart index 1bf1345..4135785 100644 --- a/lib/network/handler.dart +++ b/lib/network/handler.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:network_proxy/network/bin/configuration.dart'; import 'package:network_proxy/network/http/http.dart'; import 'package:network_proxy/network/http/http_headers.dart'; import 'package:network_proxy/network/util/attribute_keys.dart'; @@ -20,7 +21,8 @@ HostAndPort getHostAndPort(HttpRequest request) { if (request.uri.startsWith("/")) { requestUri = request.headers.get(HttpHeaders.HOST)!; } - return HostAndPort.of(requestUri); + + return HostAndPort.of(requestUri, ssl: request.method == HttpMethod.connect ? true : null); } abstract class EventListener { @@ -144,7 +146,7 @@ class HttpChannelHandler extends ChannelHandler { var proxyHandler = HttpResponseProxyHandler(clientChannel, listener: listener, requestRewrites: requestRewrites); - //远程代理 + //远程转发 HostAndPort? remote = clientChannel.getAttribute(AttributeKeys.remote); if (remote != null) { var proxyChannel = await HttpClients.rawConnect(remote, proxyHandler); @@ -153,9 +155,17 @@ class HttpChannelHandler extends ChannelHandler { return proxyChannel; } + //https代理 + ProxyInfo? proxyInfo = clientChannel.getAttribute(AttributeKeys.proxyInfo); + if (proxyInfo != null) { + var proxyChannel = await HttpClients.rawConnect(HostAndPort.host(proxyInfo.host, proxyInfo.port!), proxyHandler); + clientChannel.putAttribute(clientId, proxyChannel); + await proxyChannel.write(httpRequest); + return proxyChannel; + } + var proxyChannel = await HttpClients.rawConnect(hostAndPort, proxyHandler); clientChannel.putAttribute(clientId, proxyChannel); - //https代理新建连接请求 if (httpRequest.method == HttpMethod.connect) { await clientChannel.write(HttpResponse(httpRequest.protocolVersion, HttpStatus.ok)); diff --git a/lib/network/http/http.dart b/lib/network/http/http.dart index c3f2a6b..585a98a 100644 --- a/lib/network/http/http.dart +++ b/lib/network/http/http.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:network_proxy/network/channel.dart'; +import 'package:network_proxy/utils/compress.dart'; import 'http_headers.dart'; @@ -49,6 +50,9 @@ abstract class HttpMessage { return ""; } try { + if (headers.contentEncoding == 'br') { + return utf8.decode(brDecode(body!)); + } return utf8.decode(body!); } catch (e) { return String.fromCharCodes(body!); @@ -97,24 +101,38 @@ class HttpRequest extends HttpMessage { 'uri': requestUrl, 'method': method.name, 'headers': headers.toJson(), - 'body': bodyAsString, + 'body': body == null ? null : String.fromCharCodes(body!), }; } factory HttpRequest.fromJson(Map json) { var request = HttpRequest(HttpMethod.valueOf(json['method']), json['uri']); request.headers.addAll(HttpHeaders.fromJson(json['headers'])); - request.body = utf8.encode(json['body']); + request.body = json['body']?.toString().codeUnits; return request; } @override String toString() { - return 'HttpReqeust{version: $protocolVersion, url: $uri, method: ${method.name}, headers: $headers, contentLength: $contentLength, bodyLength: ${body?.length}}'; + return 'HttpRequest{version: $protocolVersion, url: $uri, method: ${method.name}, headers: $headers, contentLength: $contentLength, bodyLength: ${body?.length}}'; } } -enum ContentType { json, formUrl, js, html, text, css, font, image, http } +enum ContentType { + json, + formUrl, + js, + html, + text, + css, + font, + image, + http; + + static ContentType valueOf(String name) { + return ContentType.values.firstWhere((element) => element.name == name.toLowerCase(), orElse: () => http); + } +} ///HTTP响应。 class HttpResponse extends HttpMessage { @@ -134,7 +152,7 @@ class HttpResponse extends HttpMessage { factory HttpResponse.fromJson(Map json) { return HttpResponse(json['protocolVersion'], HttpStatus(json['status']['code'], json['status']['reasonPhrase'])) ..headers.addAll(HttpHeaders.fromJson(json['headers'])) - ..body = utf8.encode(json['body']); + ..body = json['body']?.toString().codeUnits; } @override @@ -147,7 +165,7 @@ class HttpResponse extends HttpMessage { 'reasonPhrase': status.reasonPhrase, }, 'headers': headers.toJson(), - 'body': bodyAsString, + 'body': body == null ? null : String.fromCharCodes(body!), }; } diff --git a/lib/network/http/http_headers.dart b/lib/network/http/http_headers.dart index 9f041a1..040a381 100644 --- a/lib/network/http/http_headers.dart +++ b/lib/network/http/http_headers.dart @@ -86,7 +86,9 @@ class HttpHeaders { set contentLength(int contentLength) => set(CONTENT_LENGTH, contentLength.toString()); - bool get isGzip => get(HttpHeaders.CONTENT_ENCODING) == "gzip"; + String? get contentEncoding => get(HttpHeaders.CONTENT_ENCODING); + + bool get isGzip => contentEncoding == "gzip"; bool get isChunked => get(HttpHeaders.TRANSFER_ENCODING) == "chunked"; diff --git a/lib/network/util/attribute_keys.dart b/lib/network/util/attribute_keys.dart index ac0ef7e..364ef58 100644 --- a/lib/network/util/attribute_keys.dart +++ b/lib/network/util/attribute_keys.dart @@ -5,4 +5,5 @@ interface class AttributeKeys { static const String uri = "URI"; static const String request = "REQUEST"; static const String remote = "REMOTE"; + static const String proxyInfo = "PROXY_INFO"; } diff --git a/lib/ui/component/guide.dart b/lib/ui/component/guide.dart deleted file mode 100644 index e69de29..0000000 diff --git a/lib/ui/desktop/left/domain.dart b/lib/ui/desktop/left/domain.dart index b44d342..ee014c9 100644 --- a/lib/ui/desktop/left/domain.dart +++ b/lib/ui/desktop/left/domain.dart @@ -9,6 +9,7 @@ import 'package:network_proxy/network/http/http.dart'; import 'package:network_proxy/network/util/attribute_keys.dart'; import 'package:network_proxy/network/util/host_filter.dart'; import 'package:network_proxy/ui/component/transition.dart'; +import 'package:network_proxy/ui/desktop/left/model/search.dart'; import 'package:network_proxy/ui/desktop/left/path.dart'; import 'package:network_proxy/ui/content/panel.dart'; import 'package:network_proxy/ui/desktop/left/search.dart'; @@ -29,8 +30,8 @@ class DomainWidget extends StatefulWidget { class DomainWidgetState extends State { LinkedHashMap containerMap = LinkedHashMap(); - //搜索的文本 - String? searchText; + //搜索的内容 + SearchModel? searchModel; bool changing = false; //是否存在刷新任务 changeState() { @@ -48,27 +49,24 @@ class DomainWidgetState extends State { Widget build(BuildContext context) { var list = containerMap.values; //根究搜素文本过滤 - if (searchText?.trim().isNotEmpty == true) { - list = searchFilter(searchText!); + if (searchModel?.isNotEmpty == true) { + list = searchFilter(searchModel!); } return Scaffold( body: SingleChildScrollView(child: Column(children: list.toList())), bottomNavigationBar: Search(onSearch: (val) { - if (val == searchText) { - return; - } setState(() { - searchText = val.toLowerCase(); + searchModel = val; }); })); } ///搜索过滤 - List searchFilter(String text) { + List searchFilter(SearchModel searchModel) { var result = []; containerMap.forEach((key, headerBody) { - var body = headerBody.filter(text); + var body = headerBody.filter(searchModel.keyword?.toLowerCase(), searchModel.contentType); if (body.isNotEmpty) { result.add(headerBody.copy(body: body, selected: true)); } @@ -86,7 +84,7 @@ class DomainWidgetState extends State { headerBody.addBody(channel.id, listURI); //搜索状态,刷新数据 - if (searchText?.isNotEmpty == true) { + if (searchModel?.isNotEmpty == true) { changeState(); } return; @@ -154,15 +152,22 @@ class HeaderBody extends StatefulWidget { } ///根据文本过滤 - Iterable filter(String text) { + Iterable filter(String? keyword, ContentType? contentType) { return _body.where((element) { - if (element.request.method.name.toLowerCase() == text) { + if (contentType != null && element.response.get()?.contentType != contentType) { + return false; + } + + if (keyword == null) { return true; } - if (element.request.requestUrl.toLowerCase().contains(text)) { + if (element.request.method.name.toLowerCase() == keyword) { return true; } - return element.response.get()?.contentType.name.toLowerCase().contains(text) == true; + if (element.request.requestUrl.toLowerCase().contains(keyword)) { + return true; + } + return element.response.get()?.contentType.name.toLowerCase().contains(keyword) == true; }); } diff --git a/lib/ui/desktop/left/model/search.dart b/lib/ui/desktop/left/model/search.dart new file mode 100644 index 0000000..8c75358 --- /dev/null +++ b/lib/ui/desktop/left/model/search.dart @@ -0,0 +1,17 @@ +import 'package:network_proxy/network/http/http.dart'; + +class SearchModel { + String? keyword; + ContentType? contentType; + + SearchModel(this.keyword, this.contentType); + + bool get isNotEmpty { + return keyword?.trim().isNotEmpty == true || contentType != null; + } + + @override + String toString() { + return 'SearchModel{keyword: $keyword, contentType: $contentType}'; + } +} diff --git a/lib/ui/desktop/left/search.dart b/lib/ui/desktop/left/search.dart index 0d72280..054b049 100644 --- a/lib/ui/desktop/left/search.dart +++ b/lib/ui/desktop/left/search.dart @@ -1,14 +1,24 @@ import 'package:flutter/material.dart'; +import 'package:network_proxy/network/http/http.dart'; +import 'package:network_proxy/ui/desktop/left/model/search.dart'; -class Search extends StatelessWidget { - final Function(String val)? onSearch; +class Search extends StatefulWidget { + final Function(SearchModel searchModel)? onSearch; const Search({super.key, this.onSearch}); + @override + State createState() { + return _SearchState(); + } +} + +class _SearchState extends State { + final SearchModel searchModel = SearchModel("", null); + bool changing = false; + @override Widget build(BuildContext context) { - bool changing = false; - String value = ""; return Container( height: 32, width: 300, @@ -19,35 +29,85 @@ class Search extends StatelessWidget { child: TextField( cursorHeight: 22, onChanged: (val) async { - value = val; + if (searchModel.keyword == val) { + return; + } + searchModel.keyword = val; if (!changing) { changing = true; - Future.delayed(const Duration(milliseconds: 800), () { + Future.delayed(const Duration(milliseconds: 500), () { changing = false; - onSearch?.call(value); + widget.onSearch?.call(searchModel); }); } }, - decoration: const InputDecoration( - contentPadding: EdgeInsets.all(0), + decoration: InputDecoration( + contentPadding: const EdgeInsets.all(0), border: InputBorder.none, - prefixIcon: Icon(Icons.search), + prefixIcon: const Icon(Icons.search), hintText: 'Search', - // suffixIcon: DropdownButton( - // value: "ALL", - // icon: const Icon(Icons.arrow_drop_up), - // isDense: true, - // hint: const Text('全部', style: TextStyle(fontSize: 12)), - // items: const [ - // DropdownMenuItem(value: "JSON", child: Text('JSON', style: TextStyle(fontSize: 12))), - // DropdownMenuItem(value: "HTML", child: Text('HTML', style: TextStyle(fontSize: 12))), - // DropdownMenuItem(value: "ALL", child: Text('全部', style: TextStyle(fontSize: 12))), - // ], - // onChanged: (value) {}, - // ), + suffixIcon: ContentTypeSelect(onSelected: (contentType) { + if (searchModel.contentType == contentType) { + return; + } + searchModel.contentType = contentType; + widget.onSearch?.call(searchModel); + }), ), ), ); } } + +class ContentTypeSelect extends StatefulWidget { + final Function(ContentType? contentType) onSelected; + + const ContentTypeSelect({super.key, required this.onSelected}); + + @override + State createState() { + return ContentTypeState(); + } +} + +class ContentTypeState extends State { + String value = "全部"; + List types = ["JSON", "HTML", "JS", "CSS", "IMAGE", 'FONT', "其他", "全部"]; + + @override + Widget build(BuildContext context) { + ContentType.json; + return PopupMenuButton( + initialValue: value, + offset: Offset(-10, (types.length - types.indexOf(value)) * -30.0 - 10), + tooltip: '响应类型', + constraints: const BoxConstraints(maxWidth: 75), + child: Wrap(runAlignment: WrapAlignment.center, children: [ + Text(value, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500)), + const Icon(Icons.arrow_drop_up, size: 20) + ]), + onSelected: (String value) { + print(value); + if (this.value == value) { + return; + } + setState(() { + this.value = value; + }); + widget.onSelected(value == "全部" ? null : ContentType.valueOf(value)); + }, + itemBuilder: (BuildContext context) { + return types.map(item).toList(); + }, + ); + } + + PopupMenuItem item(String value) { + return PopupMenuItem( + height: 30, + value: value, + child: Text(value, style: const TextStyle(fontSize: 12)), + ); + } +} diff --git a/lib/ui/desktop/toolbar/setting/external_proxy.dart b/lib/ui/desktop/toolbar/setting/external_proxy.dart new file mode 100644 index 0000000..86827da --- /dev/null +++ b/lib/ui/desktop/toolbar/setting/external_proxy.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:network_proxy/network/bin/configuration.dart'; + +class ExternalProxyDialog extends StatefulWidget { + final Configuration configuration; + + const ExternalProxyDialog({super.key, required this.configuration}); + + @override + State createState() { + return _ExternalProxyDialogState(); + } +} + +class _ExternalProxyDialogState extends State { + final formKey = GlobalKey(); + late ProxyInfo externalProxy; + + @override + void initState() { + super.initState(); + externalProxy = widget.configuration.externalProxy ?? ProxyInfo(); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + scrollable: true, + title: const Text("外部代理设置", style: TextStyle(fontSize: 15)), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text("取消")), + TextButton( + onPressed: () { + if (!formKey.currentState!.validate()) { + return; + } + widget.configuration.externalProxy = externalProxy; + widget.configuration.flushConfig(); + if (externalProxy.enable) { + + } + Navigator.of(context).pop(); + }, + child: const Text("确定")) + ], + content: Form( + key: formKey, + child: Column(mainAxisSize: MainAxisSize.min, children: [ + Row(children: [ + const Text("是否启用:"), + Expanded( + child: Switch( + value: externalProxy.enable, + onChanged: (val) { + setState(() => externalProxy.enable = val); + }, + )) + ]), + Row(children: [ + const Text("地址:"), + Expanded( + child: TextFormField( + initialValue: externalProxy.host, + validator: (val) => val == null || val.isEmpty ? "地址不能为空" : null, + onChanged: (val) => externalProxy.host = val, + )) + ]), + Row(children: [ + const Text("端口:"), + Expanded( + child: TextFormField( + initialValue: externalProxy.port?.toString() ?? '', + inputFormatters: [ + LengthLimitingTextInputFormatter(5), + FilteringTextInputFormatter.allow(RegExp("[0-9]")) + ], + onChanged: (val) => externalProxy.port = int.parse(val), + validator: (val) => val == null || val.isEmpty ? "端口不能为空" : null, + decoration: const InputDecoration(), + )) + ]), + ]))); + } +} diff --git a/lib/ui/desktop/toolbar/setting/setting.dart b/lib/ui/desktop/toolbar/setting/setting.dart index a809b80..0b09758 100644 --- a/lib/ui/desktop/toolbar/setting/setting.dart +++ b/lib/ui/desktop/toolbar/setting/setting.dart @@ -3,6 +3,7 @@ import 'package:flutter/services.dart'; import 'package:network_proxy/network/bin/configuration.dart'; import 'package:network_proxy/network/bin/server.dart'; import 'package:network_proxy/network/util/system_proxy.dart'; +import 'package:network_proxy/ui/desktop/toolbar/setting/external_proxy.dart'; import 'package:network_proxy/ui/desktop/toolbar/setting/request_rewrite.dart'; import 'package:network_proxy/ui/desktop/toolbar/setting/theme.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -51,22 +52,13 @@ class _SettingState extends State { PopupMenuItem( padding: const EdgeInsets.all(0), child: ValueListenableBuilder( - valueListenable: enableDesktopListenable, - builder: (_, val, __) => SwitchListTile( - hoverColor: Colors.transparent, - title: const Text("抓取电脑请求"), - visualDensity: const VisualDensity(horizontal: -4), - dense: true, - value: configuration.enableDesktop, - onChanged: (val) { - SystemProxy.setSystemProxyEnable(widget.proxyServer.port, val, widget.proxyServer.enableSsl); - configuration.enableDesktop = val; - enableDesktopListenable.value = !enableDesktopListenable.value; - configuration.flushConfig(); - }))), + valueListenable: enableDesktopListenable, + builder: (_, val, __) => setSystemProxy(), + )), const PopupMenuItem(padding: EdgeInsets.all(0), child: ThemeSetting(dense: true)), - menuItem("域名过滤", onTap: () => hostFilter()), - menuItem("请求重写", onTap: () => requestRewrite()), + menuItem("域名过滤", onTap: hostFilter), + menuItem("请求重写", onTap: requestRewrite), + menuItem("外部代理设置", onTap: setExternalProxy), menuItem( "Github", onTap: () { @@ -91,6 +83,32 @@ class _SettingState extends State { )); } + ///设置外部代理地址 + setExternalProxy() { + showDialog( + barrierDismissible: false, + context: context, + builder: (context) { + return ExternalProxyDialog(configuration: widget.proxyServer.configuration); + }); + } + + ///设置系统代理 + Widget setSystemProxy() { + return SwitchListTile( + hoverColor: Colors.transparent, + title: const Text("设置为系统代理"), + visualDensity: const VisualDensity(horizontal: -4), + dense: true, + value: configuration.enableDesktop, + onChanged: (val) { + SystemProxy.setSystemProxyEnable(widget.proxyServer.port, val, widget.proxyServer.enableSsl); + configuration.enableDesktop = val; + enableDesktopListenable.value = !enableDesktopListenable.value; + configuration.flushConfig(); + }); + } + ///请求重写Dialog void requestRewrite() { showDialog( diff --git a/lib/ui/mobile/mobile.dart b/lib/ui/mobile/mobile.dart index b6ec3db..0e432ba 100644 --- a/lib/ui/mobile/mobile.dart +++ b/lib/ui/mobile/mobile.dart @@ -55,18 +55,6 @@ class MobileHomeState extends State implements EventListener { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { - if (widget.configuration.guide) { - //首次引导 - String content = '默认不会开启HTTPS抓包,请安装证书后再开启HTTPS抓包。\n' - '点击的设置 -> HTTPS抓包,根据提示安装证书操作即可。'; - showAlertDialog('提示', content, () { - widget.configuration.guide = false; - widget.configuration.upgradeNotice = false; - widget.configuration.flushConfig(); - }); - return; - } - if (widget.configuration.upgradeNotice) { showUpgradeNotice(); } @@ -117,7 +105,7 @@ class MobileHomeState extends State implements EventListener { child: Text("已连接${value.os?.toUpperCase()},手机抓包已关闭", style: Theme.of(context).textTheme.titleMedium), )), - Expanded(child: RequestListWidget(key: requestStateKey, proxyServer: proxyServer)) + Expanded(child: RequestListWidget(key: requestStateKey, proxyServer: proxyServer)) ]); }), ); @@ -125,8 +113,8 @@ class MobileHomeState extends State implements EventListener { showUpgradeNotice() { String content = '1. 手机版启动默认不再自动开启抓包,请手动点击启动按钮。\n' - '2. 增加外部代理,可配置其他VPN软件地址,开启抓包不会影响访问外网。\n' - '3. 搜索功能增强,可直接搜索响应类型和请求方法。'; + '2. 搜索功能增强,可直接搜索响应类型和请求方法。\n' + '3. 支持brotli编码,br响应类型编码不会再显示乱码'; showAlertDialog('更新内容', content, () { widget.configuration.upgradeNotice = false; widget.configuration.flushConfig(); diff --git a/lib/ui/mobile/request/list.dart b/lib/ui/mobile/request/list.dart index 1cf131b..b405b94 100644 --- a/lib/ui/mobile/request/list.dart +++ b/lib/ui/mobile/request/list.dart @@ -342,13 +342,13 @@ class DomainListState extends State with AutomaticKeepAliveClientMix } Widget title(int index) { - var time = - formatDate(containerMap[list.elementAt(index)]!.last.requestTime, [m, '/', d, ' ', HH, ':', nn, ':', ss]); + var value = containerMap[list.elementAt(index)]; + var time = value == null ? '' : formatDate(value.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}", + subtitle: Text("最后请求时间: $time, 次数: ${value?.length}", maxLines: 1, overflow: TextOverflow.ellipsis), onLongPress: () => menu(index), onTap: () { diff --git a/lib/utils/compress.dart b/lib/utils/compress.dart index 98b6971..e3678d5 100644 --- a/lib/utils/compress.dart +++ b/lib/utils/compress.dart @@ -1,5 +1,7 @@ import 'dart:io'; +import 'package:brotli/brotli.dart'; + ///GZIP 解压缩 List gzipDecode(List byteBuffer) { GZipCodec gzipCodec = GZipCodec(); @@ -11,7 +13,17 @@ List gzipDecode(List byteBuffer) { } } -///GZIP 解压缩 +///GZIP 压缩 List gzipEncode(List input) { return GZipCodec().encode(input); } + +///br 解压缩 +List brDecode(List byteBuffer) { + try { + return brotli.decode(byteBuffer); + } catch (e) { + print("brDecode error: $e"); + return byteBuffer; + } +} diff --git a/lib/utils/lang.dart b/lib/utils/lang.dart index 155f066..51883f4 100644 --- a/lib/utils/lang.dart +++ b/lib/utils/lang.dart @@ -18,3 +18,10 @@ class Strings { return null; } } + +class Pair { + final K key; + final V value; + + Pair(this.key, this.value); +} diff --git a/pubspec.lock b/pubspec.lock index a567e3c..96b89a9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -25,6 +25,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + brotli: + dependency: "direct main" + description: + name: brotli + sha256: "7f891558ed779aab2bed874f0a36b8123f9ff3f19cf6efbee89e18ed294945ae" + url: "https://pub.dev" + source: hosted + version: "0.6.0" characters: dependency: transitive description: @@ -553,10 +561,10 @@ packages: dependency: transitive description: name: win32 - sha256: dfdf0136e0aa7a1b474ea133e67cb0154a0acd2599c4f3ada3b49d38d38793ee + sha256: f2add6fa510d3ae152903412227bda57d0d5a8da61d2c39c1fb022c9429a41c0 url: "https://pub.dev" source: hosted - version: "5.0.5" + version: "5.0.6" window_manager: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 54ff056..d47d8fb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -26,6 +26,7 @@ dependencies: flutter_barcode_scanner: ^2.0.0 flutter_toastr: ^1.0.3 share_plus: ^7.0.2 + brotli: ^0.6.0 dev_dependencies: flutter_test: diff --git a/test/web_test.dart b/test/web_test.dart new file mode 100644 index 0000000..f74b9c0 --- /dev/null +++ b/test/web_test.dart @@ -0,0 +1,90 @@ +import 'dart:io'; +import 'dart:typed_data'; + +main() async { + var connect = await Socket.connect("127.0.0.1", 7890); + var httpRequest = HttpRequest("CONNECT", "https://www.google.com:443"); + connect.add(encode(httpRequest)); + + // var httpClient = HttpClient(); + // httpClient .findProxy = (uri) => "PROXY 127.0.0.1:7890"; + // httpClient.getUrl(Uri.parse("https://www.youtube.com:443")); + await connect.flush(); + // var first = await connect.first; + // print(String.fromCharCodes(first)); + await Future.delayed(const Duration(seconds: 1)); + + SecurityContext.defaultContext.allowLegacyUnsafeRenegotiation = true; + await SecureSocket.secure(connect); +} + +class HttpConstants { + /// Line feed character /n + static const int lf = 10; + + /// Carriage return /r + static const int cr = 13; + + /// Horizontal space + static const int sp = 32; + + /// Colon ':' + static const int colon = 58; +} + +///HTTP请求。 +class HttpRequest { + final String protocolVersion; + + final Map headers = {}; + int contentLength = -1; + + List? body; + final String uri; + late String method; + + final DateTime requestTime = DateTime.now(); + HttpResponse? response; + + HttpRequest(this.method, this.uri, {this.protocolVersion = "HTTP/1.1"}); + + @override + String toString() { + return 'HttpRequest{version: $protocolVersion, url: $uri, method: ${method}, headers: $headers, contentLength: $contentLength, bodyLength: ${body?.length}}'; + } +} + +List encode(HttpRequest message) { + BytesBuilder builder = BytesBuilder(); + builder + ..add(message.method.codeUnits) + ..addByte(HttpConstants.sp) + ..add(message.uri.codeUnits) + ..addByte(HttpConstants.sp) + ..add(message.protocolVersion.codeUnits) + ..addByte(HttpConstants.cr) + ..addByte(HttpConstants.lf); + List? body = message.body; + + //请求头 + if (body != null && body.isNotEmpty) { + message.headers['Content-Length'] = body.length; + } + message.headers.forEach((key, values) { + for (var v in values) { + builder + ..add(key.codeUnits) + ..addByte(HttpConstants.colon) + ..addByte(HttpConstants.sp) + ..add(v.codeUnits) + ..addByte(HttpConstants.cr) + ..addByte(HttpConstants.lf); + } + }); + builder.addByte(HttpConstants.cr); + builder.addByte(HttpConstants.lf); + + //请求体 + builder.add(body ?? Uint8List(0)); + return builder.toBytes(); +}