From 213f40dd3568526feb81d57a2ce69718ddff8a51 Mon Sep 17 00:00:00 2001 From: liyi Date: Thu, 28 Nov 2024 14:57:49 +0800 Subject: [PATCH 1/6] =?UTF-8?q?feat:=E5=A2=9E=E5=8A=A0=E6=96=B0=E7=9A=84?= =?UTF-8?q?=E4=B8=AD=E7=BB=A7=E5=8D=8F=E8=AE=AE=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lan/lan_zh.json | 8 +- lib/login/login/starLock_login_page.dart | 43 +- lib/main.dart | 2 + .../lockMian/lockMain/lockMain_logic.dart | 2 + lib/network/api.dart | 7 + lib/network/api_provider_base.dart | 57 ++- lib/network/start_chart_api.dart | 58 +++ .../startChart/command/message_command.dart | 69 ++++ lib/talk/startChart/constant/ip_constant.dart | 7 + .../constant/listen_addr_type_constant.dart | 5 + .../constant/payload_type_constant.dart | 10 + .../constant/protocol_flag_constant.dart | 3 + .../startChart/entity/relay_info_entity.dart | 90 +++++ .../entity/report_information_data.dart | 107 +++++ lib/talk/startChart/entity/scp_message.dart | 143 +++++++ .../star_chart_register_node_entity.dart | 59 +++ lib/talk/startChart/start_chart_manage.dart | 379 ++++++++++++++++++ lib/talk/udp/udp_help.dart | 2 +- lib/talk/udp/udp_manage.dart | 7 +- lib/talk/udp/udp_reciverData.dart | 2 +- lib/tools/deviceInfo_utils.dart | 39 ++ lib/tools/storage.dart | 38 ++ pubspec.yaml | 9 +- 23 files changed, 1118 insertions(+), 28 deletions(-) create mode 100644 lib/network/start_chart_api.dart create mode 100644 lib/talk/startChart/command/message_command.dart create mode 100644 lib/talk/startChart/constant/ip_constant.dart create mode 100644 lib/talk/startChart/constant/listen_addr_type_constant.dart create mode 100644 lib/talk/startChart/constant/payload_type_constant.dart create mode 100644 lib/talk/startChart/constant/protocol_flag_constant.dart create mode 100644 lib/talk/startChart/entity/relay_info_entity.dart create mode 100644 lib/talk/startChart/entity/report_information_data.dart create mode 100644 lib/talk/startChart/entity/scp_message.dart create mode 100644 lib/talk/startChart/entity/star_chart_register_node_entity.dart create mode 100644 lib/talk/startChart/start_chart_manage.dart create mode 100644 lib/tools/deviceInfo_utils.dart diff --git a/lan/lan_zh.json b/lan/lan_zh.json index 9155e4c1..12437e7e 100755 --- a/lan/lan_zh.json +++ b/lan/lan_zh.json @@ -1090,20 +1090,18 @@ "失效时间要大于当前时间": "失效时间要大于当前时间", "修改名字": "修改名字", "时": "时", - "分": "分" + "分": "分", "Amazon Alexa": "Amazon Alexa", "您可以使用Alexa进行开锁、闭锁和查看锁状态": "您可以使用Alexa进行开锁、闭锁和查看锁状态", "支持的国家": "支持的国家", "支持的国家值": "美国、加拿大、英国、澳大利亚、印度、德国、法国、意大利、西班牙、日本", "操作流程": "操作流程", - "密码需至少包含数字/字母/字符中的2种组合": "密码需至少包含数字/字母/字符中的2种组合", - - "操作流程值":"1 用智能锁APP添加锁和网关\n\n2 在APP里开启锁的远程开锁功能(这个功能默认是关闭的)。如果没有这个选项,则锁不支持Alexa \n\n3 在Alexa中添加Skill,并用智能锁APP的账号和密码进行授权。授权成功后就可以发现账号下的设备\n\n4 在Alexa app里找到锁,开启语音开锁的功能,并设置语言密码\n\n5 可以通过Alexa操作锁了", + "操作流程值": "1 用智能锁APP添加锁和网关\n\n2 在APP里开启锁的远程开锁功能(这个功能默认是关闭的)。如果没有这个选项,则锁不支持Alexa \n\n3 在Alexa中添加Skill,并用智能锁APP的账号和密码进行授权。授权成功后就可以发现账号下的设备\n\n4 在Alexa app里找到锁,开启语音开锁的功能,并设置语言密码\n\n5 可以通过Alexa操作锁了", "Google Home": "Google Home", "Action name": "Action name", "ScienerSmart": "ScienerSmart", "支持的语言": "支持的语言", "英语": "英语", - "Google Home操作流程的值": "1.用智能锁APP添加锁和网关\n\n2.在APP里开启锁的远程开锁功能(这个功能默认是关闭的)。如果没有这个选项,则锁不支持Google Home \n\n3.安装Google Home APP,点击左上角的“+”按钮\n\n4.在设置页面,选择“与Google协同工作”\n\n5.搜索“ScienerSmart”,并用智能锁APP的账号和密码进行授权\n\n", + "Google Home操作流程的值": "1.用智能锁APP添加锁和网关\n\n2.在APP里开启锁的远程开锁功能(这个功能默认是关闭的)。如果没有这个选项,则锁不支持Google Home \n\n3.安装Google Home APP,点击左上角的“+”按钮\n\n4.在设置页面,选择“与Google协同工作”\n\n5.搜索“ScienerSmart”,并用智能锁APP的账号和密码进行授权\n\n" } diff --git a/lib/login/login/starLock_login_page.dart b/lib/login/login/starLock_login_page.dart index 42f00b78..ec43b33b 100755 --- a/lib/login/login/starLock_login_page.dart +++ b/lib/login/login/starLock_login_page.dart @@ -1,10 +1,10 @@ - import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import 'package:star_lock/flavors.dart'; import 'package:star_lock/login/login/starLock_login_state.dart'; +import 'package:star_lock/talk/startChart/start_chart_manage.dart'; import 'package:star_lock/tools/appFirstEnterHandle.dart'; import 'package:star_lock/tools/wechat/customer_tool.dart'; import 'package:star_lock/tools/storage.dart'; @@ -48,8 +48,8 @@ class _StarLockLoginPageState extends State { haveBack: false, backgroundColor: AppColors.mainColor, actionsList: [ - IconButton( - onPressed: (){ + IconButton( + onPressed: () { WechatManageTool.getAppInfo(CustomerTool.openCustomerService); }, icon: const Icon( @@ -84,8 +84,7 @@ class _StarLockLoginPageState extends State { width: 110.w, height: 110.w))), SizedBox(height: 50.w), Obx(() => CommonItem( - leftTitel: - '你所在的国家/地区'.tr, + leftTitel: '你所在的国家/地区'.tr, rightTitle: '', isHaveLine: true, isPadding: false, @@ -123,7 +122,7 @@ class _StarLockLoginPageState extends State { height: 36.w, ), ), - hintText:'请输入手机号或者邮箱'.tr, + hintText: '请输入手机号或者邮箱'.tr, // keyboardType: TextInputType.number, inputFormatters: [ // FilteringTextInputFormatter.allow(RegExp('[0-9]')), @@ -185,8 +184,7 @@ class _StarLockLoginPageState extends State { WidgetSpan( alignment: PlaceholderAlignment.middle, child: GestureDetector( - child: Text( - '《${'用户协议'.tr}》', + child: Text('《${'用户协议'.tr}》', style: TextStyle( color: AppColors.mainColor, fontSize: 20.sp)), @@ -201,8 +199,7 @@ class _StarLockLoginPageState extends State { WidgetSpan( alignment: PlaceholderAlignment.middle, child: GestureDetector( - child: Text( - '《${'隐私政策'.tr}》', + child: Text('《${'隐私政策'.tr}》', style: TextStyle( color: AppColors.mainColor, fontSize: 20.sp)), @@ -236,6 +233,29 @@ class _StarLockLoginPageState extends State { } } : null)), + SubmitBtn( + btnName: '发送上线请求', + onClick: () { + // 注册星图节点信息 + StartChartManage().clientRegister(); + // 查询中继信息 + StartChartManage().relayQuery(); + // 发送上线请求 + StartChartManage().onlineRelayService(); + }, + ), + SubmitBtn( + btnName: '启动心跳包', + onClick: () { + StartChartManage().sendHeartbeatMessage(); + }, + ), + SubmitBtn( + btnName: '结束心跳包', + onClick: () { + StartChartManage().stopHeartbeat(); + }, + ), SizedBox(height: 50.w), Row( mainAxisAlignment: MainAxisAlignment.center, @@ -246,8 +266,7 @@ class _StarLockLoginPageState extends State { height: 50.h, // color: Colors.red, child: Center( - child: Text( - '${'忘记密码'.tr}?', + child: Text('${'忘记密码'.tr}?', style: TextStyle( fontSize: 22.sp, color: AppColors.mainColor)), ), diff --git a/lib/main.dart b/lib/main.dart index aa07e5a9..d4898717 100755 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,6 +11,7 @@ import 'package:star_lock/flavors.dart'; import 'package:star_lock/mine/about/debug/debug_tool.dart'; import 'package:star_lock/network/api_provider.dart'; import 'package:star_lock/network/api_repository.dart'; +import 'package:star_lock/network/start_chart_api.dart'; import 'package:star_lock/tools/bugly/bugly_tool.dart'; import 'package:star_lock/tools/device_info_service.dart'; import 'package:star_lock/tools/platform_info_services.dart'; @@ -62,6 +63,7 @@ Future _initTranslation() async => TranslationLoader.loadTranslation(); Future _setCommonServices() async { await Get.putAsync(() => StoreService().init()); Get.put(ApiProvider()); + Get.put(StartChartApi()); Get.put(ApiRepository(Get.find())); if (F.isLite) { //上架审核注释 获取设备信息 diff --git a/lib/main/lockMian/lockMain/lockMain_logic.dart b/lib/main/lockMian/lockMain/lockMain_logic.dart index e65b36c7..dc2de263 100755 --- a/lib/main/lockMian/lockMain/lockMain_logic.dart +++ b/lib/main/lockMian/lockMain/lockMain_logic.dart @@ -11,6 +11,7 @@ import 'package:star_lock/flavors.dart'; import 'package:star_lock/login/login/entity/LoginEntity.dart'; import 'package:star_lock/main/lockDetail/lockDetail/lockDetail_logic.dart'; import 'package:star_lock/main/lockMian/lockList/lockList_logic.dart'; +import 'package:star_lock/talk/startChart/start_chart_manage.dart'; import 'package:star_lock/tools/eventBusEventManage.dart'; import 'package:star_lock/tools/push/xs_jPhush.dart'; import 'package:star_lock/tools/showTipView.dart'; @@ -311,6 +312,7 @@ class LockMainLogic extends BaseGetXController { // AppLog.log('onReady() LockMainLogic'); UdpHelp().openUDP(); BlueManage(); + } @override diff --git a/lib/network/api.dart b/lib/network/api.dart index e670f0c4..686e9b5b 100755 --- a/lib/network/api.dart +++ b/lib/network/api.dart @@ -282,4 +282,11 @@ abstract class Api { final String updateZoneOffsetsAndLanguagesURL = '/cloudUser/updateSettings'; //更新云用户时区偏移与语言 + + final String starChartRegisterNodeURL = + '/SL-A-1.0/peer/register'; // 星图--注册节点 + final String relayQueryInfoURL = + '/SL-A-1.0/relay/query'; // 星图--中继查询 + final String reportInformationDataURL = + '/SL-A-1.0/peer/login'; // 星图--信息上报 } diff --git a/lib/network/api_provider_base.dart b/lib/network/api_provider_base.dart index e6563cdb..2d9b92d1 100755 --- a/lib/network/api_provider_base.dart +++ b/lib/network/api_provider_base.dart @@ -78,7 +78,62 @@ class BaseProvider extends GetConnect with Api { statusText: res.statusText, ); } else {} - if(isShowNetworkErrorMsg ?? true){ + if (isShowNetworkErrorMsg ?? true) { + getDataResult(res.body); + } + return res; + } + + @override + Future> get( + String url, { + String? contentType, + Map? headers, + Map? query, + Decoder? decoder, + bool? isUnShowLoading = false, // 是否显示loading + bool? isUserBaseUrl = true, // 文件上传不使用baseUrl + bool? isShowErrMsg = true, // 是否显示没有网络时的提示 + bool? isShowNetworkErrorMsg = true, // 是否显示网络其他报错 如403 500等 + }) async { + AppLog.log('get: url:$url'); + if (isUnShowLoading == false) { + EasyLoading.show(); + } + if (isUserBaseUrl == false) { + httpClient.baseUrl = ''; + } else { + httpClient.baseUrl = '${F.apiPrefix}/api'; + } + var res = await super.get(url, + contentType: contentType, + headers: headers, + query: query, + decoder: decoder); + if (EasyLoading.isShow && !isUnShowLoading!) { + await EasyLoading.dismiss(animation: true); + } + if (res.body == null) { + if (res.statusCode == null && isShowErrMsg!) { + EasyLoading.showToast('网络访问失败,请检查网络是否正常'.tr, + duration: 2000.milliseconds); + } + var rs = { + 'errorMsg': 'Network Error!', + 'errorCode': -1, + 'data': null, + 'description': '表示成功或是。' + }; + return Response( + request: res.request, + statusCode: -1, + bodyString: res.bodyString, + bodyBytes: res.bodyBytes, + body: rs as T, + statusText: res.statusText, + ); + } + if (isShowNetworkErrorMsg ?? true) { getDataResult(res.body); } return res; diff --git a/lib/network/start_chart_api.dart b/lib/network/start_chart_api.dart new file mode 100644 index 00000000..1a6901c8 --- /dev/null +++ b/lib/network/start_chart_api.dart @@ -0,0 +1,58 @@ +import 'dart:convert'; + +import 'package:get/get.dart'; +import 'package:star_lock/network/api_provider.dart'; +import 'package:star_lock/network/api_provider_base.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'; + +class StartChartApi extends BaseProvider { + // 星图url + final String _startChartHost = 'http://sls1-scd.star-lock.cn:8080'; + + static StartChartApi get to => Get.find(); + + // 星图--注册节点 + Future starChartRegisterNode({ + required String product, + required String model, + required String name, + required String unique, + }) async { + final response = await post( + _startChartHost + starChartRegisterNodeURL.toUrl, + jsonEncode({ + 'product': product, + 'model': model, + 'name': name, + 'unique': unique, + }), + isUnShowLoading: true, + isUserBaseUrl: false, + ); + return StarChartRegisterNodeEntity.fromJson(response.body); + } + + // 星图--中继查询 + Future relayQueryInfo() async { + final response = await get( + _startChartHost + relayQueryInfoURL.toUrl, + isUnShowLoading: true, + isUserBaseUrl: false, + ); + return RelayInfoEntity.fromJson(response.body); + } + + // 星图--上报信息至发现服务 + Future reportInformation({ + required ReportInformationData reportInformationData, + }) async { + final response = await post( + _startChartHost + reportInformationDataURL.toUrl, + jsonEncode(reportInformationData.toJson()), + isUnShowLoading: true, + isUserBaseUrl: false, + ); + } +} diff --git a/lib/talk/startChart/command/message_command.dart b/lib/talk/startChart/command/message_command.dart new file mode 100644 index 00000000..0082d83c --- /dev/null +++ b/lib/talk/startChart/command/message_command.dart @@ -0,0 +1,69 @@ +import 'package:star_lock/talk/startChart/constant/payload_type_constant.dart'; +import 'package:star_lock/talk/startChart/constant/protocol_flag_constant.dart'; +import 'package:star_lock/talk/startChart/entity/scp_message.dart'; + +class MessageCommand { + /// 客户端去中继上线命令 + static List goOnlineRelay() { + String serializedBytesString = ScpMessage( + ProtocolFlag: ProtocolFlagConstant.scp01, + MessageType: PayloadTypeConstant.goOnline, + MessageId: 1, + SpTotal: 0, + SpIndex: 0, + ).serialize(); + return _hexToBytes(serializedBytesString); + } + + // 回声测试消息 + static List echoMessage({ + required String ToPeerId, + required String FromPeerId, + }) { + ScpMessage message = ScpMessage( + ProtocolFlag: ProtocolFlagConstant.scp01, + MessageType: PayloadTypeConstant.echoTest, + MessageId: 1, + SpTotal: 0, + SpIndex: 0, + FromPeerId: FromPeerId, + ToPeerId: ToPeerId, + Payload: 'hello', + PayloadCRC: 55230, + PayloadLength: 5, + PayloadType: 1, + ); + + String serializedBytesString = message.serialize(); + return _hexToBytes(serializedBytesString); + } + + // 心跳消息 + static List heartbeatMessage() { + ScpMessage message = ScpMessage( + ProtocolFlag: ProtocolFlagConstant.scp01, + MessageType: PayloadTypeConstant.heartbeat, + MessageId: 1, + SpTotal: 0, + SpIndex: 0, + // FromPeerId: FromPeerId, + // ToPeerId: ToPeerId, + // Payload: 'hello', + // PayloadCRC: 55230, + // PayloadLength: 5, + // PayloadType: 1, + ); + + String serializedBytesString = message.serialize(); + return _hexToBytes(serializedBytesString); + } + + // 辅助方法:将16进制字符串转换为字节列表 + static List _hexToBytes(String hex) { + final bytes = []; + for (int i = 0; i < hex.length; i += 2) { + bytes.add(int.parse(hex.substring(i, i + 2), radix: 16)); + } + return bytes; + } +} diff --git a/lib/talk/startChart/constant/ip_constant.dart b/lib/talk/startChart/constant/ip_constant.dart new file mode 100644 index 00000000..bf1f2d40 --- /dev/null +++ b/lib/talk/startChart/constant/ip_constant.dart @@ -0,0 +1,7 @@ +class IpConstant { + // 上报时需要排除的ip + static const List reportExcludeIp = ['127.0.0.1','::1%1']; + static const String udpUrl = 'udp://'; + static const String tcpUrl = 'tcp://'; + static const String httpsUrl = 'https://'; +} diff --git a/lib/talk/startChart/constant/listen_addr_type_constant.dart b/lib/talk/startChart/constant/listen_addr_type_constant.dart new file mode 100644 index 00000000..5b63a599 --- /dev/null +++ b/lib/talk/startChart/constant/listen_addr_type_constant.dart @@ -0,0 +1,5 @@ +class ListenAddrTypeConstant { + static const String local = 'local'; + static const String externally = 'externally'; + static const String relay = 'relay'; +} \ No newline at end of file diff --git a/lib/talk/startChart/constant/payload_type_constant.dart b/lib/talk/startChart/constant/payload_type_constant.dart new file mode 100644 index 00000000..eec9bd75 --- /dev/null +++ b/lib/talk/startChart/constant/payload_type_constant.dart @@ -0,0 +1,10 @@ +class PayloadTypeConstant { + // 上线 + static const int goOnline = 100; + // 回声测试 + static const int echoTest = 8; + // 心跳 + static const int heartbeat = 110; + // UDP协议的SCD发现服务器查询中继信息 + static const int query = 120; +} \ No newline at end of file diff --git a/lib/talk/startChart/constant/protocol_flag_constant.dart b/lib/talk/startChart/constant/protocol_flag_constant.dart new file mode 100644 index 00000000..366a7009 --- /dev/null +++ b/lib/talk/startChart/constant/protocol_flag_constant.dart @@ -0,0 +1,3 @@ +class ProtocolFlagConstant { + static const String scp01 = 'SC01'; +} \ No newline at end of file diff --git a/lib/talk/startChart/entity/relay_info_entity.dart b/lib/talk/startChart/entity/relay_info_entity.dart new file mode 100644 index 00000000..8c0d61d8 --- /dev/null +++ b/lib/talk/startChart/entity/relay_info_entity.dart @@ -0,0 +1,90 @@ +class RelayInfoEntity { + RelayInfoEntity({ + this.msg, + this.time, + this.stun_server, + this.client_addr, + this.relay_list, + }); + + RelayInfoEntity.fromJson(dynamic json) { + msg = json['msg']; + time = json['time']; + stun_server = json['stun_server']; + client_addr = json['client_addr']; + relay_list = json['relay_list'] != null + ? List.from( + json['relay_list'].map((x) => RelayData.fromJson(x))) + : null; + } + + String? msg; + int? time; + String? stun_server; + String? client_addr; + List? relay_list; + + Map toJson() { + return { + 'msg': msg, + 'time': time, + 'stun_server': stun_server, + 'client_addr': client_addr, + 'relay_list': relay_list?.map((x) => x.toJson()).toList(), + }; + } + + @override + String toString() { + return 'RelayInfoEntity{msg: $msg, time: $time, stun_server: $stun_server, client_addr: $client_addr, relay_list: $relay_list}'; + } +} + +class RelayData { + RelayData({ + this.peerID, + this.name, + this.listenAddr, + this.peerMax, + this.peerCurrent, + this.health, + this.latency, + }); + + RelayData.fromJson(dynamic json) { + peerID = json['peerID']; + name = json['name']; + listenAddr = json['listenAddr']; + peerMax = json['peerMax']; + peerCurrent = json['peerCurrent']; + health = json['health']; + latency = json['latency']; + } + + String? peerID; + int? time; + String? name; + String? listenAddr; + int? peerMax; + int? peerCurrent; + int? health; + int? latency; + + Map toJson() { + return { + 'peerID': peerID, + 'time': time, + 'name': name, + 'listenAddr': listenAddr, + 'peerMax': peerMax, + 'peerCurrent': peerCurrent, + 'health': health, + 'latency': latency, + }; + } + + @override + String toString() { + return 'RelayData{peerID: $peerID, time: $time, name: $name, listenAddr: $listenAddr, peerMax: $peerMax, peerCurrent: $peerCurrent, health: $health, latency: $latency}'; + } +} diff --git a/lib/talk/startChart/entity/report_information_data.dart b/lib/talk/startChart/entity/report_information_data.dart new file mode 100644 index 00000000..5fbc797f --- /dev/null +++ b/lib/talk/startChart/entity/report_information_data.dart @@ -0,0 +1,107 @@ +class ReportInformationData { + ReportInformationData({ + this.id, + this.public_key, + this.listen_addr, + this.relay_service, + this.time, + this.sign, + }); + + ReportInformationData.fromJson(dynamic json) { + id = json['id']; + public_key = json['public_key']; + time = json['time']; + sign = json['sign']; + listen_addr = json['listen_addr'] != null + ? List.from( + json['listen_addr'].map((x) => ListenAddrData.fromJson(x))) + : null; + relay_service = json['relay_service']; + } + + String? id; + String? public_key; + String? sign; + List? listen_addr; + RelayServiceData? relay_service; + int? time; + + Map toJson() { + return { + 'id': id, + 'public_key': public_key, + 'time': time, + 'sign': sign, + 'listen_addr': listen_addr?.map((x) => x.toJson()).toList(), + 'relay_service': relay_service, + }; + } + + @override + String toString() { + return 'ReportInformationData{id: $id, public_key: $public_key, sign: $sign, listen_addr: $listen_addr, relay_service: $relay_service, time: $time}'; + } +} + +class ListenAddrData { + String? type; + String? address; + + ListenAddrData({ + this.type, + this.address, + }); + + ListenAddrData.fromJson(dynamic json) { + type = json['type']; + address = json['address']; + } + + Map toJson() { + return { + 'type': type, + 'address': address, + }; + } + + @override + String toString() { + return 'ListenAddrData{type: $type, address: $address}'; + } +} + +class RelayServiceData { + String? name; + String? listen_addr; + int? peers_max; + int? peers_current; + + RelayServiceData({ + this.name, + this.listen_addr, + this.peers_max, + this.peers_current, + }); + + RelayServiceData.fromJson(dynamic json) { + name = json['name']; + listen_addr = json['listen_addr']; + peers_max = json['peers_max']; + peers_current = json['peers_current']; + } + + Map toJson() { + return { + 'name': name, + 'listen_addr': listen_addr, + 'peers_max': peers_max, + 'peers_current': peers_current, + }; + } + + @override + String toString() { + return 'RelayServiceData{name: $name, listen_addr: $listen_addr, peers_max: $peers_max, peers_current: $peers_current}'; + } +} diff --git a/lib/talk/startChart/entity/scp_message.dart b/lib/talk/startChart/entity/scp_message.dart new file mode 100644 index 00000000..f7dc124e --- /dev/null +++ b/lib/talk/startChart/entity/scp_message.dart @@ -0,0 +1,143 @@ +import 'dart:convert'; +import 'package:crc32_checksum/crc32_checksum.dart'; +import 'package:crypto/crypto.dart'; +import 'package:star_lock/app_settings/app_settings.dart'; + +class ScpMessage { + ScpMessage({ + this.ProtocolFlag, + this.MessageType, + this.MessageId, + this.SpTotal, + this.SpIndex, + this.FromPeerId, + this.ToPeerId, + this.PayloadType, + this.PayloadCRC, + this.PayloadLength, + this.Payload, + }); + + String? ProtocolFlag; + int? MessageType; + int? MessageId; + int? SpTotal; + int? SpIndex; + String? FromPeerId; + String? ToPeerId; + int? PayloadType; + int? PayloadCRC; + int? PayloadLength; + String? Payload; + + ScpMessage.fromJson(dynamic json) { + ProtocolFlag = json['ProtocolFlag']; + MessageType = json['MessageType']; + MessageId = json['MessageId']; + SpTotal = json['SpTotal']; + SpIndex = json['SpIndex']; + FromPeerId = json['FromPeerId']; + ToPeerId = json['ToPeerId']; + PayloadType = json['PayloadType']; + PayloadCRC = json['PayloadCRC']; + PayloadLength = json['PayloadLength']; + Payload = json['Payload']; + } + + Map toJson() { + return { + 'ProtocolFlag': ProtocolFlag, + 'MessageType': MessageType, + 'MessageId': MessageId, + 'SpTotal': SpTotal, + 'SpIndex': SpIndex, + 'FromPeerId': FromPeerId, + 'ToPeerId': ToPeerId, + 'PayloadType': PayloadType, + 'PayloadCRC': PayloadCRC, + 'PayloadLength': PayloadLength, + 'Payload': Payload, + }; + } + + String serialize() { + final bytes = []; + + // ProtocolFlag (4 bytes) + if (ProtocolFlag != null) { + bytes.addAll(utf8.encode(ProtocolFlag!)); + } + + // MessageType (1 byte) + if (MessageType != null) { + bytes.add(MessageType!); + } + + // MessageId (2 bytes) + if (MessageId != null) { + final highByteMessageId = (MessageId! >> 8) & 0xFF; + final lowByteMessageId = MessageId! & 0xFF; + bytes.add(lowByteMessageId); // 交换位置 + bytes.add(highByteMessageId); // 交换位置 + } + + // SpTotal (1 byte) + if (SpTotal != null) { + bytes.add(SpTotal!); + } + + // SpIndex (1 byte) + if (SpIndex != null) { + bytes.add(SpIndex!); + } + + // FromPeerId (字符串,记录长度) + if (FromPeerId != null) { + bytes.addAll(utf8.encode(FromPeerId!)); + } + + // ToPeerId (字符串,假设长度固定为32字节) + if (ToPeerId != null) { + bytes.addAll(utf8.encode(ToPeerId!)); + } + + // PayloadType (2 bytes) + if (PayloadType != null) { + final highBytePayloadType = (PayloadType! >> 8) & 0xFF; + final lowBytePayloadType = PayloadType! & 0xFF; + bytes.add(lowBytePayloadType); // 交换位置 + bytes.add(highBytePayloadType); // 交换位置 + } + + // 计算 PayloadCRC (2 bytes) + if (PayloadCRC != null) { + final highBytePayloadCRC = (PayloadCRC! >> 8) & 0xFF; + final lowBytePayloadCRC = PayloadCRC! & 0xFF; + bytes.add(lowBytePayloadCRC); // 交换位置 + bytes.add(highBytePayloadCRC); // 交换位置 + } + + // PayloadLength (4 bytes) + if (PayloadLength != null) { + bytes.add(PayloadLength! & 0xFF); + bytes.add((PayloadLength! >> 8) & 0xFF); + bytes.add((PayloadLength! >> 16) & 0xFF); + bytes.add((PayloadLength! >> 24) & 0xFF); + } + + // Payload (字符串,转换为字节) + if (Payload != null) { + bytes.addAll(utf8.encode(Payload!)); + } + + // 转16进制字符串 + final bytesToHexString = bytesToHex(bytes); + + + return bytesToHexString; + } + + static String bytesToHex(List bytes) { + return bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(''); + } +} diff --git a/lib/talk/startChart/entity/star_chart_register_node_entity.dart b/lib/talk/startChart/entity/star_chart_register_node_entity.dart new file mode 100644 index 00000000..0d860d87 --- /dev/null +++ b/lib/talk/startChart/entity/star_chart_register_node_entity.dart @@ -0,0 +1,59 @@ +class StarChartRegisterNodeEntity { + StarChartRegisterNodeEntity({ + this.msg, + this.peer, + }); + + StarChartRegisterNodeEntity.fromJson(dynamic json) { + msg = json['msg']; + peer = json['peer'] != null ? PeerData.fromJson(json['peer']) : null; + } + + String? msg; + PeerData? peer; + + Map toJson() { + final map = {}; + map['msg'] = msg; + if (peer != null) { + map['peer'] = peer!.toJson(); + } + return map; + } + + @override + String toString() { + return 'StarChartRegisterNodeEntity{msg: $msg, peer: $peer}'; + } +} + +class PeerData { + PeerData({ + this.id, + this.publicKey, + this.privateKey, + }); + + PeerData.fromJson(dynamic json) { + id = json['id']; + publicKey = json['publicKey']; + privateKey = json['privateKey']; + } + + String? id; + String? publicKey; + String? privateKey; + + Map toJson() { + final map = {}; + map['id'] = id; + map['publicKey'] = publicKey; + map['privateKey'] = privateKey; + return map; + } + + @override + String toString() { + return 'PeerData{id: $id, publicKey: $publicKey, privateKey: $privateKey}'; + } +} diff --git a/lib/talk/startChart/start_chart_manage.dart b/lib/talk/startChart/start_chart_manage.dart new file mode 100644 index 00000000..0baa1f6a --- /dev/null +++ b/lib/talk/startChart/start_chart_manage.dart @@ -0,0 +1,379 @@ +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 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 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 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 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 message = MessageCommand.heartbeatMessage(); + _sendMessage(message: message); + }, + ); + _heartBeatTimerRunning = true; + } + + // 停止定时发送心跳包 + void stopHeartbeat() { + _heartBeatTimer?.cancel(); + _heartBeatTimer = null; // 清除定时器引用 + _heartBeatTimerRunning = false; + _log(text: '发送心跳包结束'); + } + + // 发送消息 + void _sendMessage({required List message}) { + _log(text: '发送给中继的消息体:${message},序列化之后的数据:【${bytesToHex(message)}】'); + _udpSocket!.send(message, InternetAddress(remoteHost), remotePort); + } + + // 请求注册节点 + Future _requestStarChartRegisterNode() async { + // 获取设备信息 + final Map 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 _saveStarChartRegisterNodeToStorage( + StarChartRegisterNodeEntity starChartRegisterNodeEntity) async { + if (starChartRegisterNodeEntity != null) { + await Storage.saveStarChartRegisterNodeInfo(starChartRegisterNodeEntity); + _log(text: '注册成功'); + } + } + + // 保存星图中继服务器信息至缓存 + Future _saveRelayInfoEntityToStorage( + RelayInfoEntity relayInfoEntity) async { + if (relayInfoEntity != null) { + await Storage.saveRelayInfo(relayInfoEntity); + } + } + + // 构造上报信息数据参数 + Future _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 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> _makeListenAddrDataList() async { + final List listenAddrDataList = []; + final List 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> _getAllIpAddresses() async { + final List ipAddresses = []; + try { + final List 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> _getDeviceInfo() async { + final Map deviceInfo = + await DeviceInfoUtils.getDeviceInfo(); + return deviceInfo; + } + + /// 解析 UDP URL 并提取 IP 地址和端口号 + Map _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 bytes) { + return bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(''); + } + + // 生成签名sing + Future _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 getPublicKey() async { + // 从缓存中获取星图注册节点信息 + final StarChartRegisterNodeEntity? starChartRegisterNodeInfo = + await Storage.getStarChartRegisterNodeInfo(); + return starChartRegisterNodeInfo?.peer?.publicKey ?? ''; + } + + Future getPrivateKey() async { + // 从缓存中获取星图注册节点信息 + final StarChartRegisterNodeEntity? starChartRegisterNodeInfo = + await Storage.getStarChartRegisterNodeInfo(); + return starChartRegisterNodeInfo?.peer?.privateKey ?? ''; + } +} diff --git a/lib/talk/udp/udp_help.dart b/lib/talk/udp/udp_help.dart index 8bde68dc..dbb4f8c1 100755 --- a/lib/talk/udp/udp_help.dart +++ b/lib/talk/udp/udp_help.dart @@ -52,7 +52,7 @@ class UdpHelp { ipList: serversList, tokenStr: 'b989fa15f75c2ac02718b7c9bb64f80e', ); - AppLog.log('发送心跳了'); + // AppLog.log('发送心跳了'); } else { timer.cancel(); } diff --git a/lib/talk/udp/udp_manage.dart b/lib/talk/udp/udp_manage.dart index c8857bcf..62c4f5d4 100755 --- a/lib/talk/udp/udp_manage.dart +++ b/lib/talk/udp/udp_manage.dart @@ -34,9 +34,6 @@ class UDPManage { StreamSubscription? _streamSubscription; RawDatagramSocket? _udpSocket; - // String host = '47.106.143.213'; - // int port = 8056; - // String? _mIp = ''; String host = ''; int port = 0; String lockId = ''; // 锁id 通过锁id来判断是哪把锁发对讲过来 @@ -46,7 +43,7 @@ class UDPManage { var listAddress = InternetAddress.lookup(host); listAddress.then((list) { list.forEach((element) { - // AppLog.log('Udp ----> element.address:${element.address} element.host:${element.host}'); + AppLog.log('Udp ----> element.address:${element.address} element.host:${element.host}'); host = element.address; }); }); @@ -59,7 +56,7 @@ class UDPManage { // AppLog.log('❌ Udp ----> _port == 0'); return; } - // AppLog.log('Udp ----> host:$host port:$port'); + AppLog.log('Udp ----> host:$host port:$port'); var addressIListenFrom = InternetAddress.anyIPv4; int portIListenOn = 62288; // if(addressIListenFrom.address != '0.0.0.0'){ diff --git a/lib/talk/udp/udp_reciverData.dart b/lib/talk/udp/udp_reciverData.dart index 6dc9dd25..5b99d14a 100755 --- a/lib/talk/udp/udp_reciverData.dart +++ b/lib/talk/udp/udp_reciverData.dart @@ -20,7 +20,7 @@ class CommandUDPReciverManager { if (dataSize < 4) { return; } - AppLog.log('appReceiveUDPData:$data'); + // AppLog.log('appReceiveUDPData:$data'); final Uint8List data1 = Uint8List.fromList(data); if (data1.length == 1) { diff --git a/lib/tools/deviceInfo_utils.dart b/lib/tools/deviceInfo_utils.dart new file mode 100644 index 00000000..c8ff3c02 --- /dev/null +++ b/lib/tools/deviceInfo_utils.dart @@ -0,0 +1,39 @@ +import 'package:device_info_plus/device_info_plus.dart'; + +import 'package:get/get.dart'; +import 'package:package_info_plus/package_info_plus.dart'; + +class DeviceInfoUtils { + static Future> getDeviceInfo() async { + Map deviceInfo = {}; + + try { + // 获取设备信息 + DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin(); + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + + if (GetPlatform.isAndroid) { + AndroidDeviceInfo androidInfo = await deviceInfoPlugin.androidInfo; + deviceInfo['model'] = androidInfo.model; + deviceInfo['deviceName'] = androidInfo.device; + deviceInfo['brand'] = androidInfo.brand; + deviceInfo['id'] = androidInfo.id; + // deviceInfo['uniqueIdentifier'] = androidInfo.androidId ?? 'N/A'; // 使用 androidId 作为替代 + } else if (GetPlatform.isIOS) { + IosDeviceInfo iosInfo = await deviceInfoPlugin.iosInfo; + // deviceInfo['model'] = iosInfo.model; + // deviceInfo['deviceName'] = iosInfo.name; + deviceInfo['uniqueIdentifier'] = + iosInfo.identifierForVendor ?? 'N/A'; // 使用 identifierForVendor 作为替代 + } + + // 获取 APP 版本 + deviceInfo['appVersion'] = packageInfo.version; + deviceInfo['appName'] = packageInfo.appName; + } catch (e) { + print("Failed to get device info: $e"); + } + + return deviceInfo; + } +} diff --git a/lib/tools/storage.dart b/lib/tools/storage.dart index 842d18e5..9b9aff0c 100755 --- a/lib/tools/storage.dart +++ b/lib/tools/storage.dart @@ -4,6 +4,8 @@ import 'dart:convert'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:star_lock/talk/startChart/entity/relay_info_entity.dart'; +import 'package:star_lock/talk/startChart/entity/star_chart_register_node_entity.dart'; import '../login/login/entity/LoginData.dart'; import '../main/lockMian/entity/lockListInfo_entity.dart'; @@ -29,6 +31,8 @@ const String saveLockMainListData = 'lockMainListData'; const String isOpenDeBug = 'isOpenDeBug'; //是否打开 debug const String automaticLockOffTime = 'automaticLockOffTime'; //自动关锁时间 const String associationUrl = 'associationUrl'; //ios跳转微信的 url +const String starChartRegisterNodeInfo = 'starChartRegisterNodeInfo'; //星图注册信息 +const String relayInfo = 'relayInfo'; //星图中继服务器信息 class Storage { factory Storage() => _instance; @@ -234,4 +238,38 @@ class Storage { final String data = await Storage.getString(associationUrl) ?? '0'; return data; } + + // 获取星图注册节点信息 + static Future + getStarChartRegisterNodeInfo() async { + StarChartRegisterNodeEntity? starChartRegisterNodeEntity; + final String? data = await Storage.getString(starChartRegisterNodeInfo); + if (data != null && data.isNotEmpty) { + starChartRegisterNodeEntity = + StarChartRegisterNodeEntity.fromJson(jsonDecode(data)); + } + return starChartRegisterNodeEntity; + } + + // 保存星图注册节点信息 + static Future saveStarChartRegisterNodeInfo( + StarChartRegisterNodeEntity starChartRegisterNodeEntity) async { + await Storage.setString( + starChartRegisterNodeInfo, jsonEncode(starChartRegisterNodeEntity)); + } + + // 保存中继服务信息 + static Future saveRelayInfo(RelayInfoEntity relayInfoEntity) async { + await Storage.setString(relayInfo, jsonEncode(relayInfoEntity)); + } + + // 获取星图注册节点信息 + static Future getRelayInfo() async { + RelayInfoEntity? relayInfoEntity; + final String? data = await Storage.getString(relayInfo); + if (data != null && data.isNotEmpty) { + relayInfoEntity = RelayInfoEntity.fromJson(jsonDecode(data)); + } + return relayInfoEntity; + } } diff --git a/pubspec.yaml b/pubspec.yaml index fef5ed4c..df02b993 100755 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -184,6 +184,7 @@ dependencies: #加密解密 encrypt: ^5.0.1 crypto: ^3.0.3 + pointycastle: ^3.3.0 date_format: ^2.0.7 # 下拉刷新 @@ -233,7 +234,7 @@ dependencies: timelines: ^0.1.0 #侧滑删除 flutter_slidable: ^3.0.1 -# audio_service: ^0.18.12 + # audio_service: ^0.18.12 app_settings: ^5.1.1 flutter_local_notifications: ^17.0.0 fluwx: ^4.5.5 @@ -242,10 +243,13 @@ dependencies: colorfilter_generator: ^0.0.8 file_picker: ^5.3.1 # 错误日志监控 -# flutter_bugly_plugin: ^0.0.9 + # flutter_bugly_plugin: ^0.0.9 flutter_bugly: ^1.0.2 open_filex: ^4.4.0 + crc32_checksum: ^0.0.2 + fast_rsa: ^3.6.6 + dependency_overrides: #强制设置google_maps_flutter_ios 为 2.5.2 @@ -255,7 +259,6 @@ dependency_overrides: dev_dependencies: flutter_test: sdk: flutter - # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint fset provided by the package is # activated in the `analysis_options.yaml` file located at the root of your From 974fdd5bc9e5e1df62d61ea2a75487d888c30663 Mon Sep 17 00:00:00 2001 From: liyi Date: Thu, 28 Nov 2024 14:57:49 +0800 Subject: [PATCH 2/6] =?UTF-8?q?feat:=E5=A2=9E=E5=8A=A0=E6=96=B0=E7=9A=84?= =?UTF-8?q?=E4=B8=AD=E7=BB=A7=E5=8D=8F=E8=AE=AE=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lan/lan_zh.json | 4 +- lib/login/login/starLock_login_page.dart | 43 +- lib/main.dart | 2 + .../lockMian/lockMain/lockMain_logic.dart | 2 + lib/network/api.dart | 7 + lib/network/api_provider_base.dart | 57 ++- lib/network/start_chart_api.dart | 58 +++ .../startChart/command/message_command.dart | 69 ++++ lib/talk/startChart/constant/ip_constant.dart | 7 + .../constant/listen_addr_type_constant.dart | 5 + .../constant/payload_type_constant.dart | 10 + .../constant/protocol_flag_constant.dart | 3 + .../startChart/entity/relay_info_entity.dart | 90 +++++ .../entity/report_information_data.dart | 107 +++++ lib/talk/startChart/entity/scp_message.dart | 143 +++++++ .../star_chart_register_node_entity.dart | 59 +++ lib/talk/startChart/start_chart_manage.dart | 379 ++++++++++++++++++ lib/talk/udp/udp_help.dart | 2 +- lib/talk/udp/udp_manage.dart | 7 +- lib/talk/udp/udp_reciverData.dart | 2 +- lib/tools/deviceInfo_utils.dart | 39 ++ lib/tools/storage.dart | 38 ++ pubspec.yaml | 9 +- 23 files changed, 1116 insertions(+), 26 deletions(-) create mode 100644 lib/network/start_chart_api.dart create mode 100644 lib/talk/startChart/command/message_command.dart create mode 100644 lib/talk/startChart/constant/ip_constant.dart create mode 100644 lib/talk/startChart/constant/listen_addr_type_constant.dart create mode 100644 lib/talk/startChart/constant/payload_type_constant.dart create mode 100644 lib/talk/startChart/constant/protocol_flag_constant.dart create mode 100644 lib/talk/startChart/entity/relay_info_entity.dart create mode 100644 lib/talk/startChart/entity/report_information_data.dart create mode 100644 lib/talk/startChart/entity/scp_message.dart create mode 100644 lib/talk/startChart/entity/star_chart_register_node_entity.dart create mode 100644 lib/talk/startChart/start_chart_manage.dart create mode 100644 lib/tools/deviceInfo_utils.dart diff --git a/lan/lan_zh.json b/lan/lan_zh.json index e9662925..12437e7e 100755 --- a/lan/lan_zh.json +++ b/lan/lan_zh.json @@ -1096,10 +1096,8 @@ "支持的国家": "支持的国家", "支持的国家值": "美国、加拿大、英国、澳大利亚、印度、德国、法国、意大利、西班牙、日本", "操作流程": "操作流程", - "密码需至少包含数字/字母/字符中的2种组合": "密码需至少包含数字/字母/字符中的2种组合", - - "操作流程值":"1 用智能锁APP添加锁和网关\n\n2 在APP里开启锁的远程开锁功能(这个功能默认是关闭的)。如果没有这个选项,则锁不支持Alexa \n\n3 在Alexa中添加Skill,并用智能锁APP的账号和密码进行授权。授权成功后就可以发现账号下的设备\n\n4 在Alexa app里找到锁,开启语音开锁的功能,并设置语言密码\n\n5 可以通过Alexa操作锁了", + "操作流程值": "1 用智能锁APP添加锁和网关\n\n2 在APP里开启锁的远程开锁功能(这个功能默认是关闭的)。如果没有这个选项,则锁不支持Alexa \n\n3 在Alexa中添加Skill,并用智能锁APP的账号和密码进行授权。授权成功后就可以发现账号下的设备\n\n4 在Alexa app里找到锁,开启语音开锁的功能,并设置语言密码\n\n5 可以通过Alexa操作锁了", "Google Home": "Google Home", "Action name": "Action name", "ScienerSmart": "ScienerSmart", diff --git a/lib/login/login/starLock_login_page.dart b/lib/login/login/starLock_login_page.dart index 42f00b78..ec43b33b 100755 --- a/lib/login/login/starLock_login_page.dart +++ b/lib/login/login/starLock_login_page.dart @@ -1,10 +1,10 @@ - import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import 'package:star_lock/flavors.dart'; import 'package:star_lock/login/login/starLock_login_state.dart'; +import 'package:star_lock/talk/startChart/start_chart_manage.dart'; import 'package:star_lock/tools/appFirstEnterHandle.dart'; import 'package:star_lock/tools/wechat/customer_tool.dart'; import 'package:star_lock/tools/storage.dart'; @@ -48,8 +48,8 @@ class _StarLockLoginPageState extends State { haveBack: false, backgroundColor: AppColors.mainColor, actionsList: [ - IconButton( - onPressed: (){ + IconButton( + onPressed: () { WechatManageTool.getAppInfo(CustomerTool.openCustomerService); }, icon: const Icon( @@ -84,8 +84,7 @@ class _StarLockLoginPageState extends State { width: 110.w, height: 110.w))), SizedBox(height: 50.w), Obx(() => CommonItem( - leftTitel: - '你所在的国家/地区'.tr, + leftTitel: '你所在的国家/地区'.tr, rightTitle: '', isHaveLine: true, isPadding: false, @@ -123,7 +122,7 @@ class _StarLockLoginPageState extends State { height: 36.w, ), ), - hintText:'请输入手机号或者邮箱'.tr, + hintText: '请输入手机号或者邮箱'.tr, // keyboardType: TextInputType.number, inputFormatters: [ // FilteringTextInputFormatter.allow(RegExp('[0-9]')), @@ -185,8 +184,7 @@ class _StarLockLoginPageState extends State { WidgetSpan( alignment: PlaceholderAlignment.middle, child: GestureDetector( - child: Text( - '《${'用户协议'.tr}》', + child: Text('《${'用户协议'.tr}》', style: TextStyle( color: AppColors.mainColor, fontSize: 20.sp)), @@ -201,8 +199,7 @@ class _StarLockLoginPageState extends State { WidgetSpan( alignment: PlaceholderAlignment.middle, child: GestureDetector( - child: Text( - '《${'隐私政策'.tr}》', + child: Text('《${'隐私政策'.tr}》', style: TextStyle( color: AppColors.mainColor, fontSize: 20.sp)), @@ -236,6 +233,29 @@ class _StarLockLoginPageState extends State { } } : null)), + SubmitBtn( + btnName: '发送上线请求', + onClick: () { + // 注册星图节点信息 + StartChartManage().clientRegister(); + // 查询中继信息 + StartChartManage().relayQuery(); + // 发送上线请求 + StartChartManage().onlineRelayService(); + }, + ), + SubmitBtn( + btnName: '启动心跳包', + onClick: () { + StartChartManage().sendHeartbeatMessage(); + }, + ), + SubmitBtn( + btnName: '结束心跳包', + onClick: () { + StartChartManage().stopHeartbeat(); + }, + ), SizedBox(height: 50.w), Row( mainAxisAlignment: MainAxisAlignment.center, @@ -246,8 +266,7 @@ class _StarLockLoginPageState extends State { height: 50.h, // color: Colors.red, child: Center( - child: Text( - '${'忘记密码'.tr}?', + child: Text('${'忘记密码'.tr}?', style: TextStyle( fontSize: 22.sp, color: AppColors.mainColor)), ), diff --git a/lib/main.dart b/lib/main.dart index aa07e5a9..d4898717 100755 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,6 +11,7 @@ import 'package:star_lock/flavors.dart'; import 'package:star_lock/mine/about/debug/debug_tool.dart'; import 'package:star_lock/network/api_provider.dart'; import 'package:star_lock/network/api_repository.dart'; +import 'package:star_lock/network/start_chart_api.dart'; import 'package:star_lock/tools/bugly/bugly_tool.dart'; import 'package:star_lock/tools/device_info_service.dart'; import 'package:star_lock/tools/platform_info_services.dart'; @@ -62,6 +63,7 @@ Future _initTranslation() async => TranslationLoader.loadTranslation(); Future _setCommonServices() async { await Get.putAsync(() => StoreService().init()); Get.put(ApiProvider()); + Get.put(StartChartApi()); Get.put(ApiRepository(Get.find())); if (F.isLite) { //上架审核注释 获取设备信息 diff --git a/lib/main/lockMian/lockMain/lockMain_logic.dart b/lib/main/lockMian/lockMain/lockMain_logic.dart index e65b36c7..dc2de263 100755 --- a/lib/main/lockMian/lockMain/lockMain_logic.dart +++ b/lib/main/lockMian/lockMain/lockMain_logic.dart @@ -11,6 +11,7 @@ import 'package:star_lock/flavors.dart'; import 'package:star_lock/login/login/entity/LoginEntity.dart'; import 'package:star_lock/main/lockDetail/lockDetail/lockDetail_logic.dart'; import 'package:star_lock/main/lockMian/lockList/lockList_logic.dart'; +import 'package:star_lock/talk/startChart/start_chart_manage.dart'; import 'package:star_lock/tools/eventBusEventManage.dart'; import 'package:star_lock/tools/push/xs_jPhush.dart'; import 'package:star_lock/tools/showTipView.dart'; @@ -311,6 +312,7 @@ class LockMainLogic extends BaseGetXController { // AppLog.log('onReady() LockMainLogic'); UdpHelp().openUDP(); BlueManage(); + } @override diff --git a/lib/network/api.dart b/lib/network/api.dart index e670f0c4..686e9b5b 100755 --- a/lib/network/api.dart +++ b/lib/network/api.dart @@ -282,4 +282,11 @@ abstract class Api { final String updateZoneOffsetsAndLanguagesURL = '/cloudUser/updateSettings'; //更新云用户时区偏移与语言 + + final String starChartRegisterNodeURL = + '/SL-A-1.0/peer/register'; // 星图--注册节点 + final String relayQueryInfoURL = + '/SL-A-1.0/relay/query'; // 星图--中继查询 + final String reportInformationDataURL = + '/SL-A-1.0/peer/login'; // 星图--信息上报 } diff --git a/lib/network/api_provider_base.dart b/lib/network/api_provider_base.dart index e6563cdb..2d9b92d1 100755 --- a/lib/network/api_provider_base.dart +++ b/lib/network/api_provider_base.dart @@ -78,7 +78,62 @@ class BaseProvider extends GetConnect with Api { statusText: res.statusText, ); } else {} - if(isShowNetworkErrorMsg ?? true){ + if (isShowNetworkErrorMsg ?? true) { + getDataResult(res.body); + } + return res; + } + + @override + Future> get( + String url, { + String? contentType, + Map? headers, + Map? query, + Decoder? decoder, + bool? isUnShowLoading = false, // 是否显示loading + bool? isUserBaseUrl = true, // 文件上传不使用baseUrl + bool? isShowErrMsg = true, // 是否显示没有网络时的提示 + bool? isShowNetworkErrorMsg = true, // 是否显示网络其他报错 如403 500等 + }) async { + AppLog.log('get: url:$url'); + if (isUnShowLoading == false) { + EasyLoading.show(); + } + if (isUserBaseUrl == false) { + httpClient.baseUrl = ''; + } else { + httpClient.baseUrl = '${F.apiPrefix}/api'; + } + var res = await super.get(url, + contentType: contentType, + headers: headers, + query: query, + decoder: decoder); + if (EasyLoading.isShow && !isUnShowLoading!) { + await EasyLoading.dismiss(animation: true); + } + if (res.body == null) { + if (res.statusCode == null && isShowErrMsg!) { + EasyLoading.showToast('网络访问失败,请检查网络是否正常'.tr, + duration: 2000.milliseconds); + } + var rs = { + 'errorMsg': 'Network Error!', + 'errorCode': -1, + 'data': null, + 'description': '表示成功或是。' + }; + return Response( + request: res.request, + statusCode: -1, + bodyString: res.bodyString, + bodyBytes: res.bodyBytes, + body: rs as T, + statusText: res.statusText, + ); + } + if (isShowNetworkErrorMsg ?? true) { getDataResult(res.body); } return res; diff --git a/lib/network/start_chart_api.dart b/lib/network/start_chart_api.dart new file mode 100644 index 00000000..1a6901c8 --- /dev/null +++ b/lib/network/start_chart_api.dart @@ -0,0 +1,58 @@ +import 'dart:convert'; + +import 'package:get/get.dart'; +import 'package:star_lock/network/api_provider.dart'; +import 'package:star_lock/network/api_provider_base.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'; + +class StartChartApi extends BaseProvider { + // 星图url + final String _startChartHost = 'http://sls1-scd.star-lock.cn:8080'; + + static StartChartApi get to => Get.find(); + + // 星图--注册节点 + Future starChartRegisterNode({ + required String product, + required String model, + required String name, + required String unique, + }) async { + final response = await post( + _startChartHost + starChartRegisterNodeURL.toUrl, + jsonEncode({ + 'product': product, + 'model': model, + 'name': name, + 'unique': unique, + }), + isUnShowLoading: true, + isUserBaseUrl: false, + ); + return StarChartRegisterNodeEntity.fromJson(response.body); + } + + // 星图--中继查询 + Future relayQueryInfo() async { + final response = await get( + _startChartHost + relayQueryInfoURL.toUrl, + isUnShowLoading: true, + isUserBaseUrl: false, + ); + return RelayInfoEntity.fromJson(response.body); + } + + // 星图--上报信息至发现服务 + Future reportInformation({ + required ReportInformationData reportInformationData, + }) async { + final response = await post( + _startChartHost + reportInformationDataURL.toUrl, + jsonEncode(reportInformationData.toJson()), + isUnShowLoading: true, + isUserBaseUrl: false, + ); + } +} diff --git a/lib/talk/startChart/command/message_command.dart b/lib/talk/startChart/command/message_command.dart new file mode 100644 index 00000000..0082d83c --- /dev/null +++ b/lib/talk/startChart/command/message_command.dart @@ -0,0 +1,69 @@ +import 'package:star_lock/talk/startChart/constant/payload_type_constant.dart'; +import 'package:star_lock/talk/startChart/constant/protocol_flag_constant.dart'; +import 'package:star_lock/talk/startChart/entity/scp_message.dart'; + +class MessageCommand { + /// 客户端去中继上线命令 + static List goOnlineRelay() { + String serializedBytesString = ScpMessage( + ProtocolFlag: ProtocolFlagConstant.scp01, + MessageType: PayloadTypeConstant.goOnline, + MessageId: 1, + SpTotal: 0, + SpIndex: 0, + ).serialize(); + return _hexToBytes(serializedBytesString); + } + + // 回声测试消息 + static List echoMessage({ + required String ToPeerId, + required String FromPeerId, + }) { + ScpMessage message = ScpMessage( + ProtocolFlag: ProtocolFlagConstant.scp01, + MessageType: PayloadTypeConstant.echoTest, + MessageId: 1, + SpTotal: 0, + SpIndex: 0, + FromPeerId: FromPeerId, + ToPeerId: ToPeerId, + Payload: 'hello', + PayloadCRC: 55230, + PayloadLength: 5, + PayloadType: 1, + ); + + String serializedBytesString = message.serialize(); + return _hexToBytes(serializedBytesString); + } + + // 心跳消息 + static List heartbeatMessage() { + ScpMessage message = ScpMessage( + ProtocolFlag: ProtocolFlagConstant.scp01, + MessageType: PayloadTypeConstant.heartbeat, + MessageId: 1, + SpTotal: 0, + SpIndex: 0, + // FromPeerId: FromPeerId, + // ToPeerId: ToPeerId, + // Payload: 'hello', + // PayloadCRC: 55230, + // PayloadLength: 5, + // PayloadType: 1, + ); + + String serializedBytesString = message.serialize(); + return _hexToBytes(serializedBytesString); + } + + // 辅助方法:将16进制字符串转换为字节列表 + static List _hexToBytes(String hex) { + final bytes = []; + for (int i = 0; i < hex.length; i += 2) { + bytes.add(int.parse(hex.substring(i, i + 2), radix: 16)); + } + return bytes; + } +} diff --git a/lib/talk/startChart/constant/ip_constant.dart b/lib/talk/startChart/constant/ip_constant.dart new file mode 100644 index 00000000..bf1f2d40 --- /dev/null +++ b/lib/talk/startChart/constant/ip_constant.dart @@ -0,0 +1,7 @@ +class IpConstant { + // 上报时需要排除的ip + static const List reportExcludeIp = ['127.0.0.1','::1%1']; + static const String udpUrl = 'udp://'; + static const String tcpUrl = 'tcp://'; + static const String httpsUrl = 'https://'; +} diff --git a/lib/talk/startChart/constant/listen_addr_type_constant.dart b/lib/talk/startChart/constant/listen_addr_type_constant.dart new file mode 100644 index 00000000..5b63a599 --- /dev/null +++ b/lib/talk/startChart/constant/listen_addr_type_constant.dart @@ -0,0 +1,5 @@ +class ListenAddrTypeConstant { + static const String local = 'local'; + static const String externally = 'externally'; + static const String relay = 'relay'; +} \ No newline at end of file diff --git a/lib/talk/startChart/constant/payload_type_constant.dart b/lib/talk/startChart/constant/payload_type_constant.dart new file mode 100644 index 00000000..eec9bd75 --- /dev/null +++ b/lib/talk/startChart/constant/payload_type_constant.dart @@ -0,0 +1,10 @@ +class PayloadTypeConstant { + // 上线 + static const int goOnline = 100; + // 回声测试 + static const int echoTest = 8; + // 心跳 + static const int heartbeat = 110; + // UDP协议的SCD发现服务器查询中继信息 + static const int query = 120; +} \ No newline at end of file diff --git a/lib/talk/startChart/constant/protocol_flag_constant.dart b/lib/talk/startChart/constant/protocol_flag_constant.dart new file mode 100644 index 00000000..366a7009 --- /dev/null +++ b/lib/talk/startChart/constant/protocol_flag_constant.dart @@ -0,0 +1,3 @@ +class ProtocolFlagConstant { + static const String scp01 = 'SC01'; +} \ No newline at end of file diff --git a/lib/talk/startChart/entity/relay_info_entity.dart b/lib/talk/startChart/entity/relay_info_entity.dart new file mode 100644 index 00000000..8c0d61d8 --- /dev/null +++ b/lib/talk/startChart/entity/relay_info_entity.dart @@ -0,0 +1,90 @@ +class RelayInfoEntity { + RelayInfoEntity({ + this.msg, + this.time, + this.stun_server, + this.client_addr, + this.relay_list, + }); + + RelayInfoEntity.fromJson(dynamic json) { + msg = json['msg']; + time = json['time']; + stun_server = json['stun_server']; + client_addr = json['client_addr']; + relay_list = json['relay_list'] != null + ? List.from( + json['relay_list'].map((x) => RelayData.fromJson(x))) + : null; + } + + String? msg; + int? time; + String? stun_server; + String? client_addr; + List? relay_list; + + Map toJson() { + return { + 'msg': msg, + 'time': time, + 'stun_server': stun_server, + 'client_addr': client_addr, + 'relay_list': relay_list?.map((x) => x.toJson()).toList(), + }; + } + + @override + String toString() { + return 'RelayInfoEntity{msg: $msg, time: $time, stun_server: $stun_server, client_addr: $client_addr, relay_list: $relay_list}'; + } +} + +class RelayData { + RelayData({ + this.peerID, + this.name, + this.listenAddr, + this.peerMax, + this.peerCurrent, + this.health, + this.latency, + }); + + RelayData.fromJson(dynamic json) { + peerID = json['peerID']; + name = json['name']; + listenAddr = json['listenAddr']; + peerMax = json['peerMax']; + peerCurrent = json['peerCurrent']; + health = json['health']; + latency = json['latency']; + } + + String? peerID; + int? time; + String? name; + String? listenAddr; + int? peerMax; + int? peerCurrent; + int? health; + int? latency; + + Map toJson() { + return { + 'peerID': peerID, + 'time': time, + 'name': name, + 'listenAddr': listenAddr, + 'peerMax': peerMax, + 'peerCurrent': peerCurrent, + 'health': health, + 'latency': latency, + }; + } + + @override + String toString() { + return 'RelayData{peerID: $peerID, time: $time, name: $name, listenAddr: $listenAddr, peerMax: $peerMax, peerCurrent: $peerCurrent, health: $health, latency: $latency}'; + } +} diff --git a/lib/talk/startChart/entity/report_information_data.dart b/lib/talk/startChart/entity/report_information_data.dart new file mode 100644 index 00000000..5fbc797f --- /dev/null +++ b/lib/talk/startChart/entity/report_information_data.dart @@ -0,0 +1,107 @@ +class ReportInformationData { + ReportInformationData({ + this.id, + this.public_key, + this.listen_addr, + this.relay_service, + this.time, + this.sign, + }); + + ReportInformationData.fromJson(dynamic json) { + id = json['id']; + public_key = json['public_key']; + time = json['time']; + sign = json['sign']; + listen_addr = json['listen_addr'] != null + ? List.from( + json['listen_addr'].map((x) => ListenAddrData.fromJson(x))) + : null; + relay_service = json['relay_service']; + } + + String? id; + String? public_key; + String? sign; + List? listen_addr; + RelayServiceData? relay_service; + int? time; + + Map toJson() { + return { + 'id': id, + 'public_key': public_key, + 'time': time, + 'sign': sign, + 'listen_addr': listen_addr?.map((x) => x.toJson()).toList(), + 'relay_service': relay_service, + }; + } + + @override + String toString() { + return 'ReportInformationData{id: $id, public_key: $public_key, sign: $sign, listen_addr: $listen_addr, relay_service: $relay_service, time: $time}'; + } +} + +class ListenAddrData { + String? type; + String? address; + + ListenAddrData({ + this.type, + this.address, + }); + + ListenAddrData.fromJson(dynamic json) { + type = json['type']; + address = json['address']; + } + + Map toJson() { + return { + 'type': type, + 'address': address, + }; + } + + @override + String toString() { + return 'ListenAddrData{type: $type, address: $address}'; + } +} + +class RelayServiceData { + String? name; + String? listen_addr; + int? peers_max; + int? peers_current; + + RelayServiceData({ + this.name, + this.listen_addr, + this.peers_max, + this.peers_current, + }); + + RelayServiceData.fromJson(dynamic json) { + name = json['name']; + listen_addr = json['listen_addr']; + peers_max = json['peers_max']; + peers_current = json['peers_current']; + } + + Map toJson() { + return { + 'name': name, + 'listen_addr': listen_addr, + 'peers_max': peers_max, + 'peers_current': peers_current, + }; + } + + @override + String toString() { + return 'RelayServiceData{name: $name, listen_addr: $listen_addr, peers_max: $peers_max, peers_current: $peers_current}'; + } +} diff --git a/lib/talk/startChart/entity/scp_message.dart b/lib/talk/startChart/entity/scp_message.dart new file mode 100644 index 00000000..f7dc124e --- /dev/null +++ b/lib/talk/startChart/entity/scp_message.dart @@ -0,0 +1,143 @@ +import 'dart:convert'; +import 'package:crc32_checksum/crc32_checksum.dart'; +import 'package:crypto/crypto.dart'; +import 'package:star_lock/app_settings/app_settings.dart'; + +class ScpMessage { + ScpMessage({ + this.ProtocolFlag, + this.MessageType, + this.MessageId, + this.SpTotal, + this.SpIndex, + this.FromPeerId, + this.ToPeerId, + this.PayloadType, + this.PayloadCRC, + this.PayloadLength, + this.Payload, + }); + + String? ProtocolFlag; + int? MessageType; + int? MessageId; + int? SpTotal; + int? SpIndex; + String? FromPeerId; + String? ToPeerId; + int? PayloadType; + int? PayloadCRC; + int? PayloadLength; + String? Payload; + + ScpMessage.fromJson(dynamic json) { + ProtocolFlag = json['ProtocolFlag']; + MessageType = json['MessageType']; + MessageId = json['MessageId']; + SpTotal = json['SpTotal']; + SpIndex = json['SpIndex']; + FromPeerId = json['FromPeerId']; + ToPeerId = json['ToPeerId']; + PayloadType = json['PayloadType']; + PayloadCRC = json['PayloadCRC']; + PayloadLength = json['PayloadLength']; + Payload = json['Payload']; + } + + Map toJson() { + return { + 'ProtocolFlag': ProtocolFlag, + 'MessageType': MessageType, + 'MessageId': MessageId, + 'SpTotal': SpTotal, + 'SpIndex': SpIndex, + 'FromPeerId': FromPeerId, + 'ToPeerId': ToPeerId, + 'PayloadType': PayloadType, + 'PayloadCRC': PayloadCRC, + 'PayloadLength': PayloadLength, + 'Payload': Payload, + }; + } + + String serialize() { + final bytes = []; + + // ProtocolFlag (4 bytes) + if (ProtocolFlag != null) { + bytes.addAll(utf8.encode(ProtocolFlag!)); + } + + // MessageType (1 byte) + if (MessageType != null) { + bytes.add(MessageType!); + } + + // MessageId (2 bytes) + if (MessageId != null) { + final highByteMessageId = (MessageId! >> 8) & 0xFF; + final lowByteMessageId = MessageId! & 0xFF; + bytes.add(lowByteMessageId); // 交换位置 + bytes.add(highByteMessageId); // 交换位置 + } + + // SpTotal (1 byte) + if (SpTotal != null) { + bytes.add(SpTotal!); + } + + // SpIndex (1 byte) + if (SpIndex != null) { + bytes.add(SpIndex!); + } + + // FromPeerId (字符串,记录长度) + if (FromPeerId != null) { + bytes.addAll(utf8.encode(FromPeerId!)); + } + + // ToPeerId (字符串,假设长度固定为32字节) + if (ToPeerId != null) { + bytes.addAll(utf8.encode(ToPeerId!)); + } + + // PayloadType (2 bytes) + if (PayloadType != null) { + final highBytePayloadType = (PayloadType! >> 8) & 0xFF; + final lowBytePayloadType = PayloadType! & 0xFF; + bytes.add(lowBytePayloadType); // 交换位置 + bytes.add(highBytePayloadType); // 交换位置 + } + + // 计算 PayloadCRC (2 bytes) + if (PayloadCRC != null) { + final highBytePayloadCRC = (PayloadCRC! >> 8) & 0xFF; + final lowBytePayloadCRC = PayloadCRC! & 0xFF; + bytes.add(lowBytePayloadCRC); // 交换位置 + bytes.add(highBytePayloadCRC); // 交换位置 + } + + // PayloadLength (4 bytes) + if (PayloadLength != null) { + bytes.add(PayloadLength! & 0xFF); + bytes.add((PayloadLength! >> 8) & 0xFF); + bytes.add((PayloadLength! >> 16) & 0xFF); + bytes.add((PayloadLength! >> 24) & 0xFF); + } + + // Payload (字符串,转换为字节) + if (Payload != null) { + bytes.addAll(utf8.encode(Payload!)); + } + + // 转16进制字符串 + final bytesToHexString = bytesToHex(bytes); + + + return bytesToHexString; + } + + static String bytesToHex(List bytes) { + return bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(''); + } +} diff --git a/lib/talk/startChart/entity/star_chart_register_node_entity.dart b/lib/talk/startChart/entity/star_chart_register_node_entity.dart new file mode 100644 index 00000000..0d860d87 --- /dev/null +++ b/lib/talk/startChart/entity/star_chart_register_node_entity.dart @@ -0,0 +1,59 @@ +class StarChartRegisterNodeEntity { + StarChartRegisterNodeEntity({ + this.msg, + this.peer, + }); + + StarChartRegisterNodeEntity.fromJson(dynamic json) { + msg = json['msg']; + peer = json['peer'] != null ? PeerData.fromJson(json['peer']) : null; + } + + String? msg; + PeerData? peer; + + Map toJson() { + final map = {}; + map['msg'] = msg; + if (peer != null) { + map['peer'] = peer!.toJson(); + } + return map; + } + + @override + String toString() { + return 'StarChartRegisterNodeEntity{msg: $msg, peer: $peer}'; + } +} + +class PeerData { + PeerData({ + this.id, + this.publicKey, + this.privateKey, + }); + + PeerData.fromJson(dynamic json) { + id = json['id']; + publicKey = json['publicKey']; + privateKey = json['privateKey']; + } + + String? id; + String? publicKey; + String? privateKey; + + Map toJson() { + final map = {}; + map['id'] = id; + map['publicKey'] = publicKey; + map['privateKey'] = privateKey; + return map; + } + + @override + String toString() { + return 'PeerData{id: $id, publicKey: $publicKey, privateKey: $privateKey}'; + } +} diff --git a/lib/talk/startChart/start_chart_manage.dart b/lib/talk/startChart/start_chart_manage.dart new file mode 100644 index 00000000..0baa1f6a --- /dev/null +++ b/lib/talk/startChart/start_chart_manage.dart @@ -0,0 +1,379 @@ +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 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 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 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 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 message = MessageCommand.heartbeatMessage(); + _sendMessage(message: message); + }, + ); + _heartBeatTimerRunning = true; + } + + // 停止定时发送心跳包 + void stopHeartbeat() { + _heartBeatTimer?.cancel(); + _heartBeatTimer = null; // 清除定时器引用 + _heartBeatTimerRunning = false; + _log(text: '发送心跳包结束'); + } + + // 发送消息 + void _sendMessage({required List message}) { + _log(text: '发送给中继的消息体:${message},序列化之后的数据:【${bytesToHex(message)}】'); + _udpSocket!.send(message, InternetAddress(remoteHost), remotePort); + } + + // 请求注册节点 + Future _requestStarChartRegisterNode() async { + // 获取设备信息 + final Map 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 _saveStarChartRegisterNodeToStorage( + StarChartRegisterNodeEntity starChartRegisterNodeEntity) async { + if (starChartRegisterNodeEntity != null) { + await Storage.saveStarChartRegisterNodeInfo(starChartRegisterNodeEntity); + _log(text: '注册成功'); + } + } + + // 保存星图中继服务器信息至缓存 + Future _saveRelayInfoEntityToStorage( + RelayInfoEntity relayInfoEntity) async { + if (relayInfoEntity != null) { + await Storage.saveRelayInfo(relayInfoEntity); + } + } + + // 构造上报信息数据参数 + Future _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 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> _makeListenAddrDataList() async { + final List listenAddrDataList = []; + final List 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> _getAllIpAddresses() async { + final List ipAddresses = []; + try { + final List 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> _getDeviceInfo() async { + final Map deviceInfo = + await DeviceInfoUtils.getDeviceInfo(); + return deviceInfo; + } + + /// 解析 UDP URL 并提取 IP 地址和端口号 + Map _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 bytes) { + return bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(''); + } + + // 生成签名sing + Future _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 getPublicKey() async { + // 从缓存中获取星图注册节点信息 + final StarChartRegisterNodeEntity? starChartRegisterNodeInfo = + await Storage.getStarChartRegisterNodeInfo(); + return starChartRegisterNodeInfo?.peer?.publicKey ?? ''; + } + + Future getPrivateKey() async { + // 从缓存中获取星图注册节点信息 + final StarChartRegisterNodeEntity? starChartRegisterNodeInfo = + await Storage.getStarChartRegisterNodeInfo(); + return starChartRegisterNodeInfo?.peer?.privateKey ?? ''; + } +} diff --git a/lib/talk/udp/udp_help.dart b/lib/talk/udp/udp_help.dart index 8bde68dc..dbb4f8c1 100755 --- a/lib/talk/udp/udp_help.dart +++ b/lib/talk/udp/udp_help.dart @@ -52,7 +52,7 @@ class UdpHelp { ipList: serversList, tokenStr: 'b989fa15f75c2ac02718b7c9bb64f80e', ); - AppLog.log('发送心跳了'); + // AppLog.log('发送心跳了'); } else { timer.cancel(); } diff --git a/lib/talk/udp/udp_manage.dart b/lib/talk/udp/udp_manage.dart index c8857bcf..62c4f5d4 100755 --- a/lib/talk/udp/udp_manage.dart +++ b/lib/talk/udp/udp_manage.dart @@ -34,9 +34,6 @@ class UDPManage { StreamSubscription? _streamSubscription; RawDatagramSocket? _udpSocket; - // String host = '47.106.143.213'; - // int port = 8056; - // String? _mIp = ''; String host = ''; int port = 0; String lockId = ''; // 锁id 通过锁id来判断是哪把锁发对讲过来 @@ -46,7 +43,7 @@ class UDPManage { var listAddress = InternetAddress.lookup(host); listAddress.then((list) { list.forEach((element) { - // AppLog.log('Udp ----> element.address:${element.address} element.host:${element.host}'); + AppLog.log('Udp ----> element.address:${element.address} element.host:${element.host}'); host = element.address; }); }); @@ -59,7 +56,7 @@ class UDPManage { // AppLog.log('❌ Udp ----> _port == 0'); return; } - // AppLog.log('Udp ----> host:$host port:$port'); + AppLog.log('Udp ----> host:$host port:$port'); var addressIListenFrom = InternetAddress.anyIPv4; int portIListenOn = 62288; // if(addressIListenFrom.address != '0.0.0.0'){ diff --git a/lib/talk/udp/udp_reciverData.dart b/lib/talk/udp/udp_reciverData.dart index 6dc9dd25..5b99d14a 100755 --- a/lib/talk/udp/udp_reciverData.dart +++ b/lib/talk/udp/udp_reciverData.dart @@ -20,7 +20,7 @@ class CommandUDPReciverManager { if (dataSize < 4) { return; } - AppLog.log('appReceiveUDPData:$data'); + // AppLog.log('appReceiveUDPData:$data'); final Uint8List data1 = Uint8List.fromList(data); if (data1.length == 1) { diff --git a/lib/tools/deviceInfo_utils.dart b/lib/tools/deviceInfo_utils.dart new file mode 100644 index 00000000..c8ff3c02 --- /dev/null +++ b/lib/tools/deviceInfo_utils.dart @@ -0,0 +1,39 @@ +import 'package:device_info_plus/device_info_plus.dart'; + +import 'package:get/get.dart'; +import 'package:package_info_plus/package_info_plus.dart'; + +class DeviceInfoUtils { + static Future> getDeviceInfo() async { + Map deviceInfo = {}; + + try { + // 获取设备信息 + DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin(); + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + + if (GetPlatform.isAndroid) { + AndroidDeviceInfo androidInfo = await deviceInfoPlugin.androidInfo; + deviceInfo['model'] = androidInfo.model; + deviceInfo['deviceName'] = androidInfo.device; + deviceInfo['brand'] = androidInfo.brand; + deviceInfo['id'] = androidInfo.id; + // deviceInfo['uniqueIdentifier'] = androidInfo.androidId ?? 'N/A'; // 使用 androidId 作为替代 + } else if (GetPlatform.isIOS) { + IosDeviceInfo iosInfo = await deviceInfoPlugin.iosInfo; + // deviceInfo['model'] = iosInfo.model; + // deviceInfo['deviceName'] = iosInfo.name; + deviceInfo['uniqueIdentifier'] = + iosInfo.identifierForVendor ?? 'N/A'; // 使用 identifierForVendor 作为替代 + } + + // 获取 APP 版本 + deviceInfo['appVersion'] = packageInfo.version; + deviceInfo['appName'] = packageInfo.appName; + } catch (e) { + print("Failed to get device info: $e"); + } + + return deviceInfo; + } +} diff --git a/lib/tools/storage.dart b/lib/tools/storage.dart index 842d18e5..9b9aff0c 100755 --- a/lib/tools/storage.dart +++ b/lib/tools/storage.dart @@ -4,6 +4,8 @@ import 'dart:convert'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:star_lock/talk/startChart/entity/relay_info_entity.dart'; +import 'package:star_lock/talk/startChart/entity/star_chart_register_node_entity.dart'; import '../login/login/entity/LoginData.dart'; import '../main/lockMian/entity/lockListInfo_entity.dart'; @@ -29,6 +31,8 @@ const String saveLockMainListData = 'lockMainListData'; const String isOpenDeBug = 'isOpenDeBug'; //是否打开 debug const String automaticLockOffTime = 'automaticLockOffTime'; //自动关锁时间 const String associationUrl = 'associationUrl'; //ios跳转微信的 url +const String starChartRegisterNodeInfo = 'starChartRegisterNodeInfo'; //星图注册信息 +const String relayInfo = 'relayInfo'; //星图中继服务器信息 class Storage { factory Storage() => _instance; @@ -234,4 +238,38 @@ class Storage { final String data = await Storage.getString(associationUrl) ?? '0'; return data; } + + // 获取星图注册节点信息 + static Future + getStarChartRegisterNodeInfo() async { + StarChartRegisterNodeEntity? starChartRegisterNodeEntity; + final String? data = await Storage.getString(starChartRegisterNodeInfo); + if (data != null && data.isNotEmpty) { + starChartRegisterNodeEntity = + StarChartRegisterNodeEntity.fromJson(jsonDecode(data)); + } + return starChartRegisterNodeEntity; + } + + // 保存星图注册节点信息 + static Future saveStarChartRegisterNodeInfo( + StarChartRegisterNodeEntity starChartRegisterNodeEntity) async { + await Storage.setString( + starChartRegisterNodeInfo, jsonEncode(starChartRegisterNodeEntity)); + } + + // 保存中继服务信息 + static Future saveRelayInfo(RelayInfoEntity relayInfoEntity) async { + await Storage.setString(relayInfo, jsonEncode(relayInfoEntity)); + } + + // 获取星图注册节点信息 + static Future getRelayInfo() async { + RelayInfoEntity? relayInfoEntity; + final String? data = await Storage.getString(relayInfo); + if (data != null && data.isNotEmpty) { + relayInfoEntity = RelayInfoEntity.fromJson(jsonDecode(data)); + } + return relayInfoEntity; + } } diff --git a/pubspec.yaml b/pubspec.yaml index fef5ed4c..df02b993 100755 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -184,6 +184,7 @@ dependencies: #加密解密 encrypt: ^5.0.1 crypto: ^3.0.3 + pointycastle: ^3.3.0 date_format: ^2.0.7 # 下拉刷新 @@ -233,7 +234,7 @@ dependencies: timelines: ^0.1.0 #侧滑删除 flutter_slidable: ^3.0.1 -# audio_service: ^0.18.12 + # audio_service: ^0.18.12 app_settings: ^5.1.1 flutter_local_notifications: ^17.0.0 fluwx: ^4.5.5 @@ -242,10 +243,13 @@ dependencies: colorfilter_generator: ^0.0.8 file_picker: ^5.3.1 # 错误日志监控 -# flutter_bugly_plugin: ^0.0.9 + # flutter_bugly_plugin: ^0.0.9 flutter_bugly: ^1.0.2 open_filex: ^4.4.0 + crc32_checksum: ^0.0.2 + fast_rsa: ^3.6.6 + dependency_overrides: #强制设置google_maps_flutter_ios 为 2.5.2 @@ -255,7 +259,6 @@ dependency_overrides: dev_dependencies: flutter_test: sdk: flutter - # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint fset provided by the package is # activated in the `analysis_options.yaml` file located at the root of your From 625c5d8e937986d700793e726eb6cff690ab1081 Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 29 Nov 2024 14:18:22 +0800 Subject: [PATCH 3/6] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4=E6=96=B0=E7=9A=84?= =?UTF-8?q?=E4=B8=AD=E7=BB=A7=E5=8D=8F=E8=AE=AE=E4=B8=AD=E7=9A=84=E4=B8=8A?= =?UTF-8?q?=E6=8A=A5=E7=AD=BE=E5=90=8D=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/login/login/starLock_login_page.dart | 8 +- lib/network/api.dart | 2 + lib/network/start_chart_api.dart | 11 ++ lib/talk/startChart/start_chart_manage.dart | 137 +++++++++++++++++--- lib/talk/udp/udp_reciverData.dart | 2 +- pubspec.yaml | 6 +- 6 files changed, 142 insertions(+), 24 deletions(-) diff --git a/lib/login/login/starLock_login_page.dart b/lib/login/login/starLock_login_page.dart index ec43b33b..ca3615b2 100755 --- a/lib/login/login/starLock_login_page.dart +++ b/lib/login/login/starLock_login_page.dart @@ -235,13 +235,13 @@ class _StarLockLoginPageState extends State { : null)), SubmitBtn( btnName: '发送上线请求', - onClick: () { + onClick: () async { // 注册星图节点信息 - StartChartManage().clientRegister(); + await StartChartManage().clientRegister(); // 查询中继信息 - StartChartManage().relayQuery(); + await StartChartManage().relayQuery(); // 发送上线请求 - StartChartManage().onlineRelayService(); + await StartChartManage().onlineRelayService(); }, ), SubmitBtn( diff --git a/lib/network/api.dart b/lib/network/api.dart index 686e9b5b..deb1701b 100755 --- a/lib/network/api.dart +++ b/lib/network/api.dart @@ -289,4 +289,6 @@ abstract class Api { '/SL-A-1.0/relay/query'; // 星图--中继查询 final String reportInformationDataURL = '/SL-A-1.0/peer/login'; // 星图--信息上报 + final String analyzeInformationOtherEndURL = + '/SL-A-1.0/peer/nslookup'; // 星图--解析对端信息 } diff --git a/lib/network/start_chart_api.dart b/lib/network/start_chart_api.dart index 1a6901c8..26e148d6 100644 --- a/lib/network/start_chart_api.dart +++ b/lib/network/start_chart_api.dart @@ -55,4 +55,15 @@ class StartChartApi extends BaseProvider { isUserBaseUrl: false, ); } + + // 星图--解析对端信息 + Future analyzeInformationOtherEnd({ + required String peerId, + }) async { + final response = await get( + _startChartHost + analyzeInformationOtherEndURL.toUrl + '?id=$peerId', + isUnShowLoading: true, + isUserBaseUrl: false, + ); + } } diff --git a/lib/talk/startChart/start_chart_manage.dart b/lib/talk/startChart/start_chart_manage.dart index 0baa1f6a..e5330c81 100644 --- a/lib/talk/startChart/start_chart_manage.dart +++ b/lib/talk/startChart/start_chart_manage.dart @@ -1,12 +1,10 @@ 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:pointycastle/asn1/asn1_parser.dart'; +import 'package:pointycastle/asn1/primitives/asn1_integer.dart'; +import 'package:pointycastle/asn1/primitives/asn1_sequence.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'; @@ -20,6 +18,12 @@ import 'package:star_lock/tools/deviceInfo_utils.dart'; import 'package:star_lock/tools/storage.dart'; import 'package:uuid/uuid.dart'; +import 'dart:convert'; +import 'dart:typed_data'; +import 'package:crypto/crypto.dart'; +import 'package:pointycastle/export.dart' as pc; // 为 Pointy Castle 添加命名空间 +import 'package:asn1lib/asn1lib.dart' as asn1lib; // Prefix for asn1lib + class StartChartManage { // 私有构造函数,防止外部直接new对象 StartChartManage._internal(); @@ -225,7 +229,7 @@ class StartChartManage { // 生成签名 final sign = await _generateSign( currentTimestamp: currentTimestamp, - privateKey: privateKey, + privateKeyHex: privateKey, ); // 获取本机所有ip地址和中继返回的外网地址 @@ -341,28 +345,127 @@ class StartChartManage { // 生成签名sing Future _generateSign({ required int currentTimestamp, - required String privateKey, + required String privateKeyHex, }) async { String resultSign = ''; try { - // 2. 将时间戳编码为小端字节序(Little Endian) + // 1. 获取当前时间戳并编码为小端字节序 Uint8List signData = Uint8List(4); - signData.buffer - .asByteData() + ByteData.view(signData.buffer) .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; + // 2. 对时间戳数据计算 SHA-256 哈希值 + Digest hash = sha256.convert(signData); + + // 3. 使用 RSA 私钥进行签名 (需要提供 RsaPrivateKey) + pc.RSAPrivateKey privateKey = + loadPrivateKey(privateKeyHex); // 需要加载你的 RSA 私钥 + Uint8List signature = rsaSign(privateKey, hash.bytes); + + // 4. 将签名结果转换为十六进制字符串 + String hexSignature = signature + .map((byte) => byte.toRadixString(16).padLeft(2, '0')) + .join(); + resultSign = hexSignature; } catch (e) { - _log(text: '❌--->生成签名时出现错误: $e'); + _log(text: '❌--->上报信息生成签名时出现错误: $e'); } return resultSign ?? ''; } + /// 自定义 PEM 格式的 RSA 私钥解析器 + pc.RSAPrivateKey loadPrivateKey(String privateKeyHex) { + // 将十六进制字符串转换为字节数组 + final uint8list = Uint8List.fromList(hexToBytes(privateKeyHex)); + try { + // 使用 asn1lib 的 ASN1Parser 解析 + final asn1Parser = asn1lib.ASN1Parser(uint8list); + final topLevelSeq = asn1Parser.nextObject() as asn1lib.ASN1Sequence; + + final modulus = bytesToBigInt( + (topLevelSeq.elements[1] as asn1lib.ASN1Integer).valueBytes()); + final privateExponent = bytesToBigInt( + (topLevelSeq.elements[3] as asn1lib.ASN1Integer).valueBytes()); + final p = bytesToBigInt( + (topLevelSeq.elements[4] as asn1lib.ASN1Integer).valueBytes()); + final q = bytesToBigInt( + (topLevelSeq.elements[5] as asn1lib.ASN1Integer).valueBytes()); + + return pc.RSAPrivateKey(modulus, privateExponent, p, q); + } catch (e) { + // 如果发生解码错误,打印错误信息 + print("Error decoding private key: $e"); + rethrow; + } + } + + // 解析对端数据 + Future analyzeInformationOtherEnd() async { + await StartChartApi.to.analyzeInformationOtherEnd(peerId: ToPeerId); + } + + // 将十六进制字符串转换为字节数组 + List hexToBytes(String hex) { + return List.generate(hex.length ~/ 2, + (i) => int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16)); + } + + BigInt bytesToBigInt(Uint8List bytes) { + return BigInt.parse( + bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(), + radix: 16, + ); + } + + /// 使用 RSA 私钥进行 PKCS#1 v1.5 签名 + Uint8List rsaSign(pc.RSAPrivateKey privateKey, List data) { + final signer = pc.RSASigner(pc.SHA256Digest(), '06052b24030203') + ..init(true, pc.PrivateKeyParameter(privateKey)); + return signer.generateSignature(Uint8List.fromList(data)).bytes; + } + + // 解析 RSA 私钥 + // RSAPrivateKey parseRSAPrivateKey1(Uint8List bytes) { + // // 这里假设私钥是以 PKCS#1 DER 编码的 + // ASN1Parser parser = ASN1Parser(bytes); + // ASN1Sequence seq = parser.nextObject() as ASN1Sequence; + // if (seq.elements == null || seq.elements!.length < 9) { + // throw ArgumentError("Invalid RSA private key"); + // } + // + // // 解析各个元素 + // ASN1Integer version = seq.elements![0] as ASN1Integer; + // ASN1Integer modulus = seq.elements![1] as ASN1Integer; + // // ASN1Integer publicExponent = seq.elements![2] as ASN1Integer; + // ASN1Integer privateExponent = seq.elements![3] as ASN1Integer; + // ASN1Integer p = seq.elements![2] as ASN1Integer; + // ASN1Integer q = seq.elements![4] as ASN1Integer; + // ASN1Integer dP = seq.elements![5] as ASN1Integer; + // ASN1Integer dQ = seq.elements![6] as ASN1Integer; + // ASN1Integer qInv = seq.elements![7] as ASN1Integer; + // + // // 将 ASN1Integer 转换为 BigInt + // BigInt modulusValue = _convertToBigInt(modulus); + // // BigInt publicExponentValue = _convertToBigInt(publicExponent); + // BigInt privateExponentValue = _convertToBigInt(privateExponent); + // BigInt pValue = _convertToBigInt(p); + // BigInt qValue = _convertToBigInt(q); + // BigInt dPValue = _convertToBigInt(dP); + // BigInt dQPValue = _convertToBigInt(dQ); + // BigInt qInvQPValue = _convertToBigInt(qInv); + // + // // 创建 RSAPrivateKey 对象 + // return RSAPrivateKey(modulusValue, privateExponentValue, pValue, qValue); + // } + // + // // 将 ASN1Integer 转换为 BigInt + // BigInt _convertToBigInt(ASN1Integer integer) { + // // 获取 ASN1Integer 的字节数据 + // Uint8List bytes = integer.valueBytes!; + // // 将字节数据转换为 BigInt + // return BigInt.parse(hex.encode(bytes), radix: 16); + // } + Future getPublicKey() async { // 从缓存中获取星图注册节点信息 final StarChartRegisterNodeEntity? starChartRegisterNodeInfo = diff --git a/lib/talk/udp/udp_reciverData.dart b/lib/talk/udp/udp_reciverData.dart index 5b99d14a..6dc9dd25 100755 --- a/lib/talk/udp/udp_reciverData.dart +++ b/lib/talk/udp/udp_reciverData.dart @@ -20,7 +20,7 @@ class CommandUDPReciverManager { if (dataSize < 4) { return; } - // AppLog.log('appReceiveUDPData:$data'); + AppLog.log('appReceiveUDPData:$data'); final Uint8List data1 = Uint8List.fromList(data); if (data1.length == 1) { diff --git a/pubspec.yaml b/pubspec.yaml index df02b993..5feface0 100755 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -184,7 +184,7 @@ dependencies: #加密解密 encrypt: ^5.0.1 crypto: ^3.0.3 - pointycastle: ^3.3.0 + pointycastle: ^3.4.0 date_format: ^2.0.7 # 下拉刷新 @@ -248,7 +248,8 @@ dependencies: open_filex: ^4.4.0 crc32_checksum: ^0.0.2 - fast_rsa: ^3.6.6 + cryptography: ^2.7.0 + asn1lib: ^1.0.0 dependency_overrides: @@ -256,6 +257,7 @@ dependency_overrides: google_maps_flutter_ios: 2.5.2 flutter_plugin_android_lifecycle: 2.0.18 + dev_dependencies: flutter_test: sdk: flutter From 81cb26682c915f77a89e42cb2afbc7f0f64fecf3 Mon Sep 17 00:00:00 2001 From: liyi Date: Sat, 30 Nov 2024 15:39:06 +0800 Subject: [PATCH 4/6] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4=E6=96=B0=E7=9A=84?= =?UTF-8?q?=E4=B8=AD=E7=BB=A7=E5=8D=8F=E8=AE=AE=E4=B8=AD=E7=9A=84=E4=B8=8A?= =?UTF-8?q?=E6=8A=A5=E7=AD=BE=E5=90=8D=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/login/login/starLock_login_page.dart | 22 +-- lib/network/start_chart_api.dart | 6 +- .../startChart/command/message_command.dart | 31 ++-- .../constant/message_type_constant.dart | 23 +++ lib/talk/startChart/entity/scp_message.dart | 99 +++++++++++- lib/talk/startChart/start_chart_manage.dart | 143 ++++++++---------- 6 files changed, 215 insertions(+), 109 deletions(-) create mode 100644 lib/talk/startChart/constant/message_type_constant.dart diff --git a/lib/login/login/starLock_login_page.dart b/lib/login/login/starLock_login_page.dart index ca3615b2..701eddc9 100755 --- a/lib/login/login/starLock_login_page.dart +++ b/lib/login/login/starLock_login_page.dart @@ -234,28 +234,20 @@ class _StarLockLoginPageState extends State { } : null)), SubmitBtn( - btnName: '发送上线请求', + btnName: '初始化星图服务', onClick: () async { - // 注册星图节点信息 - await StartChartManage().clientRegister(); - // 查询中继信息 - await StartChartManage().relayQuery(); - // 发送上线请求 - await StartChartManage().onlineRelayService(); + await StartChartManage().init(); }, ), + SubmitBtn( - btnName: '启动心跳包', + btnName: '发送回声测试消息', onClick: () { - StartChartManage().sendHeartbeatMessage(); - }, - ), - SubmitBtn( - btnName: '结束心跳包', - onClick: () { - StartChartManage().stopHeartbeat(); + StartChartManage().sendEchoMessage( + ToPeerId: '3phX8Ng2cZHz5NtP8xAf6nYy2z1BYytoejgjoHrWMGhH'); }, ), + SizedBox(height: 50.w), Row( mainAxisAlignment: MainAxisAlignment.center, diff --git a/lib/network/start_chart_api.dart b/lib/network/start_chart_api.dart index 26e148d6..925ce779 100644 --- a/lib/network/start_chart_api.dart +++ b/lib/network/start_chart_api.dart @@ -45,7 +45,7 @@ class StartChartApi extends BaseProvider { } // 星图--上报信息至发现服务 - Future reportInformation({ + Future reportInformation({ required ReportInformationData reportInformationData, }) async { final response = await post( @@ -54,10 +54,11 @@ class StartChartApi extends BaseProvider { isUnShowLoading: true, isUserBaseUrl: false, ); + return response; } // 星图--解析对端信息 - Future analyzeInformationOtherEnd({ + Future analyzeInformationOtherEnd({ required String peerId, }) async { final response = await get( @@ -65,5 +66,6 @@ class StartChartApi extends BaseProvider { isUnShowLoading: true, isUserBaseUrl: false, ); + return response; } } diff --git a/lib/talk/startChart/command/message_command.dart b/lib/talk/startChart/command/message_command.dart index 0082d83c..d05fc0c5 100644 --- a/lib/talk/startChart/command/message_command.dart +++ b/lib/talk/startChart/command/message_command.dart @@ -1,3 +1,4 @@ +import 'package:star_lock/talk/startChart/constant/message_type_constant.dart'; import 'package:star_lock/talk/startChart/constant/payload_type_constant.dart'; import 'package:star_lock/talk/startChart/constant/protocol_flag_constant.dart'; import 'package:star_lock/talk/startChart/entity/scp_message.dart'; @@ -7,10 +8,16 @@ class MessageCommand { static List goOnlineRelay() { String serializedBytesString = ScpMessage( ProtocolFlag: ProtocolFlagConstant.scp01, - MessageType: PayloadTypeConstant.goOnline, + MessageType: MessageTypeConstant.Req, MessageId: 1, SpTotal: 0, SpIndex: 0, + FromPeerId: 'ToPeerId', + ToPeerId: 'ToPeerId', + Payload: 'hello', + PayloadCRC: 55230, + PayloadLength: 5, + PayloadType: PayloadTypeConstant.goOnline, ).serialize(); return _hexToBytes(serializedBytesString); } @@ -21,8 +28,8 @@ class MessageCommand { required String FromPeerId, }) { ScpMessage message = ScpMessage( - ProtocolFlag: ProtocolFlagConstant.scp01, - MessageType: PayloadTypeConstant.echoTest, + ProtocolFlag: ProtocolFlagConstant.scp01, + MessageType: MessageTypeConstant.Req, MessageId: 1, SpTotal: 0, SpIndex: 0, @@ -31,7 +38,7 @@ class MessageCommand { Payload: 'hello', PayloadCRC: 55230, PayloadLength: 5, - PayloadType: 1, + PayloadType: PayloadTypeConstant.echoTest, ); String serializedBytesString = message.serialize(); @@ -41,17 +48,17 @@ class MessageCommand { // 心跳消息 static List heartbeatMessage() { ScpMessage message = ScpMessage( - ProtocolFlag: ProtocolFlagConstant.scp01, - MessageType: PayloadTypeConstant.heartbeat, + ProtocolFlag: ProtocolFlagConstant.scp01, + MessageType: MessageTypeConstant.Req, MessageId: 1, SpTotal: 0, SpIndex: 0, - // FromPeerId: FromPeerId, - // ToPeerId: ToPeerId, - // Payload: 'hello', - // PayloadCRC: 55230, - // PayloadLength: 5, - // PayloadType: 1, + FromPeerId: 'FromPeerId', + ToPeerId: 'ToPeerId', + Payload: 'hello', + PayloadCRC: 55230, + PayloadLength: 5, + PayloadType: PayloadTypeConstant.heartbeat, ); String serializedBytesString = message.serialize(); diff --git a/lib/talk/startChart/constant/message_type_constant.dart b/lib/talk/startChart/constant/message_type_constant.dart new file mode 100644 index 00000000..ea674e8d --- /dev/null +++ b/lib/talk/startChart/constant/message_type_constant.dart @@ -0,0 +1,23 @@ +class MessageTypeConstant { + // Req 普通的主动消息, 一般用于指令类消息, 需要对方回复, 即使没有任何回复内容, 也需要回复一个空包 + // 如果没有收到Resp, 则应在超时时间后重发Req, 重发次数不限, 但是重发间隔应逐渐增加 + // 直到收到Resp后才可以从缓存区域删除Req + static const int Req = 1; + + // Resp 回复消息, 用于回复Req消息 + // 一般用于指令类消息的完成后回复, 如果指令耗时较长也没关系, 例如重发req的超时时间为1秒, 那么我执行指令超过1秒导致请求方重发了,也没事呀. + // 对于超时超长的异步任务,不应该使用默认的Req/Resp模式,而应该在收到请求后即回复确认, 在异步完成任务后再主动发送req告诉发起处任务已完成. + static const int Resp = 2; + + // RealTimeData 实时数据, 无需回复, 丢包不重发,乱序不重排, 一般用于音视频场景 + // 需要 额外的手段来检测在线状态, 否则会无限发送导致变成Dos攻击 + // 例如发送方需要每秒发送1个普通的状态确认包(Req)询问接收方是否仍处于等待接收状态, 如果询问 结果为否/超时,则停止发送. + // 这种补充手段是硬性要求, 不能期望接收方主动发消息来停止, 因为接收方可能不是该业务端口, 或者是掉线了. + static const int RealTimeData = 3; + + // InvalidReq Req/Resp模型中的error Resp,即为告知无效请求,类似HTTP协议的500服务器错误,payload里面是一个utf8的字符串表明错误信息 + // 可能的情况举例:1.0的终端收到了1.1版本的新指令;APP终端收到了关闭电源指令(无法处理) + // 对于用户界面,建议的交互为“未知错误” + // 对于程序开发,需要避免出现,消息处理函数将该事件记录到错误日志 + static const int InvalidReq = 4; +} diff --git a/lib/talk/startChart/entity/scp_message.dart b/lib/talk/startChart/entity/scp_message.dart index f7dc124e..113cd974 100644 --- a/lib/talk/startChart/entity/scp_message.dart +++ b/lib/talk/startChart/entity/scp_message.dart @@ -58,6 +58,14 @@ class ScpMessage { 'PayloadLength': PayloadLength, 'Payload': Payload, }; + + + + } + + @override + String toString() { + return 'ScpMessage{ProtocolFlag: $ProtocolFlag, MessageType: $MessageType, MessageId: $MessageId, SpTotal: $SpTotal, SpIndex: $SpIndex, FromPeerId: $FromPeerId, ToPeerId: $ToPeerId, PayloadType: $PayloadType, PayloadCRC: $PayloadCRC, PayloadLength: $PayloadLength, Payload: $Payload}'; } String serialize() { @@ -133,11 +141,100 @@ class ScpMessage { // 转16进制字符串 final bytesToHexString = bytesToHex(bytes); - return bytesToHexString; } + // 反序列化方法 + static ScpMessage deserialize(List bytes) { + final message = ScpMessage(); + + int offset = 0; + + // ProtocolFlag (4 bytes) + if (bytes.length - offset >= 4) { + message.ProtocolFlag = utf8.decode(bytes.sublist(offset, offset + 4)); + offset += 4; + } + + // MessageType (1 byte) + if (bytes.length - offset >= 1) { + message.MessageType = bytes[offset]; + offset += 1; + } + + // MessageId (2 bytes, little-endian) + if (bytes.length - offset >= 2) { + message.MessageId = (bytes[offset + 1] << 8) | bytes[offset]; + offset += 2; + } + + // SpTotal (1 byte) + if (bytes.length - offset >= 1) { + message.SpTotal = bytes[offset]; + offset += 1; + } + + // SpIndex (1 byte) + if (bytes.length - offset >= 1) { + message.SpIndex = bytes[offset]; + offset += 1; + } + + // FromPeerId (字符串,长度固定为44字节) + if (bytes.length - offset >= 44) { + message.FromPeerId = utf8.decode(bytes.sublist(offset, offset + 44)); + offset += 44; + } + + // ToPeerId (字符串,长度固定为44字节) + if (bytes.length - offset >= 44) { + message.ToPeerId = utf8.decode(bytes.sublist(offset, offset + 44)); + offset += 44; + } + + // PayloadType (2 bytes, little-endian) + if (bytes.length - offset >= 2) { + message.PayloadType = (bytes[offset + 1] << 8) | bytes[offset]; + offset += 2; + } + + // PayloadCRC (2 bytes, little-endian) + if (bytes.length - offset >= 2) { + message.PayloadCRC = (bytes[offset + 1] << 8) | bytes[offset]; + offset += 2; + } + + // PayloadLength (4 bytes, big-endian) + if (bytes.length - offset >= 4) { + message.PayloadLength = (bytes[offset] | + (bytes[offset + 1] << 8) | + (bytes[offset + 2] << 16) | + (bytes[offset + 3] << 24)); + offset += 4; + } + + // Payload (字符串,转换为字节) + if (message.PayloadLength != null && + bytes.length - offset >= message.PayloadLength!) { + message.Payload = + utf8.decode(bytes.sublist(offset, offset + message.PayloadLength!)); + offset += message.PayloadLength!; + } + + return message; + } + static String bytesToHex(List bytes) { return bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(''); } + + // 辅助函数:将16进制字符串转换为字节数组 + static List hexToBytes(String hexString) { + final bytes = []; + for (int i = 0; i < hexString.length; i += 2) { + final hexByte = hexString.substring(i, i + 2); + bytes.add(int.parse(hexByte, radix: 16)); + } + return bytes; + } } diff --git a/lib/talk/startChart/start_chart_manage.dart b/lib/talk/startChart/start_chart_manage.dart index e5330c81..9d67903d 100644 --- a/lib/talk/startChart/start_chart_manage.dart +++ b/lib/talk/startChart/start_chart_manage.dart @@ -5,6 +5,8 @@ import 'dart:typed_data'; import 'package:pointycastle/asn1/asn1_parser.dart'; import 'package:pointycastle/asn1/primitives/asn1_integer.dart'; import 'package:pointycastle/asn1/primitives/asn1_sequence.dart'; +import 'package:pointycastle/asymmetric/api.dart'; +import 'package:pointycastle/digests/sha256.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'; @@ -13,6 +15,7 @@ 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/scp_message.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'; @@ -43,6 +46,7 @@ class StartChartManage { late String remoteHost = ''; // 远程主机地址(服务器返回) late int remotePort = 0; // 远程主机端口(服务器返回) final int localPort = 62289; // 本地端口 + String localPublicHost = ''; // 本地公网ip地址 int heartbeatIntervalTime = 1; // 心跳包间隔时间(s) Timer? _heartBeatTimer; // 心跳包定时器 @@ -50,27 +54,47 @@ class StartChartManage { String ToPeerId = ''; // 对端ID String FromPeerId = ''; // 我的ID + + bool _isLoginSuccessfulToStartChart = false; // 是否在星图登录成功 + + // 星图服务初始化 + Future init() async { + // 客户端注册 + await _clientRegister(); + + // 中继查询 + await _relayQuery(); + + // 上报 + await reportInformation(); + + // 初始化udp服务 + await _onlineRelayService(); + + // 发送心跳消息 + _sendHeartbeatMessage(); + + // 发送送上线消息 + await _sendOnlineMessage(); + } + /// 客户端注册 - Future 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 _clientRegister() async { + _log(text: '开始注册客户端'); + final StarChartRegisterNodeEntity requestStarChartRegisterNode = + await _requestStarChartRegisterNode(); + await _saveStarChartRegisterNodeToStorage(requestStarChartRegisterNode); + _log(text: '获取到星图注册节点信息:$requestStarChartRegisterNode'); } // 中继查询 - Future relayQuery() async { + Future _relayQuery() async { final RelayInfoEntity relayInfoEntity = await StartChartApi.to.relayQueryInfo(); _saveRelayInfoEntityToStorage(relayInfoEntity); + if (relayInfoEntity.client_addr != null) { + localPublicHost = relayInfoEntity.client_addr!; + } if (relayInfoEntity.relay_list?.length != 0) { final data = relayInfoEntity.relay_list?[0]; @@ -78,7 +102,7 @@ class StartChartManage { final parseUdpUrl = _parseUdpUrl(data?.listenAddr ?? ''); remoteHost = parseUdpUrl['host'] ?? ''; remotePort = parseUdpUrl['port'] ?? ''; - _log(text: '中继信息:${data}'); + _log(text: '中继信息----》${data}'); } } @@ -88,9 +112,8 @@ class StartChartManage { } } - // 在中继服务器中上线 - Future onlineRelayService() async { - await relayQuery(); + // 初始化udp + Future _onlineRelayService() async { var addressIListenFrom = InternetAddress.anyIPv4; RawDatagramSocket.bind(addressIListenFrom, localPort) .then((RawDatagramSocket socket) { @@ -101,14 +124,6 @@ class StartChartManage { /// 设置数据接收回调 _onReceiveData(_udpSocket!); - - // 发送上线消息 - //_sendOnlineMessage(); - // 发送回声测试消息 - //_sendEchoMessage(); - - // 上报信息 - reportInformation(); }).catchError((error) { _log(text: 'Failed to bind UDP socket: $error'); }); @@ -121,6 +136,10 @@ class StartChartManage { Datagram? dg = socket.receive(); try { _log(text: '收到消息---> 长度:${dg?.data?.length}, 数据:${dg?.data}'); + if (dg?.data != null) { + final deserialize = ScpMessage.deserialize(dg!.data); + _log(text: 'Udp收到结构体数据---》$deserialize'); + } } catch (e) { _log(text: '❌ Udp ----> $e'); } @@ -133,29 +152,33 @@ class StartChartManage { _log(text: '上报信息至发现服务'); // 构建参数 ReportInformationData data = await _makeReportInformationData(); - await StartChartApi.to.reportInformation( + final response = await StartChartApi.to.reportInformation( reportInformationData: data, ); + if (response.statusCode == 200) { + // TODO 登录成功之后的逻辑 + _log(text: '星图登录成功'); + } } // 发送上线消息 - void _sendOnlineMessage() { + Future _sendOnlineMessage() async { // 组装上线消息 final message = MessageCommand.goOnlineRelay(); - _sendMessage(message: message); + await _sendMessage(message: message); } // 发送回声测试消息 - void _sendEchoMessage() { + void sendEchoMessage({required String ToPeerId}) async { final message = MessageCommand.echoMessage( ToPeerId: ToPeerId, FromPeerId: FromPeerId, ); - _sendMessage(message: message); + await _sendMessage(message: message); } // 发送心跳包消息 - void sendHeartbeatMessage() { + void _sendHeartbeatMessage() { if (_heartBeatTimerRunning) { _log(text: '心跳已经开始了. 请勿重复发送心跳包消息'); return; @@ -164,9 +187,9 @@ class StartChartManage { Duration( seconds: heartbeatIntervalTime, ), - (Timer timer) { + (Timer timer) async { final List message = MessageCommand.heartbeatMessage(); - _sendMessage(message: message); + await _sendMessage(message: message); }, ); _heartBeatTimerRunning = true; @@ -181,9 +204,14 @@ class StartChartManage { } // 发送消息 - void _sendMessage({required List message}) { + Future _sendMessage({required List message}) async { _log(text: '发送给中继的消息体:${message},序列化之后的数据:【${bytesToHex(message)}】'); - _udpSocket!.send(message, InternetAddress(remoteHost), remotePort); + var result = await _udpSocket?.send( + message, InternetAddress(remoteHost), remotePort); + if (result != message.length) { + AppLog.log('❌Udp ----> send data error $result ${message.length}'); + _udpSocket = null; + } } // 请求注册节点 @@ -334,7 +362,6 @@ class StartChartManage { return {'host': ip, 'port': port}; } } - throw FormatException('无法解析 URL 格式: $url'); } @@ -424,48 +451,6 @@ class StartChartManage { return signer.generateSignature(Uint8List.fromList(data)).bytes; } - // 解析 RSA 私钥 - // RSAPrivateKey parseRSAPrivateKey1(Uint8List bytes) { - // // 这里假设私钥是以 PKCS#1 DER 编码的 - // ASN1Parser parser = ASN1Parser(bytes); - // ASN1Sequence seq = parser.nextObject() as ASN1Sequence; - // if (seq.elements == null || seq.elements!.length < 9) { - // throw ArgumentError("Invalid RSA private key"); - // } - // - // // 解析各个元素 - // ASN1Integer version = seq.elements![0] as ASN1Integer; - // ASN1Integer modulus = seq.elements![1] as ASN1Integer; - // // ASN1Integer publicExponent = seq.elements![2] as ASN1Integer; - // ASN1Integer privateExponent = seq.elements![3] as ASN1Integer; - // ASN1Integer p = seq.elements![2] as ASN1Integer; - // ASN1Integer q = seq.elements![4] as ASN1Integer; - // ASN1Integer dP = seq.elements![5] as ASN1Integer; - // ASN1Integer dQ = seq.elements![6] as ASN1Integer; - // ASN1Integer qInv = seq.elements![7] as ASN1Integer; - // - // // 将 ASN1Integer 转换为 BigInt - // BigInt modulusValue = _convertToBigInt(modulus); - // // BigInt publicExponentValue = _convertToBigInt(publicExponent); - // BigInt privateExponentValue = _convertToBigInt(privateExponent); - // BigInt pValue = _convertToBigInt(p); - // BigInt qValue = _convertToBigInt(q); - // BigInt dPValue = _convertToBigInt(dP); - // BigInt dQPValue = _convertToBigInt(dQ); - // BigInt qInvQPValue = _convertToBigInt(qInv); - // - // // 创建 RSAPrivateKey 对象 - // return RSAPrivateKey(modulusValue, privateExponentValue, pValue, qValue); - // } - // - // // 将 ASN1Integer 转换为 BigInt - // BigInt _convertToBigInt(ASN1Integer integer) { - // // 获取 ASN1Integer 的字节数据 - // Uint8List bytes = integer.valueBytes!; - // // 将字节数据转换为 BigInt - // return BigInt.parse(hex.encode(bytes), radix: 16); - // } - Future getPublicKey() async { // 从缓存中获取星图注册节点信息 final StarChartRegisterNodeEntity? starChartRegisterNodeInfo = From 92490f22afef7c8f3910a8e2535c22b29526ed13 Mon Sep 17 00:00:00 2001 From: liyi Date: Mon, 2 Dec 2024 15:43:59 +0800 Subject: [PATCH 5/6] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4=E6=96=B0=E7=9A=84?= =?UTF-8?q?=E4=B8=AD=E7=BB=A7=E5=8D=8F=E8=AE=AE=E4=B8=AD=E7=9A=84=E4=B8=8A?= =?UTF-8?q?=E6=8A=A5=E7=AD=BE=E5=90=8D=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../startChart/command/message_command.dart | 25 ++- lib/talk/startChart/entity/scp_message.dart | 74 +++++-- lib/talk/startChart/start_chart_manage.dart | 180 ++++++++++++++---- pubspec.yaml | 2 +- 4 files changed, 221 insertions(+), 60 deletions(-) diff --git a/lib/talk/startChart/command/message_command.dart b/lib/talk/startChart/command/message_command.dart index d05fc0c5..76a272a0 100644 --- a/lib/talk/startChart/command/message_command.dart +++ b/lib/talk/startChart/command/message_command.dart @@ -1,19 +1,24 @@ +import 'package:crc32_checksum/crc32_checksum.dart'; import 'package:star_lock/talk/startChart/constant/message_type_constant.dart'; import 'package:star_lock/talk/startChart/constant/payload_type_constant.dart'; import 'package:star_lock/talk/startChart/constant/protocol_flag_constant.dart'; import 'package:star_lock/talk/startChart/entity/scp_message.dart'; class MessageCommand { + /// 客户端去中继上线命令 - static List goOnlineRelay() { + static List goOnlineRelay({ + required String FromPeerId, + required String ToPeerId, +}) { String serializedBytesString = ScpMessage( ProtocolFlag: ProtocolFlagConstant.scp01, MessageType: MessageTypeConstant.Req, MessageId: 1, SpTotal: 0, SpIndex: 0, - FromPeerId: 'ToPeerId', - ToPeerId: 'ToPeerId', + FromPeerId: FromPeerId, + ToPeerId: ToPeerId, Payload: 'hello', PayloadCRC: 55230, PayloadLength: 5, @@ -46,15 +51,18 @@ class MessageCommand { } // 心跳消息 - static List heartbeatMessage() { + static List heartbeatMessage({ + required String FromPeerId, + required String ToPeerId, + }) { ScpMessage message = ScpMessage( ProtocolFlag: ProtocolFlagConstant.scp01, MessageType: MessageTypeConstant.Req, MessageId: 1, SpTotal: 0, SpIndex: 0, - FromPeerId: 'FromPeerId', - ToPeerId: 'ToPeerId', + FromPeerId: FromPeerId, + ToPeerId: ToPeerId, Payload: 'hello', PayloadCRC: 55230, PayloadLength: 5, @@ -73,4 +81,9 @@ class MessageCommand { } return bytes; } + + static int calculationCrc(payload){ + var checkSumResult = Crc32.calculate(payload); + return checkSumResult; + } } diff --git a/lib/talk/startChart/entity/scp_message.dart b/lib/talk/startChart/entity/scp_message.dart index 113cd974..3fc80678 100644 --- a/lib/talk/startChart/entity/scp_message.dart +++ b/lib/talk/startChart/entity/scp_message.dart @@ -144,85 +144,135 @@ class ScpMessage { return bytesToHexString; } - // 反序列化方法 static ScpMessage deserialize(List bytes) { final message = ScpMessage(); - int offset = 0; // ProtocolFlag (4 bytes) if (bytes.length - offset >= 4) { message.ProtocolFlag = utf8.decode(bytes.sublist(offset, offset + 4)); offset += 4; + } else { + throw FormatException("Invalid ProtocolFlag length"); } // MessageType (1 byte) if (bytes.length - offset >= 1) { message.MessageType = bytes[offset]; offset += 1; + } else { + throw FormatException("Invalid MessageType length"); } // MessageId (2 bytes, little-endian) if (bytes.length - offset >= 2) { message.MessageId = (bytes[offset + 1] << 8) | bytes[offset]; offset += 2; + } else { + throw FormatException("Invalid MessageId length"); } // SpTotal (1 byte) if (bytes.length - offset >= 1) { message.SpTotal = bytes[offset]; offset += 1; + } else { + throw FormatException("Invalid SpTotal length"); } // SpIndex (1 byte) if (bytes.length - offset >= 1) { message.SpIndex = bytes[offset]; offset += 1; + } else { + throw FormatException("Invalid SpIndex length"); } // FromPeerId (字符串,长度固定为44字节) if (bytes.length - offset >= 44) { - message.FromPeerId = utf8.decode(bytes.sublist(offset, offset + 44)); + message.FromPeerId = utf8.decode(bytes.sublist(offset, offset + 44)).trimRight(); offset += 44; + } else { + throw FormatException("Invalid FromPeerId length"); } // ToPeerId (字符串,长度固定为44字节) if (bytes.length - offset >= 44) { - message.ToPeerId = utf8.decode(bytes.sublist(offset, offset + 44)); + message.ToPeerId = utf8.decode(bytes.sublist(offset, offset + 44)).trimRight(); offset += 44; + } else { + throw FormatException("Invalid ToPeerId length"); } // PayloadType (2 bytes, little-endian) if (bytes.length - offset >= 2) { message.PayloadType = (bytes[offset + 1] << 8) | bytes[offset]; offset += 2; + } else { + throw FormatException("Invalid PayloadType length"); } // PayloadCRC (2 bytes, little-endian) if (bytes.length - offset >= 2) { message.PayloadCRC = (bytes[offset + 1] << 8) | bytes[offset]; offset += 2; + } else { + throw FormatException("Invalid PayloadCRC length"); } - // PayloadLength (4 bytes, big-endian) + // PayloadLength (4 bytes, little-endian) if (bytes.length - offset >= 4) { + // 打印PayloadLength对应的4个字节 + print('PayloadLength bytes: ${bytes.sublist(offset, offset + 4)}'); + message.PayloadLength = (bytes[offset] | - (bytes[offset + 1] << 8) | - (bytes[offset + 2] << 16) | - (bytes[offset + 3] << 24)); + (bytes[offset + 1] << 8) | + (bytes[offset + 2] << 16) | + (bytes[offset + 3] << 24)); // 修正为 little-endian offset += 4; + } else { + throw FormatException("Invalid PayloadLength length"); } // Payload (字符串,转换为字节) - if (message.PayloadLength != null && - bytes.length - offset >= message.PayloadLength!) { - message.Payload = - utf8.decode(bytes.sublist(offset, offset + message.PayloadLength!)); + if (message.PayloadLength != null && bytes.length - offset >= message.PayloadLength!) { + message.Payload = utf8.decode(bytes.sublist(offset, offset + message.PayloadLength!)); offset += message.PayloadLength!; + } else { + throw FormatException("Invalid Payload or PayloadLength"); } + // 验证PayloadCRC + // if (message.Payload != null) { + // var crcBytes = List.from(utf8.encode(message.Payload!)); + // var calculatedCrc = _calculateCrc16(crcBytes); + // if (calculatedCrc != message.PayloadCRC) { + // throw FormatException("PayloadCRC verification failed. Expected: ${message.PayloadCRC}, Actual: $calculatedCrc"); + // } + // } + return message; } + // CRC-16 计算函数(示例实现,可能需要根据具体协议调整) + static int _calculateCrc16(List data) { + const poly = 0x8005; + int crc = 0xFFFF; + + for (final b in data) { + crc ^= b << 8; + for (int i = 0; i < 8; i++) { + if ((crc & 0x8000) != 0) { + crc = (crc << 1) ^ poly; + } else { + crc <<= 1; + } + crc &= 0xFFFF; + } + } + + return crc; + } + static String bytesToHex(List bytes) { return bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(''); diff --git a/lib/talk/startChart/start_chart_manage.dart b/lib/talk/startChart/start_chart_manage.dart index 9d67903d..1de800a0 100644 --- a/lib/talk/startChart/start_chart_manage.dart +++ b/lib/talk/startChart/start_chart_manage.dart @@ -2,11 +2,17 @@ import 'dart:async'; import 'dart:io'; import 'dart:typed_data'; +import 'package:convert/convert.dart'; +import 'package:cryptography/cryptography.dart'; +import 'package:encrypt/encrypt.dart'; +import 'package:fast_rsa/fast_rsa.dart' as fastRsa; +import 'package:get/get.dart'; import 'package:pointycastle/asn1/asn1_parser.dart'; import 'package:pointycastle/asn1/primitives/asn1_integer.dart'; import 'package:pointycastle/asn1/primitives/asn1_sequence.dart'; import 'package:pointycastle/asymmetric/api.dart'; import 'package:pointycastle/digests/sha256.dart'; +import 'package:pointycastle/export.dart' as pc; import 'package:star_lock/app_settings/app_settings.dart'; import 'package:star_lock/flavors.dart'; import 'package:star_lock/network/start_chart_api.dart'; @@ -55,11 +61,11 @@ class StartChartManage { String ToPeerId = ''; // 对端ID String FromPeerId = ''; // 我的ID - bool _isLoginSuccessfulToStartChart = false; // 是否在星图登录成功 + final String echoPeerId = '3phX8Ng2cZHz5NtP8xAf6nYy2z1BYytoejgjoHrWMGhH'; // 星图服务初始化 Future init() async { - // 客户端注册 + // 节点注册 await _clientRegister(); // 中继查询 @@ -70,12 +76,6 @@ class StartChartManage { // 初始化udp服务 await _onlineRelayService(); - - // 发送心跳消息 - _sendHeartbeatMessage(); - - // 发送送上线消息 - await _sendOnlineMessage(); } /// 客户端注册 @@ -85,6 +85,8 @@ class StartChartManage { await _requestStarChartRegisterNode(); await _saveStarChartRegisterNodeToStorage(requestStarChartRegisterNode); _log(text: '获取到星图注册节点信息:$requestStarChartRegisterNode'); + FromPeerId = requestStarChartRegisterNode.peer!.id ?? ''; + ToPeerId = echoPeerId; } // 中继查询 @@ -98,12 +100,11 @@ class StartChartManage { 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}'); } + _log(text: '中继信息----》${relayInfoEntity}'); } void closeUdpSocket() { @@ -138,6 +139,7 @@ class StartChartManage { _log(text: '收到消息---> 长度:${dg?.data?.length}, 数据:${dg?.data}'); if (dg?.data != null) { final deserialize = ScpMessage.deserialize(dg!.data); + _log(text: '=============${bytesToHex(dg!.data)}'); _log(text: 'Udp收到结构体数据---》$deserialize'); } } catch (e) { @@ -156,15 +158,19 @@ class StartChartManage { reportInformationData: data, ); if (response.statusCode == 200) { - // TODO 登录成功之后的逻辑 _log(text: '星图登录成功'); + // 发送送上线消息 + await _sendOnlineMessage(); + // 发送心跳消息 + _sendHeartbeatMessage(); } } // 发送上线消息 Future _sendOnlineMessage() async { // 组装上线消息 - final message = MessageCommand.goOnlineRelay(); + final message = MessageCommand.goOnlineRelay( + FromPeerId: FromPeerId, ToPeerId: ToPeerId); await _sendMessage(message: message); } @@ -178,7 +184,10 @@ class StartChartManage { } // 发送心跳包消息 - void _sendHeartbeatMessage() { + void _sendHeartbeatMessage() async { + // 从缓存中获取中继信息 + final RelayInfoEntity? relayInfoEntity = await Storage.getRelayInfo(); + final String relayPeerId = relayInfoEntity?.relay_list?[0].peerID ?? ''; if (_heartBeatTimerRunning) { _log(text: '心跳已经开始了. 请勿重复发送心跳包消息'); return; @@ -188,7 +197,10 @@ class StartChartManage { seconds: heartbeatIntervalTime, ), (Timer timer) async { - final List message = MessageCommand.heartbeatMessage(); + final List message = MessageCommand.heartbeatMessage( + FromPeerId: FromPeerId, + ToPeerId: relayPeerId, + ); await _sendMessage(message: message); }, ); @@ -248,15 +260,16 @@ class StartChartManage { // 构造上报信息数据参数 Future _makeReportInformationData() async { - // 获取当前时间戳 - int currentTimestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000; + // 从缓存中获取中继信息 + final RelayInfoEntity? relayInfoEntity = await Storage.getRelayInfo(); + // 获取公钥 final publicKey = await getPublicKey(); // 获取私钥 final privateKey = await getPrivateKey(); // 生成签名 final sign = await _generateSign( - currentTimestamp: currentTimestamp, + currentTimestamp: relayInfoEntity!.time ?? 0, privateKeyHex: privateKey, ); @@ -264,8 +277,7 @@ class StartChartManage { final List 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 ?? '', @@ -278,7 +290,7 @@ class StartChartManage { public_key: publicKey, listen_addr: listenAddrDataList, relay_service: relayServiceData, - time: currentTimestamp, + time: relayInfoEntity.time ?? 0, sign: sign, ); @@ -289,7 +301,7 @@ class StartChartManage { Future> _makeListenAddrDataList() async { final List listenAddrDataList = []; final List localIp = await _getAllIpAddresses(); - // 从缓存中获取中继信息 + // 从缓存中获取中继信息,取出返回的客户端ip地址 final RelayInfoEntity? relayInfoEntity = await Storage.getRelayInfo(); if (relayInfoEntity != null && relayInfoEntity.client_addr != null) { listenAddrDataList.add( @@ -313,7 +325,7 @@ class StartChartManage { return listenAddrDataList ?? []; } - /// 获取本机所有ip + /// 获取本机所有 IP 地址 Future> _getAllIpAddresses() async { final List ipAddresses = []; try { @@ -324,9 +336,21 @@ class StartChartManage { for (final interface in interfaces) { for (final address in interface.addresses) { - if (address.address.isNotEmpty && - !IpConstant.reportExcludeIp.contains(address.address)) { - ipAddresses.add(address.address); + // 获取原始 IP 地址 + String ipAddress = address.address; + + // 解码 URL 编码的字符串 + ipAddress = Uri.decodeFull(ipAddress); + + // 移除 IPv6 地址中的接口标识符(如果有) + if (ipAddress.contains('%')) { + ipAddress = ipAddress.split('%').first; + } + + // 确保 IP 地址不为空且不在排除列表中 + if (ipAddress.isNotEmpty && + !IpConstant.reportExcludeIp.contains(ipAddress)) { + ipAddresses.add(ipAddress); } } } @@ -376,30 +400,104 @@ class StartChartManage { }) async { String resultSign = ''; try { - // 1. 获取当前时间戳并编码为小端字节序 - Uint8List signData = Uint8List(4); - ByteData.view(signData.buffer) - .setUint32(0, currentTimestamp, Endian.little); + // 1. 将 32 位时间戳以小端字节序编码为二进制数据 + Uint8List signData = encodeTimestampToLittleEndianBytes(currentTimestamp); - // 2. 对时间戳数据计算 SHA-256 哈希值 - Digest hash = sha256.convert(signData); + // 2.将十六进制字符串转换为字节数组 + List privateKeyBytes = hexToBytes(privateKeyHex); - // 3. 使用 RSA 私钥进行签名 (需要提供 RsaPrivateKey) - pc.RSAPrivateKey privateKey = - loadPrivateKey(privateKeyHex); // 需要加载你的 RSA 私钥 - Uint8List signature = rsaSign(privateKey, hash.bytes); - - // 4. 将签名结果转换为十六进制字符串 - String hexSignature = signature - .map((byte) => byte.toRadixString(16).padLeft(2, '0')) - .join(); - resultSign = hexSignature; + // 3.将私钥转换为 PEM 格式 + final pemPrivateKey = + convertToPemPrivateKey(privateKeyBytes, isPKCS8: true); + // 4.签名 + var result = await fastRsa.RSA + .signPKCS1v15Bytes(signData, fastRsa.Hash.SHA256, pemPrivateKey); + resultSign = hex.encode(result); } catch (e) { _log(text: '❌--->上报信息生成签名时出现错误: $e'); + e.printError(); } return resultSign ?? ''; } +// 将 32 位时间戳以小端字节序编码为二进制数据 + Uint8List encodeTimestampToLittleEndianBytes(int timestamp) { + // 创建一个 4 字节的 ByteData 对象 + ByteData byteData = ByteData(4); + + // 将 32 位时间戳写入 ByteData,使用小端字节序 + byteData.setUint32(0, timestamp, Endian.little); + + // 将 ByteData 转换为 Uint8List + Uint8List bytes = byteData.buffer.asUint8List(); + + return bytes; + } + +// 将字节数组转换为 PEM 格式的公钥 + String convertToPemPublicKey(List publicKeyBytes, + {bool isPKCS8 = true}) { + // 将字节数组转换为Base64编码的字符串 + String base64PublicKey = base64Encode(publicKeyBytes); +// 添加PEM格式的头尾标签 + String pemHeader; + String pemFooter; + if (isPKCS8) { + // 添加PEM格式的头尾标签 + pemHeader = "-----BEGIN PUBLIC KEY-----"; + pemFooter = "-----END PUBLIC KEY-----"; + } else { + // 添加PEM格式的头尾标签 + pemHeader = "-----BEGIN RSA PUBLIC KEY-----"; + pemFooter = "-----END RSA PUBLIC KEY-----"; + } + + // 将Base64字符串分行为每行64个字符 + const lineLength = 64; + List lines = []; + for (int i = 0; i < base64PublicKey.length; i += lineLength) { + int end = (i + lineLength < base64PublicKey.length) + ? i + lineLength + : base64PublicKey.length; + lines.add(base64PublicKey.substring(i, end)); + } + + // 组合成完整的PEM格式字符串 + return "$pemHeader\n${lines.join('\n')}\n$pemFooter"; + } + + String convertToPemPrivateKey(List privateKeyBytes, + {bool isPKCS8 = true}) { + // 将字节数组转换为Base64编码的字符串 + String base64PrivateKey = base64Encode(privateKeyBytes); + + // 添加PEM格式的头尾标签 + String pemHeader; + String pemFooter; + + if (isPKCS8) { + pemHeader = "-----BEGIN PRIVATE KEY-----"; + pemFooter = "-----END PRIVATE KEY-----"; + } else { + pemHeader = "-----BEGIN RSA PRIVATE KEY-----"; + pemFooter = "-----END RSA PRIVATE KEY-----"; + } + + // 将Base64字符串分行为每行64个字符 + const lineLength = 64; + List lines = []; // 用于存储每一行 + + for (int i = 0; i < base64PrivateKey.length; i += lineLength) { + int end = (i + lineLength < base64PrivateKey.length) + ? i + lineLength + : base64PrivateKey.length; + lines.add(base64PrivateKey.substring(i, end)); + } + + // 组合成完整的PEM格式字符串 + return "$pemHeader\n${lines.join('\n')}\n$pemFooter"; + } + /// 自定义 PEM 格式的 RSA 私钥解析器 pc.RSAPrivateKey loadPrivateKey(String privateKeyHex) { // 将十六进制字符串转换为字节数组 diff --git a/pubspec.yaml b/pubspec.yaml index 5feface0..53741cbd 100755 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -250,7 +250,7 @@ dependencies: crc32_checksum: ^0.0.2 cryptography: ^2.7.0 asn1lib: ^1.0.0 - + fast_rsa: ^3.6.6 dependency_overrides: #强制设置google_maps_flutter_ios 为 2.5.2 From 0883ddf80b116141665ae0d0784a935214f6bd2c Mon Sep 17 00:00:00 2001 From: liyi Date: Mon, 2 Dec 2024 16:13:07 +0800 Subject: [PATCH 6/6] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4=E6=96=B0=E7=9A=84?= =?UTF-8?q?=E4=B8=AD=E7=BB=A7=E5=8D=8F=E8=AE=AE=E4=B8=AD=E7=9A=84=E4=B8=8A?= =?UTF-8?q?=E6=8A=A5=E7=AD=BE=E5=90=8D=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../startChart/entity/heartbeat_response.dart | 39 +++++++++++++++ lib/talk/startChart/entity/scp_message.dart | 48 ++++++++++++++----- lib/talk/startChart/start_chart_manage.dart | 3 +- 3 files changed, 77 insertions(+), 13 deletions(-) create mode 100644 lib/talk/startChart/entity/heartbeat_response.dart diff --git a/lib/talk/startChart/entity/heartbeat_response.dart b/lib/talk/startChart/entity/heartbeat_response.dart new file mode 100644 index 00000000..263c3d42 --- /dev/null +++ b/lib/talk/startChart/entity/heartbeat_response.dart @@ -0,0 +1,39 @@ +class HeartbeatResponse { + int statusCode; // 状态码,1字节无符号 + int nextPingTime; // 下次ping时间秒数,2字节无符号 + + HeartbeatResponse({required this.statusCode, required this.nextPingTime}); + + @override + String toString() { + return 'HeartbeatResponse{statusCode: $statusCode, nextPingTime: $nextPingTime}'; + } + + // 反序列化方法 + static HeartbeatResponse deserialize(List bytes) { + if (bytes.length < 3) { + throw FormatException("Invalid HeartbeatResponse length"); + } + + final response = HeartbeatResponse( + statusCode: bytes[0], // 状态码,1字节 + nextPingTime: (bytes[2] << 8) | bytes[1], // 下次ping时间,2字节小端序 + ); + + return response; + } + + // 序列化方法 + static List serialize(HeartbeatResponse response) { + final List bytes = []; + + // 序列化状态码 + bytes.add(response.statusCode); + + // 序列化下次ping时间(2字节小端序) + bytes.add(response.nextPingTime & 0xFF); + bytes.add((response.nextPingTime >> 8) & 0xFF); + + return bytes; + } +} \ No newline at end of file diff --git a/lib/talk/startChart/entity/scp_message.dart b/lib/talk/startChart/entity/scp_message.dart index 3fc80678..fe3bd152 100644 --- a/lib/talk/startChart/entity/scp_message.dart +++ b/lib/talk/startChart/entity/scp_message.dart @@ -2,6 +2,8 @@ import 'dart:convert'; import 'package:crc32_checksum/crc32_checksum.dart'; import 'package:crypto/crypto.dart'; import 'package:star_lock/app_settings/app_settings.dart'; +import 'package:star_lock/talk/startChart/constant/payload_type_constant.dart'; +import 'package:star_lock/talk/startChart/entity/heartbeat_response.dart'; class ScpMessage { ScpMessage({ @@ -28,7 +30,7 @@ class ScpMessage { int? PayloadType; int? PayloadCRC; int? PayloadLength; - String? Payload; + dynamic Payload; // Payload可以是任何类型,这里用dynamic表示 ScpMessage.fromJson(dynamic json) { ProtocolFlag = json['ProtocolFlag']; @@ -58,9 +60,6 @@ class ScpMessage { 'PayloadLength': PayloadLength, 'Payload': Payload, }; - - - } @override @@ -190,7 +189,8 @@ class ScpMessage { // FromPeerId (字符串,长度固定为44字节) if (bytes.length - offset >= 44) { - message.FromPeerId = utf8.decode(bytes.sublist(offset, offset + 44)).trimRight(); + message.FromPeerId = + utf8.decode(bytes.sublist(offset, offset + 44)).trimRight(); offset += 44; } else { throw FormatException("Invalid FromPeerId length"); @@ -198,7 +198,8 @@ class ScpMessage { // ToPeerId (字符串,长度固定为44字节) if (bytes.length - offset >= 44) { - message.ToPeerId = utf8.decode(bytes.sublist(offset, offset + 44)).trimRight(); + message.ToPeerId = + utf8.decode(bytes.sublist(offset, offset + 44)).trimRight(); offset += 44; } else { throw FormatException("Invalid ToPeerId length"); @@ -226,22 +227,45 @@ class ScpMessage { print('PayloadLength bytes: ${bytes.sublist(offset, offset + 4)}'); message.PayloadLength = (bytes[offset] | - (bytes[offset + 1] << 8) | - (bytes[offset + 2] << 16) | - (bytes[offset + 3] << 24)); // 修正为 little-endian + (bytes[offset + 1] << 8) | + (bytes[offset + 2] << 16) | + (bytes[offset + 3] << 24)); // 修正为 little-endian offset += 4; } else { throw FormatException("Invalid PayloadLength length"); } // Payload (字符串,转换为字节) - if (message.PayloadLength != null && bytes.length - offset >= message.PayloadLength!) { - message.Payload = utf8.decode(bytes.sublist(offset, offset + message.PayloadLength!)); + if (message.PayloadLength != null && + bytes.length - offset >= message.PayloadLength!) { + message.Payload = + utf8.decode(bytes.sublist(offset, offset + message.PayloadLength!)); offset += message.PayloadLength!; } else { throw FormatException("Invalid Payload or PayloadLength"); } + // 解析Payload + // 解析Payload + if (message.PayloadType == 110) { // 假设110表示HeartbeatResponse类型 + if (message.PayloadLength != null && bytes.length - offset >= message.PayloadLength!) { + final payloadBytes = bytes.sublist(offset, offset + message.PayloadLength!); + message.Payload = HeartbeatResponse.deserialize(payloadBytes); + offset += message.PayloadLength!; + } else { + throw FormatException("Invalid Payload or PayloadLength"); + } + } else { + // 处理其他类型的Payload + if (message.PayloadLength != null && bytes.length - offset >= message.PayloadLength!) { + message.Payload = utf8.decode(bytes.sublist(offset, offset + message.PayloadLength!)); + offset += message.PayloadLength!; + } else { + throw FormatException("Invalid Payload or PayloadLength"); + } + } + + // 验证PayloadCRC // if (message.Payload != null) { // var crcBytes = List.from(utf8.encode(message.Payload!)); @@ -253,6 +277,7 @@ class ScpMessage { return message; } + // CRC-16 计算函数(示例实现,可能需要根据具体协议调整) static int _calculateCrc16(List data) { const poly = 0x8005; @@ -273,7 +298,6 @@ class ScpMessage { return crc; } - static String bytesToHex(List bytes) { return bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(''); } diff --git a/lib/talk/startChart/start_chart_manage.dart b/lib/talk/startChart/start_chart_manage.dart index 1de800a0..9b060557 100644 --- a/lib/talk/startChart/start_chart_manage.dart +++ b/lib/talk/startChart/start_chart_manage.dart @@ -61,6 +61,7 @@ class StartChartManage { String ToPeerId = ''; // 对端ID String FromPeerId = ''; // 我的ID + // echo测试peer对端 final String echoPeerId = '3phX8Ng2cZHz5NtP8xAf6nYy2z1BYytoejgjoHrWMGhH'; // 星图服务初始化 @@ -217,7 +218,7 @@ class StartChartManage { // 发送消息 Future _sendMessage({required List message}) async { - _log(text: '发送给中继的消息体:${message},序列化之后的数据:【${bytesToHex(message)}】'); + // _log(text: '发送给中继的消息体:${message},序列化之后的数据:【${bytesToHex(message)}】'); var result = await _udpSocket?.send( message, InternetAddress(remoteHost), remotePort); if (result != message.length) {