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