diff --git a/.metadata b/.metadata index 1dea856..25d90ec 100644 --- a/.metadata +++ b/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled. version: - revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff + revision: 796c8ef79279f9c774545b3771238c3098dbefab channel: stable project_type: app @@ -13,17 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff - base_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff - - platform: linux - create_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff - base_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff - - platform: macos - create_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff - base_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff - - platform: windows - create_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff - base_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff + create_revision: 796c8ef79279f9c774545b3771238c3098dbefab + base_revision: 796c8ef79279f9c774545b3771238c3098dbefab + - platform: android + create_revision: 796c8ef79279f9c774545b3771238c3098dbefab + base_revision: 796c8ef79279f9c774545b3771238c3098dbefab # User provided section diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..6f56801 --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 0000000..85a89f6 --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,72 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + namespace "com.network.proxy" + compileSdkVersion flutter.compileSdkVersion + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + 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 flutter.minSdkVersion + targetSdkVersion flutter.targetSdkVersion + 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 + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" +} diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..77172ea --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/com/network/proxy/MainActivity.kt b/android/app/src/main/kotlin/com/network/proxy/MainActivity.kt new file mode 100644 index 0000000..21f926b --- /dev/null +++ b/android/app/src/main/kotlin/com/network/proxy/MainActivity.kt @@ -0,0 +1,79 @@ +package com.network.proxy + +import android.content.Intent +import android.net.VpnService +import android.os.Bundle +import android.util.Log +import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugin.common.MethodChannel + + +class MainActivity : FlutterActivity() { + private val CHANNEL = "com.proxy/proxyVpn" + + private val VPN_REQUEST_CODE: Int = 24 + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + prepareVpn() + } + + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) + MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL) + .setMethodCallHandler { call, result -> + when (call.method) { + "startVpn" -> { + val host = call.argument("proxyHost") + val port = call.argument("proxyPort") + startVpn(host!!, port!!) + } + + "stopVpn" -> { + stopVpn() + } + + else -> { + result.notImplemented() + } + } + } + } + + /** + * 准备vpn
+ * 设备可能弹出连接vpn提示 + */ + private fun prepareVpn() { + val intent = VpnService.prepare(this@MainActivity) + if (intent != null) { + startActivityForResult(intent, VPN_REQUEST_CODE) + } + } + + /** + * 启动vpn服务 + */ + private fun startVpn(host: String, port: Int) { + Log.i("com.network.proxy", "startVpn") + val intent = Intent(this, ProxyVpnService::class.java) + intent.putExtra(ProxyVpnService.ProxyHost, host) + intent.putExtra(ProxyVpnService.ProxyPort, port) + startService(intent) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + } + + /** + * 停止vpn服务 + */ + private fun stopVpn() { + startService(Intent(this@MainActivity, ProxyVpnService::class.java).also { + it.action = ProxyVpnService.ACTION_DISCONNECT + }) + } + +} diff --git a/android/app/src/main/kotlin/com/network/proxy/ProxyVpnService.kt b/android/app/src/main/kotlin/com/network/proxy/ProxyVpnService.kt new file mode 100644 index 0000000..740408f --- /dev/null +++ b/android/app/src/main/kotlin/com/network/proxy/ProxyVpnService.kt @@ -0,0 +1,60 @@ +package com.network.proxy + +import android.content.Intent +import android.net.ProxyInfo +import android.net.VpnService +import android.os.Build +import android.os.ParcelFileDescriptor + +class ProxyVpnService : VpnService() { + private lateinit var vpnInterface: ParcelFileDescriptor + + companion object { + const val ProxyHost = "ProxyHost" + const val ProxyPort = "ProxyPort" + + /** + * 动作:断开连接 + */ + const val ACTION_DISCONNECT = "DISCONNECT" + } + + override fun onDestroy() { + super.onDestroy() + disconnect() + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + return if (intent?.action == ACTION_DISCONNECT) { + disconnect() + START_NOT_STICKY + } else { + connect(intent?.getStringExtra(ProxyHost)!!, intent.getIntExtra(ProxyPort, 0)) + START_STICKY + } + } + + private fun disconnect() { + vpnInterface.close() + } + + private fun connect(proxyHost: String, proxyPort: Int) { + vpnInterface = createVpnInterface(proxyHost, proxyPort) + } + + private fun createVpnInterface(proxyHost: String, proxyPort: Int): ParcelFileDescriptor { + return Builder() + .addAddress("10.0.0.2", 32) + .addRoute("0.0.0.0", 0) + .setSession(baseContext.applicationInfo.name) + .also { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + it.addDisallowedApplication(baseContext.packageName) + .setHttpProxy(ProxyInfo.buildDirectProxy(proxyHost, proxyPort)) + } + } + .establish() ?: throw IllegalStateException("无法初始化vpnInterface") + } + + +} diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..3964e4b Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-ldpi/ic_launcher.png b/android/app/src/main/res/mipmap-ldpi/ic_launcher.png new file mode 100644 index 0000000..9a6b754 Binary files /dev/null and b/android/app/src/main/res/mipmap-ldpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..62a2d6d Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..849765c Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d2250dc Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..f813c81 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..f7eb7f6 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,31 @@ +buildscript { + ext.kotlin_version = '1.7.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.3.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..94adc3a --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..3c472b9 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..44e62bc --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/lib/main.dart b/lib/main.dart index b8cac0a..194e3ea 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,9 +4,11 @@ import 'package:chinese_font_library/chinese_font_library.dart'; import 'package:flutter/material.dart'; import 'package:network_proxy/network/bin/server.dart'; import 'package:network_proxy/ui/component/split_view.dart'; -import 'package:network_proxy/ui/left/domain.dart'; +import 'package:network_proxy/ui/desktop/left/domain.dart'; +import 'package:network_proxy/ui/desktop/toolbar/toolbar.dart'; +import 'package:network_proxy/ui/mobile.dart'; import 'package:network_proxy/ui/panel.dart'; -import 'package:network_proxy/ui/toolbar/toolbar.dart'; +import 'package:network_proxy/utils/platform.dart'; import 'package:window_manager/window_manager.dart'; import 'network/channel.dart'; @@ -15,19 +17,20 @@ import 'network/http/http.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - //设置窗口大小 - await windowManager.ensureInitialized(); + if (Platforms.isDesktop()) { + //设置窗口大小 + await windowManager.ensureInitialized(); - WindowOptions windowOptions = WindowOptions( - minimumSize: const Size(980, 600), - size: Platform.isMacOS ? const Size(1200, 750) : const Size(1080, 650), - center: true, - titleBarStyle: - Platform.isMacOS ? TitleBarStyle.hidden : TitleBarStyle.normal); - windowManager.waitUntilReadyToShow(windowOptions, () async { - await windowManager.show(); - await windowManager.focus(); - }); + WindowOptions windowOptions = WindowOptions( + minimumSize: const Size(980, 600), + size: Platform.isMacOS ? const Size(1200, 750) : const Size(1080, 650), + center: true, + titleBarStyle: Platform.isMacOS ? TitleBarStyle.hidden : TitleBarStyle.normal); + windowManager.waitUntilReadyToShow(windowOptions, () async { + await windowManager.show(); + await windowManager.focus(); + }); + } runApp(const FluentApp()); } @@ -41,7 +44,7 @@ class FluentApp extends StatelessWidget { @override Widget build(BuildContext context) { var lightTheme = ThemeData.light(useMaterial3: true); - var darkTheme = ThemeData.dark(useMaterial3: false); + var darkTheme = ThemeData.dark(useMaterial3: !Platforms.isDesktop()); if (Platform.isWindows) { lightTheme = lightTheme.useSystemChineseFont(); darkTheme = darkTheme.useSystemChineseFont(); @@ -56,23 +59,22 @@ class FluentApp extends StatelessWidget { theme: lightTheme, darkTheme: darkTheme, themeMode: currentMode, - home: const NetworkHomePage(), + home: Platforms.isDesktop() ? const DesktopHomePage() : const MobileHomePage(), ); }); } } -class NetworkHomePage extends StatefulWidget { - const NetworkHomePage({super.key}); +class DesktopHomePage extends StatefulWidget { + const DesktopHomePage({super.key}); @override - State createState() => _NetworkHomePagePageState(); + State createState() => _DesktopHomePagePageState(); } -class _NetworkHomePagePageState extends State - implements EventListener { +class _DesktopHomePagePageState extends State implements EventListener { final domainStateKey = GlobalKey(); - final NetworkTabController panel = NetworkTabController(); + final NetworkTabController panel = NetworkTabController(tabStyle: const TextStyle(fontSize: 18)); late ProxyServer proxyServer; @@ -94,8 +96,7 @@ class _NetworkHomePagePageState extends State @override Widget build(BuildContext context) { - final domainWidget = DomainWidget( - key: domainStateKey, proxyServer: proxyServer, panel: panel); + final domainWidget = DomainWidget(key: domainStateKey, proxyServer: proxyServer, panel: panel); return Scaffold( appBar: Tab( diff --git a/lib/network/bin/server.dart b/lib/network/bin/server.dart index 41fd508..a2f036b 100644 --- a/lib/network/bin/server.dart +++ b/lib/network/bin/server.dart @@ -3,6 +3,8 @@ import 'dart:convert'; import 'dart:io'; import 'package:network_proxy/network/util/host_filter.dart'; +import 'package:network_proxy/utils/platform.dart'; +import 'package:path_provider/path_provider.dart'; import '../channel.dart'; import '../handler.dart'; @@ -29,16 +31,23 @@ class ProxyServer { bool get enableSsl => _enableSsl; - File homeDir() { - var userHome = Platform.environment['HOME'] ?? Platform.environment['USERPROFILE']; + Future homeDir() async { + String? userHome; + if (Platforms.isDesktop()) { + userHome = Platform.environment['HOME'] ?? Platform.environment['USERPROFILE']; + } else { + userHome = (await getApplicationSupportDirectory()).path; + } + var separator = Platform.pathSeparator; return File("${userHome!}$separator.proxypin"); } /// 配置文件 - File configFile() { + Future configFile() async { var separator = Platform.pathSeparator; - return File("${homeDir().path}${separator}config.cnf"); + var home = await homeDir(); + return File("${home.path}${separator}config.cnf"); } /// 是否启用ssl @@ -95,8 +104,11 @@ class ProxyServer { /// 刷新配置文件 flushConfig() async { - var file = configFile(); - await file.create(recursive: true); + var file = await configFile(); + var exists = await file.exists(); + if (!exists) { + file = await file.create(recursive: true); + } HostFilter.whitelist.toJson(); HostFilter.blacklist.toJson(); var json = jsonEncode(toJson()); @@ -106,7 +118,7 @@ class ProxyServer { /// 加载配置文件 Future _loadConfig() async { - var file = configFile(); + var file = await configFile(); var exits = await file.exists(); if (!exits) { return; @@ -123,7 +135,8 @@ class ProxyServer { /// 加载请求重写配置文件 Future _loadRequestRewriteConfig() async { - var file = File('${homeDir().path}${Platform.pathSeparator}request_rewrite.json'); + var home = await homeDir(); + var file = File('${home.path}${Platform.pathSeparator}request_rewrite.json'); var exits = await file.exists(); if (!exits) { return; @@ -137,7 +150,8 @@ class ProxyServer { /// 保存请求重写配置文件 flushRequestRewriteConfig() async { - var file = File('${homeDir().path}${Platform.pathSeparator}request_rewrite.json'); + var home = await homeDir(); + var file = File('${home.path}${Platform.pathSeparator}request_rewrite.json'); bool exists = await file.exists(); if (!exists) { await file.create(recursive: true); diff --git a/lib/network/channel.dart b/lib/network/channel.dart index 4945b24..4a8fec7 100644 --- a/lib/network/channel.dart +++ b/lib/network/channel.dart @@ -2,7 +2,6 @@ import 'dart:io'; import 'dart:math'; import 'dart:typed_data'; -import 'package:logger/logger.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'; @@ -13,15 +12,7 @@ import 'handler.dart'; ///处理I/O事件或截获I/O操作 abstract class ChannelHandler { - var log = Logger( - printer: PrettyPrinter( - methodCount: 0, - errorMethodCount: 8, - lineLength: 120, - colors: true, - printEmojis: false, - excludeBox: {Level.info: true, Level.debug: true}, - )); + var log = logger; void channelActive(Channel channel) {} @@ -51,7 +42,9 @@ 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; @@ -130,7 +123,7 @@ class ChannelPipeline extends ChannelHandler { return; } if (data is HttpRequest) { - data.hostAndPort ??= getHostAndPort(data); + data.hostAndPort = channel.getAttribute(AttributeKeys.host) ?? getHostAndPort(data); data.remoteDomain = data.hostAndPort?.url; data.requestUrl = data.uri.startsWith("/") ? '${data.remoteDomain}${data.uri}' : data.uri; try { @@ -178,7 +171,7 @@ class HostAndPort { String domain = url; String? scheme; //域名格式 直接解析 - if (url.startsWith(httpScheme)) { + if (url.startsWith(httpScheme) || url.startsWith(httpsScheme)) { //httpScheme scheme = url.startsWith(httpsScheme) ? httpsScheme : httpScheme; domain = url.substring(scheme.length).split("/")[0]; @@ -202,11 +195,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; @@ -292,7 +285,7 @@ class Network { //客户端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 diff --git a/lib/network/handler.dart b/lib/network/handler.dart index 6378d4f..9293d59 100644 --- a/lib/network/handler.dart +++ b/lib/network/handler.dart @@ -56,7 +56,8 @@ class HttpChannelHandler extends ChannelHandler { Future forward(Channel channel, HttpRequest httpRequest) async { channel.putAttribute(AttributeKeys.request, httpRequest); - if (httpRequest.uri == 'http://proxy.pin/ssl') { + if (httpRequest.uri == 'http://proxy.pin/ssl' || + httpRequest.requestUrl == 'http://127.0.0.1:${channel.socket.port}/ssl') { _crtDownload(channel, httpRequest); return; } @@ -65,12 +66,13 @@ class HttpChannelHandler extends ChannelHandler { //实现抓包代理转发 if (httpRequest.method != HttpMethod.connect) { + // log.i("[${channel.id}] ${httpRequest.requestUrl}"); + var replaceBody = requestRewrites?.findRequestReplaceWith(httpRequest.path); if (replaceBody?.isNotEmpty == true) { httpRequest.body = utf8.encode(replaceBody!); } - // log.i("[${channel.id}] ${remoteChannel.getAttribute(AttributeKeys.uri)}"); listener?.onRequest(channel, httpRequest); //实现抓包代理转发 await remoteChannel.write(httpRequest); @@ -132,6 +134,7 @@ class HttpResponseProxyHandler extends ChannelHandler { @override void channelRead(Channel channel, HttpResponse msg) { msg.request = clientChannel.getAttribute(AttributeKeys.request); + msg.request?.response= msg; // log.i("[${clientChannel.id}] Response ${msg.bodyAsString}"); var replaceBody = requestRewrites?.findResponseReplaceWith(msg.request?.path); diff --git a/lib/network/http/body_reader.dart b/lib/network/http/body_reader.dart index 26cca57..24caba4 100644 --- a/lib/network/http/body_reader.dart +++ b/lib/network/http/body_reader.dart @@ -4,6 +4,7 @@ import 'dart:typed_data'; import 'package:network_proxy/network/http/http.dart'; import '../../utils/num.dart'; +import '../channel.dart'; import 'codec.dart'; class Result { @@ -29,6 +30,11 @@ class BodyReader { : _state = message.headers.isChunked ? ReaderState.readChunkSize : ReaderState.readFixedLengthContent; Result readBody(Uint8List data) { + if (_bodyBuffer.length > Codec.maxBodyLength) { + _bodyBuffer.clear(); + throw Exception('Body length exceeds ${Codec.maxBodyLength}'); + } + _offset = 0; //chunked编码 diff --git a/lib/network/http/http.dart b/lib/network/http/http.dart index 1814341..2a2da12 100644 --- a/lib/network/http/http.dart +++ b/lib/network/http/http.dart @@ -51,10 +51,12 @@ class HttpRequest extends HttpMessage { final String uri; late HttpMethod method; late String requestUrl; + String? path; HostAndPort? hostAndPort; final DateTime requestTime = DateTime.now(); String? remoteDomain; + HttpResponse? response; HttpRequest(this.method, this.uri, String protocolVersion) : super(protocolVersion); diff --git a/lib/network/util/request_rewrite.dart b/lib/network/util/request_rewrite.dart index d8a4300..7c9200c 100644 --- a/lib/network/util/request_rewrite.dart +++ b/lib/network/util/request_rewrite.dart @@ -1,5 +1,5 @@ class RequestRewrites { - bool enabled = false; + bool enabled = true; final List rules = []; load(Map? map) { diff --git a/lib/network/util/system_proxy.dart b/lib/network/util/system_proxy.dart index d8bbeb9..c155f1b 100644 --- a/lib/network/util/system_proxy.dart +++ b/lib/network/util/system_proxy.dart @@ -4,53 +4,41 @@ import 'package:network_proxy/utils/ip.dart'; import 'package:proxy_manager/proxy_manager.dart'; class SystemProxy { + static String? _hardwarePort; /// 设置系统代理 static void setSystemProxy(int port, bool enableSsl) async { if (Platform.isMacOS) { - _setProxyServerMacOS("127.0.0.1:$port", enableSsl); + _setProxyServerMacOS(port, enableSsl); } else if (Platform.isWindows) { _setProxyServerWindows(port, enableSsl); } } - static Future _setProxyServerMacOS( - String proxyServer, bool enableSsl) async { - var match = RegExp(r"^(?:http://)?(?.+):(?\d+)$") - .firstMatch(proxyServer); - if (match == null) { - print('proxyServer parse error!'); - return false; - } - var host = match.namedGroup('host'); - var port = match.namedGroup('port'); - var name = await hardwarePort(); + static Future _setProxyServerMacOS(int port, bool enableSsl) async { + _hardwarePort = await hardwarePort(); var results = await Process.run('bash', [ '-c', _concatCommands([ - 'networksetup -setwebproxy $name $host $port', - enableSsl == true - ? 'networksetup -setsecurewebproxy $name $host $port' - : '', - 'networksetup -setproxybypassdomains $name 192.168.0.0/16 10.0.0.0/8 172.16.0.0/12 127.0.0.1 localhost *.local timestamp.apple.com sequoia.apple.com seed-sequoia.siri.apple.com *.google.com', + 'networksetup -setwebproxy $_hardwarePort 127.0.0.1 $port', + enableSsl == true ? 'networksetup -setsecurewebproxy $_hardwarePort 127.0.0.1 $port' : '', + 'networksetup -setproxybypassdomains $_hardwarePort 192.168.0.0/16 10.0.0.0/8 172.16.0.0/12 127.0.0.1 localhost *.local timestamp.apple.com sequoia.apple.com seed-sequoia.siri.apple.com *.google.com', ]) ]); - print( - 'set proxyServer, exitCode: ${results.exitCode}, stdout: ${results.stdout}'); + print('set proxyServer, exitCode: ${results.exitCode}, stdout: ${results.stdout}'); return results.exitCode == 0; } - static Future setProxyEnableMacOS( - bool proxyEnable, bool enableSsl) async { + static Future setProxyEnableMacOS(bool proxyEnable, bool enableSsl) async { var proxyMode = proxyEnable ? 'on' : 'off'; - var name = await hardwarePort(); + _hardwarePort ??= await hardwarePort(); + print('set proxyEnable: $proxyEnable, name: $_hardwarePort'); + var results = await Process.run('bash', [ '-c', _concatCommands([ - 'networksetup -setwebproxystate $name $proxyMode', - enableSsl - ? 'networksetup -setsecurewebproxystate $name $proxyMode' - : '', + 'networksetup -setwebproxystate $_hardwarePort $proxyMode', + enableSsl ? 'networksetup -setsecurewebproxystate $_hardwarePort $proxyMode' : '', ]) ]); return results.exitCode == 0; @@ -76,17 +64,14 @@ class SystemProxy { 'networksetup -listnetworkserviceorder |grep "Device: $name" -A 1 |grep "Hardware Port" |awk -F ": " \'{print \$2}\'', ]) ]); - + print(results); return results.stdout.toString().split(", ")[0]; } - static Future _setProxyServerWindows( - int proxyPort, bool enableSsl) async { + static Future _setProxyServerWindows(int proxyPort, bool enableSsl) async { ProxyManager manager = ProxyManager(); await manager.setAsSystemProxy(ProxyTypes.http, "127.0.0.1", proxyPort); - if (enableSsl) { - // await manager.setAsSystemProxy(ProxyTypes.https, "127.0.0.1", 8888); - } + var results = await Process.run('reg', [ 'add', 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings', @@ -99,8 +84,7 @@ class SystemProxy { '/f', ]); - print( - 'set proxyServer $proxyPort, exitCode: ${results.exitCode}, stdout: ${results.stderr}'); + print('set proxyServer $proxyPort, exitCode: ${results.exitCode}, stdout: ${results.stderr}'); return results.exitCode == 0; } diff --git a/lib/ui/left/domain.dart b/lib/ui/desktop/left/domain.dart similarity index 94% rename from lib/ui/left/domain.dart rename to lib/ui/desktop/left/domain.dart index 3099bdd..7002a03 100644 --- a/lib/ui/left/domain.dart +++ b/lib/ui/desktop/left/domain.dart @@ -3,14 +3,13 @@ import 'dart:collection'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:network_proxy/network/bin/server.dart'; +import 'package:network_proxy/network/channel.dart'; 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/left/path.dart'; - -import '../../network/channel.dart'; -import '../../network/util/attribute_keys.dart'; -import '../panel.dart'; +import 'package:network_proxy/ui/desktop/left/path.dart'; +import 'package:network_proxy/ui/panel.dart'; ///左侧域名 class DomainWidget extends StatefulWidget { @@ -143,7 +142,7 @@ class _HeaderBodyState extends State { visualDensity: const VisualDensity(vertical: -3.6), title: Text(title, textAlign: TextAlign.left, - style: const TextStyle(fontSize: 16), + style: const TextStyle(fontSize: 14), maxLines: 1, overflow: TextOverflow.ellipsis), onTap: () { diff --git a/lib/ui/left/path.dart b/lib/ui/desktop/left/path.dart similarity index 100% rename from lib/ui/left/path.dart rename to lib/ui/desktop/left/path.dart diff --git a/lib/ui/toolbar/launch/launch.dart b/lib/ui/desktop/toolbar/launch/launch.dart similarity index 65% rename from lib/ui/toolbar/launch/launch.dart rename to lib/ui/desktop/toolbar/launch/launch.dart index bdf211a..f5f62e8 100644 --- a/lib/ui/toolbar/launch/launch.dart +++ b/lib/ui/desktop/toolbar/launch/launch.dart @@ -5,8 +5,11 @@ import 'package:window_manager/window_manager.dart'; class SocketLaunch extends StatefulWidget { final ProxyServer proxyServer; + final int size; + final Function? onStart; + final Function? onStop; - const SocketLaunch({super.key, required this.proxyServer}); + const SocketLaunch({super.key, required this.proxyServer, this.size = 25, this.onStart, this.onStop}); @override State createState() { @@ -22,6 +25,7 @@ class _SocketLaunchState extends State with WindowListener { super.initState(); windowManager.addListener(this); widget.proxyServer.start(); + widget.onStart?.call(); } @override @@ -31,23 +35,25 @@ class _SocketLaunchState extends State with WindowListener { } @override - void onWindowClose() { + void onWindowClose() async { + print("onWindowClose"); + await widget.proxyServer.stop(); started = false; - widget.proxyServer.stop(); - setState(() {}); } @override Widget build(BuildContext context) { return IconButton( tooltip: started ? "停止" : "启动", - icon: Icon( - started ? Icons.stop : Icons.play_arrow_sharp, - color: started ? Colors.red : Colors.green, - size: 25, - ), + icon: Icon(started ? Icons.stop : Icons.play_arrow_sharp, + color: started ? Colors.red : Colors.green, size: widget.size.toDouble()), onPressed: () async { Future result = started ? widget.proxyServer.stop() : widget.proxyServer.start(); + if (started) { + widget.onStop?.call(); + } else { + widget.onStart?.call(); + } result.then((value) => setState(() { started = !started; })); diff --git a/lib/ui/toolbar/setting/filter.dart b/lib/ui/desktop/toolbar/setting/filter.dart similarity index 99% rename from lib/ui/toolbar/setting/filter.dart rename to lib/ui/desktop/toolbar/setting/filter.dart index 997e896..cdb2dd8 100644 --- a/lib/ui/toolbar/setting/filter.dart +++ b/lib/ui/desktop/toolbar/setting/filter.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:network_proxy/network/bin/server.dart'; +import 'package:network_proxy/network/util/host_filter.dart'; -import '../../../network/util/host_filter.dart'; class FilterDialog extends StatefulWidget { final ProxyServer proxyServer; diff --git a/lib/ui/toolbar/setting/request_rewrite.dart b/lib/ui/desktop/toolbar/setting/request_rewrite.dart similarity index 100% rename from lib/ui/toolbar/setting/request_rewrite.dart rename to lib/ui/desktop/toolbar/setting/request_rewrite.dart diff --git a/lib/ui/toolbar/setting/setting.dart b/lib/ui/desktop/toolbar/setting/setting.dart similarity index 89% rename from lib/ui/toolbar/setting/setting.dart rename to lib/ui/desktop/toolbar/setting/setting.dart index f6ef990..d35c820 100644 --- a/lib/ui/toolbar/setting/setting.dart +++ b/lib/ui/desktop/toolbar/setting/setting.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:network_proxy/network/bin/server.dart'; -import 'package:network_proxy/ui/toolbar/setting/request_rewrite.dart'; -import 'package:network_proxy/ui/toolbar/setting/theme.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'; import 'filter.dart'; @@ -26,11 +26,8 @@ class _SettingState extends State { offset: const Offset(10, 30), itemBuilder: (context) { return [ - PopupMenuItem(padding: const EdgeInsets.all(0), child: PortWidget(proxyServer: widget.proxyServer)), - const PopupMenuItem( - padding: EdgeInsets.all(0), - child: ThemeSetting(), - ), + PopupMenuItem(padding: const EdgeInsets.all(0), child: PortWidget(proxyServer: widget.proxyServer, textStyle: const TextStyle(fontSize: 13))), + const PopupMenuItem(padding: EdgeInsets.all(0), child: ThemeSetting(dense: true)), PopupMenuItem( padding: const EdgeInsets.all(0), child: ListTile( @@ -97,8 +94,9 @@ class _SettingState extends State { class PortWidget extends StatefulWidget { final ProxyServer proxyServer; + final TextStyle? textStyle; - const PortWidget({super.key, required this.proxyServer}); + const PortWidget({super.key, required this.proxyServer, this.textStyle}); @override State createState() { @@ -135,7 +133,7 @@ class _PortState extends State { Widget build(BuildContext context) { return Row(children: [ const Padding(padding: EdgeInsets.only(left: 16)), - const Text("端口号:", style: TextStyle(fontSize: 13)), + Text("端口号:", style: widget.textStyle), SizedBox( width: 80, child: TextFormField( diff --git a/lib/ui/toolbar/setting/theme.dart b/lib/ui/desktop/toolbar/setting/theme.dart similarity index 76% rename from lib/ui/toolbar/setting/theme.dart rename to lib/ui/desktop/toolbar/setting/theme.dart index 5d45702..f344439 100644 --- a/lib/ui/toolbar/setting/theme.dart +++ b/lib/ui/desktop/toolbar/setting/theme.dart @@ -1,15 +1,17 @@ import 'package:flutter/material.dart'; +import 'package:network_proxy/main.dart'; -import '../../../main.dart'; class ThemeSetting extends StatelessWidget { - const ThemeSetting({Key? key}) : super(key: key); + final bool dense; + + const ThemeSetting({Key? key, this.dense = false}) : super(key: key); @override Widget build(BuildContext context) { return PopupMenuButton( tooltip: themeNotifier.value.name, - surfaceTintColor: Colors.white70, + surfaceTintColor: Theme.of(context).colorScheme.onPrimary, offset: const Offset(150, 0), itemBuilder: (BuildContext context) { return [ @@ -30,10 +32,10 @@ class ThemeSetting extends StatelessWidget { }), ]; }, - child: const ListTile( - title: Text("主题"), - trailing: Icon(Icons.arrow_right), - dense: true, + child: ListTile( + title: const Text("主题"), + trailing: const Icon(Icons.arrow_right), + dense: dense, )); } } diff --git a/lib/ui/toolbar/ssl/ssl.dart b/lib/ui/desktop/toolbar/ssl/ssl.dart similarity index 100% rename from lib/ui/toolbar/ssl/ssl.dart rename to lib/ui/desktop/toolbar/ssl/ssl.dart diff --git a/lib/ui/toolbar/toolbar.dart b/lib/ui/desktop/toolbar/toolbar.dart similarity index 82% rename from lib/ui/toolbar/toolbar.dart rename to lib/ui/desktop/toolbar/toolbar.dart index b030b68..73406ad 100644 --- a/lib/ui/toolbar/toolbar.dart +++ b/lib/ui/desktop/toolbar/toolbar.dart @@ -2,11 +2,11 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:network_proxy/ui/toolbar/setting/setting.dart'; -import 'package:network_proxy/ui/toolbar/ssl/ssl.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:window_manager/window_manager.dart'; -import '../../network/bin/server.dart'; import '../left/domain.dart'; import 'launch/launch.dart'; @@ -41,6 +41,11 @@ class _ToolbarState extends State { windowManager.blur(); return; } + if (event.isKeyPressed(LogicalKeyboardKey.metaLeft) && event.isKeyPressed(LogicalKeyboardKey.keyQ)) { + print(" windowManager.close()"); + windowManager.close(); + return; + } } diff --git a/lib/ui/mobile.dart b/lib/ui/mobile.dart new file mode 100644 index 0000000..d514a73 --- /dev/null +++ b/lib/ui/mobile.dart @@ -0,0 +1,122 @@ +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 'mobile/request.dart'; +import 'mobile/request_rewrite.dart'; + +class MobileHomePage extends StatefulWidget { + const MobileHomePage({super.key}); + + @override + State createState() { + return MobileHomeState(); + } +} + +class MobileHomeState extends State implements EventListener { + static const MethodChannel proxyVpnChannel = MethodChannel('com.proxy/proxyVpn'); + + late ProxyServer proxyServer; + final requestStateKey = GlobalKey(); + + @override + void onRequest(Channel channel, HttpRequest request) { + requestStateKey.currentState!.add(channel, request); + } + + @override + void onResponse(Channel channel, HttpResponse response) { + requestStateKey.currentState!.addResponse(channel, response); + } + + @override + void initState() { + proxyServer = ProxyServer(listener: this); + super.initState(); + } + + @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()), + IconButton( + tooltip: "Https代理", + icon: const Icon(Icons.https), + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute(builder: (BuildContext context) { + return MobileSslWidget(proxyServer: proxyServer); + }), + ); + }) + ]), + 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()) + ], + )); + } + + void _filter(HostList hostList) { + Navigator.of(context).push( + MaterialPageRoute(builder: (BuildContext context) { + return MobileFilterWidget(proxyServer: proxyServer, hostList: hostList); + }), + ); + } + + void _reqeustRewrite() { + Navigator.of(context).push( + MaterialPageRoute(builder: (BuildContext context) { + return MobileRequestRewrite(proxyServer: proxyServer); + }), + ); + } +} diff --git a/lib/ui/mobile/filter.dart b/lib/ui/mobile/filter.dart new file mode 100644 index 0000000..394279f --- /dev/null +++ b/lib/ui/mobile/filter.dart @@ -0,0 +1,219 @@ +import 'package:flutter/material.dart'; +import 'package:network_proxy/network/bin/server.dart'; + +import '../../../network/util/host_filter.dart'; + +class MobileFilterWidget extends StatefulWidget { + final ProxyServer proxyServer; + final HostList hostList; + + const MobileFilterWidget({super.key, required this.proxyServer, required this.hostList}); + + @override + State createState() => _MobileFilterState(); +} + +class _MobileFilterState extends State { + final ValueNotifier hostEnableNotifier = ValueNotifier(false); + + @override + void dispose() { + hostEnableNotifier.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + var title = widget.hostList.runtimeType == Whites ? "白名单" : "黑名单"; + var subtitle = widget.hostList.runtimeType == Whites ? "只代理白名单中的域名, 白名单启用黑名单将会失效" : "黑名单中的域名不会代理"; + return Scaffold( + appBar: AppBar(title: const Text("域名过滤", style: TextStyle(fontSize: 16))), + body: Container( + padding: const EdgeInsets.all(10), + child: DomainFilter( + title: title, + subtitle: subtitle, + hostList: widget.hostList, + proxyServer: widget.proxyServer, + hostEnableNotifier: hostEnableNotifier), + ) + ); + } +} + +class DomainFilter extends StatefulWidget { + final String title; + final String subtitle; + final HostList hostList; + final ProxyServer proxyServer; + final ValueNotifier hostEnableNotifier; + + const DomainFilter( + {super.key, + required this.title, + required this.subtitle, + required this.hostList, + required this.hostEnableNotifier, + required this.proxyServer}); + + @override + State createState() { + return _DomainFilterState(); + } +} + +class _DomainFilterState extends State { + late DomainList domainList; + bool changed = false; + + @override + Widget build(BuildContext context) { + domainList = DomainList(widget.hostList); + + return Column( + children: [ + ListTile( + title: Text(widget.title), + subtitle: Text(widget.subtitle, style: const TextStyle(fontSize: 12)), + titleAlignment: ListTileTitleAlignment.center, + ), + const SizedBox(height: 10), + ValueListenableBuilder( + valueListenable: widget.hostEnableNotifier, + builder: (_, bool enable, __) { + return SwitchListTile( + title: const Text('是否启用'), + dense: true, + value: widget.hostList.enabled, + onChanged: (value) { + widget.hostList.enabled = value; + changed = true; + widget.hostEnableNotifier.value = !widget.hostEnableNotifier.value; + }); + }), + Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ + FilledButton.icon( + icon: const Icon(Icons.add, size: 14), + onPressed: () { + add(); + }, + label: const Text("增加", style: TextStyle(fontSize: 12))), + const SizedBox(width: 10), + TextButton.icon( + icon: const Icon(Icons.remove, size: 14), + label: const Text("删除", style: TextStyle(fontSize: 12)), + onPressed: () { + if (domainList.selected().isEmpty) { + return; + } + changed = true; + setState(() { + widget.hostList.removeIndex(domainList.selected()); + }); + }) + ]), + domainList + ], + ); + } + + @override + void dispose() { + if (changed) { + widget.proxyServer.flushConfig(); + } + super.dispose(); + } + + void add() { + GlobalKey formKey = GlobalKey(); + String? host; + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return AlertDialog( + scrollable: true, + content: Padding( + padding: const EdgeInsets.all(8.0), + child: Form( + key: formKey, + child: Column(children: [ + TextFormField( + decoration: const InputDecoration(labelText: 'Host', hintText: '*.example.com'), + onSaved: (val) => host = val) + ]))), + actions: [ + FilledButton( + child: const Text("添加"), + onPressed: () { + (formKey.currentState as FormState).save(); + if (host != null && host!.isNotEmpty) { + try { + changed = true; + widget.hostList.add(host!); + setState(() {}); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString()))); + } + } + Navigator.of(context).pop(); + }), + ElevatedButton( + child: const Text("关闭"), + onPressed: () { + Navigator.of(context).pop(); + }) + ]); + }); + } +} + +///域名列表 +class DomainList extends StatefulWidget { + final HostList hostList; + + DomainList(this.hostList) : super(key: GlobalKey<_DomainListState>()); + + @override + State createState() => _DomainListState(); + + List selected() { + var state = (key as GlobalKey<_DomainListState>).currentState; + List list = []; + state?.selected.forEach((key, value) { + if (value == true) { + list.add(key); + } + }); + return list; + } +} + +class _DomainListState extends State { + late Map selected = {}; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.only(top: 10), + height: 300, + child: SingleChildScrollView( + child: DataTable( + border: TableBorder.symmetric(outside: BorderSide(width: 1, color: Theme.of(context).highlightColor)), + columns: const [ + DataColumn(label: Text('域名')), + ], + rows: List.generate( + widget.hostList.list.length, + (index) => DataRow( + cells: [DataCell(Text(widget.hostList.list[index].pattern.replaceAll(".*", "*")))], + selected: selected[index] == true, + onSelectChanged: (value) { + setState(() { + selected[index] = value!; + }); + })), + ))); + } +} diff --git a/lib/ui/mobile/request.dart b/lib/ui/mobile/request.dart new file mode 100644 index 0000000..83af6e3 --- /dev/null +++ b/lib/ui/mobile/request.dart @@ -0,0 +1,285 @@ +import 'dart:collection'; + +import 'package:date_format/date_format.dart'; +import 'package:flutter/material.dart'; +import 'package:network_proxy/network/bin/server.dart'; +import 'package:network_proxy/network/http/http.dart'; + +import '../../network/channel.dart'; +import '../panel.dart'; + +class RequestWidget extends StatefulWidget { + final ProxyServer proxyServer; + + const RequestWidget({super.key, required this.proxyServer}); + + @override + State createState() { + return RequestWidgetState(); + } +} + +class RequestWidgetState extends State { + final tabs = [ + const Tab(child: Text('全部请求')), + const Tab(child: Text('域名列表')), + ]; + + GlobalKey requestSequenceKey = GlobalKey(); + GlobalKey domainListKey = GlobalKey(); + + static List container = []; + + @override + Widget build(BuildContext context) { + return DefaultTabController( + length: tabs.length, + child: Scaffold( + appBar: AppBar(title: TabBar(tabs: tabs)), + body: TabBarView( + children: [ + RequestSequence(key: requestSequenceKey, list: container), + DomainList(key: domainListKey, list: container), + ], + ), + )); + } + + ///添加请求 + add(Channel channel, HttpRequest request) { + container.add(request); + requestSequenceKey.currentState?.add(request); + domainListKey.currentState?.add(request); + } + + ///添加响应 + addResponse(Channel channel, HttpResponse response) { + response.request?.response = response; + requestSequenceKey.currentState?.addResponse(response); + domainListKey.currentState?.addResponse(response); + } + + ///清理 + clean() { + setState(() { + domainListKey.currentState?.clean(); + requestSequenceKey.currentState?.clean(); + container.clear(); + }); + } +} + +class RequestSequence extends StatefulWidget { + final List list; + + const RequestSequence({super.key, required this.list}); + + @override + State createState() { + return RequestSequenceState(); + } +} + +class RequestSequenceState extends State { + GlobalKey listKey = GlobalKey(); + Map> indexes = HashMap(); + + late Queue list = Queue(); + + @override + initState() { + super.initState(); + print("initState ${widget.list.length}"); + list.addAll(widget.list.reversed); + } + + ///添加请求 + add(HttpRequest request) { + list.addFirst(request); + listKey.currentState?.insertItem(0); + } + + ///添加响应 + addResponse(HttpResponse response) { + response.request?.response = response; + var state = indexes.remove(response.request); + state?.currentState?.change(response); + } + + clean() { + setState(() { + list.clear(); + indexes.clear(); + listKey.currentState?.removeAllItems((context, animation) => Container()); + }); + } + + @override + Widget build(BuildContext context) { + return AnimatedList( + key: listKey, + initialItemCount: list.length, + itemBuilder: (context, index, Animation animation) { + GlobalKey key = GlobalKey(); + indexes[list.elementAt(index)] = key; + return Container( + decoration: + BoxDecoration(border: Border(bottom: BorderSide(width: 0.5, color: Theme.of(context).focusColor))), + child: RequestRow(key: key, request: list.elementAt(index))); + }); + } +} + +class RequestRow extends StatefulWidget { + final HttpRequest request; + + const RequestRow({super.key, required this.request}); + + @override + State createState() { + return RequestRowState(); + } +} + +class RequestRowState extends State { + late HttpRequest request; + HttpResponse? response; + + change(HttpResponse response) { + setState(() { + this.response = response; + }); + } + + @override + void initState() { + request = widget.request; + response = request.response; + super.initState(); + } + + @override + Widget build(BuildContext context) { + var title = '${request.method.name} ${request.requestUrl}'; + var time = formatDate(request.requestTime, [HH, ':', nn, ':', ss]); + return ListTile( + leading: Icon(getIcon(response), size: 16, color: Colors.green), + title: Text(title, overflow: TextOverflow.ellipsis, maxLines: 1), + subtitle: Text( + '$time - [${response?.status.code ?? ''}] ${response?.contentType.name.toUpperCase() ?? ''} ${response?.costTime() ?? ''} ', + maxLines: 1), + trailing: const Icon(Icons.chevron_right), + onTap: () { + Navigator.push(context, MaterialPageRoute(builder: (context) { + return NetworkTabController( + httpRequest: request, + httpResponse: response, + title: const Text("抓包详情", style: TextStyle(fontSize: 16))); + })); + }); + } + + IconData getIcon(HttpResponse? response) { + var map = { + ContentType.json: Icons.data_object, + ContentType.html: Icons.html, + ContentType.js: Icons.javascript, + ContentType.image: Icons.image, + ContentType.text: Icons.text_fields, + ContentType.css: Icons.css, + ContentType.font: Icons.font_download, + }; + if (response == null) { + return Icons.question_mark; + } + var contentType = response.contentType; + return map[contentType] ?? Icons.http; + } +} + +class DomainList extends StatefulWidget { + final List list; + + const DomainList({super.key, required this.list}); + + @override + State createState() { + return DomainListState(); + } +} + +class DomainListState extends State { + GlobalKey requestSequenceKey = GlobalKey(); + + Map> containerMap = {}; + + LinkedHashSet container = LinkedHashSet(); + HostAndPort? showHostAndPort; + + @override + initState() { + super.initState(); + for (var request in widget.list) { + var hostAndPort = request.hostAndPort!; + container.add(hostAndPort); + var list = containerMap[hostAndPort]; + if (list == null) { + list = []; + containerMap[hostAndPort] = list; + } + list.add(request); + } + } + + add(HttpRequest request) { + var hostAndPort = request.hostAndPort!; + container.add(hostAndPort); + var list = containerMap[hostAndPort]; + if (list == null) { + list = []; + containerMap[hostAndPort] = list; + } + list.add(request); + if (showHostAndPort == request.hostAndPort) { + requestSequenceKey.currentState?.add(request); + } + + setState(() {}); + } + + addResponse(HttpResponse response) { + if (showHostAndPort == response.request?.hostAndPort) { + requestSequenceKey.currentState?.addResponse(response); + } + } + + clean() { + setState(() { + containerMap.clear(); + container.clear(); + }); + } + + @override + Widget build(BuildContext context) { + return ListView.builder( + itemCount: container.length, + itemBuilder: (context, index) { + var time = formatDate( + containerMap[container.elementAt(index)]!.last.requestTime, [m, '/', d, ' ', HH, ':', nn, ':', ss]); + return ListTile( + title: Text(container.elementAt(index).url, maxLines: 1, overflow: TextOverflow.ellipsis), + trailing: const Icon(Icons.chevron_right), + subtitle: Text("最后请求时间: $time, 次数: ${containerMap[container.elementAt(index)]!.length}", + maxLines: 1, overflow: TextOverflow.ellipsis), + onTap: () { + Navigator.push(context, MaterialPageRoute(builder: (context) { + showHostAndPort = container.elementAt(index); + return Scaffold( + appBar: AppBar(title: const Text("请求列表")), + body: RequestSequence(key: requestSequenceKey, list: containerMap[container.elementAt(index)]!)); + })); + }); + }); + } +} diff --git a/lib/ui/mobile/request_rewrite.dart b/lib/ui/mobile/request_rewrite.dart new file mode 100644 index 0000000..1539db6 --- /dev/null +++ b/lib/ui/mobile/request_rewrite.dart @@ -0,0 +1,284 @@ +import 'package:flutter/material.dart'; +import 'package:network_proxy/network/bin/server.dart'; +import 'package:network_proxy/network/util/request_rewrite.dart'; + +class MobileRequestRewrite extends StatefulWidget { + final ProxyServer proxyServer; + + const MobileRequestRewrite({super.key, required this.proxyServer}); + + @override + State createState() => _MobileRequestRewriteState(); +} + +class _MobileRequestRewriteState extends State { + late RequestRuleList requestRuleList; + late ValueNotifier enableNotifier; + bool changed = false; + + @override + void initState() { + super.initState(); + requestRuleList = RequestRuleList(widget.proxyServer.requestRewrites); + enableNotifier = ValueNotifier(widget.proxyServer.requestRewrites.enabled); + } + + @override + void dispose() { + if (changed || enableNotifier.value != widget.proxyServer.requestRewrites.enabled) { + widget.proxyServer.requestRewrites.enabled = enableNotifier.value; + widget.proxyServer.flushRequestRewriteConfig(); + } + + enableNotifier.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text("请求重写")), + body: Container( + padding: const EdgeInsets.all(10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + child: ValueListenableBuilder( + valueListenable: enableNotifier, + builder: (_, bool v, __) { + return SwitchListTile( + contentPadding: const EdgeInsets.only(left: 2), + title: const Text('是否启用请求重写'), + value: enableNotifier.value, + onChanged: (value) { + enableNotifier.value = value; + }); + })), + const SizedBox(height: 10), + Row(children: [ + FilledButton.icon( + icon: const Icon(Icons.add), + onPressed: () { + add(); + }, + label: const Text("增加")), + const SizedBox(width: 10), + OutlinedButton.icon( + onPressed: () { + var selectedIndex = requestRuleList.currentSelectedIndex(); + add(selectedIndex); + }, + icon: const Icon(Icons.edit), + label: const Text("编辑")), + TextButton.icon( + icon: const Icon(Icons.remove), + label: const Text("删除"), + onPressed: () { + var removeSelected = requestRuleList.removeSelected(); + if (removeSelected.isEmpty) { + return; + } + + changed = true; + setState(() { + widget.proxyServer.requestRewrites.removeIndex(removeSelected); + requestRuleList.changeState(); + }); + }) + ]), + const SizedBox(height: 10), + requestRuleList, + ], + ))); + } + + void add([int currentIndex = -1]) { + showDialog( + context: context, + builder: (BuildContext context) { + return RuleAddDialog( + requestRewrites: widget.proxyServer.requestRewrites, + currentIndex: currentIndex, + onChange: () { + changed = true; + requestRuleList.changeState(); + }); + }); + } +} + +///请求重写规则添加对话框 +class RuleAddDialog extends StatelessWidget { + final RequestRewrites requestRewrites; + final int currentIndex; + final Function onChange; + + const RuleAddDialog({super.key, required this.currentIndex, required this.onChange, required this.requestRewrites}); + + @override + Widget build(BuildContext context) { + GlobalKey formKey = GlobalKey(); + RequestRewriteRule? rule; + if (currentIndex >= 0) { + rule = requestRewrites.rules[currentIndex]; + } + + ValueNotifier enableNotifier = ValueNotifier(rule == null || rule.enabled); + String? url = rule?.url; + String? requestBody = rule?.requestBody; + String? responseBody = rule?.responseBody; + + return AlertDialog( + title: const Text("添加请求重写规则", style: TextStyle(fontSize: 16)), + 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: 'URL', hintText: '/api/v1/*'), + validator: (val) { + if (val == null || val.isEmpty) { + return 'URL不能为空'; + } + return null; + }, + initialValue: url, + onSaved: (val) => url = val), + TextFormField( + initialValue: requestBody, + decoration: const InputDecoration(labelText: '请求体替换为:'), + onSaved: (val) => requestBody = val), + TextFormField( + initialValue: responseBody, + minLines: 3, + maxLines: 15, + decoration: const InputDecoration(labelText: '响应体替换为:', hintText: '{"code":"200","data":{}}'), + onSaved: (val) => responseBody = val) + ])), + actions: [ + FilledButton( + child: const Text("保存"), + onPressed: () { + if ((formKey.currentState as FormState).validate()) { + (formKey.currentState as FormState).save(); + + if (currentIndex >= 0) { + requestRewrites.rules[currentIndex] = RequestRewriteRule(enableNotifier.value, url!, + requestBody: requestBody, responseBody: responseBody); + } else { + requestRewrites.addRule(RequestRewriteRule(enableNotifier.value, url!, + requestBody: requestBody, responseBody: responseBody)); + } + + enableNotifier.dispose(); + onChange.call(); + Navigator.of(context).pop(); + } + }), + ElevatedButton( + child: const Text("关闭"), + onPressed: () { + Navigator.of(context).pop(); + }) + ]); + } +} + +class RequestRuleList extends StatefulWidget { + final RequestRewrites requestRewrites; + + RequestRuleList(this.requestRewrites) : super(key: GlobalKey<_RequestRuleListState>()); + + @override + State createState() => _RequestRuleListState(); + + List removeSelected() { + var state = (key as GlobalKey<_RequestRuleListState>).currentState; + List list = []; + state?.selected.forEach((key, value) { + if (value == true) { + list.add(key); + } + }); + state?.selected.clear(); + return list; + } + + int currentSelectedIndex() { + var state = (key as GlobalKey<_RequestRuleListState>).currentState; + return state?.currentSelectedIndex ?? -1; + } + + changeState() { + var state = (key as GlobalKey<_RequestRuleListState>).currentState; + state?.changeState(); + } +} + +class _RequestRuleListState extends State { + final Map selected = {}; + int currentSelectedIndex = -1; + + changeState() { + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.only(top: 10), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: DataTable( + dataRowMaxHeight: 100, + border: TableBorder.symmetric(outside: BorderSide(width: 1, color: Theme.of(context).highlightColor)), + columns: const [ + DataColumn(label: Text('启用')), + DataColumn(label: Text('URL')), + 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), + child: Text(widget.requestRewrites.rules[index].url))), + DataCell(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: selected[index] == true, + onSelectChanged: (value) { + setState(() { + selected[index] = value!; + currentSelectedIndex = index; + }); + })), + ))); + } +} diff --git a/lib/ui/mobile/ssl.dart b/lib/ui/mobile/ssl.dart new file mode 100644 index 0000000..5c5ba9b --- /dev/null +++ b/lib/ui/mobile/ssl.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:network_proxy/network/bin/server.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class MobileSslWidget extends StatefulWidget { + final ProxyServer proxyServer; + + const MobileSslWidget({super.key, required this.proxyServer}); + + @override + State createState() => _MobileSslState(); +} + +class _MobileSslState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("Https代理"), + centerTitle: true, + ), + body: Column(children: [ + _Switch(proxyServer: widget.proxyServer), + ExpansionTile( + title: const Text("安装根证书"), + initiallyExpanded: true, + childrenPadding: const EdgeInsets.only(left: 20), + expandedAlignment: Alignment.topLeft, + expandedCrossAxisAlignment: CrossAxisAlignment.start, + shape: const Border(), + children: [ + TextButton(onPressed: () => _downloadCert(), child: const Text("1. 下载根证书安装到本系统")), + TextButton(onPressed: () {}, child: const Text("2. 去系统设置信任根证书")), + ]) + ])); + } + + void _downloadCert() async { + launchUrl(Uri.parse("http://127.0.0.1:${widget.proxyServer.port}/ssl"), mode: LaunchMode.externalApplication); + } +} + +class _Switch extends StatefulWidget { + final ProxyServer proxyServer; + + const _Switch({Key? key, required this.proxyServer}) : super(key: key); + + @override + State<_Switch> createState() => _SwitchState(); +} + +class _SwitchState extends State<_Switch> { + bool changed = false; + + @override + Widget build(BuildContext context) { + return SwitchListTile( + hoverColor: Colors.transparent, + title: const Text("启用Https代理", style: TextStyle(fontSize: 16)), + value: widget.proxyServer.enableSsl, + onChanged: (val) { + widget.proxyServer.enableSsl = val; + changed = true; + setState(() {}); + }); + } + + @override + void dispose() { + super.dispose(); + if (changed) { + widget.proxyServer.flushConfig(); + } + } +} diff --git a/lib/ui/panel.dart b/lib/ui/panel.dart index af5b5fc..84953ef 100644 --- a/lib/ui/panel.dart +++ b/lib/ui/panel.dart @@ -6,17 +6,23 @@ import 'package:network_proxy/network/http/http.dart'; import 'package:network_proxy/utils/lang.dart'; class NetworkTabController extends StatefulWidget { - final tabs = [ - const Tab(child: Text('General', style: TextStyle(fontSize: 18))), - const Tab(child: Text('Request', style: TextStyle(fontSize: 18))), - const Tab(child: Text('Response', style: TextStyle(fontSize: 18))), - const Tab(child: Text('Cookies', style: TextStyle(fontSize: 18))), + final tabs = [ + 'General', + 'Request', + 'Response', + 'Cookies', ]; final ValueWrap request = ValueWrap(); final ValueWrap response = ValueWrap(); + final Widget? title; + final TextStyle? tabStyle; - NetworkTabController() : super(key: GlobalKey()); + NetworkTabController({HttpRequest? httpRequest, HttpResponse? httpResponse, this.title, this.tabStyle}) + : super(key: GlobalKey()) { + request.set(httpRequest); + response.set(httpResponse); + } void change(HttpRequest? request, HttpResponse? response) { this.request.set(request); @@ -31,26 +37,46 @@ class NetworkTabController extends StatefulWidget { } } -class NetworkTabState extends State { +class NetworkTabState extends State with SingleTickerProviderStateMixin { + late TabController _tabController; + void changeState() { setState(() {}); } + @override + void initState() { + super.initState(); + _tabController = TabController(length: widget.tabs.length, vsync: this); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { - return DefaultTabController( - length: widget.tabs.length, - child: Scaffold( - appBar: AppBar(title: TabBar(tabs: widget.tabs)), - body: TabBarView( - children: [ - general(), - request(), - response(), - cookies(), - ], - ), - )); + var tabBar = TabBar( + controller: _tabController, + labelPadding: const EdgeInsets.symmetric(horizontal: 10), + tabs: widget.tabs.map((title) => Tab(child: Text(title, style: widget.tabStyle, maxLines: 1))).toList(), + ); + + Widget appBar = widget.title == null ? tabBar : AppBar(title: widget.title, bottom: tabBar); + return Scaffold( + appBar: appBar as PreferredSizeWidget?, + body: TabBarView( + controller: _tabController, + children: [ + general(), + request(), + response(), + cookies(), + ], + ), + ); } Widget general() { @@ -138,8 +164,7 @@ class NetworkTabState extends State { Widget? getBody(String type, HttpMessage message) { if (message.body?.isNotEmpty == true) { if (message.contentType == ContentType.image) { - return expansionTile("$type Body", - [Image.memory(Uint8List.fromList(message.body ?? []), fit: BoxFit.cover, width: 200, height: 200)]); + return expansionTile("$type Body", [Image.memory(Uint8List.fromList(message.body ?? []), fit: BoxFit.cover)]); } else { try { if (message.contentType == ContentType.json) { @@ -168,8 +193,7 @@ class NetworkTabState extends State { } Widget rowWidget(final String name, String? value) { - return Row( - children: [ + return Row(children: [ Expanded(flex: 2, child: SelectableText(name)), Expanded(flex: 4, child: SelectableText(value ?? '')) ]); diff --git a/lib/utils/platform.dart b/lib/utils/platform.dart new file mode 100644 index 0000000..15ec409 --- /dev/null +++ b/lib/utils/platform.dart @@ -0,0 +1,7 @@ +import 'dart:io'; + +class Platforms { + static bool isDesktop() { + return Platform.isWindows || Platform.isMacOS || Platform.isLinux; + } +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index d76db18..6139551 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,6 +7,7 @@ #include "generated_plugin_registrant.h" #include +#include #include #include #include @@ -15,6 +16,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); file_selector_plugin_register_with_registrar(file_selector_linux_registrar); + g_autoptr(FlPluginRegistrar) proxy_manager_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "ProxyManagerPlugin"); + proxy_manager_plugin_register_with_registrar(proxy_manager_registrar); g_autoptr(FlPluginRegistrar) screen_retriever_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin"); screen_retriever_plugin_register_with_registrar(screen_retriever_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 7432170..6fb4385 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_linux + proxy_manager screen_retriever url_launcher_linux window_manager diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index f7e5268..229ee96 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,6 +7,7 @@ import Foundation import file_selector_macos import path_provider_foundation +import proxy_manager import screen_retriever import url_launcher_macos import window_manager @@ -14,6 +15,7 @@ import window_manager func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + ProxyManagerPlugin.register(with: registry.registrar(forPlugin: "ProxyManagerPlugin")) ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 02032b4..65f3b80 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -117,10 +117,18 @@ packages: dependency: "direct main" description: name: file_selector - sha256: "2b9acc3587127132da2a8d88d069172c49e32f977211cdf8f61f4b8e68e2a165" + sha256: "1d2fde93dddf634a9c3c0faa748169d7ac0d83757135555707e52f02c017ad4f" url: "https://pub.dev" source: hosted - version: "0.9.4" + version: "0.9.5" + file_selector_android: + dependency: transitive + description: + name: file_selector_android + sha256: "65d41d2fbed893c5eb8842674ed08b920dc7d276b6c7e74ee8b1759dce4b2067" + url: "https://pub.dev" + source: hosted + version: "0.5.0" file_selector_ios: dependency: transitive description: @@ -178,10 +186,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + sha256: "2118df84ef0c3ca93f96123a616ae8540879991b8b57af2f81b76a7ada49b2a4" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.2" flutter_test: dependency: "direct dev" description: flutter @@ -521,10 +529,10 @@ packages: dependency: "direct main" description: name: window_manager - sha256: "95096fede562cbb65f30d38b62d819a458f59ba9fe4a317f6cee669710f6676b" + sha256: "9eef00e393e7f9308309ce9a8b2398c9ee3ca78b50c96e8b4f9873945693ac88" url: "https://pub.dev" source: hosted - version: "0.3.4" + version: "0.3.5" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index ccdcdeb..d2c2385 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ dependencies: logger: ^1.4.0 date_format: ^2.0.7 file_selector: ^0.9.3 - window_manager: ^0.3.4 + window_manager: ^0.3.5 path_provider: ^2.0.15 url_launcher: ^6.1.11 proxy_manager: ^0.0.3 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 0d3b5b1..bb1a7e2 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,6 +7,7 @@ #include "generated_plugin_registrant.h" #include +#include #include #include #include @@ -14,6 +15,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); + ProxyManagerPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ProxyManagerPlugin")); ScreenRetrieverPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ScreenRetrieverPlugin")); UrlLauncherWindowsRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index c0677f7..8cd3f07 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_windows + proxy_manager screen_retriever url_launcher_windows window_manager