ssl配置

This commit is contained in:
wanghongen
2023-06-18 15:53:23 +08:00
parent 40d759ac85
commit 4f6192a96d
27 changed files with 441 additions and 207 deletions

2
.gitignore vendored
View File

@@ -9,7 +9,7 @@
.history
.svn/
migrate_working_dir/
Podfile.lock
# IntelliJ related
*.iml
*.ipr

View File

@@ -1,5 +1,6 @@
# network proxy
# network proxy
network proxy
http、https抓包工具支持windows、mac、linux
mac会提示已损坏需要到系统偏好设置-安全性与隐私-允许任何来源
https://blog.niekun.net/archives/1629.html

View File

@@ -1,20 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDMjCCAhoCCQC+wWaWR1OT6jANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJD
TjELMAkGA1UECAwCQkoxCzAJBgNVBAcMAkJKMQ0wCwYDVQQKDAR3YW5nMQ4wDAYD
VQQLDAVzdHVkeTESMBAGA1UEAwwJSHR0cFByb3h5MCAXDTIzMDUyNTA4MTE0NVoY
DzIxMjMwNTAxMDgxMTQ1WjBaMQswCQYDVQQGEwJDTjELMAkGA1UECAwCQkoxCzAJ
BgNVBAcMAkJKMQ0wCwYDVQQKDAR3YW5nMQ4wDAYDVQQLDAVzdHVkeTESMBAGA1UE
AwwJSHR0cFByb3h5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw4KN
x5QOzL02eU0vsyEatfT76b6Brdr+/Dky4GzUoIKWX+6Rit5avzUH1whCV8tbahZw
Ixs5Qw7oHbHAt0MdY7TJJgpsaiQKQsIJH4M88YILR405GtgsfdU32crdVlMwTTl3
qABncvQkmT1KkNyOgQvvlQErl8rx7uXTOZa5WhyX2c/egFu7BG+uDLKvwxBBW7ih
JlqonS5XzWzP/jd91gukcsPCxtUdWbQVjZwS37oDg12m7nXdL4vACQNnSa4v8VFL
Euaq9+62EHdj4A3M45MAh2nytyzOpg6/U17R4/0BicOiOfYM0UtAubirEWggj/4i
pzo1an79HHWffVw5/QIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQB71piBuwMvKlyz
SAX55hLCvPSxgIIjrLRiH3xqYTAbxvyaxgxlCjexNZZjuf5tJtrZ18OY+blw9SFT
SSY943q+6Go/rPo89vOa64fZ/yafR2AIF8S7K44F48QS0avz3kn1ljYu3CPEv9Ca
XFaYV8iPbMymIj0ahIM+edoVP4Iv6fWSqrSt+LyIMTWrB59pS91169kISoyDHUO0
isE8xqT82i2Jzi/AowVs9gDkVUumwv8GlkhoqQFQzaQgLVggK2bkYkRw7PaH3992
tezjL5KswWcBaVBPELCDAsh4tJ/2SzyUm6kKjoU446W4CXcjCjxYj140UhBj7tWw
l1IW1yiL
MIIDPDCCAiQCCQDjt1nXbWzgyDANBgkqhkiG9w0BAQsFADBgMQswCQYDVQQGEwJD
TjELMAkGA1UECAwCQkoxCzAJBgNVBAcMAkJKMQ4wDAYDVQQKDAVQcm94eTERMA8G
A1UECwwIUHJveHlQaW4xFDASBgNVBAMMC1Byb3h5UGluIENBMB4XDTIzMDYxODA2
MTg1NFoXDTMzMDYxNTA2MTg1NFowYDELMAkGA1UEBhMCQ04xCzAJBgNVBAgMAkJK
MQswCQYDVQQHDAJCSjEOMAwGA1UECgwFUHJveHkxETAPBgNVBAsMCFByb3h5UGlu
MRQwEgYDVQQDDAtQcm94eVBpbiBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
AQoCggEBAMRjfFvFDZS+PsdedUNq0Kn5t7RFNS0iQrZALr4LJm3UwtatHtMEWBb9
ptam8pWezxrZPZ81+qnTcaq/To82yus5hJa4JRk223YWn5JDd4izH4gcnSomhUQ6
Ycrc0v+I7UEaHV+bQsleHEfYi2+E1qF+FBhRveLSPmz2QORd/U4+gDlOptgNWMQ9
OTRHsMoDzb8J4SlcBu+s0dnq2WHOM9boGnfk2wIgE+16uB23epPoYjex8zYGUswh
8gNrIzXsr7i9IGtGf67FQYCWOXfZLeGgy0Q0/r1lwSmywUkNaZIsiGZHveZsLtW9
3MWMFw0uneEvHsuQV+e8sdLI/633TGcCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEA
VaGFU7tprM5irUOajfKJ3CXidmRCR3j2IBjOEkMHohGJWZukBphuTHM5OTAlseGl
gO/kFDn1K/Exit67koqqJGPQCg3bmtjLGApInQzVD1UFs6ePtVGj6oXuBPdz2gl8
StnZfOgIf8m8gxs8h8hsovR8ra3NFMnfgt5TJdvarsqKF8+CepEXDkJrtp6geAvp
ELJ5y1DA7OSPjBzXQqlr+X4Xh/kqCe4h7EZ48NJRtWX59jruMxnbclV86FNkWjS8
P5gm+MygFCMaJYTyyZwT+YztVPiDZIco+IJ1MSRajgaAErqH9FZ4lvcgxiXtz/mc
0uPLBkXBktcf2DGR2AAa7w==
-----END CERTIFICATE-----

