diff --git a/README.md b/README.md index 7b8e059..37e7b35 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ ## 免费开源Http、Https抓包工具,支持Windows、Mac、Android、IOS, 全平台系统 +支持手机扫码连接,不用手动配置Wifi代理,包括配置同步。所有终端都可以互相扫码连接转发流量。 **Mac首次打开会提示已损坏,需要到系统偏好设置-安全性与隐私-允许任何来源。** @@ -10,10 +11,8 @@ - [ ] 接下来会完善功能体验,JSON格式化展示,URL解码,UI优化。 - [ ] IOS上架app Store。 -- [ ] 后面桌面端和手机端整合,扫码连接啥的,不用手动配置Wifi代理,包括配置同步。 - [ ] 支持安卓微信小程序抓包,安卓分为系统证书和用户证书,下载的自签名根证书安装都是用户证书,微信不信任用户证书,不Root导致Https抓不了了, 目前市场上所有抓包软件抓不了微信的包,后面单独做个运行空间插件,动态反编译修改配置,信任用户证书来解决。 - image. image diff --git a/android/app/build.gradle b/android/app/build.gradle index 3b60783..64c55a5 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -45,17 +45,16 @@ android { defaultConfig { applicationId "com.network.proxy" - // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. - minSdkVersion 19 + minSdkVersion 21 targetSdkVersion flutter.targetSdkVersion + multiDexEnabled true versionCode flutterVersionCode.toInteger() versionName flutterVersionName } buildTypes { release { - // TODO: Add your own signing config for the release build. // Signing with the debug keys for now, so `flutter run --release` works. signingConfig signingConfigs.debug } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 77172ea..7d94389 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -2,6 +2,7 @@ + when (call.method) { "startVpn" -> { diff --git a/ios/Podfile b/ios/Podfile index fdcc671..164df53 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '11.0' +platform :ios, '12.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/ios/ProxyPin/PacketTunnelProvider.swift b/ios/ProxyPin/PacketTunnelProvider.swift index 7fd3da7..9d5421a 100644 --- a/ios/ProxyPin/PacketTunnelProvider.swift +++ b/ios/ProxyPin/PacketTunnelProvider.swift @@ -18,7 +18,6 @@ class PacketTunnelProvider: NEPacketTunnelProvider { exit(EXIT_FAILURE) } let networkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "127.0.0.1") - networkSettings.mtu = 1500 NSLog(conf.debugDescription) //http代理 let host = conf["proxyHost"] as! String diff --git a/ios/ProxyPin/ProxyPin.entitlements b/ios/ProxyPin/ProxyPin.entitlements index de73917..cea3252 100644 --- a/ios/ProxyPin/ProxyPin.entitlements +++ b/ios/ProxyPin/ProxyPin.entitlements @@ -14,9 +14,6 @@ allow-vpn - com.apple.security.network.client - - com.apple.security.network.server - + diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index b5eed67..182555d 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 55; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 4702458..558abf2 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -61,5 +61,7 @@ UIViewControllerBasedStatusBarAppearance + NSCameraUsageDescription + 扫描二维码 diff --git a/ios/Runner/VpnManager.swift b/ios/Runner/VpnManager.swift index c8ab2db..381c8a0 100755 --- a/ios/Runner/VpnManager.swift +++ b/ios/Runner/VpnManager.swift @@ -155,10 +155,11 @@ extension VpnManager{ } } - func disconnect(){ + func disconnect() { if (activeVPN != nil) { print("stopVPNTunnel activeVPN") activeVPN?.connection.stopVPNTunnel() + activeVPN = nil return } diff --git a/lib/network/bin/server.dart b/lib/network/bin/server.dart index 70776a0..3e8f2eb 100644 --- a/lib/network/bin/server.dart +++ b/lib/network/bin/server.dart @@ -27,7 +27,7 @@ class ProxyServer { EventListener? listener; RequestRewrites requestRewrites = RequestRewrites(); - List _initializedListeners = []; + final List _initializedListeners = []; ProxyServer({this.listener}); diff --git a/lib/network/channel.dart b/lib/network/channel.dart index 8e9b2a9..60f9272 100644 --- a/lib/network/channel.dart +++ b/lib/network/channel.dart @@ -42,9 +42,7 @@ class Channel { final int remotePort; Channel(this._socket) - : _id = DateTime - .now() - .millisecondsSinceEpoch + Random().nextInt(999999), + : _id = DateTime.now().millisecondsSinceEpoch + Random().nextInt(999999), remoteAddress = _socket.remoteAddress, remotePort = _socket.remotePort; @@ -56,6 +54,10 @@ class Channel { set secureSocket(SecureSocket secureSocket) => _socket = secureSocket; Future write(Object obj) async { + if (isClosed) { + return; + } + var data = pipeline._encoder.encode(obj); _socket.add(data); await _socket.flush(); @@ -115,9 +117,23 @@ class ChannelPipeline extends ChannelHandler { _handler.channelActive(channel); } + /// 转发请求 + void relay(Channel clientChannel, Channel remoteChannel) { + var rawCodec = RawCodec(); + clientChannel.pipeline.handle(rawCodec, rawCodec, RelayHandler(remoteChannel)); + remoteChannel.pipeline.handle(rawCodec, rawCodec, RelayHandler(clientChannel)); + } + @override void channelRead(Channel channel, Uint8List msg) { try { + HostAndPort? remote = channel.getAttribute(AttributeKeys.remote); + if (remote != null && channel.getAttribute(channel.id) != null) { + relay(channel, channel.getAttribute(channel.id)); + _handler.channelRead(channel, msg); + return; + } + var data = _decoder.decode(msg); if (data == null) { return; @@ -199,11 +215,11 @@ class HostAndPort { @override bool operator ==(Object other) => identical(this, other) || - other is HostAndPort && - runtimeType == other.runtimeType && - scheme == other.scheme && - host == other.host && - port == other.port; + other is HostAndPort && + runtimeType == other.runtimeType && + scheme == other.scheme && + host == other.host && + port == other.port; @override int get hashCode => scheme.hashCode ^ host.hashCode ^ port.hashCode; @@ -250,6 +266,7 @@ abstract interface class ChannelInitializer { class Network { late Function _channelInitializer; bool enableSsl = false; + String? remoteHost; Network initChannel(void Function(Channel channel) initializer) { _channelInitializer = initializer; @@ -268,6 +285,11 @@ 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)) { relay(channel, channel.getAttribute(channel.id)); @@ -288,8 +310,9 @@ class Network { try { //客户端ssl Channel remoteChannel = channel.getAttribute(channel.id); + remoteChannel.secureSocket = - await SecureSocket.secure(remoteChannel.socket, onBadCertificate: (certificate) => true); + await SecureSocket.secure(remoteChannel.socket, onBadCertificate: (certificate) => true); remoteChannel.pipeline.listen(remoteChannel); //服务端ssl @@ -326,13 +349,14 @@ class Server extends Network { Future stop() async { if (!isRunning) return serverSocket; isRunning = false; - return serverSocket.close(); + await serverSocket.close(); + return serverSocket; } } class Client extends Network { Future connect(HostAndPort hostAndPort) async { - return Socket.connect(hostAndPort.host, hostAndPort.port, timeout: const Duration(seconds: 5)) + return Socket.connect(hostAndPort.host, hostAndPort.port, timeout: const Duration(seconds: 3)) .then((socket) => listen(socket)); } } diff --git a/lib/network/handler.dart b/lib/network/handler.dart index 9293d59..f571e4d 100644 --- a/lib/network/handler.dart +++ b/lib/network/handler.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:io'; @@ -5,7 +6,9 @@ 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'; import 'package:network_proxy/network/util/file_read.dart'; +import 'package:network_proxy/network/util/host_filter.dart'; import 'package:network_proxy/network/util/request_rewrite.dart'; +import 'package:network_proxy/utils/ip.dart'; import 'channel.dart'; import 'http/codec.dart'; @@ -35,6 +38,33 @@ class HttpChannelHandler extends ChannelHandler { @override void channelRead(Channel channel, HttpRequest msg) async { + channel.putAttribute(AttributeKeys.request, msg); + + if (msg.path == '/config' && (await localIp()) == msg.hostAndPort?.host) { + var response = HttpResponse(msg.protocolVersion, HttpStatus.ok); + var body = { + "requestRewrites": requestRewrites?.toJson(), + 'whitelist': HostFilter.whitelist.toJson(), + 'blacklist': HostFilter.blacklist.toJson(), + }; + response.body = utf8.encode(json.encode(body)); + channel.writeAndClose(response); + return; + } + if ((await localIp()) == msg.hostAndPort?.host) { + var response = HttpResponse(msg.protocolVersion, HttpStatus.ok); + response.body = utf8.encode('pong'); + response.headers.set("os", Platform.operatingSystem); + response.headers.set("hostname", Platform.isAndroid ? Platform.operatingSystem : Platform.localHostname); + channel.writeAndClose(response); + return; + } + + if (msg.uri == 'http://proxy.pin/ssl' || msg.requestUrl == 'http://127.0.0.1:${channel.socket.port}/ssl') { + _crtDownload(channel, msg); + return; + } + forward(channel, msg).catchError((error, trace) { channel.close(); if (error is SocketException && @@ -54,19 +84,11 @@ class HttpChannelHandler extends ChannelHandler { /// 转发请求 Future forward(Channel channel, HttpRequest httpRequest) async { - channel.putAttribute(AttributeKeys.request, httpRequest); - - if (httpRequest.uri == 'http://proxy.pin/ssl' || - httpRequest.requestUrl == 'http://127.0.0.1:${channel.socket.port}/ssl') { - _crtDownload(channel, httpRequest); - return; - } - var remoteChannel = await _getRemoteChannel(channel, httpRequest); //实现抓包代理转发 if (httpRequest.method != HttpMethod.connect) { - // log.i("[${channel.id}] ${httpRequest.requestUrl}"); + log.i("[${channel.id}] ${httpRequest.requestUrl}"); var replaceBody = requestRewrites?.findRequestReplaceWith(httpRequest.path); if (replaceBody?.isNotEmpty == true) { @@ -110,6 +132,15 @@ class HttpChannelHandler extends ChannelHandler { clientChannel.putAttribute(AttributeKeys.host, hostAndPort); var proxyHandler = HttpResponseProxyHandler(clientChannel, listener: listener, requestRewrites: requestRewrites); + + HostAndPort? remote = clientChannel.getAttribute(AttributeKeys.remote); + if (remote != null) { + var proxyChannel = await HttpClients.connect(remote, proxyHandler); + clientChannel.putAttribute(clientId, proxyChannel); + proxyChannel.write(httpRequest); + return proxyChannel; + } + var proxyChannel = await HttpClients.connect(hostAndPort, proxyHandler); clientChannel.putAttribute(clientId, proxyChannel); @@ -134,7 +165,7 @@ class HttpResponseProxyHandler extends ChannelHandler { @override void channelRead(Channel channel, HttpResponse msg) { msg.request = clientChannel.getAttribute(AttributeKeys.request); - msg.request?.response= msg; + msg.request?.response = msg; // log.i("[${clientChannel.id}] Response ${msg.bodyAsString}"); var replaceBody = requestRewrites?.findResponseReplaceWith(msg.request?.path); @@ -172,10 +203,39 @@ class RelayHandler extends ChannelHandler { class HttpClients { /// 建立连接 - static Future connect(HostAndPort hostAndPort, ChannelHandler handler) async { + static Future connect(HostAndPort hostAndPort, ChannelHandler handler) async { var client = Client() ..initChannel((channel) => channel.pipeline.handle(HttpResponseCodec(), HttpRequestCodec(), handler)); return client.connect(hostAndPort); } + + static Future get(String url, {Duration duration = const Duration(seconds: 3)}) async { + var httpResponseHandler = HttpResponseHandler(); + + var client = Client() + ..initChannel((channel) => channel.pipeline.handle(HttpResponseCodec(), HttpRequestCodec(), httpResponseHandler)); + + Channel channel = await client.connect(HostAndPort.of(url)); + HttpRequest msg = HttpRequest(HttpMethod.get, url); + + await channel.write(msg); + + return httpResponseHandler.getResponse(duration).whenComplete(() => channel.close()); + } +} + +class HttpResponseHandler extends ChannelHandler { + final Completer _completer = Completer(); + + @override + void channelRead(Channel channel, HttpResponse msg) { + log.i("[${channel.id}] Response ${msg.bodyAsString}"); + _completer.complete(msg); + channel.close(); + } + + Future getResponse(Duration duration) { + return _completer.future.timeout(duration); + } } diff --git a/lib/network/http/codec.dart b/lib/network/http/codec.dart index 3a821d7..f617e3a 100644 --- a/lib/network/http/codec.dart +++ b/lib/network/http/codec.dart @@ -155,7 +155,7 @@ class HttpRequestCodec extends HttpCodec { @override HttpRequest createMessage(List reqLine) { HttpMethod httpMethod = HttpMethod.valueOf(reqLine[0]); - return HttpRequest(httpMethod, reqLine[1], reqLine[2]); + return HttpRequest(httpMethod, reqLine[1], protocolVersion: reqLine[2]); } @override diff --git a/lib/network/http/http.dart b/lib/network/http/http.dart index 2a2da12..8017652 100644 --- a/lib/network/http/http.dart +++ b/lib/network/http/http.dart @@ -58,7 +58,7 @@ class HttpRequest extends HttpMessage { String? remoteDomain; HttpResponse? response; - HttpRequest(this.method, this.uri, String protocolVersion) : super(protocolVersion); + HttpRequest(this.method, this.uri, {String protocolVersion = "HTTP/1.1"}) : super(protocolVersion); @override String toString() { diff --git a/lib/network/util/attribute_keys.dart b/lib/network/util/attribute_keys.dart index 68e0a60..ac0ef7e 100644 --- a/lib/network/util/attribute_keys.dart +++ b/lib/network/util/attribute_keys.dart @@ -2,6 +2,7 @@ /// 2023/5/23 interface class AttributeKeys { static const String host = "HOST"; - static const String uri= "URI"; - static const String request= "REQUEST"; + static const String uri = "URI"; + static const String request = "REQUEST"; + static const String remote = "REMOTE"; } diff --git a/lib/ui/desktop/toolbar/toolbar.dart b/lib/ui/desktop/toolbar/toolbar.dart index 262c2ce..a4b04fa 100644 --- a/lib/ui/desktop/toolbar/toolbar.dart +++ b/lib/ui/desktop/toolbar/toolbar.dart @@ -5,7 +5,9 @@ import 'package:flutter/services.dart'; import 'package:network_proxy/network/bin/server.dart'; import 'package:network_proxy/ui/desktop/toolbar/setting/setting.dart'; import 'package:network_proxy/ui/desktop/toolbar/ssl/ssl.dart'; +import 'package:network_proxy/utils/ip.dart'; import 'package:window_manager/window_manager.dart'; +import 'package:qr_flutter/qr_flutter.dart'; import '../left/domain.dart'; import 'launch/launch.dart'; @@ -23,7 +25,6 @@ class Toolbar extends StatefulWidget { } class _ToolbarState extends State { - @override void initState() { super.initState(); @@ -62,18 +63,59 @@ class _ToolbarState extends State { children: [ Padding(padding: EdgeInsets.only(left: Platform.isMacOS ? 80 : 30)), SocketLaunch(proxyServer: widget.proxyServer), - const Padding(padding: EdgeInsets.only(left: 30)), + const Padding(padding: EdgeInsets.only(left: 20)), IconButton( tooltip: "清理", icon: const Icon(Icons.cleaning_services_outlined), onPressed: () { widget.domainStateKey.currentState?.clean(); }), - const Padding(padding: EdgeInsets.only(left: 30)), + const Padding(padding: EdgeInsets.only(left: 20)), SslWidget(proxyServer: widget.proxyServer), - const Padding(padding: EdgeInsets.only(left: 30)), + const Padding(padding: EdgeInsets.only(left: 20)), Setting(proxyServer: widget.proxyServer), + const Padding(padding: EdgeInsets.only(left: 20)), + IconButton( + tooltip: "手机连接", + icon: const Icon(Icons.phone_iphone), + onPressed: () async { + final host = await localIp(); + phoneConnect(host, widget.proxyServer.port); + }), ], ); } + + phoneConnect(String host, int port) { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text("手机连接", style: TextStyle(fontSize: 16)), + content: SizedBox( + height: 250, + width: 300, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + QrImageView( + backgroundColor: Colors.white, + data: "proxypin://connect?host=$host&port=${widget.proxyServer.port}", + version: QrVersions.auto, + size: 200.0, + ), + const SizedBox(height: 20), + const Text("请使用手机版扫描二维码"), + ], + )), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text("取消")), + ], + ); + }); + } } diff --git a/lib/ui/mobile/connect_remote.dart b/lib/ui/mobile/connect_remote.dart new file mode 100644 index 0000000..dee0f74 --- /dev/null +++ b/lib/ui/mobile/connect_remote.dart @@ -0,0 +1,165 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:network_proxy/network/bin/server.dart'; +import 'package:network_proxy/network/handler.dart'; +import 'package:network_proxy/network/util/host_filter.dart'; + +class RemoteModel { + final bool connect; + final String? host; + final int? port; + final String? os; + final String? hostname; + + RemoteModel({ + required this.connect, + this.host, + this.port, + this.os, + this.hostname, + }); +} + +class ConnectRemote extends StatefulWidget { + final ProxyServer proxyServer; + final ValueNotifier desktop; + + const ConnectRemote({super.key, required this.desktop, required this.proxyServer}); + + @override + State createState() { + return ConnectRemoteState(); + } +} + +class ConnectRemoteState extends State { + bool syncConfig = false; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('已连接远程', style: TextStyle(fontSize: 16))), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('已连接:${widget.desktop.value.hostname}', style: Theme.of(context).textTheme.titleMedium), + const SizedBox(height: 10), + OutlinedButton( + child: const Text('断开连接'), + onPressed: () { + widget.desktop.value = RemoteModel(connect: false); + Navigator.pop(context); + }), + const SizedBox(height: 10), + OutlinedButton( + child: const Text('同步配置'), + onPressed: () { + pullConfig(); + }, + ), + ], + ), + ), + ); + } + + //拉取桌面配置 + pullConfig() { + var desktopModel = widget.desktop.value; + HttpClients.get('http://${desktopModel.host}:${desktopModel.port}/config').then((response) { + if (response.status.isSuccessful()) { + var config = jsonDecode(response.bodyAsString); + syncConfig = true; + showDialog( + context: context, + builder: (context) { + return ConfigSyncWidget(proxyServer: widget.proxyServer, config: config); + }); + } + }).onError((error, stackTrace) { + print(error); + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('拉取配置失败, 请检查网络连接'))); + }); + } +} + +class ConfigSyncWidget extends StatefulWidget { + final ProxyServer proxyServer; + final Map config; + + const ConfigSyncWidget({super.key, required this.proxyServer, required this.config}); + + @override + State createState() { + return ConfigSyncState(); + } +} + +class ConfigSyncState extends State { + bool syncWhiteList = true; + bool syncBlackList = true; + bool syncRewrite = true; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('同步配置', style: TextStyle(fontSize: 16)), + content: SizedBox( + height: 230, + child: Column( + children: [ + SwitchListTile( + subtitle: const Text("同步白名单过滤"), + value: syncWhiteList, + onChanged: (val) { + setState(() { + syncWhiteList = val; + }); + }), + SwitchListTile( + subtitle: const Text("同步黑名单过滤"), + value: syncBlackList, + onChanged: (val) { + setState(() { + syncBlackList = val; + }); + }), + SwitchListTile( + subtitle: const Text("同步请求重写"), + value: syncRewrite, + onChanged: (val) { + setState(() { + syncRewrite = val; + }); + }), + ], + )), + actions: [ + TextButton( + child: const Text('取消'), + onPressed: () { + Navigator.pop(context); + }), + TextButton( + child: const Text('开始同步'), + onPressed: () { + if (syncWhiteList) { + HostFilter.whitelist.load(widget.config['whitelist']); + } + if (syncBlackList) { + HostFilter.blacklist.load(widget.config['blacklist']); + } + if (syncRewrite) { + widget.proxyServer.requestRewrites.load(widget.config['requestRewrites']); + widget.proxyServer.flushRequestRewriteConfig(); + } + widget.proxyServer.flushConfig(); + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('同步成功'))); + }), + ], + ); + } +} diff --git a/lib/ui/mobile/menu.dart b/lib/ui/mobile/menu.dart new file mode 100644 index 0000000..8234042 --- /dev/null +++ b/lib/ui/mobile/menu.dart @@ -0,0 +1,220 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_barcode_scanner/flutter_barcode_scanner.dart'; +import 'package:network_proxy/network/bin/server.dart'; +import 'package:network_proxy/network/handler.dart'; +import 'package:network_proxy/network/util/host_filter.dart'; +import 'package:network_proxy/ui/desktop/toolbar/setting/setting.dart'; +import 'package:network_proxy/ui/desktop/toolbar/setting/theme.dart'; +import 'package:network_proxy/ui/mobile/connect_remote.dart'; +import 'package:network_proxy/ui/mobile/setting/filter.dart'; +import 'package:network_proxy/ui/mobile/setting/request_rewrite.dart'; +import 'package:network_proxy/ui/mobile/setting/ssl.dart'; +import 'package:network_proxy/utils/ip.dart'; +import 'package:qr_flutter/qr_flutter.dart'; +import 'package:qrscan/qrscan.dart' as scanner; +import 'package:url_launcher/url_launcher.dart'; + +class DrawerWidget extends StatelessWidget { + final ProxyServer proxyServer; + + const DrawerWidget({Key? key, required this.proxyServer}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Drawer( + child: ListView( + padding: EdgeInsets.zero, + children: [ + DrawerHeader( + decoration: BoxDecoration(color: Theme.of(context).colorScheme.primaryContainer), + child: const Text('设置'), + ), + PortWidget(proxyServer: proxyServer), + ListTile( + title: const Text("Https代理"), + trailing: const Icon(Icons.arrow_right), + onTap: () => navigator(context, MobileSslWidget(proxyServer: proxyServer))), + const ThemeSetting(), + ListTile( + title: const Text("域名白名单"), + trailing: const Icon(Icons.arrow_right), + onTap: () => + navigator(context, MobileFilterWidget(proxyServer: proxyServer, hostList: HostFilter.whitelist))), + ListTile( + title: const Text("域名黑名单"), + trailing: const Icon(Icons.arrow_right), + onTap: () => + navigator(context, MobileFilterWidget(proxyServer: proxyServer, hostList: HostFilter.blacklist))), + ListTile( + title: const Text("请求重写"), + trailing: const Icon(Icons.arrow_right), + onTap: () => navigator(context, MobileRequestRewrite(proxyServer: proxyServer))), + ListTile( + title: const Text("Github"), + trailing: const Icon(Icons.arrow_right), + onTap: () { + launchUrl(Uri.parse("https://github.com/wanghongenpin/network_proxy_flutter"), + mode: LaunchMode.externalApplication); + }), + ListTile( + title: const Text("下载地址"), + trailing: const Icon(Icons.arrow_right), + onTap: () { + launchUrl(Uri.parse("https://gitee.com/wanghongenpin/network-proxy-flutter/releases/tag/0.0.1"), + mode: LaunchMode.externalApplication); + }) + ], + )); + } + + navigator(BuildContext context, Widget widget) { + Navigator.of(context).push( + MaterialPageRoute(builder: (BuildContext context) { + return widget; + }), + ); + } +} + +/// +号菜单 +class MoreEnum extends StatelessWidget { + final ProxyServer proxyServer; + final ValueNotifier desktop; + + const MoreEnum({super.key, required this.proxyServer, required this.desktop}); + + @override + Widget build(BuildContext context) { + return PopupMenuButton( + offset: const Offset(0, 30), + child: const Icon(Icons.add_circle_outline, size: 26), + itemBuilder: (BuildContext context) { + return [ + PopupMenuItem( + child: ListTile( + title: const Text("Https代理"), + leading: Icon(Icons.https, color: proxyServer.enableSsl ? null : Colors.red), + onTap: () { + Navigator.of(context).push( + MaterialPageRoute(builder: (BuildContext context) { + return MobileSslWidget(proxyServer: proxyServer); + }), + ); + })), + PopupMenuItem( + child: ListTile( + leading: const Icon(Icons.qr_code_scanner_outlined), + title: const Text("连接终端"), + onTap: () { + connectRemote(context); + }, + )), + PopupMenuItem( + child: ListTile( + leading: const Icon(Icons.phone_iphone), + title: const Text("我的二维码"), + onTap: () async { + var ip = await localIp(); + if (context.mounted) { + phoneConnect(context, ip, proxyServer.port); + } + }, + )), + ]; + }, + ); + } + + connectRemote(BuildContext context) async { + String scanRes; + if (Platform.isAndroid) { + scanRes = await scanner.scan() ?? "-1"; + } else { + scanRes = await FlutterBarcodeScanner.scanBarcode("#ff6666", "取消", true, ScanMode.QR); + } + + print(scanRes); + if (scanRes == "-1") return; + if (scanRes.startsWith("http")) { + launchUrl(Uri.parse(scanRes), mode: LaunchMode.externalApplication); + return; + } + + if (scanRes.startsWith("proxypin://connect")) { + Uri uri = Uri.parse(scanRes); + var host = uri.queryParameters['host']; + var port = uri.queryParameters['port']; + + try { + var response = await HttpClients.get("http://$host:$port/ping").timeout(const Duration(seconds: 1)); + if (response.bodyAsString == "pong") { + desktop.value = RemoteModel( + connect: true, + host: host, + port: int.parse(port!), + os: response.headers.get("os"), + hostname: response.headers.get("hostname")); + + if (context.mounted && Navigator.canPop(context)) { + showSnackBar(context, "连接成功"); + Navigator.pop(context); + } + } + } catch (e) { + print(e); + if (context.mounted) { + showDialog( + context: context, + builder: (BuildContext context) { + return const AlertDialog(content: Text("连接失败,请检查是否在同一局域网")); + }); + } + } + return; + } + if (context.mounted) { + showSnackBar(context, "无法识别的二维码"); + } + } + + showSnackBar(BuildContext context, String text) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(text))); + } + } + + phoneConnect(BuildContext context, String host, int port) { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text("远程连接,将请求转发到其他终端", style: TextStyle(fontSize: 16)), + content: SizedBox( + height: 240, + width: 300, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + QrImageView( + backgroundColor: Colors.white, + data: "proxypin://connect?host=$host&port=${proxyServer.port}", + version: QrVersions.auto, + size: 200.0, + ), + const SizedBox(height: 20), + const Text("请使用手机扫描二维码"), + ], + )), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text("取消")), + ], + ); + }); + } +} diff --git a/lib/ui/mobile/mobile.dart b/lib/ui/mobile/mobile.dart index 0fdb293..d2f447f 100644 --- a/lib/ui/mobile/mobile.dart +++ b/lib/ui/mobile/mobile.dart @@ -1,19 +1,17 @@ +import 'dart:async'; + +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:network_proxy/network/bin/server.dart'; import 'package:network_proxy/network/channel.dart'; import 'package:network_proxy/network/handler.dart'; import 'package:network_proxy/network/http/http.dart'; -import 'package:network_proxy/network/util/host_filter.dart'; import 'package:network_proxy/ui/desktop/toolbar/launch/launch.dart'; -import 'package:network_proxy/ui/desktop/toolbar/setting/setting.dart'; -import 'package:network_proxy/ui/desktop/toolbar/setting/theme.dart'; -import 'package:network_proxy/ui/mobile/filter.dart'; -import 'package:network_proxy/ui/mobile/ssl.dart'; -import 'package:url_launcher/url_launcher.dart'; +import 'package:network_proxy/ui/mobile/connect_remote.dart'; +import 'package:network_proxy/ui/mobile/menu.dart'; import 'request.dart'; -import 'request_rewrite.dart'; class MobileHomePage extends StatefulWidget { const MobileHomePage({super.key}); @@ -26,10 +24,13 @@ class MobileHomePage extends StatefulWidget { class MobileHomeState extends State implements EventListener { static const MethodChannel proxyVpnChannel = MethodChannel('com.proxy/proxyVpn'); - final ValueNotifier sllEnableListenable = ValueNotifier(true); + + final requestStateKey = GlobalKey(); late ProxyServer proxyServer; - final requestStateKey = GlobalKey(); + ValueNotifier desktop = ValueNotifier(RemoteModel(connect: false)); + + Timer? _connectCheckTimer; @override void onRequest(Channel channel, HttpRequest request) { @@ -44,100 +45,96 @@ class MobileHomeState extends State implements EventListener { @override void initState() { proxyServer = ProxyServer(listener: this); - proxyServer.initializedListener(() { - sllEnableListenable.value = proxyServer.enableSsl; + desktop.addListener(() { + if (desktop.value.connect) { + proxyServer.server?.remoteHost = "http://${desktop.value.host}:${desktop.value.port}"; + checkConnectTask(context); + } else { + proxyServer.server?.remoteHost = null; + _connectCheckTimer?.cancel(); + } }); super.initState(); } @override void dispose() { - sllEnableListenable.dispose(); + desktop.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(centerTitle: true, title: const Text("ProxyPin"), actions: [ - IconButton( - tooltip: "清理", - icon: const Icon(Icons.cleaning_services_outlined), - onPressed: () => requestStateKey.currentState?.clean()), - ValueListenableBuilder( - valueListenable: sllEnableListenable, - builder: (_, bool enabled, __) => IconButton( - tooltip: "Https代理", - icon: Icon(Icons.https, color: enabled ? null : Colors.red), - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute(builder: (BuildContext context) { - return MobileSslWidget( - proxyServer: proxyServer, onEnableChange: (val) => sllEnableListenable.value = val); - }), - ); - })) - ]), - drawer: drawer(), - floatingActionButton: FloatingActionButton( - onPressed: () {}, - child: SocketLaunch( - proxyServer: proxyServer, - size: 38, - onStart: () { - proxyVpnChannel.invokeMethod("startVpn", {"proxyHost": "127.0.0.1", "proxyPort": proxyServer.port}); - }, - onStop: () { - proxyVpnChannel.invokeMethod("stopVpn"); - }, - )), - body: RequestWidget(key: requestStateKey, proxyServer: proxyServer)); - } - - Drawer drawer() { - return Drawer( - child: ListView( - padding: EdgeInsets.zero, - children: [ - DrawerHeader( - decoration: BoxDecoration(color: Theme.of(context).colorScheme.primaryContainer), - child: const Text('设置'), - ), - PortWidget(proxyServer: proxyServer), - const ThemeSetting(), - ListTile( - title: const Text("域名白名单"), - trailing: const Icon(Icons.arrow_right), - onTap: () => _filter(HostFilter.whitelist)), - ListTile( - title: const Text("域名黑名单"), - trailing: const Icon(Icons.arrow_right), - onTap: () => _filter(HostFilter.blacklist)), - ListTile(title: const Text("请求重写"), trailing: const Icon(Icons.arrow_right), onTap: () => _reqeustRewrite()), - ListTile( - title: const Text("Github"), - trailing: const Icon(Icons.arrow_right), - onTap: () { - launchUrl(Uri.parse("https://github.com/wanghongenpin/network_proxy_flutter"), - mode: LaunchMode.externalApplication); - }) - ], - )); - } - - void _filter(HostList hostList) { - Navigator.of(context).push( - MaterialPageRoute(builder: (BuildContext context) { - return MobileFilterWidget(proxyServer: proxyServer, hostList: hostList); - }), + drawerDragStartBehavior: DragStartBehavior.down, + appBar: AppBar(centerTitle: true, title: const Text("ProxyPin", style: TextStyle(fontSize: 16)), actions: [ + IconButton( + tooltip: "清理", + icon: const Icon(Icons.cleaning_services_outlined), + onPressed: () => requestStateKey.currentState?.clean()), + const SizedBox(width: 10), + MoreEnum(proxyServer: proxyServer, desktop: desktop), + const SizedBox(width: 20) + ]), + drawer: DrawerWidget(proxyServer: proxyServer), + floatingActionButton: FloatingActionButton( + onPressed: () {}, + child: SocketLaunch(proxyServer: proxyServer, size: 38, onStart: () => startVpn(), onStop: () => stopVpn())), + body: ValueListenableBuilder( + valueListenable: desktop, + builder: (context, value, _) { + return Column(children: [ + value.connect == false + ? const SizedBox() + : Container( + margin: const EdgeInsets.only(top: 5, bottom: 5), + height: 50, + width: double.infinity, + child: ElevatedButton( + onPressed: () => Navigator.of(context).push(MaterialPageRoute(builder: (BuildContext context) { + return ConnectRemote(desktop: desktop, proxyServer: proxyServer); + })), + child: Text("已连接${value.os?.toUpperCase()},手机抓包已关闭", + style: Theme.of(context).textTheme.titleMedium), + )), + Expanded(child: RequestWidget(key: requestStateKey, proxyServer: proxyServer)) + ]); + }), ); } - void _reqeustRewrite() { - Navigator.of(context).push( - MaterialPageRoute(builder: (BuildContext context) { - return MobileRequestRewrite(proxyServer: proxyServer); - }), - ); + checkConnectTask(BuildContext context) async { + int retry = 0; + _connectCheckTimer = Timer.periodic(const Duration(milliseconds: 1000), (timer) async { + try { + var response = await HttpClients.get("http://${desktop.value.host}:${desktop.value.port}/ping") + .timeout(const Duration(seconds: 1)); + if (response.bodyAsString == "pong") { + retry = 0; + return; + } + } catch (e) { + retry++; + } + + if (retry > 3) { + _connectCheckTimer?.cancel(); + _connectCheckTimer = null; + desktop.value = RemoteModel(connect: false); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("检查远程连接失败,已断开"))); + } + } + }); + } + + stopVpn() { + proxyVpnChannel.invokeMethod("stopVpn"); + } + + startVpn() { + String host = "127.0.0.1"; + int port = proxyServer.port; + proxyVpnChannel.invokeMethod("startVpn", {"proxyHost": host, "proxyPort": port}); } } diff --git a/lib/ui/mobile/request.dart b/lib/ui/mobile/request.dart index c0e9fec..8cd4b4a 100644 --- a/lib/ui/mobile/request.dart +++ b/lib/ui/mobile/request.dart @@ -102,7 +102,7 @@ class RequestSequenceState extends State { //防止频繁刷新 if (!changing) { changing = true; - Future.delayed(const Duration(milliseconds: 500), () { + Future.delayed(const Duration(milliseconds: 200), () { setState(() { changing = false; }); @@ -261,7 +261,7 @@ class DomainListState extends State { //防止频繁刷新 if (!changing) { changing = true; - Future.delayed(const Duration(milliseconds: 500), () { + Future.delayed(const Duration(milliseconds: 200), () { setState(() { changing = false; }); diff --git a/lib/ui/mobile/filter.dart b/lib/ui/mobile/setting/filter.dart similarity index 99% rename from lib/ui/mobile/filter.dart rename to lib/ui/mobile/setting/filter.dart index 394279f..389c844 100644 --- a/lib/ui/mobile/filter.dart +++ b/lib/ui/mobile/setting/filter.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:network_proxy/network/bin/server.dart'; -import '../../../network/util/host_filter.dart'; +import '../../../../network/util/host_filter.dart'; class MobileFilterWidget extends StatefulWidget { final ProxyServer proxyServer; diff --git a/lib/ui/mobile/request_rewrite.dart b/lib/ui/mobile/setting/request_rewrite.dart similarity index 100% rename from lib/ui/mobile/request_rewrite.dart rename to lib/ui/mobile/setting/request_rewrite.dart diff --git a/lib/ui/mobile/ssl.dart b/lib/ui/mobile/setting/ssl.dart similarity index 89% rename from lib/ui/mobile/ssl.dart rename to lib/ui/mobile/setting/ssl.dart index 8ecde2b..85b9df6 100644 --- a/lib/ui/mobile/ssl.dart +++ b/lib/ui/mobile/setting/ssl.dart @@ -4,9 +4,9 @@ import 'package:url_launcher/url_launcher.dart'; class MobileSslWidget extends StatefulWidget { final ProxyServer proxyServer; - final Function(bool val) onEnableChange; + final Function(bool val)? onEnableChange; - const MobileSslWidget({super.key, required this.proxyServer, required this.onEnableChange}); + const MobileSslWidget({super.key, required this.proxyServer, this.onEnableChange}); @override State createState() => _MobileSslState(); @@ -17,10 +17,10 @@ class _MobileSslState extends State { @override void dispose() { - super.dispose(); if (changed) { widget.proxyServer.flushConfig(); } + super.dispose(); } @override @@ -37,7 +37,7 @@ class _MobileSslState extends State { value: widget.proxyServer.enableSsl, onChanged: (val) { widget.proxyServer.enableSsl = val; - widget.onEnableChange(val); + if (widget.onEnableChange != null) widget.onEnableChange!(val); changed = true; setState(() {}); }), diff --git a/lib/utils/ip.dart b/lib/utils/ip.dart index 76015c5..e3154c9 100644 --- a/lib/utils/ip.dart +++ b/lib/utils/ip.dart @@ -11,9 +11,13 @@ void main() { })); } +String? ip; + Future localIp() async { - String ip = await NetworkInterface.list().then((interfaces) => interfaces.first.addresses.first.address); - return ip; + ip ??= await NetworkInterface.list().then((interfaces) { + return interfaces.firstWhere((it) => it.name == "en0", orElse: () => interfaces.first).addresses.first.address; + }); + return ip!; } Future networkName() { diff --git a/pubspec.lock b/pubspec.lock index 65f3b80..3eec9b1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -125,10 +125,10 @@ packages: dependency: transitive description: name: file_selector_android - sha256: "65d41d2fbed893c5eb8842674ed08b920dc7d276b6c7e74ee8b1759dce4b2067" + sha256: "59e694afad4609d689185a608958c85fbccb3e6feab12a6b8c95a2c0f90ad2f7" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.5.0+1" file_selector_ios: dependency: transitive description: @@ -182,6 +182,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_barcode_scanner: + dependency: "direct main" + description: + name: flutter_barcode_scanner + sha256: a4ba37daf9933f451a5e812c753ddd045d6354e4a3280342d895b07fecaab3fa + url: "https://pub.dev" + source: hosted + version: "2.0.0" flutter_lints: dependency: "direct dev" description: @@ -190,6 +198,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "950e77c2bbe1692bc0874fc7fb491b96a4dc340457f4ea1641443d0a6c1ea360" + url: "https://pub.dev" + source: hosted + version: "2.0.15" flutter_test: dependency: "direct dev" description: flutter @@ -376,6 +392,30 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.3" + qr: + dependency: transitive + description: + name: qr + sha256: "64957a3930367bf97cc211a5af99551d630f2f4625e38af10edd6b19131b64b3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + qr_flutter: + dependency: "direct main" + description: + name: qr_flutter + sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + qrscan: + dependency: "direct main" + description: + name: qrscan + sha256: "0ee72eca0dcbc35ab74894010e3589c3675ddb7c5a551d5f29ab0d3bb1bfb135" + url: "https://pub.dev" + source: hosted + version: "0.3.3" screen_retriever: dependency: transitive description: @@ -449,10 +489,10 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: eb1e00ab44303d50dd487aab67ebc575456c146c6af44422f9c13889984c00f3 + sha256: "781bd58a1eb16069412365c98597726cd8810ae27435f04b3b4d3a470bacd61e" url: "https://pub.dev" source: hosted - version: "6.1.11" + version: "6.1.12" url_launcher_android: dependency: transitive description: @@ -497,18 +537,18 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "6bb1e5d7fe53daf02a8fee85352432a40b1f868a81880e99ec7440113d5cfcab" + sha256: cc26720eefe98c1b71d85f9dc7ef0cada5132617046369d9dc296b3ecaa5cbb4 url: "https://pub.dev" source: hosted - version: "2.0.17" + version: "2.0.18" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "254708f17f7c20a9c8c471f67d86d76d4a3f9c1591aad1e15292008aceb82771" + sha256: "7967065dd2b5fccc18c653b97958fdf839c5478c28e767c61ee879f4e7882422" url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "3.0.7" vector_math: dependency: transitive description: @@ -543,4 +583,4 @@ packages: version: "1.0.0" sdks: dart: ">=3.0.2 <4.0.0" - flutter: ">=3.3.0" + flutter: ">=3.10.0" diff --git a/pubspec.yaml b/pubspec.yaml index b358354..84a1cb9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,6 +20,10 @@ dependencies: url_launcher: ^6.1.11 proxy_manager: ^0.0.3 chinese_font_library: + qr_flutter: ^4.1.0 + qrscan: ^0.3.3 + flutter_barcode_scanner: ^2.0.0 + dev_dependencies: flutter_test: sdk: flutter diff --git a/test/tests.dart b/test/tests.dart index 189cfff..fdfe25d 100644 --- a/test/tests.dart +++ b/test/tests.dart @@ -1,5 +1,8 @@ -import 'package:basic_utils/basic_utils.dart'; -import 'package:network_proxy/network/channel.dart'; +import 'dart:io'; void main() { + print(Platform.version); + print(Platform.localHostname); + print(Platform.operatingSystem); + print(Platform.localeName); }