380 lines
12 KiB
Dart
380 lines
12 KiB
Dart
|
|
import 'dart:async';
|
|||
|
|
import 'dart:convert';
|
|||
|
|
import 'dart:io';
|
|||
|
|
import 'dart:typed_data';
|
|||
|
|
|
|||
|
|
import 'package:convert/convert.dart';
|
|||
|
|
import 'package:crypto/crypto.dart';
|
|||
|
|
import 'package:encrypt/encrypt.dart';
|
|||
|
|
import 'package:pointycastle/asymmetric/api.dart';
|
|||
|
|
import 'package:star_lock/app_settings/app_settings.dart';
|
|||
|
|
import 'package:star_lock/flavors.dart';
|
|||
|
|
import 'package:star_lock/network/start_chart_api.dart';
|
|||
|
|
import 'package:star_lock/talk/startChart/command/message_command.dart';
|
|||
|
|
import 'package:star_lock/talk/startChart/constant/ip_constant.dart';
|
|||
|
|
import 'package:star_lock/talk/startChart/constant/listen_addr_type_constant.dart';
|
|||
|
|
import 'package:star_lock/talk/startChart/entity/relay_info_entity.dart';
|
|||
|
|
import 'package:star_lock/talk/startChart/entity/report_information_data.dart';
|
|||
|
|
import 'package:star_lock/talk/startChart/entity/star_chart_register_node_entity.dart';
|
|||
|
|
import 'package:star_lock/tools/deviceInfo_utils.dart';
|
|||
|
|
import 'package:star_lock/tools/storage.dart';
|
|||
|
|
import 'package:uuid/uuid.dart';
|
|||
|
|
|
|||
|
|
class StartChartManage {
|
|||
|
|
// 私有构造函数,防止外部直接new对象
|
|||
|
|
StartChartManage._internal();
|
|||
|
|
|
|||
|
|
// 单例对象
|
|||
|
|
static final StartChartManage _instance = StartChartManage._internal();
|
|||
|
|
|
|||
|
|
// 工厂构造函数,返回单例对象
|
|||
|
|
factory StartChartManage() {
|
|||
|
|
return _instance;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 产品昵称
|
|||
|
|
final String _productName = F.navTitle;
|
|||
|
|
|
|||
|
|
RawDatagramSocket? _udpSocket;
|
|||
|
|
late String remoteHost = ''; // 远程主机地址(服务器返回)
|
|||
|
|
late int remotePort = 0; // 远程主机端口(服务器返回)
|
|||
|
|
final int localPort = 62289; // 本地端口
|
|||
|
|
|
|||
|
|
int heartbeatIntervalTime = 1; // 心跳包间隔时间(s)
|
|||
|
|
Timer? _heartBeatTimer; // 心跳包定时器
|
|||
|
|
bool _heartBeatTimerRunning = false; // 心跳包定时任务发送状态
|
|||
|
|
|
|||
|
|
String ToPeerId = ''; // 对端ID
|
|||
|
|
String FromPeerId = ''; // 我的ID
|
|||
|
|
/// 客户端注册
|
|||
|
|
Future<void> clientRegister() async {
|
|||
|
|
// 从缓存中获取星图注册节点信息
|
|||
|
|
final StarChartRegisterNodeEntity? starChartRegisterNodeInfo =
|
|||
|
|
await Storage.getStarChartRegisterNodeInfo();
|
|||
|
|
if (starChartRegisterNodeInfo == null) {
|
|||
|
|
_log(text: '开始注册客户端');
|
|||
|
|
final StarChartRegisterNodeEntity requestStarChartRegisterNode =
|
|||
|
|
await _requestStarChartRegisterNode();
|
|||
|
|
_saveStarChartRegisterNodeToStorage(requestStarChartRegisterNode);
|
|||
|
|
} else {
|
|||
|
|
final entity = await Storage.getStarChartRegisterNodeInfo();
|
|||
|
|
_log(text: '获取到星图注册节点信息:$entity');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 中继查询
|
|||
|
|
Future<void> relayQuery() async {
|
|||
|
|
final RelayInfoEntity relayInfoEntity =
|
|||
|
|
await StartChartApi.to.relayQueryInfo();
|
|||
|
|
_saveRelayInfoEntityToStorage(relayInfoEntity);
|
|||
|
|
|
|||
|
|
if (relayInfoEntity.relay_list?.length != 0) {
|
|||
|
|
final data = relayInfoEntity.relay_list?[0];
|
|||
|
|
FromPeerId = data?.peerID ?? '';
|
|||
|
|
final parseUdpUrl = _parseUdpUrl(data?.listenAddr ?? '');
|
|||
|
|
remoteHost = parseUdpUrl['host'] ?? '';
|
|||
|
|
remotePort = parseUdpUrl['port'] ?? '';
|
|||
|
|
_log(text: '中继信息:${data}');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void closeUdpSocket() {
|
|||
|
|
if (_udpSocket != null) {
|
|||
|
|
_udpSocket?.close();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 在中继服务器中上线
|
|||
|
|
Future<void> onlineRelayService() async {
|
|||
|
|
await relayQuery();
|
|||
|
|
var addressIListenFrom = InternetAddress.anyIPv4;
|
|||
|
|
RawDatagramSocket.bind(addressIListenFrom, localPort)
|
|||
|
|
.then((RawDatagramSocket socket) {
|
|||
|
|
_udpSocket = socket;
|
|||
|
|
|
|||
|
|
/// 广播功能
|
|||
|
|
_udpSocket!.broadcastEnabled = true;
|
|||
|
|
|
|||
|
|
/// 设置数据接收回调
|
|||
|
|
_onReceiveData(_udpSocket!);
|
|||
|
|
|
|||
|
|
// 发送上线消息
|
|||
|
|
//_sendOnlineMessage();
|
|||
|
|
// 发送回声测试消息
|
|||
|
|
//_sendEchoMessage();
|
|||
|
|
|
|||
|
|
// 上报信息
|
|||
|
|
reportInformation();
|
|||
|
|
}).catchError((error) {
|
|||
|
|
_log(text: 'Failed to bind UDP socket: $error');
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 接收返回的数据
|
|||
|
|
void _onReceiveData(RawDatagramSocket socket) {
|
|||
|
|
socket.listen((RawSocketEvent event) {
|
|||
|
|
if (event == RawSocketEvent.read) {
|
|||
|
|
Datagram? dg = socket.receive();
|
|||
|
|
try {
|
|||
|
|
_log(text: '收到消息---> 长度:${dg?.data?.length}, 数据:${dg?.data}');
|
|||
|
|
} catch (e) {
|
|||
|
|
_log(text: '❌ Udp ----> $e');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 上报信息至发现服务
|
|||
|
|
Future<void> reportInformation() async {
|
|||
|
|
_log(text: '上报信息至发现服务');
|
|||
|
|
// 构建参数
|
|||
|
|
ReportInformationData data = await _makeReportInformationData();
|
|||
|
|
await StartChartApi.to.reportInformation(
|
|||
|
|
reportInformationData: data,
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 发送上线消息
|
|||
|
|
void _sendOnlineMessage() {
|
|||
|
|
// 组装上线消息
|
|||
|
|
final message = MessageCommand.goOnlineRelay();
|
|||
|
|
_sendMessage(message: message);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 发送回声测试消息
|
|||
|
|
void _sendEchoMessage() {
|
|||
|
|
final message = MessageCommand.echoMessage(
|
|||
|
|
ToPeerId: ToPeerId,
|
|||
|
|
FromPeerId: FromPeerId,
|
|||
|
|
);
|
|||
|
|
_sendMessage(message: message);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 发送心跳包消息
|
|||
|
|
void sendHeartbeatMessage() {
|
|||
|
|
if (_heartBeatTimerRunning) {
|
|||
|
|
_log(text: '心跳已经开始了. 请勿重复发送心跳包消息');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
_heartBeatTimer ??= Timer.periodic(
|
|||
|
|
Duration(
|
|||
|
|
seconds: heartbeatIntervalTime,
|
|||
|
|
),
|
|||
|
|
(Timer timer) {
|
|||
|
|
final List<int> message = MessageCommand.heartbeatMessage();
|
|||
|
|
_sendMessage(message: message);
|
|||
|
|
},
|
|||
|
|
);
|
|||
|
|
_heartBeatTimerRunning = true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 停止定时发送心跳包
|
|||
|
|
void stopHeartbeat() {
|
|||
|
|
_heartBeatTimer?.cancel();
|
|||
|
|
_heartBeatTimer = null; // 清除定时器引用
|
|||
|
|
_heartBeatTimerRunning = false;
|
|||
|
|
_log(text: '发送心跳包结束');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 发送消息
|
|||
|
|
void _sendMessage({required List<int> message}) {
|
|||
|
|
_log(text: '发送给中继的消息体:${message},序列化之后的数据:【${bytesToHex(message)}】');
|
|||
|
|
_udpSocket!.send(message, InternetAddress(remoteHost), remotePort);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 请求注册节点
|
|||
|
|
Future<StarChartRegisterNodeEntity> _requestStarChartRegisterNode() async {
|
|||
|
|
// 获取设备信息
|
|||
|
|
final Map<String, String> deviceInfo = await _getDeviceInfo();
|
|||
|
|
// 发送注册节点请求
|
|||
|
|
final StarChartRegisterNodeEntity response =
|
|||
|
|
await StartChartApi.to.starChartRegisterNode(
|
|||
|
|
product: _productName,
|
|||
|
|
model: '${deviceInfo['brand']}_${deviceInfo['model']}',
|
|||
|
|
name: '${deviceInfo['id']}',
|
|||
|
|
unique: deviceInfo['id'] ?? Uuid().v1(),
|
|||
|
|
);
|
|||
|
|
return response;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 保存星图注册节点信息至缓存
|
|||
|
|
Future<void> _saveStarChartRegisterNodeToStorage(
|
|||
|
|
StarChartRegisterNodeEntity starChartRegisterNodeEntity) async {
|
|||
|
|
if (starChartRegisterNodeEntity != null) {
|
|||
|
|
await Storage.saveStarChartRegisterNodeInfo(starChartRegisterNodeEntity);
|
|||
|
|
_log(text: '注册成功');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 保存星图中继服务器信息至缓存
|
|||
|
|
Future<void> _saveRelayInfoEntityToStorage(
|
|||
|
|
RelayInfoEntity relayInfoEntity) async {
|
|||
|
|
if (relayInfoEntity != null) {
|
|||
|
|
await Storage.saveRelayInfo(relayInfoEntity);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 构造上报信息数据参数
|
|||
|
|
Future<ReportInformationData> _makeReportInformationData() async {
|
|||
|
|
// 获取当前时间戳
|
|||
|
|
int currentTimestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
|||
|
|
// 获取公钥
|
|||
|
|
final publicKey = await getPublicKey();
|
|||
|
|
// 获取私钥
|
|||
|
|
final privateKey = await getPrivateKey();
|
|||
|
|
// 生成签名
|
|||
|
|
final sign = await _generateSign(
|
|||
|
|
currentTimestamp: currentTimestamp,
|
|||
|
|
privateKey: privateKey,
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// 获取本机所有ip地址和中继返回的外网地址
|
|||
|
|
final List<ListenAddrData> listenAddrDataList =
|
|||
|
|
await _makeListenAddrDataList();
|
|||
|
|
|
|||
|
|
// 从缓存中获取中继信息
|
|||
|
|
final RelayInfoEntity? relayInfoEntity = await Storage.getRelayInfo();
|
|||
|
|
final RelayServiceData relayServiceData = RelayServiceData(
|
|||
|
|
name: relayInfoEntity?.relay_list?[0].name ?? '',
|
|||
|
|
listen_addr: relayInfoEntity?.relay_list?[0].listenAddr ?? '',
|
|||
|
|
peers_max: relayInfoEntity?.relay_list?[0].peerMax ?? 0,
|
|||
|
|
peers_current: relayInfoEntity?.relay_list?[0].peerCurrent ?? 0,
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
ReportInformationData data = ReportInformationData(
|
|||
|
|
id: FromPeerId,
|
|||
|
|
public_key: publicKey,
|
|||
|
|
listen_addr: listenAddrDataList,
|
|||
|
|
relay_service: relayServiceData,
|
|||
|
|
time: currentTimestamp,
|
|||
|
|
sign: sign,
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
return data;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取本机所有ip地址和中继返回的外网地址
|
|||
|
|
Future<List<ListenAddrData>> _makeListenAddrDataList() async {
|
|||
|
|
final List<ListenAddrData> listenAddrDataList = [];
|
|||
|
|
final List<String> localIp = await _getAllIpAddresses();
|
|||
|
|
// 从缓存中获取中继信息
|
|||
|
|
final RelayInfoEntity? relayInfoEntity = await Storage.getRelayInfo();
|
|||
|
|
if (relayInfoEntity != null && relayInfoEntity.client_addr != null) {
|
|||
|
|
listenAddrDataList.add(
|
|||
|
|
ListenAddrData(
|
|||
|
|
type: ListenAddrTypeConstant.relay,
|
|||
|
|
address: IpConstant.udpUrl +
|
|||
|
|
relayInfoEntity.client_addr! +
|
|||
|
|
':' +
|
|||
|
|
localPort.toString(),
|
|||
|
|
),
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
localIp.forEach((element) {
|
|||
|
|
listenAddrDataList.add(
|
|||
|
|
ListenAddrData(
|
|||
|
|
type: ListenAddrTypeConstant.local,
|
|||
|
|
address: IpConstant.udpUrl + element + ':' + localPort.toString(),
|
|||
|
|
),
|
|||
|
|
);
|
|||
|
|
});
|
|||
|
|
return listenAddrDataList ?? [];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 获取本机所有ip
|
|||
|
|
Future<List<String>> _getAllIpAddresses() async {
|
|||
|
|
final List<String> ipAddresses = [];
|
|||
|
|
try {
|
|||
|
|
final List<NetworkInterface> interfaces = await NetworkInterface.list(
|
|||
|
|
includeLoopback: true,
|
|||
|
|
type: InternetAddressType.any,
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
for (final interface in interfaces) {
|
|||
|
|
for (final address in interface.addresses) {
|
|||
|
|
if (address.address.isNotEmpty &&
|
|||
|
|
!IpConstant.reportExcludeIp.contains(address.address)) {
|
|||
|
|
ipAddresses.add(address.address);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} catch (e) {
|
|||
|
|
_log(text: '❌--->获取本机IP时出现错误: $e');
|
|||
|
|
}
|
|||
|
|
return ipAddresses ?? [];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void _log({required String text}) {
|
|||
|
|
AppLog.log('$_productName=====${text}');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 获取设备信息
|
|||
|
|
Future<Map<String, String>> _getDeviceInfo() async {
|
|||
|
|
final Map<String, String> deviceInfo =
|
|||
|
|
await DeviceInfoUtils.getDeviceInfo();
|
|||
|
|
return deviceInfo;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 解析 UDP URL 并提取 IP 地址和端口号
|
|||
|
|
Map<String, dynamic> _parseUdpUrl(String url) {
|
|||
|
|
// 使用正则表达式匹配 IP 地址和端口号
|
|||
|
|
final regex = RegExp(r'udp://(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):(\d+)')
|
|||
|
|
.firstMatch(url);
|
|||
|
|
|
|||
|
|
if (regex != null) {
|
|||
|
|
final ip = regex.group(1);
|
|||
|
|
final portStr = regex.group(2);
|
|||
|
|
final port = int.tryParse(portStr ?? '');
|
|||
|
|
|
|||
|
|
if (ip != null && port != null) {
|
|||
|
|
return {'host': ip, 'port': port};
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
throw FormatException('无法解析 URL 格式: $url');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
String bytesToHex(List<int> bytes) {
|
|||
|
|
return bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join('');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 生成签名sing
|
|||
|
|
Future<String> _generateSign({
|
|||
|
|
required int currentTimestamp,
|
|||
|
|
required String privateKey,
|
|||
|
|
}) async {
|
|||
|
|
String resultSign = '';
|
|||
|
|
try {
|
|||
|
|
// 2. 将时间戳编码为小端字节序(Little Endian)
|
|||
|
|
Uint8List signData = Uint8List(4);
|
|||
|
|
signData.buffer
|
|||
|
|
.asByteData()
|
|||
|
|
.setUint32(0, currentTimestamp, Endian.little);
|
|||
|
|
|
|||
|
|
// 3. 使用 SHA-256 对 signData 进行哈希运算
|
|||
|
|
final sha256Hash = sha256.convert(signData);
|
|||
|
|
var parser = RSAKeyParser();
|
|||
|
|
final RSAPrivateKey parsePrivateKey =
|
|||
|
|
parser.parse('-----BEGIN RSA PRIVATE KEY-----\n' + privateKey)
|
|||
|
|
as RSAPrivateKey;
|
|||
|
|
} catch (e) {
|
|||
|
|
_log(text: '❌--->生成签名时出现错误: $e');
|
|||
|
|
}
|
|||
|
|
return resultSign ?? '';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Future<String> getPublicKey() async {
|
|||
|
|
// 从缓存中获取星图注册节点信息
|
|||
|
|
final StarChartRegisterNodeEntity? starChartRegisterNodeInfo =
|
|||
|
|
await Storage.getStarChartRegisterNodeInfo();
|
|||
|
|
return starChartRegisterNodeInfo?.peer?.publicKey ?? '';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Future<String> getPrivateKey() async {
|
|||
|
|
// 从缓存中获取星图注册节点信息
|
|||
|
|
final StarChartRegisterNodeEntity? starChartRegisterNodeInfo =
|
|||
|
|
await Storage.getStarChartRegisterNodeInfo();
|
|||
|
|
return starChartRegisterNodeInfo?.peer?.privateKey ?? '';
|
|||
|
|
}
|
|||
|
|
}
|