mirror of
https://github.com/wanghongenpin/proxypin.git
synced 2026-06-01 17:15:48 +08:00
391 lines
18 KiB
Dart
391 lines
18 KiB
Dart
import 'dart:io';
|
||
|
||
import 'package:file_picker/file_picker.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:proxypin/l10n/app_localizations.dart';
|
||
import 'package:flutter_toastr/flutter_toastr.dart';
|
||
import 'package:proxypin/network/bin/server.dart';
|
||
import 'package:proxypin/network/util/crts.dart';
|
||
import 'package:proxypin/network/util/logger.dart';
|
||
import 'package:proxypin/ui/component/utils.dart';
|
||
import 'package:proxypin/ui/desktop/ssl/pc_cert.dart';
|
||
import 'package:proxypin/utils/ip.dart';
|
||
import 'package:url_launcher/url_launcher.dart';
|
||
|
||
class SslWidget extends StatefulWidget {
|
||
final ProxyServer proxyServer;
|
||
|
||
const SslWidget({super.key, required this.proxyServer});
|
||
|
||
@override
|
||
State<SslWidget> createState() => _SslState();
|
||
}
|
||
|
||
class _SslState extends State<SslWidget> {
|
||
AppLocalizations get localizations => AppLocalizations.of(context)!;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return MenuAnchor(
|
||
builder: (context, controller, child) {
|
||
return IconButton(
|
||
icon: widget.proxyServer.enableSsl
|
||
? Icon(Icons.lock_open, size: 21)
|
||
: Icon(Icons.https_outlined, color: Colors.red, size: 21),
|
||
tooltip: localizations.httpsProxy,
|
||
onPressed: () {
|
||
if (controller.isOpen) {
|
||
controller.close();
|
||
} else {
|
||
controller.open();
|
||
}
|
||
});
|
||
},
|
||
menuChildren: [
|
||
_Switch(proxyServer: widget.proxyServer, onEnableChange: (val) => setState(() {})),
|
||
item(localizations.installCaLocal,
|
||
onPressed: () => showDialog(context: context, builder: (context) => PCCert())),
|
||
item("${localizations.installRootCa} iOS", onPressed: () async => iosCer(await localIp())),
|
||
item("${localizations.installRootCa} Android", onPressed: () async => androidCer(await localIp())),
|
||
const Divider(thickness: 0.3, height: 3),
|
||
exportMenu(),
|
||
const Divider(thickness: 0.3, height: 3),
|
||
importMenu(),
|
||
const Divider(thickness: 0.3, height: 3),
|
||
item(localizations.generateCA, onPressed: () async {
|
||
showConfirmDialog(context, title: localizations.generateCA, content: localizations.generateCADescribe,
|
||
onConfirm: () async {
|
||
await CertificateManager.generateNewRootCA();
|
||
if (context.mounted) FlutterToastr.show(localizations.success, context);
|
||
});
|
||
}),
|
||
const Divider(thickness: 0.3, height: 3),
|
||
item(localizations.resetDefaultCA, onPressed: () async {
|
||
showConfirmDialog(context,
|
||
title: localizations.resetDefaultCA,
|
||
content: localizations.resetDefaultCADescribe, onConfirm: () async {
|
||
await CertificateManager.resetDefaultRootCA();
|
||
if (context.mounted) FlutterToastr.show(localizations.success, context);
|
||
});
|
||
}),
|
||
]);
|
||
}
|
||
|
||
//import method
|
||
Widget importMenu() {
|
||
return item(localizations.importCaP12, onPressed: () async {
|
||
FilePickerResult? result =
|
||
await FilePicker.platform.pickFiles(type: FileType.custom, allowedExtensions: ['p12', 'pfx']);
|
||
if (result == null || !mounted) return;
|
||
|
||
//entry password
|
||
showDialog(
|
||
context: context,
|
||
builder: (BuildContext context) {
|
||
String? password;
|
||
return SimpleDialog(
|
||
title: Text(localizations.importCaP12, style: const TextStyle(fontSize: 16)),
|
||
children: [
|
||
Padding(
|
||
padding: const EdgeInsets.all(10),
|
||
child: TextField(
|
||
decoration: const InputDecoration(
|
||
hintText: "Enter the password of the p12 file",
|
||
border: OutlineInputBorder(),
|
||
),
|
||
onChanged: (val) => password = val,
|
||
),
|
||
),
|
||
Row(mainAxisAlignment: MainAxisAlignment.end, children: [
|
||
TextButton(onPressed: () => Navigator.pop(context), child: Text(localizations.cancel)),
|
||
TextButton(
|
||
onPressed: () async {
|
||
var file = File(result.files.single.path!);
|
||
var bytes = await file.readAsBytes();
|
||
try {
|
||
await CertificateManager.importPkcs12(bytes, password);
|
||
if (context.mounted) {
|
||
FlutterToastr.show(localizations.success, context);
|
||
Navigator.pop(context);
|
||
}
|
||
} catch (e, stackTrace) {
|
||
logger.e('import p12 error [$password]', error: e, stackTrace: stackTrace);
|
||
if (context.mounted) FlutterToastr.show(localizations.importFailed, context);
|
||
return;
|
||
}
|
||
},
|
||
child: Text(localizations.import),
|
||
)
|
||
])
|
||
]);
|
||
});
|
||
});
|
||
}
|
||
|
||
Widget exportMenu() {
|
||
return SubmenuButton(
|
||
menuChildren: [
|
||
MenuItemButton(
|
||
child: Padding(
|
||
padding: const EdgeInsets.only(left: 10, right: 10),
|
||
child: Text(localizations.exportCA, style: const TextStyle(fontSize: 14))),
|
||
onPressed: () async {
|
||
String? path = (await FilePicker.platform.saveFile(fileName: "ProxyPinCA.crt"));
|
||
if (path == null) return;
|
||
|
||
var caFile = await CertificateManager.certificateFile();
|
||
await caFile.copy(path);
|
||
}),
|
||
const Divider(thickness: 0.3, height: 8),
|
||
MenuItemButton(
|
||
child: Padding(
|
||
padding: const EdgeInsets.only(left: 10, right: 10),
|
||
child: Text(localizations.exportCaP12, style: const TextStyle(fontSize: 14))),
|
||
onPressed: () async {
|
||
//show p12 password
|
||
String? password;
|
||
showDialog(
|
||
context: context,
|
||
builder: (BuildContext context) {
|
||
return SimpleDialog(
|
||
title: Text(localizations.exportCaP12, style: const TextStyle(fontSize: 16)),
|
||
children: [
|
||
Padding(
|
||
padding: const EdgeInsets.all(10),
|
||
child: TextField(
|
||
decoration: const InputDecoration(
|
||
hintStyle: TextStyle(color: Colors.grey),
|
||
hintText: "Enter a password to protect p12 file",
|
||
border: OutlineInputBorder(),
|
||
),
|
||
onChanged: (val) => password = val,
|
||
),
|
||
),
|
||
Row(mainAxisAlignment: MainAxisAlignment.end, children: [
|
||
TextButton(onPressed: () => Navigator.pop(context), child: Text(localizations.cancel)),
|
||
TextButton(
|
||
onPressed: () async {
|
||
String? path = (await FilePicker.platform.saveFile(fileName: "ProxyPinPkcs12.p12"));
|
||
if (path == null) return;
|
||
|
||
var p12Bytes = await CertificateManager.generatePkcs12(
|
||
password?.isNotEmpty == true ? password : null);
|
||
await File(path).writeAsBytes(p12Bytes);
|
||
if (context.mounted) Navigator.pop(context);
|
||
},
|
||
child: Text(localizations.export),
|
||
)
|
||
])
|
||
]);
|
||
});
|
||
}),
|
||
MenuItemButton(
|
||
child: Padding(
|
||
padding: const EdgeInsets.only(left: 10, right: 10),
|
||
child: Text(localizations.exportPrivateKey, style: const TextStyle(fontSize: 14))),
|
||
onPressed: () async {
|
||
String? path = (await FilePicker.platform.saveFile(fileName: "ProxyPinKey.pem"));
|
||
if (path == null) return;
|
||
|
||
var keyFile = await CertificateManager.privateKeyFile();
|
||
await keyFile.copy(path);
|
||
}),
|
||
],
|
||
child: Padding(
|
||
padding: const EdgeInsets.only(left: 10),
|
||
child: Text(localizations.export, style: const TextStyle(fontSize: 14))));
|
||
}
|
||
|
||
Widget item(String text, {VoidCallback? onPressed}) {
|
||
return MenuItemButton(
|
||
onPressed: onPressed,
|
||
child: Padding(
|
||
padding: const EdgeInsets.only(left: 10, right: 5),
|
||
child: Text(text, style: const TextStyle(fontSize: 14))));
|
||
}
|
||
|
||
void iosCer(String host) {
|
||
showDialog(
|
||
context: context,
|
||
barrierDismissible: false,
|
||
builder: (BuildContext context) {
|
||
return SimpleDialog(
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10.0)),
|
||
contentPadding: const EdgeInsets.symmetric(vertical: 5, horizontal: 15),
|
||
title: Row(children: [
|
||
const Expanded(child: SizedBox()),
|
||
Text("iOS ${localizations.caInstallGuide}",
|
||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
|
||
const Expanded(child: SizedBox()),
|
||
Align(alignment: Alignment.topRight, child: CloseButton())
|
||
]),
|
||
alignment: Alignment.center,
|
||
children: [
|
||
SelectableText.rich(
|
||
TextSpan(text: "1. ${localizations.configWifiProxy} Host:$host Port:${widget.proxyServer.port}")),
|
||
const SizedBox(height: 10),
|
||
Row(
|
||
children: [
|
||
Text("2. ${localizations.caIosBrowser}\t"),
|
||
const SelectableText.rich(
|
||
TextSpan(text: "http://proxy.pin/ssl", style: TextStyle(decoration: TextDecoration.underline)))
|
||
],
|
||
),
|
||
const SizedBox(height: 10),
|
||
Text("3. ${localizations.installRootCa} -> ${localizations.trustCa}"),
|
||
const SizedBox(height: 10),
|
||
Row(children: [
|
||
Column(children: [
|
||
Text("3.1 ${localizations.installCaDescribe}", style: const TextStyle(fontSize: 12)),
|
||
const SizedBox(height: 10),
|
||
Image.network("https://foruda.gitee.com/images/1689346516243774963/c56bc546_1073801.png",
|
||
height: 270, width: 300)
|
||
]),
|
||
const SizedBox(width: 10),
|
||
Column(children: [
|
||
Text("3.2 ${localizations.trustCaDescribe}", style: const TextStyle(fontSize: 12)),
|
||
const SizedBox(height: 10),
|
||
Image.network("https://foruda.gitee.com/images/1689346614916658100/fd9b9e41_1073801.png",
|
||
height: 270, width: 300)
|
||
])
|
||
])
|
||
]);
|
||
});
|
||
}
|
||
|
||
void androidCer(String host) {
|
||
bool isCN = localizations.localeName == 'zh';
|
||
|
||
showDialog(
|
||
context: context,
|
||
barrierDismissible: false,
|
||
builder: (BuildContext context) {
|
||
return AlertDialog(
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10.0)),
|
||
contentPadding: const EdgeInsets.all(5),
|
||
title: Row(children: [
|
||
const Expanded(child: SizedBox()),
|
||
Text("Android ${localizations.caInstallGuide}",
|
||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
|
||
const Expanded(child: SizedBox()),
|
||
Align(alignment: Alignment.topRight, child: CloseButton())
|
||
]),
|
||
content: SizedBox(
|
||
width: 600,
|
||
child: DefaultTabController(
|
||
length: 2,
|
||
child: Scaffold(
|
||
appBar: TabBar(tabs: <Widget>[
|
||
Tab(text: localizations.androidRoot),
|
||
Tab(text: localizations.androidUserCA),
|
||
]),
|
||
body: Padding(
|
||
padding: const EdgeInsets.all(10),
|
||
child: TabBarView(children: [
|
||
ListView(children: [
|
||
Text(localizations.androidRootMagisk),
|
||
TextButton(
|
||
child: Text(
|
||
"https://${isCN ? 'gitee' : 'github'}.com/wanghongenpin/Magisk-ProxyPinCA/releases"),
|
||
onPressed: () {
|
||
launchUrl(Uri.parse(
|
||
"https://${isCN ? 'gitee' : 'github'}.com/wanghongenpin/Magisk-ProxyPinCA/releases"));
|
||
}),
|
||
const SizedBox(height: 10),
|
||
futureWidget(CertificateManager.systemCertificateName(),
|
||
(name) => SelectableText(localizations.androidRootRename(name))),
|
||
const SizedBox(height: 10),
|
||
ClipRRect(
|
||
child: Align(
|
||
alignment: Alignment.topCenter,
|
||
child: Image.network(
|
||
scale: 0.5,
|
||
"https://foruda.gitee.com/images/1710181660282752846/cb520c0b_1073801.png",
|
||
height: 460,
|
||
)))
|
||
]),
|
||
ListView(
|
||
children: [
|
||
Text(localizations.androidUserCATips,
|
||
style: const TextStyle(fontWeight: FontWeight.w500)),
|
||
const SizedBox(height: 10),
|
||
SelectableText.rich(TextSpan(
|
||
text:
|
||
"1. ${localizations.configWifiProxy} Host:$host Port:${widget.proxyServer.port}")),
|
||
const SizedBox(height: 10),
|
||
Row(
|
||
children: [
|
||
Text("2. ${localizations.caAndroidBrowser}\t"),
|
||
const SelectableText.rich(TextSpan(
|
||
text: "http://proxy.pin/ssl",
|
||
style: TextStyle(decoration: TextDecoration.underline)))
|
||
],
|
||
),
|
||
const SizedBox(height: 10),
|
||
Text("3. ${localizations.androidUserCAInstall}"),
|
||
const SizedBox(height: 10),
|
||
TextButton(
|
||
onPressed: () {
|
||
launchUrl(Uri.parse(isCN
|
||
? "https://gitee.com/wanghongenpin/proxypin/wikis/%E5%AE%89%E5%8D%93%E6%97%A0ROOT%E4%BD%BF%E7%94%A8Xposed%E6%A8%A1%E5%9D%97%E6%8A%93%E5%8C%85"
|
||
: "https://github.com/wanghongenpin/proxypin/wiki/Android-without-ROOT-uses-Xposed-module-to-capture-packets"));
|
||
},
|
||
child: Text(" ${localizations.androidUserXposed}")),
|
||
const SizedBox(height: 10),
|
||
ClipRRect(
|
||
child: Align(
|
||
alignment: Alignment.topCenter,
|
||
heightFactor: .7,
|
||
child: Image.network(
|
||
"https://foruda.gitee.com/images/1689352695624941051/74e3bed6_1073801.png",
|
||
height: 530,
|
||
)))
|
||
],
|
||
),
|
||
])),
|
||
))));
|
||
});
|
||
}
|
||
}
|
||
|
||
class _Switch extends StatefulWidget {
|
||
final ProxyServer proxyServer;
|
||
final Function(bool val) onEnableChange;
|
||
|
||
const _Switch({required this.proxyServer, required this.onEnableChange});
|
||
|
||
@override
|
||
State<_Switch> createState() => _SwitchState();
|
||
}
|
||
|
||
class _SwitchState extends State<_Switch> {
|
||
bool changed = false;
|
||
|
||
AppLocalizations get localizations => AppLocalizations.of(context)!;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return MenuItemButton(
|
||
onPressed: () {},
|
||
child: Row(children: [
|
||
Padding(
|
||
padding: const EdgeInsets.only(left: 10, right: 5),
|
||
child: Text(localizations.enabledHttps, style: const TextStyle(fontSize: 14))),
|
||
Transform.scale(
|
||
scale: 0.8,
|
||
child: Switch(
|
||
hoverColor: Colors.transparent,
|
||
value: widget.proxyServer.enableSsl,
|
||
onChanged: (val) {
|
||
widget.proxyServer.enableSsl = val;
|
||
changed = true;
|
||
widget.onEnableChange(val);
|
||
CertificateManager.cleanCache();
|
||
widget.proxyServer.configuration.flushConfig();
|
||
setState(() {});
|
||
}))
|
||
]));
|
||
}
|
||
|
||
}
|