安卓应用白名单,调整请求展示列表

This commit is contained in:
wanghongenpin
2023-08-16 17:54:34 +08:00
parent 9fff736536
commit 237cca3cf3
18 changed files with 358 additions and 44 deletions

1
.gitignore vendored
View File

@@ -42,3 +42,4 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release
/android

View File

@@ -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"

View File

@@ -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()
}

View File

@@ -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)
}

View File

@@ -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>

View File

@@ -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"
}
}

View File

@@ -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});
}
}

View File

@@ -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(),
};

View File

@@ -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) {

View File

@@ -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 "/";
}
}

View File

@@ -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),

View File

@@ -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,

View File

@@ -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));
}));
});

View File

@@ -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,

View 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(),
);
}
},
),
);
}
}

View File

@@ -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),

View File

@@ -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:

View File

@@ -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: