mirror of
https://github.com/wanghongenpin/proxypin.git
synced 2026-05-18 16:06:50 +08:00
安卓应用白名单,调整请求展示列表
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -42,3 +42,4 @@ app.*.map.json
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
/android
|
||||
@@ -3,6 +3,8 @@
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
|
||||
<application
|
||||
android:label="ProxyPin"
|
||||
|
||||
@@ -1,22 +1,38 @@
|
||||
package com.network.proxy
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.net.ProxyInfo
|
||||
import android.net.VpnService
|
||||
import android.os.Build
|
||||
import android.os.ParcelFileDescriptor
|
||||
import androidx.core.app.NotificationCompat
|
||||
|
||||
class ProxyVpnService : VpnService() {
|
||||
/**
|
||||
* VPN服务
|
||||
* @author wanghongen
|
||||
*/
|
||||
class ProxyVpnService : VpnService(), IProtectSocket {
|
||||
private var vpnInterface: ParcelFileDescriptor? = null
|
||||
|
||||
companion object {
|
||||
const val ProxyHost = "ProxyHost"
|
||||
const val ProxyPort = "ProxyPort"
|
||||
const val AllowApps = "AllowApps" //允许的名单
|
||||
|
||||
/**
|
||||
* 动作:断开连接
|
||||
*/
|
||||
const val ACTION_DISCONNECT = "DISCONNECT"
|
||||
|
||||
/**
|
||||
* 通知配置
|
||||
*/
|
||||
private const val NOTIFICATION_ID = 9527
|
||||
const val VPN_NOTIFICATION_CHANNEL_ID = "vpn-notifications"
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
@@ -29,36 +45,84 @@ class ProxyVpnService : VpnService() {
|
||||
disconnect()
|
||||
START_NOT_STICKY
|
||||
} else {
|
||||
connect(intent?.getStringExtra(ProxyHost)!!, intent.getIntExtra(ProxyPort, 0))
|
||||
connect(
|
||||
intent?.getStringExtra(ProxyHost)!!, intent.getIntExtra(ProxyPort, 0),
|
||||
intent.getStringArrayListExtra(AllowApps)
|
||||
)
|
||||
START_STICKY
|
||||
}
|
||||
}
|
||||
|
||||
private fun disconnect() {
|
||||
vpnInterface?.close()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
}
|
||||
vpnInterface = null
|
||||
}
|
||||
|
||||
private fun connect(proxyHost: String, proxyPort: Int) {
|
||||
vpnInterface = createVpnInterface(proxyHost, proxyPort)
|
||||
private fun connect(proxyHost: String, proxyPort: Int, allowPackages: List<String>?) {
|
||||
vpnInterface = createVpnInterface(proxyHost, proxyPort, allowPackages)
|
||||
if (vpnInterface == null) {
|
||||
val alertDialog = Intent(applicationContext, VpnAlertDialog::class.java)
|
||||
.setAction("com.network.proxy.ProxyVpnService")
|
||||
alertDialog.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
startActivity(alertDialog)
|
||||
return
|
||||
}
|
||||
showServiceNotification()
|
||||
}
|
||||
|
||||
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))
|
||||
private fun showServiceNotification() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||
val notificationChannel = NotificationChannel(
|
||||
VPN_NOTIFICATION_CHANNEL_ID,
|
||||
"VPN Status",
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
)
|
||||
notificationManager.createNotificationChannel(notificationChannel)
|
||||
}
|
||||
|
||||
val pendingActivityIntent: PendingIntent =
|
||||
Intent(this, MainActivity::class.java).let { notificationIntent ->
|
||||
PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE)
|
||||
}
|
||||
|
||||
val notification: Notification =
|
||||
NotificationCompat.Builder(this, VPN_NOTIFICATION_CHANNEL_ID)
|
||||
.setContentIntent(pendingActivityIntent)
|
||||
.setContentTitle(getString(R.string.vpn_active_notification_title))
|
||||
.setContentText(getString(R.string.vpn_active_notification_content))
|
||||
.setOngoing(true)
|
||||
.build()
|
||||
|
||||
startForeground(NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
|
||||
private fun createVpnInterface(proxyHost: String, proxyPort: Int, allowPackages: List<String>?):
|
||||
ParcelFileDescriptor? {
|
||||
val build = Builder()
|
||||
.setMtu(MAX_PACKET_LEN)
|
||||
.addAddress("10.0.0.2", 32)
|
||||
.addRoute("0.0.0.0", 0)
|
||||
.setSession(baseContext.applicationInfo.name)
|
||||
|
||||
if (allowPackages?.isNotEmpty() == true) {
|
||||
allowPackages.forEach {
|
||||
if (it != baseContext.packageName)
|
||||
build.addAllowedApplication(it)
|
||||
}
|
||||
.establish()
|
||||
} else {
|
||||
build.addDisallowedApplication(baseContext.packageName)
|
||||
}
|
||||
|
||||
return build.apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
setHttpProxy(ProxyInfo.buildDirectProxy(proxyHost, proxyPort))
|
||||
}
|
||||
}.establish()
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -20,13 +20,22 @@ class VpnServicePlugin : AndroidFlutterPlugin() {
|
||||
"startVpn" -> {
|
||||
val host = call.argument<String>("proxyHost")
|
||||
val port = call.argument<Int>("proxyPort")
|
||||
startVpn(host!!, port!!)
|
||||
val allowApps = call.argument<ArrayList<String>>("allowApps")
|
||||
startVpn(host!!, port!!, allowApps)
|
||||
}
|
||||
|
||||
"stopVpn" -> {
|
||||
stopVpn()
|
||||
}
|
||||
|
||||
"restartVpn" -> {
|
||||
val host = call.argument<String>("proxyHost")
|
||||
val port = call.argument<Int>("proxyPort")
|
||||
val allowApps = call.argument<ArrayList<String>>("allowApps")
|
||||
stopVpn()
|
||||
startVpn(host!!, port!!, allowApps)
|
||||
}
|
||||
|
||||
else -> {
|
||||
result.notImplemented()
|
||||
}
|
||||
@@ -37,11 +46,12 @@ class VpnServicePlugin : AndroidFlutterPlugin() {
|
||||
/**
|
||||
* 启动vpn服务
|
||||
*/
|
||||
private fun startVpn(host: String, port: Int) {
|
||||
Log.i("com.network.proxy", "startVpn")
|
||||
private fun startVpn(host: String, port: Int, allowApps: ArrayList<String>?) {
|
||||
Log.i("com.network.proxy", "startVpn $allowApps")
|
||||
val intent = Intent(activity, ProxyVpnService::class.java)
|
||||
intent.putExtra(ProxyVpnService.ProxyHost, host)
|
||||
intent.putExtra(ProxyVpnService.ProxyPort, port)
|
||||
intent.putStringArrayListExtra(ProxyVpnService.AllowApps, allowApps)
|
||||
activity.startService(intent)
|
||||
}
|
||||
|
||||
|
||||
@@ -15,4 +15,8 @@
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
|
||||
<string name="vpn_active_notification_title">ProxyPin Active</string>
|
||||
<string name="vpn_active_notification_content">抓包正在运行</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.7.10'
|
||||
ext.kotlin_version = '1.9.0'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.3.0'
|
||||
classpath 'com.android.tools.build:gradle:7.3.1'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,16 @@ import 'package:flutter/services.dart';
|
||||
class Vpn {
|
||||
static const MethodChannel proxyVpnChannel = MethodChannel('com.proxy/proxyVpn');
|
||||
|
||||
static startVpn(String host, int port) {
|
||||
proxyVpnChannel.invokeMethod("startVpn", {"proxyHost": host, "proxyPort": port});
|
||||
static startVpn(String host, int port, [List<String>? appList]) {
|
||||
proxyVpnChannel.invokeMethod("startVpn", {"proxyHost": host, "proxyPort": port, "allowApps": appList});
|
||||
}
|
||||
|
||||
static stopVpn() {
|
||||
proxyVpnChannel.invokeMethod("stopVpn");
|
||||
}
|
||||
|
||||
//重启vpn
|
||||
static restartVpn(String host, int port, [List<String>? appList]) {
|
||||
proxyVpnChannel.invokeMethod("restartVpn", {"proxyHost": host, "proxyPort": port, "allowApps": appList});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,10 @@ class Configuration {
|
||||
|
||||
//外部代理
|
||||
ProxyInfo? externalProxy;
|
||||
|
||||
//白名单应用
|
||||
List<String> appWhitelist = [];
|
||||
|
||||
//远程连接 不持久化保存
|
||||
String? remoteHost;
|
||||
|
||||
@@ -119,6 +123,7 @@ class Configuration {
|
||||
if (config['externalProxy'] != null) {
|
||||
externalProxy = ProxyInfo.fromJson(config['externalProxy']);
|
||||
}
|
||||
appWhitelist = List<String>.from(config['appWhitelist'] ?? []);
|
||||
HostFilter.whitelist.load(config['whitelist']);
|
||||
HostFilter.blacklist.load(config['blacklist']);
|
||||
|
||||
@@ -161,6 +166,7 @@ class Configuration {
|
||||
'enableSsl': enableSsl,
|
||||
'enableSystemProxy': enableSystemProxy,
|
||||
'externalProxy': externalProxy?.toJson(),
|
||||
'appWhitelist': appWhitelist,
|
||||
'whitelist': HostFilter.whitelist.toJson(),
|
||||
'blacklist': HostFilter.blacklist.toJson(),
|
||||
};
|
||||
|
||||
@@ -55,7 +55,7 @@ class HttpChannelHandler extends ChannelHandler<HttpRequest> {
|
||||
}
|
||||
|
||||
//请求本服务
|
||||
if ((await localIps()).contains(msg.hostAndPort?.host)) {
|
||||
if ((await localIps()).contains(msg.hostAndPort?.host) && msg.hostAndPort?.port == channel.socket.port) {
|
||||
localRequest(msg, channel);
|
||||
return;
|
||||
}
|
||||
@@ -118,12 +118,14 @@ class HttpChannelHandler extends ChannelHandler<HttpRequest> {
|
||||
|
||||
/// 转发请求
|
||||
Future<void> forward(Channel channel, HttpRequest httpRequest) async {
|
||||
// log.i("[${channel.id}] ${httpRequest.method.name} ${httpRequest.requestUrl}");
|
||||
|
||||
//获取远程连接
|
||||
var remoteChannel = await _getRemoteChannel(channel, httpRequest);
|
||||
|
||||
//实现抓包代理转发
|
||||
if (httpRequest.method != HttpMethod.connect) {
|
||||
// log.i("[${channel.id}] ${httpRequest.method.name} ${httpRequest.requestUrl}");
|
||||
log.i("[${channel.id}] ${httpRequest.method.name} ${httpRequest.requestUrl}");
|
||||
|
||||
var replaceBody = requestRewrites?.findRequestReplaceWith(httpRequest.hostAndPort?.host, httpRequest.path());
|
||||
if (replaceBody?.isNotEmpty == true) {
|
||||
|
||||
@@ -95,10 +95,15 @@ class HttpRequest extends HttpMessage {
|
||||
String get requestUrl => uri.startsWith("/") ? '${remoteDomain()}$uri' : uri;
|
||||
|
||||
String? path() {
|
||||
if (hostAndPort?.isSsl() == true && uri.startsWith("/")) {
|
||||
return uri;
|
||||
}
|
||||
|
||||
try {
|
||||
return hostAndPort?.isSsl() == true ? uri : Uri.parse(requestUrl).path;
|
||||
var requestPath = Uri.parse(requestUrl).path;
|
||||
return requestPath.isEmpty ? "/" : requestPath;
|
||||
} catch (e) {
|
||||
return null;
|
||||
return "/";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:network_proxy/network/util/host_filter.dart';
|
||||
import 'package:network_proxy/ui/desktop/toolbar/setting/setting.dart';
|
||||
import 'package:network_proxy/ui/desktop/toolbar/setting/theme.dart';
|
||||
import 'package:network_proxy/ui/mobile/connect_remote.dart';
|
||||
import 'package:network_proxy/ui/mobile/setting/app_whitelist.dart';
|
||||
import 'package:network_proxy/ui/mobile/setting/filter.dart';
|
||||
import 'package:network_proxy/ui/mobile/setting/request_rewrite.dart';
|
||||
import 'package:network_proxy/ui/mobile/setting/ssl.dart';
|
||||
@@ -40,6 +41,10 @@ class DrawerWidget extends StatelessWidget {
|
||||
trailing: const Icon(Icons.arrow_right),
|
||||
onTap: () => navigator(context, MobileSslWidget(proxyServer: proxyServer))),
|
||||
const ThemeSetting(),
|
||||
ListTile(
|
||||
title: const Text("应用白名单"),
|
||||
trailing: const Icon(Icons.arrow_right),
|
||||
onTap: () => navigator(context, AppWhitelist(proxyServer: proxyServer))),
|
||||
ListTile(
|
||||
title: const Text("域名白名单"),
|
||||
trailing: const Icon(Icons.arrow_right),
|
||||
|
||||
@@ -91,7 +91,7 @@ class MobileHomeState extends State<MobileHomePage> implements EventListener {
|
||||
proxyServer: proxyServer,
|
||||
startup: false,
|
||||
size: 38,
|
||||
onStart: () => Vpn.startVpn("127.0.0.1", proxyServer.port),
|
||||
onStart: () => Vpn.startVpn("127.0.0.1", proxyServer.port, proxyServer.configuration.appWhitelist),
|
||||
onStop: () => Vpn.stopVpn())),
|
||||
body: ValueListenableBuilder(
|
||||
valueListenable: desktop,
|
||||
|
||||
@@ -43,7 +43,8 @@ class RequestListState extends State<RequestListWidget> {
|
||||
appBar: AppBar(title: TabBar(tabs: tabs)),
|
||||
body: TabBarView(
|
||||
children: [
|
||||
RequestSequence(key: requestSequenceKey, list: container, proxyServer: widget.proxyServer),
|
||||
RequestSequence(
|
||||
key: requestSequenceKey, list: container, proxyServer: widget.proxyServer, onRemove: remove),
|
||||
DomainList(key: domainListKey, list: container, proxyServer: widget.proxyServer, onRemove: remove),
|
||||
],
|
||||
),
|
||||
@@ -89,8 +90,10 @@ class RequestSequence extends StatefulWidget {
|
||||
final List<HttpRequest> list;
|
||||
final ProxyServer proxyServer;
|
||||
final bool displayDomain;
|
||||
final Function(List<HttpRequest>)? onRemove;
|
||||
|
||||
const RequestSequence({super.key, required this.list, required this.proxyServer, this.displayDomain = true});
|
||||
const RequestSequence(
|
||||
{super.key, required this.list, required this.proxyServer, this.displayDomain = true, this.onRemove});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
@@ -141,14 +144,14 @@ class RequestSequenceState extends State<RequestSequence> with AutomaticKeepAliv
|
||||
return;
|
||||
}
|
||||
|
||||
print("object ${searchModel?.filter(response.request!, response) } ${state == null}");
|
||||
print("object ${searchModel?.filter(response.request!, response)} ${state == null}");
|
||||
//搜索视图
|
||||
if (searchModel?.filter(response.request!, response) == true && state == null) {
|
||||
print("contains ${view.contains(response.request)}");
|
||||
|
||||
if (!view.contains(response.request)) {
|
||||
view.addFirst(response.request!);
|
||||
changeState();
|
||||
view.addFirst(response.request!);
|
||||
changeState();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -176,7 +179,7 @@ class RequestSequenceState extends State<RequestSequence> with AutomaticKeepAliv
|
||||
//防止频繁刷新
|
||||
if (!changing) {
|
||||
changing = true;
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
Future.delayed(const Duration(milliseconds: 50), () {
|
||||
setState(() {
|
||||
changing = false;
|
||||
});
|
||||
@@ -202,7 +205,14 @@ class RequestSequenceState extends State<RequestSequence> with AutomaticKeepAliv
|
||||
key: key,
|
||||
request: view.elementAt(index),
|
||||
proxyServer: widget.proxyServer,
|
||||
displayDomain: widget.displayDomain);
|
||||
displayDomain: widget.displayDomain,
|
||||
onRemove: (request) {
|
||||
widget.onRemove?.call([request]);
|
||||
setState(() {
|
||||
list.remove(request);
|
||||
view.remove(request);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -332,7 +342,6 @@ class DomainListState extends State<DomainList> with AutomaticKeepAliveClientMix
|
||||
return ListView.separated(
|
||||
padding: EdgeInsets.zero,
|
||||
separatorBuilder: (context, index) => Divider(thickness: 0.2, color: Theme.of(context).dividerColor),
|
||||
cacheExtent: 1000,
|
||||
itemCount: list.length,
|
||||
itemBuilder: (ctx, index) => title(index));
|
||||
}
|
||||
@@ -355,6 +364,7 @@ class DomainListState extends State<DomainList> with AutomaticKeepAliveClientMix
|
||||
key: requestSequenceKey,
|
||||
displayDomain: false,
|
||||
list: containerMap[list.elementAt(index)]!,
|
||||
onRemove: widget.onRemove,
|
||||
proxyServer: widget.proxyServer));
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -15,8 +15,10 @@ class RequestRow extends StatefulWidget {
|
||||
final HttpRequest request;
|
||||
final ProxyServer proxyServer;
|
||||
final bool displayDomain;
|
||||
final Function(HttpRequest)? onRemove;
|
||||
|
||||
const RequestRow({super.key, required this.request, required this.proxyServer, this.displayDomain = true});
|
||||
const RequestRow(
|
||||
{super.key, required this.request, required this.proxyServer, this.displayDomain = true, this.onRemove});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
@@ -43,18 +45,19 @@ class RequestRowState extends State<RequestRow> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var title = '${request.method.name} ${widget.displayDomain ? request.requestUrl : request.path()}';
|
||||
var title = '${request.method.name} ${request.path() ?? '/'} ';
|
||||
var time = formatDate(request.requestTime, [HH, ':', nn, ':', ss]);
|
||||
var subTitle =
|
||||
'$time - [${response?.status.code ?? ''}] ${response?.contentType.name.toUpperCase() ?? ''} ${response?.costTime() ?? ''}';
|
||||
'${request.hostAndPort?.domain} \n$time - [${response?.status.code ?? ''}] ${response?.contentType.name.toUpperCase() ?? ''} ${response?.costTime() ?? ''}';
|
||||
|
||||
return ListTile(
|
||||
textColor: (response?.status.code ?? 0) < 0 ? Colors.red : null,
|
||||
visualDensity: const VisualDensity(vertical: -4),
|
||||
leading: widget.displayDomain ? null : getIcon(response),
|
||||
leading: getIcon(response),
|
||||
title: Text(title, overflow: TextOverflow.ellipsis, maxLines: 1, style: const TextStyle(fontSize: 14)),
|
||||
subtitle: Text(subTitle, maxLines: 1, style: const TextStyle(fontSize: 12)),
|
||||
subtitle: Text(subTitle, maxLines: 2, style: const TextStyle(fontSize: 12)),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
dense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
onLongPress: () => menu(menuPosition(context)),
|
||||
onTap: () {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) {
|
||||
@@ -101,6 +104,14 @@ class RequestRowState extends State<RequestRow> {
|
||||
builder: (context) =>
|
||||
MobileRequestEditor(request: widget.request, proxyServer: widget.proxyServer)));
|
||||
}),
|
||||
const Divider(thickness: 0.5),
|
||||
TextButton(
|
||||
child: const SizedBox(width: double.infinity, child: Text("删除请求", textAlign: TextAlign.center)),
|
||||
onPressed: () {
|
||||
widget.onRemove?.call(request);
|
||||
FlutterToastr.show("删除成功", context);
|
||||
Navigator.of(context).pop();
|
||||
}),
|
||||
Container(
|
||||
color: Theme.of(context).hoverColor,
|
||||
height: 8,
|
||||
|
||||
178
lib/ui/mobile/setting/app_whitelist.dart
Normal file
178
lib/ui/mobile/setting/app_whitelist.dart
Normal file
@@ -0,0 +1,178 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:installed_apps/app_info.dart';
|
||||
import 'package:installed_apps/installed_apps.dart';
|
||||
import 'package:network_proxy/native/vpn.dart';
|
||||
import 'package:network_proxy/network/bin/configuration.dart';
|
||||
import 'package:network_proxy/network/bin/server.dart';
|
||||
|
||||
//应用白名单 目前只支持安卓 ios没办法获取安装的列表
|
||||
class AppWhitelist extends StatefulWidget {
|
||||
final ProxyServer proxyServer;
|
||||
|
||||
const AppWhitelist({super.key, required this.proxyServer});
|
||||
|
||||
@override
|
||||
State<AppWhitelist> createState() => _AppWhitelistState();
|
||||
}
|
||||
|
||||
class _AppWhitelistState extends State<AppWhitelist> {
|
||||
late Configuration configuration;
|
||||
|
||||
bool changed = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
configuration = widget.proxyServer.configuration;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (changed && widget.proxyServer.isRunning) {
|
||||
Vpn.stopVpn();
|
||||
Vpn.startVpn("127.0.0.1", widget.proxyServer.port, configuration.appWhitelist);
|
||||
configuration.flushConfig();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var appWhitelist = <Future<AppInfo>>[];
|
||||
for (var element in configuration.appWhitelist) {
|
||||
try {
|
||||
appWhitelist.add(InstalledApps.getAppInfo(element));
|
||||
} catch (_) {
|
||||
appWhitelist.add(Future.value(AppInfo.create({"name": "未知应用", "package_name": element})));
|
||||
}
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text("应用白名单", style: TextStyle(fontSize: 16)),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
onPressed: () {
|
||||
//添加
|
||||
Navigator.of(context)
|
||||
.push(MaterialPageRoute(builder: (context) => const InstalledAppsWidget()))
|
||||
.then((value) {
|
||||
if (value != null) {
|
||||
if (configuration.appWhitelist.contains(value)) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
configuration.appWhitelist.add(value);
|
||||
changed = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: FutureBuilder(
|
||||
future: Future.wait(appWhitelist),
|
||||
builder: (BuildContext context, AsyncSnapshot<List<AppInfo>> snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
if (snapshot.data!.isEmpty) {
|
||||
return const Center(
|
||||
child: Text("未设置白名单应用时会对所有应用抓包", style: TextStyle(color: Colors.grey)),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: snapshot.data!.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
AppInfo appInfo = snapshot.data![index];
|
||||
return ListTile(
|
||||
leading: Image.memory(appInfo.icon ?? Uint8List(0)),
|
||||
title: Text(appInfo.name ?? ""),
|
||||
subtitle: Text(appInfo.packageName ?? ""),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: () {
|
||||
//删除
|
||||
setState(() {
|
||||
configuration.appWhitelist.remove(appInfo.packageName);
|
||||
changed = true;
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
} else {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
///已安装的app列表
|
||||
class InstalledAppsWidget extends StatefulWidget {
|
||||
const InstalledAppsWidget({super.key});
|
||||
|
||||
@override
|
||||
State<InstalledAppsWidget> createState() => _InstalledAppsWidgetState();
|
||||
}
|
||||
|
||||
class _InstalledAppsWidgetState extends State<InstalledAppsWidget> {
|
||||
static Future<List<AppInfo>> apps = InstalledApps.getInstalledApps(true, true);
|
||||
|
||||
String? keyword;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: TextField(
|
||||
decoration: const InputDecoration(
|
||||
hintText: "请输入应用名或包名",
|
||||
border: InputBorder.none,
|
||||
),
|
||||
onChanged: (String value) {
|
||||
keyword = value;
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
body: FutureBuilder(
|
||||
future: apps,
|
||||
builder: (BuildContext context, AsyncSnapshot<List<AppInfo>> snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
List<AppInfo> appInfoList = snapshot.data!;
|
||||
if (keyword != null && keyword!.isNotEmpty) {
|
||||
appInfoList = appInfoList
|
||||
.where((element) => element.name!.contains(keyword!) || element.packageName!.contains(keyword!))
|
||||
.toList();
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: appInfoList.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
AppInfo appInfo = appInfoList[index];
|
||||
return ListTile(
|
||||
leading: Image.memory(appInfo.icon ?? Uint8List(0)),
|
||||
title: Text(appInfo.name ?? ""),
|
||||
subtitle: Text(appInfo.packageName ?? ""),
|
||||
onTap: () async {
|
||||
Navigator.of(context).pop(appInfo.packageName);
|
||||
},
|
||||
);
|
||||
});
|
||||
} else {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -268,9 +268,11 @@ class _RequestRuleListState extends State<RequestRuleList> {
|
||||
constraints: const BoxConstraints(minWidth: 60),
|
||||
child: Text(
|
||||
'${widget.requestRewrites.rules[index].domain ?? ''}${widget.requestRewrites.rules[index].path}'))),
|
||||
DataCell(SelectableText.rich(
|
||||
TextSpan(text: widget.requestRewrites.rules[index].requestBody),
|
||||
style: const TextStyle(fontSize: 12))),
|
||||
DataCell(Container(
|
||||
constraints: const BoxConstraints(maxWidth: 180),
|
||||
child: SelectableText.rich(
|
||||
TextSpan(text: widget.requestRewrites.rules[index].requestBody),
|
||||
style: const TextStyle(fontSize: 12)))),
|
||||
DataCell(Container(
|
||||
constraints: const BoxConstraints(maxWidth: 300),
|
||||
padding: const EdgeInsetsDirectional.all(10),
|
||||
|
||||
@@ -200,6 +200,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.0.2"
|
||||
installed_apps:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: installed_apps
|
||||
sha256: "145af8eb6e4e7c830e9888d6de0573ae5c139e8e0742a3e67316e1db21ab6fe0"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -2,7 +2,7 @@ name: network_proxy
|
||||
description: network proxy
|
||||
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
|
||||
version: 1.0.1+0
|
||||
version: 1.0.1+1
|
||||
|
||||
environment:
|
||||
sdk: '>=3.0.2 <4.0.0'
|
||||
@@ -26,6 +26,7 @@ dependencies:
|
||||
flutter_toastr: ^1.0.3
|
||||
share_plus: ^7.1.0
|
||||
brotli: ^0.6.0
|
||||
installed_apps: ^1.3.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Reference in New Issue
Block a user