View File

@@ -1,30 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: DES-EDE3-CBC,CEF6FB49B2B5361D
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
-----END RSA PRIVATE KEY-----

Binary file not shown.

View File

@@ -11,13 +11,29 @@ Future<void> main() async {
}
class ProxyServer {
int port = 8888;
EventListener? listener;
ServerSocket? serverSocket;
Server? server;
ProxyServer({this.listener});
Future<ServerSocket> start() {
const port = 8888;
///是否启用ssl
bool get enableSsl => server?.enableSsl == true;
set enableSsl(bool enableSsl) {
server?.enableSsl = enableSsl;
if (server?.isRunning == false) {
return;
}
if (Platform.isMacOS) {
setSslProxyEnableMacOS(enableSsl);
}
}
/// 启动代理服务
Future<Server> start() {
Server server = Server(port)
..initChannel((channel) {
channel.pipeline.handle(HttpRequestCodec(), HttpResponseCodec(), HttpChannelHandler(listener: listener));
@@ -26,22 +42,24 @@ class ProxyServer {
return server.bind().then((serverSocket) {
log.i("listen on $port");
_setSystemProxy(port);
this.serverSocket = serverSocket;
return serverSocket;
this.server = server;
return server;
});
}
Future<ServerSocket?> stop() async {
log.i("stop on ${serverSocket?.port}");
/// 停止代理服务
Future<Server?> stop() async {
log.i("stop on ${server?.port}");
if (Platform.isMacOS) {
await _setProxyEnableMacOS(false);
await setProxyEnableMacOS(false);
} else if (Platform.isWindows) {
await _setProxyEnableWindows(false);
}
return serverSocket?.close();
await server?.stop();
return server;
}
/// 设置系统代理
void _setSystemProxy(int port) async {
if (Platform.isMacOS) {
_setProxyServerMacOS("127.0.0.1:$port");
@@ -52,87 +70,79 @@ class ProxyServer {
}
Future<bool> _setProxyServerMacOS(String proxyServer) async {
try {
var match = RegExp(r"^(?:http://)?(?<host>.+):(?<port>\d+)$").firstMatch(proxyServer);
if (match == null) {
print('proxyServer parse error!');
return false;
}
var host = match.namedGroup('host');
var port = match.namedGroup('port');
var results = await Process.run('bash', [
'-c',
_concatCommands([
'networksetup -setwebproxy wi-fi $host $port',
// 'networksetup -setsecurewebproxy wi-fi $host $port',
'networksetup -setproxybypassdomains wi-fi 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',
])
]);
return results.exitCode == 0;
} catch (e) {
print(e);
var match = RegExp(r"^(?:http://)?(?<host>.+):(?<port>\d+)$").firstMatch(proxyServer);
if (match == null) {
print('proxyServer parse error!');
return false;
}
var host = match.namedGroup('host');
var port = match.namedGroup('port');
var results = await Process.run('bash', [
'-c',
_concatCommands([
'networksetup -setwebproxy wi-fi $host $port',
enableSsl ? 'networksetup -setsecurewebproxy wi-fi $host $port' : '',
'networksetup -setproxybypassdomains wi-fi 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',
])
]);
print('set proxyServer, exitCode: ${results.exitCode}, stdout: ${results.stdout}');
return results.exitCode == 0;
}
Future<bool> _setProxyEnableMacOS(bool proxyEnable) async {
try {
var proxyMode = proxyEnable ? 'on' : 'off';
var results = await Process.run('bash', [
'-c',
_concatCommands([
'networksetup -setwebproxystate wi-fi $proxyMode',
// 'networksetup -setsecurewebproxystate wi-fi $proxyMode',
])
]);
return results.exitCode == 0;
} catch (e) {
print(e);
return false;
}
Future<bool> setProxyEnableMacOS(bool proxyEnable) async {
var proxyMode = proxyEnable ? 'on' : 'off';
var results = await Process.run('bash', [
'-c',
_concatCommands([
'networksetup -setwebproxystate wi-fi $proxyMode',
enableSsl ? 'networksetup -setsecurewebproxystate wi-fi $proxyMode' : '',
])
]);
return results.exitCode == 0;
}
Future<bool> setSslProxyEnableMacOS(bool proxyEnable) async {
var results = await Process.run('bash', [
'-c',
_concatCommands([
proxyEnable
? 'networksetup -setsecurewebproxy wi-fi 127.0.0.1 ${server?.port}'
: 'networksetup -setsecurewebproxystate wi-fi off',
])
]);
return results.exitCode == 0;
}
Future<bool> _setProxyEnableWindows(bool proxyEnable) async {
try {
var results = await Process.run('reg', [
'add',
'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings',
'/v',
'ProxyEnable',
'/t',
'REG_DWORD',
'/f',
'/d',
proxyEnable ? '1' : '0',
]);
print('set proxyEnable, exitCode: ${results.exitCode}, stdout: ${results.stdout}');
return results.exitCode == 0;
} catch (e) {
print(e);
return false;
}
var results = await Process.run('reg', [
'add',
'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings',
'/v',
'ProxyEnable',
'/t',
'REG_DWORD',
'/f',
'/d',
proxyEnable ? '1' : '0',
]);
return results.exitCode == 0;
}
Future<bool> _setProxyServerWindows(String proxyServer) async {
try {
var results = await Process.run('reg', [
'add',
'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings',
'/v',
'ProxyServer',
'/f',
'/d',
proxyServer,
]);
print('set proxyServer, exitCode: ${results.exitCode}, stdout: ${results.stdout}');
return results.exitCode == 0;
} catch (e) {
print(e);
return false;
}
}
_concatCommands(List<String> commands) {
return commands.join(' && ');
var results = await Process.run('reg', [
'add',
'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings',
'/v',
'ProxyServer',
'/f',
'/d',
proxyServer,
]);
print('set proxyServer, exitCode: ${results.exitCode}, stdout: ${results.stdout}');
return results.exitCode == 0;
}
}
_concatCommands(List<String> commands) {
return commands.where((element) => element.isNotEmpty).join(' && ');
}

View File

@@ -59,7 +59,7 @@ class Channel {
Socket get socket => _socket;
set secureSocket(secureSocket) => _socket = secureSocket;
set secureSocket(SecureSocket secureSocket) => _socket = secureSocket;
Future<void> write(Object obj) async {
var data = pipeline._encoder.encode(obj);
@@ -105,6 +105,12 @@ class ChannelPipeline extends ChannelHandler<Uint8List> {
_handler = handler;
}
void listen(Channel channel) {
channel.socket.listen((data) => channel.pipeline.channelRead(channel, data),
onError: (error, trace) => channel.pipeline.exceptionCaught(channel, error, trace: trace),
onDone: () => channel.pipeline.channelInactive(channel));
}
@override
void channelActive(Channel channel) {
_handler.channelActive(channel);
@@ -227,6 +233,7 @@ abstract interface class ChannelInitializer {
class Network {
late Function _channelInitializer;
bool enableSsl = false;
Network initChannel(void Function(Channel channel) initializer) {
_channelInitializer = initializer;
@@ -245,29 +252,40 @@ class Network {
_onEvent(Uint8List data, Channel channel) async {
HostAndPort? hostAndPort = channel.getAttribute(AttributeKeys.host);
//黑名单 直接转发
if (HostFilter.filter(hostAndPort?.host) || (hostAndPort?.isSsl() == true && !enableSsl)) {
relay(channel, channel.getAttribute(channel.id));
channel.pipeline.channelRead(channel, data);
return;
}
//ssl握手
if (hostAndPort?.isSsl() == true) {
try {
var certificate = await CertificateManager.getCertificateContext(hostAndPort!.host);
SecureSocket secureSocket = await SecureSocket.secureServer(channel.socket, certificate, bufferedData: data);
channel.secureSocket = secureSocket;
secureSocket.listen((event) => channel.pipeline.channelRead(channel, event));
} catch (error, trace) {
channel.pipeline._handler.exceptionCaught(channel, error, trace: trace);
}
ssl(channel, hostAndPort!, data);
return;
}
//黑名单
if (hostAndPort != null && HostFilter.filter(hostAndPort.host)) {
if (HostFilter.filter(hostAndPort.host)) {
relay(channel, channel.getAttribute(channel.id));
}
}
channel.pipeline.channelRead(channel, data);
}
void ssl(Channel channel, HostAndPort hostAndPort, Uint8List data) async {
try {
//客户端ssl
Channel remoteChannel = channel.getAttribute(channel.id);
remoteChannel.secureSocket =
await SecureSocket.secure(remoteChannel.socket, onBadCertificate: (certificate) => true);
remoteChannel.pipeline.listen(remoteChannel);
//服务端ssl
var certificate = await CertificateManager.getCertificateContext(hostAndPort.host);
SecureSocket secureSocket = await SecureSocket.secureServer(channel.socket, certificate, bufferedData: data);
channel.secureSocket = secureSocket;
channel.pipeline.listen(channel);
} catch (error, trace) {
channel.pipeline._handler.exceptionCaught(channel, error, trace: trace);
}
}
/// 转发请求
void relay(Channel clientChannel, Channel remoteChannel) {
var rawCodec = RawCodec();
@@ -284,11 +302,11 @@ class Server extends Network {
Server(this.port);
Future<ServerSocket> bind() async {
serverSocket = await ServerSocket.bind(InternetAddress.loopbackIPv4, port);
serverSocket = await ServerSocket.bind(InternetAddress.anyIPv4, port);
serverSocket.listen((socket) {
isRunning = true;
listen(socket);
});
isRunning = true;
return serverSocket;
}
@@ -300,13 +318,7 @@ class Server extends Network {
}
class Client extends Network {
var log = Logger();
Future<Channel> connect(HostAndPort hostAndPort) async {
if (hostAndPort.isSsl()) {
return SecureSocket.connect(hostAndPort.host, hostAndPort.port, onBadCertificate: (certificate) => true)
.then((socket) => listen(socket));
}
return Socket.connect(hostAndPort.host, hostAndPort.port, timeout: const Duration(seconds: 10))
.then((socket) => listen(socket));
}

View File

@@ -1,5 +1,6 @@
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:network/network/http/http.dart';
import 'package:network/network/http/http_headers.dart';
import 'package:network/network/util/attribute_keys.dart';
@@ -34,7 +35,7 @@ class HttpChannelHandler extends ChannelHandler<HttpRequest> {
void channelRead(Channel channel, HttpRequest msg) async {
forward(channel, msg).catchError((error, trace) {
if (error is SocketException &&
(error.message.contains("Failed host lookup") || error.message.contains(" Operation timed out"))) {
(error.message.contains("Failed host lookup") || error.message.contains("Connection timed out"))) {
log.e("连接失败 ${error.message}");
channel.close();
return;
@@ -54,14 +55,21 @@ class HttpChannelHandler extends ChannelHandler<HttpRequest> {
Future<void> forward(Channel channel, HttpRequest httpRequest) async {
channel.putAttribute(AttributeKeys.request, httpRequest);
if (httpRequest.uri == 'http://proxy.pin/ssl') {
_crtDownload(channel, httpRequest);
return;
}
var remoteChannel = await _getRemoteChannel(channel, httpRequest);
//实现抓包代理转发
if (httpRequest.method != HttpMethod.connect) {
if (channel.getAttribute(AttributeKeys.host) == null) {
remoteChannel.putAttribute(AttributeKeys.uri, httpRequest.uri);
} else {
if (httpRequest.uri.startsWith("/")) {
remoteChannel.putAttribute(AttributeKeys.uri, '${channel.getAttribute(AttributeKeys.host)}${httpRequest.uri}');
} else {
remoteChannel.putAttribute(AttributeKeys.uri, httpRequest.uri);
}
// log.i("[${channel.id}] ${remoteChannel.getAttribute(AttributeKeys.uri)}");
listener?.onRequest(channel, httpRequest);
//实现抓包代理转发
@@ -69,6 +77,16 @@ class HttpChannelHandler extends ChannelHandler<HttpRequest> {
}
}
void _crtDownload(Channel channel, HttpRequest request) async{
const String fileMimeType = 'application/x-x509-ca-cert';
var body = await rootBundle.load('assets/certs/ca.crt');
var response = HttpResponse(request.protocolVersion, HttpStatus.ok);
response.headers.set(HttpHeaders.CONTENT_TYPE, fileMimeType);
response.headers.set("Content-Disposition", 'attachment; filename="ProxyPin CA.crt"');
response.body = body.buffer.asUint8List();
channel.write(response);
}
/// 获取远程连接
Future<Channel> _getRemoteChannel(Channel clientChannel, HttpRequest httpRequest) async {
String clientId = clientChannel.id;
@@ -127,6 +145,10 @@ class RelayHandler extends ChannelHandler<Object> {
//发送给客户端
remoteChannel.write(msg);
}
@override
void channelInactive(Channel channel) {
remoteChannel.close();
}
}
class HttpClients {

View File

@@ -168,7 +168,7 @@ abstract class HttpCodec<T extends HttpMessage> implements Codec<T> {
List<int> _convertBody() {
List<int> bytes = _bodyBuffer.toBytes();
if (message.headers.isGzip) {
bytes = gzipDecode(bytes);
bytes = gzipDecode(bytes);
}
_bodyBuffer.clear();
return bytes;

View File

@@ -46,9 +46,15 @@ abstract class HostList {
final List<RegExp> list = [];
bool enabled = false;
List<Function> initListens = [];
List<Function> _initListens = [];
bool _inited = false;
void addInitListen(void Function() action) {
if (!_inited) {
_initListens.add(action);
}
}
Future<void> _initList() async {
if (_inited) {
return;
@@ -67,7 +73,7 @@ abstract class HostList {
}
});
_inited = true;
for (var fun in initListens) {
for (var fun in _initListens) {
fun();
}
}

View File

@@ -1,7 +1,6 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:network/network/bin/server.dart';
import 'package:network/network/channel.dart';
import 'package:window_manager/window_manager.dart';
class SocketLaunch extends StatefulWidget {
@@ -55,7 +54,7 @@ class _SocketLaunchState extends State<SocketLaunch> with WindowListener {
size: 25,
),
onPressed: () async {
Future<ServerSocket?> result = started ? widget.proxyServer.stop() : widget.proxyServer.start();
Future<Server?> result = started ? widget.proxyServer.stop() : widget.proxyServer.start();
result.then((value) => setState(() {
started = !started;
}));

View File

@@ -27,6 +27,9 @@ class _DomainFilterState extends State<DomainFilter> {
@override
Widget build(BuildContext context) {
widget.hostList.addInitListen(() {
widget.hostEnableNotifier.value = !widget.hostEnableNotifier.value;
});
domainList = DomainList(widget.hostList);
return Column(
@@ -145,7 +148,7 @@ class _DomainListState extends State<DomainList> {
@override
Widget build(BuildContext context) {
widget.hostList.initListens.add(() {
widget.hostList.addInitListen(() {
setState(() {});
});

View File

@@ -1,12 +1,15 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:network/network/bin/server.dart';
import 'package:network/ui/toolbar/setting/theme.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../../network/util/host_filter.dart';
import 'filter.dart';
class Setting extends StatefulWidget {
const Setting({super.key});
final ProxyServer proxyServer;
const Setting({super.key, required this.proxyServer});
@override
State<Setting> createState() => _SettingState();
@@ -24,25 +27,38 @@ class _SettingState extends State<Setting> {
return [
PopupMenuItem<String>(
child: Row(children: [
const Padding(padding: EdgeInsets.only(left: 18)),
const Text("端口号:", style: TextStyle(fontSize: 15)),
const Padding(padding: EdgeInsets.only(left: 16)),
const Text("端口号:", style: TextStyle(fontSize: 13)),
SizedBox(
width: 80,
child: TextFormField(
initialValue: "8888",
initialValue: widget.proxyServer.port.toString(),
textAlign: TextAlign.center,
inputFormatters: <TextInputFormatter>[
LengthLimitingTextInputFormatter(5),
FilteringTextInputFormatter.allow(RegExp("[0-9]"))
],
decoration: const InputDecoration(),
decoration: const InputDecoration( ),
))
])),
const PopupMenuItem(
child: ThemeSetting(),
),
PopupMenuItem<String>(
child: ListTile(title: const Text("域名过滤"), trailing: const Icon(Icons.arrow_right), onTap: () => _filter()),
child: ListTile(
title: const Text("域名过滤"),
dense: true,
trailing: const Icon(Icons.arrow_right),
onTap: () => _filter())
),
PopupMenuItem<String>(
child: const ListTile(
title: Text("Github"),
dense: true,
trailing: Icon(Icons.arrow_right)),
onTap: () {
launchUrl(Uri.parse("https://github.com/wanghongenpin/network-proxy-flutter"));
},
)
];
},

View File

@@ -7,27 +7,32 @@ class ThemeSetting extends StatelessWidget {
@override
Widget build(BuildContext context) {
return PopupMenuButton(
tooltip: themeNotifier.value.name,
surfaceTintColor: Colors.white70,
offset: const Offset(150, 0),
itemBuilder: (BuildContext context) {
return [
PopupMenuItem(
child: const ListTile(trailing: Icon(Icons.cached), title: Text("跟随系统")),
child: const ListTile(trailing: Icon(Icons.cached), dense: true, title: Text("跟随系统")),
onTap: () {
themeNotifier.value = ThemeMode.system;
}),
PopupMenuItem(
child: const ListTile(trailing: Icon(Icons.nightlight_outlined), title: Text("深色")),
child: const ListTile(trailing: Icon(Icons.nightlight_outlined), dense: true, title: Text("深色")),
onTap: () {
themeNotifier.value = ThemeMode.dark;
}),
PopupMenuItem(
child: const ListTile(trailing: Icon(Icons.sunny), title: Text("浅色")),
child: const ListTile(trailing: Icon(Icons.sunny), dense: true, title: Text("浅色")),
onTap: () {
themeNotifier.value = ThemeMode.light;
}),
];
},
child: const ListTile(title: Text("主题"), trailing: Icon(Icons.arrow_right)));
child: const ListTile(
title: Text("主题"),
trailing: Icon(Icons.arrow_right),
dense: true,
));
}
}

View File

@@ -1,9 +1,14 @@
import 'package:file_selector/file_selector.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../../network/bin/server.dart';
class SslWidget extends StatefulWidget {
const SslWidget({super.key});
final ProxyServer proxyServer;
const SslWidget({super.key, required this.proxyServer});
@override
State<SslWidget> createState() => _SslState();
@@ -13,36 +18,88 @@ class _SslState extends State<SslWidget> {
@override
Widget build(BuildContext context) {
return PopupMenuButton<String>(
tooltip: "ssl证书",
icon: const Icon(Icons.https),
surfaceTintColor: Colors.white70,
tooltip: "Https代理",
offset: const Offset(10, 30),
itemBuilder: (context) {
return [
PopupMenuItem(
child: SwitchListTile(title: const Text("启用Https代理"), value: true, onChanged: (d) {}),
),
PopupMenuItem(child: _Switch(proxyServer: widget.proxyServer)),
PopupMenuItem(
child: ListTile(
title: const Text("ssl证书"),
dense: true,
title: const Text("安装根证书到系统"),
trailing: const Icon(Icons.arrow_right),
onTap: () {
_downloadCert();
pcCer();
},
)),
const PopupMenuItem<String>(
child: ListTile(title: Text("安装根证书到IOS"), trailing: Icon(Icons.arrow_right)),
PopupMenuItem<String>(
child: ListTile(
title: const Text("安装根证书到手机"),
dense: true,
trailing: const Icon(Icons.arrow_right),
onTap: () {
mobileCer();
}),
),
const PopupMenuItem<String>(
child: ListTile(title: Text("安装根证书到Android"), trailing: Icon(Icons.arrow_right)),
)
// const PopupMenuItem<String>(
// child: ListTile(title: Text("安装根证书到Android"), dense: true, trailing: Icon(Icons.arrow_right)),
// )
];
},
);
}
void pcCer() async {
showDialog(
context: context,
builder: (BuildContext context) {
return SimpleDialog(
contentPadding: const EdgeInsets.all(16),
title: const Text("电脑https抓包配置", style: TextStyle(fontSize: 16)),
alignment: Alignment.center,
children: [
const Text("1.下载根证书安装到本系统(已完成忽略)"),
FilledButton(onPressed: () => _downloadCert(), child: const Text("下载证书")),
const SizedBox(height: 10),
const Text("2.双击安装证书Mac安装完选择“始终信任此证书”,\n Windows选择“受信任的根证书颁发机构” "),
]);
});
}
void mobileCer() async {
showDialog(
context: context,
builder: (BuildContext context) {
return SimpleDialog(
contentPadding: const EdgeInsets.all(16),
title: const Text("手机https抓包配置", style: TextStyle(fontSize: 16)),
alignment: Alignment.center,
children: [
const Text("1. 根证书安装到本系统(已完成忽略)"),
const SizedBox(height: 10),
const Text.rich(TextSpan(text: "2.配置手机Wifi代理 ")),
const SizedBox(height: 10),
Row(
children: [
const Text("3.打开手机系统自带浏览器访问:"),
TextButton(
onPressed: () {
launchUrl(Uri.parse("http://proxy.pin/ssl"));
},
child:
const Text("http://proxy.pin/ssl", style: TextStyle(decoration: TextDecoration.underline)))
],
),
const SizedBox(height: 10),
const Text("4.打开手机设置下载安装证书(Profile)和信任证书(Certificate) \n\t 设置 > 通用 > 关于本机 > 证书信任设置"),
]);
});
}
void _downloadCert() async {
final String? path = await getSavePath(suggestedName: "ca_root.crt");
final String? path = await getSavePath(suggestedName: "ProxyPinCA.crt");
if (path != null) {
const String fileMimeType = 'application/x-x509-ca-cert';
var body = await rootBundle.load('assets/certs/ca.crt');
@@ -54,3 +111,27 @@ class _SslState extends State<SslWidget> {
}
}
}
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> {
@override
Widget build(BuildContext context) {
return SwitchListTile(
title: const Text("启用Https代理", style: TextStyle(fontSize: 12)),
visualDensity: const VisualDensity(horizontal: -4),
dense: true,
value: widget.proxyServer.enableSsl,
onChanged: (val) {
widget.proxyServer.enableSsl = val;
setState(() {});
});
}
}

View File

@@ -30,16 +30,15 @@ class _ToolbarState extends State<Toolbar> with WindowListener {
SocketLaunch(proxyServer: widget.proxyServer),
const Padding(padding: EdgeInsets.only(left: 30)),
IconButton(
isSelected: true,
tooltip: "清理",
icon: const Icon(Icons.cleaning_services_outlined),
onPressed: () {
widget.domainWidget.clean();
}),
const Padding(padding: EdgeInsets.only(left: 30)),
const SslWidget(),
SslWidget(proxyServer: widget.proxyServer),
const Padding(padding: EdgeInsets.only(left: 30)),
const Setting(),
Setting(proxyServer: widget.proxyServer),
],
);
}

View File

@@ -3,7 +3,12 @@ import 'dart:io';
///GZIP 解压缩
List<int> gzipDecode(List<int> byteBuffer) {
GZipCodec gzipCodec = GZipCodec();
return gzipCodec.decode(byteBuffer);
try {
return gzipCodec.decode(byteBuffer);
} catch (e) {
print("gzipDecode error: $e");
return byteBuffer;
}
}
///GZIP 解压缩

View File

@@ -8,6 +8,7 @@
#include <file_selector_linux/file_selector_plugin.h>
#include <screen_retriever/screen_retriever_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
#include <window_manager/window_manager_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
@@ -17,6 +18,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) screen_retriever_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin");
screen_retriever_plugin_register_with_registrar(screen_retriever_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
g_autoptr(FlPluginRegistrar) window_manager_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin");
window_manager_plugin_register_with_registrar(window_manager_registrar);

View File

@@ -5,6 +5,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
file_selector_linux
screen_retriever
url_launcher_linux
window_manager
)

View File

@@ -8,11 +8,13 @@ import Foundation
import file_selector_macos
import path_provider_foundation
import screen_retriever
import url_launcher_macos
import window_manager
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
}

View File

@@ -69,7 +69,7 @@
331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
33CC10ED2044A3C60003C045 /* network.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = network.app; sourceTree = BUILT_PRODUCTS_DIR; };
33CC10ED2044A3C60003C045 /* ProxyPin.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ProxyPin.app; sourceTree = BUILT_PRODUCTS_DIR; };
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
@@ -160,7 +160,7 @@
33CC10EE2044A3C60003C045 /* Products */ = {
isa = PBXGroup;
children = (
33CC10ED2044A3C60003C045 /* network.app */,
33CC10ED2044A3C60003C045 /* ProxyPin.app */,
331C80D5294CF71000263BE5 /* RunnerTests.xctest */,
);
name = Products;
@@ -253,7 +253,7 @@
);
name = Runner;
productName = Runner;
productReference = 33CC10ED2044A3C60003C045 /* network.app */;
productReference = 33CC10ED2044A3C60003C045 /* ProxyPin.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
@@ -608,7 +608,7 @@
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = "";
DEVELOPMENT_TEAM = DM3F8VR243;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -745,6 +745,7 @@
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = DM3F8VR243;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -765,10 +766,11 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
CODE_SIGN_IDENTITY = "-";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = DM3F8VR243;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",

View File

@@ -15,7 +15,7 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "network.app"
BuildableName = "ProxyPin.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
@@ -31,7 +31,7 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "network.app"
BuildableName = "ProxyPin.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
@@ -65,7 +65,7 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "network.app"
BuildableName = "ProxyPin.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
@@ -82,7 +82,7 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "network.app"
BuildableName = "ProxyPin.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>

View File

@@ -6,13 +6,13 @@
<false/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.network.server</key>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.scripting-targets</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.scripting-targets</key>
<true/>
</dict>
</plist>

View File

@@ -421,6 +421,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.2"
url_launcher:
dependency: "direct main"
description:
name: url_launcher
sha256: eb1e00ab44303d50dd487aab67ebc575456c146c6af44422f9c13889984c00f3
url: "https://pub.dev"
source: hosted
version: "6.1.11"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: eed4e6a1164aa9794409325c3b707ff424d4d1c2a785e7db67f8bbda00e36e51
url: "https://pub.dev"
source: hosted
version: "6.0.35"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2"
url: "https://pub.dev"
source: hosted
version: "6.1.4"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: "207f4ddda99b95b4d4868320a352d374b0b7e05eefad95a4a26f57da413443f5"
url: "https://pub.dev"
source: hosted
version: "3.0.5"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
sha256: "91ee3e75ea9dadf38036200c5d3743518f4a5eb77a8d13fda1ee5764373f185e"
url: "https://pub.dev"
source: hosted
version: "3.0.5"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
sha256: bfdfa402f1f3298637d71ca8ecfe840b4696698213d5346e9d12d4ab647ee2ea
url: "https://pub.dev"
source: hosted
version: "2.1.3"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
sha256: "6bb1e5d7fe53daf02a8fee85352432a40b1f868a81880e99ec7440113d5cfcab"
url: "https://pub.dev"
source: hosted
version: "2.0.17"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: "254708f17f7c20a9c8c471f67d86d76d4a3f9c1591aad1e15292008aceb82771"
url: "https://pub.dev"
source: hosted
version: "3.0.6"
vector_math:
dependency: transitive
description:

View File

@@ -17,6 +17,7 @@ dependencies:
file_selector: ^0.9.3
window_manager: ^0.3.4
path_provider: ^2.0.15
url_launcher: ^6.1.11
dev_dependencies:
flutter_test:
sdk: flutter

View File

@@ -8,6 +8,7 @@
#include <file_selector_windows/file_selector_windows.h>
#include <screen_retriever/screen_retriever_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
#include <window_manager/window_manager_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
@@ -15,6 +16,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("FileSelectorWindows"));
ScreenRetrieverPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ScreenRetrieverPlugin"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
WindowManagerPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("WindowManagerPlugin"));
}

View File

@@ -5,6 +5,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
file_selector_windows
screen_retriever
url_launcher_windows
window_manager
)