Fix external proxy to forward to each other issue

This commit is contained in:
wanghongenpin
2024-06-23 01:18:48 +08:00
parent 726439c3bc
commit 423c99c862
21 changed files with 120 additions and 88 deletions

View File

@@ -109,7 +109,7 @@ class ProxyVpnService : VpnService(), ProtectSocket {
allowPackages: ArrayList<String>?,
disallowPackages: ArrayList<String>?
) {
Log.i("ProxyVpnService", "startVpn $host:$port $allowApps")
Log.i("ProxyVpnService", "startVpn $proxyHost:$proxyPort $allowPackages")
host = proxyHost
port = proxyPort

View File

@@ -74,8 +74,8 @@ class VpnServicePlugin : AndroidFlutterPlugin() {
private fun startVpn(
host: String,
port: Int,
allowApps: ArrayList<String>?,
disallowApps: ArrayList<String>?
allowApps: ArrayList<String>? = arrayListOf(),
disallowApps: ArrayList<String>? = arrayListOf(),
) {
val intent = ProxyVpnService.startVpnIntent(activity, host, port, allowApps, disallowApps)
activity.startService(intent)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -7,11 +7,11 @@ class Vpn {
static bool isVpnStarted = false; //vpn是否已经启动
static startVpn(String host, int port, Configuration configuration) {
List<String>? appList = configuration.appWhitelistEnabled ? configuration.appWhitelist : null;
List<String>? appList = configuration.appWhitelistEnabled ? configuration.appWhitelist : [];
List<String>? disallowApps;
if (appList == null || appList.isEmpty) {
disallowApps = configuration.appBlacklist;
if (appList.isEmpty) {
disallowApps = configuration.appBlacklist ?? [];
}
proxyVpnChannel.invokeMethod(
@@ -26,11 +26,11 @@ class Vpn {
//重启vpn
static restartVpn(String host, int port, Configuration configuration) {
List<String>? appList = configuration.appWhitelistEnabled ? configuration.appWhitelist : null;
List<String>? appList = configuration.appWhitelistEnabled ? configuration.appWhitelist : [];
List<String>? disallowApps;
if (appList == null || appList.isEmpty) {
disallowApps = configuration.appBlacklist;
if (appList.isEmpty) {
disallowApps = configuration.appBlacklist ?? [];
}
proxyVpnChannel.invokeMethod(
"restartVpn", {"proxyHost": host, "proxyPort": port, "allowApps": appList, "disallowApps": disallowApps});

View File

@@ -81,7 +81,19 @@ class Channel {
Socket get socket => _socket;
secureSocket(SecureSocket secureSocket, ChannelContext channelContext) {
Future<SecureSocket> secureSocket(ChannelContext channelContext,
{String? host, List<String>? supportedProtocols}) async {
SecureSocket secureSocket = await SecureSocket.secure(socket,
host: host, supportedProtocols: supportedProtocols, onBadCertificate: (certificate) => true);
_socket = secureSocket;
_socket.done.then((value) => isOpen = false);
pipeline.listen(this, channelContext);
return secureSocket;
}
serverSecureSocket(SecureSocket secureSocket, ChannelContext channelContext) {
_socket = secureSocket;
_socket.done.then((value) => isOpen = false);
pipeline.listen(this, channelContext);
@@ -275,9 +287,7 @@ class ChannelPipeline extends ChannelHandler<Uint8List> {
Channel? remoteChannel =
channelContext.serverChannel ?? await channelContext.connectServerChannel(remote, RelayHandler(clientChannel));
if (clientChannel.isSsl && !remoteChannel.isSsl) {
SecureSocket secureSocket = await SecureSocket.secure(remoteChannel.socket,
host: channelContext.getAttribute(AttributeKeys.domain), onBadCertificate: (certificate) => true);
remoteChannel.secureSocket(secureSocket, channelContext);
await remoteChannel.secureSocket(channelContext, host: channelContext.getAttribute(AttributeKeys.domain));
}
relay(clientChannel, remoteChannel);
@@ -348,7 +358,6 @@ class ChannelPipeline extends ChannelHandler<Uint8List> {
data.packageSize = length;
data.remoteAddress = '${channel.remoteSocketAddress.host}:${channel.remoteSocketAddress.port}';
data.request ??= channelContext.currentRequest;
channelContext.currentRequest?.response = data;
}
//websocket协议

View File

@@ -15,7 +15,6 @@
*/
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:network_proxy/network/components/host_filter.dart';
@@ -52,6 +51,7 @@ class HttpProxyChannelHandler extends ChannelHandler<HttpRequest> {
@override
void channelRead(ChannelContext channelContext, Channel channel, HttpRequest msg) async {
//下载证书
if (msg.uri == 'http://proxy.pin/ssl' || msg.requestUrl == 'http://127.0.0.1:${channel.socket.port}/ssl') {
ProxyHelper.crtDownload(channel, msg);
@@ -64,9 +64,13 @@ class HttpProxyChannelHandler extends ChannelHandler<HttpRequest> {
}
//代理转发请求
forward(channelContext, channel, msg).catchError((error, trace) {
try {
forward(channelContext, channel, msg).catchError((error, trace) {
exceptionCaught(channelContext, channel, error, trace: trace);
});
} catch (error, trace) {
exceptionCaught(channelContext, channel, error, trace: trace);
});
}
}
@override
@@ -95,11 +99,14 @@ class HttpProxyChannelHandler extends ChannelHandler<HttpRequest> {
try {
remoteChannel = await _getRemoteChannel(channelContext, channel, httpRequest);
} catch (error) {
channel.error = error; //记录异常
//https代理新建连接请求
log.e("[${channel.id}] 连接异常 ${httpRequest.method.name} ${httpRequest.requestUrl}", error: error);
if (httpRequest.method == HttpMethod.connect) {
channel.error = error; //记录异常
//https代理新建connect连接请求 返回ok 会继续发起正常请求 可以获取到请求内容
await channel.write(
HttpResponse(HttpStatus.ok.reason('Connection established'), protocolVersion: httpRequest.protocolVersion));
} else {
rethrow;
}
return;
}
@@ -176,17 +183,28 @@ class HttpProxyChannelHandler extends ChannelHandler<HttpRequest> {
HostAndPort? remote = channelContext.getAttribute(AttributeKeys.remote);
//外部代理
ProxyInfo? proxyInfo = channelContext.getAttribute(AttributeKeys.proxyInfo);
if (remote != null || proxyInfo != null) {
HostAndPort connectHost = remote ?? HostAndPort.host(proxyInfo!.host, proxyInfo.port!);
var proxyChannel = await connectRemote(channelContext, clientChannel, connectHost);
final proxyChannel = await connectRemote(channelContext, clientChannel, connectHost);
//代理建立完连接判断是否是https 需要发起connect请求
if (httpRequest.method == HttpMethod.connect) {
proxyChannel.write(httpRequest);
await proxyChannel.write(httpRequest);
} else {
await HttpClients.connectRequest(hostAndPort, proxyChannel);
if (clientChannel.isSsl) {
await proxyChannel.secureSocket(channelContext, host: hostAndPort.host);
}
}
return proxyChannel;
}
var proxyChannel = await connectRemote(channelContext, clientChannel, hostAndPort);
final proxyChannel = await connectRemote(channelContext, clientChannel, hostAndPort);
if (clientChannel.isSsl) {
await proxyChannel.secureSocket(channelContext, host: hostAndPort.host);
}
//https代理新建连接请求
if (httpRequest.method == HttpMethod.connect) {
await clientChannel.write(
@@ -199,12 +217,6 @@ class HttpProxyChannelHandler extends ChannelHandler<HttpRequest> {
Future<Channel> connectRemote(ChannelContext channelContext, Channel clientChannel, HostAndPort connectHost) async {
var proxyHandler = HttpResponseProxyHandler(clientChannel, listener: listener, requestRewrites: requestRewrites);
var proxyChannel = await channelContext.connectServerChannel(connectHost, proxyHandler);
if (clientChannel.isSsl) {
SecureSocket secureSocket = await SecureSocket.secure(proxyChannel.socket,
host: connectHost.host, onBadCertificate: (certificate) => true);
proxyChannel.secureSocket(secureSocket, channelContext);
}
return proxyChannel;
}
}
@@ -222,6 +234,7 @@ class HttpResponseProxyHandler extends ChannelHandler<HttpResponse> {
@override
void channelRead(ChannelContext channelContext, Channel channel, HttpResponse msg) async {
var request = channelContext.currentRequest;
request?.response = msg;
//域名是否过滤
if (HostFilter.filter(request?.hostAndPort?.host) || request?.method == HttpMethod.connect) {

View File

@@ -54,6 +54,18 @@ class HttpClients {
return channel;
}
await connectRequest(hostAndPort, channel);
if (hostAndPort.isSsl()) {
await channel.secureSocket(channelContext);
}
return channel;
}
///发起代理连接请求
static Future<Channel> connectRequest(HostAndPort hostAndPort, Channel channel) async {
ChannelHandler handler = channel.pipeline.handler;
//代理 发送connect请求
var httpResponseHandler = HttpResponseHandler();
channel.pipeline.handler = httpResponseHandler;
@@ -62,14 +74,13 @@ class HttpClients {
proxyRequest.headers.set(HttpHeaders.hostHeader, '${hostAndPort.host}:${hostAndPort.port}');
await channel.write(proxyRequest);
var response = await httpResponseHandler.getResponse(const Duration(seconds: 3));
var response = await httpResponseHandler.getResponse(const Duration(seconds: 5));
channel.pipeline.handler = handler;
if (!response.status.isSuccessful()) {
final error = "$hostAndPort Proxy failed to establish tunnel "
"(${response.status.code} ${response..status.reasonPhrase})";
throw Exception(error);
throw Exception("$hostAndPort Proxy failed to establish tunnel "
"(${response.status.code} ${response..status.reasonPhrase})");
}
return channel;
@@ -120,12 +131,8 @@ class HttpClients {
ChannelContext channelContext = ChannelContext();
var httpResponseHandler = HttpResponseHandler();
HostAndPort hostPort = HostAndPort.of(request.uri);
Channel channel = await proxyConnect(proxyInfo: proxyInfo, hostPort, httpResponseHandler, channelContext);
if (hostPort.isSsl()) {
var secureSocket = await SecureSocket.secure(channel.socket, onBadCertificate: (certificate) => true);
channel.secureSocket(secureSocket, channelContext);
}
Channel channel = await proxyConnect(proxyInfo: proxyInfo, hostPort, httpResponseHandler, channelContext);
await channel.write(request);
return httpResponseHandler.getResponse(timeout).whenComplete(() => channel.close());

View File

@@ -118,9 +118,6 @@ class Server extends Network {
//ssl握手
if (hostAndPort?.isSsl() == true || TLS.isTLSClientHello(data)) {
if (hostAndPort?.scheme == HostAndPort.httpScheme) {
hostAndPort?.scheme = HostAndPort.httpsScheme;
}
ssl(channelContext, channel, data);
return;
}
@@ -158,9 +155,7 @@ class Server extends Network {
if (remoteChannel != null && !remoteChannel.isSsl) {
var supportProtocols = configuration.enabledHttp2 ? TLS.supportProtocols(data) : null;
var secureSocket = await SecureSocket.secure(remoteChannel.socket,
supportedProtocols: supportProtocols, host: hostAndPort.host, onBadCertificate: (certificate) => true);
remoteChannel.secureSocket(secureSocket, channelContext);
await remoteChannel.secureSocket(channelContext, host: hostAndPort.host, supportedProtocols: supportProtocols);
}
//ssl自签证书
@@ -168,9 +163,9 @@ class Server extends Network {
var selectedProtocol = remoteChannel?.selectedProtocol;
if (selectedProtocol != null) certificate.setAlpnProtocols([selectedProtocol], true);
//服务端等待客户端ssl握手
//处理客户端ssl握手
var secureSocket = await SecureSocket.secureServer(channel.socket, certificate, bufferedData: data);
channel.secureSocket(secureSocket, channelContext);
channel.serverSecureSocket(secureSocket, channelContext);
} catch (error, trace) {
try {
channelContext.processInfo =

View File

@@ -44,9 +44,9 @@ String getPackage(HttpMessage? message) {
return "";
}
if (size > 1024 * 1024) {
return "${(size / 1024 / 1024).toStringAsFixed(2)}MB";
return "${(size / 1024 / 1024).toStringAsFixed(2)}M";
}
return "${(size / 1024).toStringAsFixed(2)}KB";
return "${(size / 1024).toStringAsFixed(2)}K";
}
String copyRequest(HttpRequest request, HttpResponse? response) {

View File

@@ -26,7 +26,7 @@ class AppConfiguration {
Locale? _language;
//是否显示更新内容公告
bool upgradeNoticeV9 = true;
bool upgradeNoticeV10 = true;
/// 是否启用画中画
ValueNotifier<bool> pipEnabled = ValueNotifier(true);
@@ -117,7 +117,7 @@ class AppConfiguration {
var mode =
ThemeMode.values.firstWhere((element) => element.name == config['mode'], orElse: () => ThemeMode.system);
_theme = ThemeModel(mode: mode, useMaterial3: config['useMaterial3'] ?? true);
upgradeNoticeV9 = config['upgradeNoticeV9'] ?? true;
upgradeNoticeV10 = config['upgradeNoticeV10'] ?? true;
_language = config['language'] == null ? null : Locale.fromSubtags(languageCode: config['language']);
pipEnabled.value = config['pipEnabled'] ?? true;
pipIcon.value = config['pipIcon'] ?? false;
@@ -144,7 +144,7 @@ class AppConfiguration {
return {
'mode': _theme.mode.name,
'useMaterial3': _theme.useMaterial3,
'upgradeNoticeV9': upgradeNoticeV9,
'upgradeNoticeV10': upgradeNoticeV10,
"language": _language?.languageCode,
'pipEnabled': pipEnabled.value,
'pipIcon': pipIcon.value ? true : null,

View File

@@ -85,7 +85,7 @@ class _DesktopHomePagePageState extends State<DesktopHomePage> implements EventL
proxyServer.addListener(this);
panel = NetworkTabController(tabStyle: const TextStyle(fontSize: 16), proxyServer: proxyServer);
if (widget.appConfiguration.upgradeNoticeV9) {
if (widget.appConfiguration.upgradeNoticeV10) {
WidgetsBinding.instance.addPostFrameCallback((_) {
showUpgradeNotice();
});
@@ -164,6 +164,7 @@ class _DesktopHomePagePageState extends State<DesktopHomePage> implements EventL
});
}
//left menu eg: requests, favorites, history, toolbox
Widget leftNavigation(int index) {
return NavigationRail(
minWidth: 58,
@@ -191,29 +192,33 @@ class _DesktopHomePagePageState extends State<DesktopHomePage> implements EventL
actions: [
TextButton(
onPressed: () {
widget.appConfiguration.upgradeNoticeV9 = false;
widget.appConfiguration.upgradeNoticeV10 = false;
widget.appConfiguration.flushConfig();
Navigator.pop(context);
},
child: Text(localizations.cancel))
],
title: Text(isCN ? '更新内容V1.0.9' : "Update content V1.0.9", style: const TextStyle(fontSize: 18)),
title: Text(isCN ? '更新内容V1.1.0' : "Update content V1.1.0", style: const TextStyle(fontSize: 18)),
content: Text(
isCN
? '提示默认不会开启HTTPS抓包请安装证书后再开启HTTPS抓包。\n'
'点击HTTPS抓包(加锁图标),选择安装根证书,按照提示操作即可。\n\n'
'1. 展示请求发起的应用图标;\n'
'2. 关键词匹配高亮\n'
'3. 脚本批量操作和导入导出\n'
'4. 脚本支持日志查看通过console.log()输出\n'
'5. 设置增加自动开启抓包\n'
'1. 更改应用程序图标;\n'
'2. 工具箱Javascript代码运行调试\n'
'3. 支持生成python requests代码\n'
'4. 修复mac重写不能选择文件\n'
'5. 高级重放请求支持随机间隔\n'
'6. 修复配置外部代理互相转发问题;\n'
'7. 修复ssl握手包域名为空的导致请求失败问题\n'
: 'TipsBy default, HTTPS packet capture will not be enabled. Please install the certificate before enabling HTTPS packet capture。\n'
'Click HTTPS Capture packets(Lock icon)Choose to install the root certificate and follow the prompts to proceed。\n\n'
'1. Display the application icon initiated by the request\n'
'2. Keyword matching highlights;\n'
'3. Script batch operations and import/export;\n'
'4. The script supports log viewing, output through console.log()\n'
'5. Setting Auto Start Recording Traffic',
'1. Change app icon\n'
'2. Toolbox add javascript code run\n'
'3. Support generating Python request code\n'
'4. Fix Mac rewrite unable to select files;\n'
'5. Custom repeat request support random interval\n'
'6. Fix external proxy to forward to each other issue\n'
'7. fix tls client hello data server_name is null bug',
style: const TextStyle(fontSize: 14)));
});
}

View File

@@ -249,7 +249,7 @@ class _HttpState extends State<_HttpWidget> {
message = widget.message;
body = TextEditingController(text: widget.message?.bodyAsString);
if (widget.message?.headers == null && !widget.readOnly) {
initHeader["User-Agent"] = ["ProxyPin/1.0.9"];
initHeader["User-Agent"] = ["ProxyPin/1.1.0"];
initHeader["Accept"] = ["*/*"];
return;
}

View File

@@ -131,7 +131,7 @@ class MobileHomeState extends State<MobileHomePage> implements EventListener, Li
}
});
if (widget.appConfiguration.upgradeNoticeV9) {
if (widget.appConfiguration.upgradeNoticeV10) {
WidgetsBinding.instance.addPostFrameCallback((_) {
showUpgradeNotice();
});
@@ -219,22 +219,24 @@ class MobileHomeState extends State<MobileHomePage> implements EventListener, Li
String content = isCN
? '提示默认不会开启HTTPS抓包请安装证书后再开启HTTPS抓包。\n\n'
'1. 展示请求发起的应用图标;\n'
'2. 关键词匹配高亮\n'
'3. 脚本批量操作和导入导出\n'
'4. 脚本支持日志查看通过console.log()输出\n'
'5. 设置增加自动开启抓包\n'
'6. Android证书下载优化'
'1. 更改应用程序图标;\n'
'2. 工具箱Javascript代码运行调试\n'
'3. 支持生成python requests代码\n'
'4. 修复mac重写不能选择文件\n'
'5. 高级重放请求支持随机间隔\n'
'6. 修复配置外部代理互相转发问题;\n'
'7. 修复ssl握手包域名为空的导致请求失败问题\n'
: 'TipsBy default, HTTPS packet capture will not be enabled. Please install the certificate before enabling HTTPS packet capture。\n\n'
'Click HTTPS Capture packets(Lock icon)Choose to install the root certificate and follow the prompts to proceed。\n\n'
'1. Display the application icon initiated by the request\n'
'2. Keyword matching highlights;\n'
'3. Script batch operations and import/export;\n'
'4. The script supports log viewing, output through console.log()\n'
'5. Setting Auto Start Recording Traffic\n'
'6. Android certificate download optimization; \n';
showAlertDialog(isCN ? '更新内容V1.0.9' : "Update content V1.0.9", content, () {
widget.appConfiguration.upgradeNoticeV9 = false;
'1. Change app icon\n'
'2. Toolbox add javascript code run\n'
'3. Support generating Python request code\n'
'4. Fix Mac rewrite unable to select files;\n'
'5. Custom repeat request support random interval\n'
'6. Fix external proxy to forward to each other issue\n'
'7. fix tls client hello data server_name is null bug';
showAlertDialog(isCN ? '更新内容V1.1.0' : "Update content V1.1.0", content, () {
widget.appConfiguration.upgradeNoticeV10 = false;
widget.appConfiguration.flushConfig();
});
}

View File

@@ -88,8 +88,8 @@ class RequestRowState extends State<RequestRow> {
subtitle: Text.rich(
maxLines: 1,
TextSpan(children: [
TextSpan(text: '#${widget.index} ', style: const TextStyle(fontSize: 12, color: Colors.teal)),
TextSpan(text: subTitle, style: const TextStyle(fontSize: 12, color: Colors.grey)),
TextSpan(text: '#${widget.index} ', style: const TextStyle(fontSize: 11, color: Colors.teal)),
TextSpan(text: subTitle, style: const TextStyle(fontSize: 11, color: Colors.grey)),
])),
trailing: getIcon(response),
contentPadding:

View File

@@ -225,7 +225,7 @@ class _HttpState extends State<_HttpWidget> with AutomaticKeepAliveClientMixin {
message = widget.message;
body = widget.message?.bodyAsString;
if (widget.message?.headers == null && !widget.readOnly) {
initHeader["User-Agent"] = ["ProxyPin/1.0.9"];
initHeader["User-Agent"] = ["ProxyPin/1.1.0"];
initHeader["Accept"] = ["*/*"];
return;
}

View File

@@ -85,6 +85,7 @@ class _AppWhitelistState extends State<AppWhitelist> {
title: localizations.enable,
subtitle: localizations.appWhitelistDescribe,
onChanged: (val) {
changed = true;
configuration.appWhitelistEnabled = val;
configuration.flushConfig();
}),

View File

@@ -22,7 +22,7 @@ class About extends StatelessWidget {
padding: const EdgeInsets.only(left: 10, right: 10),
child: Text(isCN ? "全平台开源免费抓包软件" : "Full platform open source free capture HTTP(S) traffic software")),
const SizedBox(height: 10),
const Text("V1.0.9"),
const Text("V1.1.0"),
ListTile(
title: const Text("Github"),
trailing: const Icon(Icons.arrow_right),

View File

@@ -65,7 +65,7 @@ class Har {
title = title.contains("ProxyPin") ? title : "[ProxyPin]$title";
har["log"] = {
"version": "1.2",
"creator": {"name": "ProxyPin", "version": "1.0.9"},
"creator": {"name": "ProxyPin", "version": "1.1.0"},
"pages": [
{
"title": title,

View File

@@ -4,7 +4,7 @@ cd ../build/linux/x64/release
rm -rf package
mkdir -p package/DEBIAN
echo "Package: ProxyPin" >> package/DEBIAN/control
echo "Version: 1.0.9" >> package/DEBIAN/control
echo "Version: 1.1.0" >> package/DEBIAN/control
echo "Priority: optional" >> package/DEBIAN/control
echo "Architecture: amd64" >> package/DEBIAN/control
echo "Depends: ca-certificates" >> package/DEBIAN/control

View File

@@ -2,7 +2,7 @@ name: network_proxy
description: ProxyPin
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.9+9
version: 1.1.0+9
environment:
sdk: '>=3.0.2 <4.0.0'
@@ -33,7 +33,7 @@ dependencies:
flutter_toastr: ^1.0.3
share_plus: ^8.0.2
brotli: ^0.6.0
file_selector: ^1.0.2
file_selector: ^1.0.3
flutter_js: ^0.8.0
flutter_code_editor:
file_picker: ^8.0.0

Binary file not shown.

Before

Width:  |  Height:  |  Size: 272 KiB

After

Width:  |  Height:  |  Size: 272 KiB