fix(app): OTA 信任链加固、引擎背压与协议正确性

OTA(update/):
- 解压前校验 7z 条目清单:sevenz-rust 0.6.1 的 decompress_file 直接 dest.join(entry.name()),不校验 ".."、绝对路径与盘符,恶意归档可越界写文件;现在要求恰好一个条目且名字与约定二进制一致(fail-closed)
- 无 .sha256 侧车时拒绝安装,不再静默跳过校验而 UI 仍宣称已校验
- 安装前重算暂存文件哈希;同意标记从仅版本号改为绑定「版本 + 哈希」
- 跨卷 rename 失败回退「复制 + fsync + 删源」(暂存在 APPDATA、exe 在安装目录,不同卷时必失败且报错误导)
- .old 回滚备份保留 7 天:原实现新版本首启即删,新版本一旦崩溃用户无任何回退路径
- 新增安装互斥锁:两实例同时安装会互删暂存包、争抢同一个 .old
- platform_triple 用 std::env::consts::ARCH:写死 x86_64 让 ARM 客户端永远挑不到产物
- pick_assets 按命名约定精确后缀匹配:非 Windows 下 ends_with("") 恒真,.tar.gz/.deb 会被当成可执行文件下载并替换自身

引擎(mqtt/):
- 事件通道改有界并分级:高频事件(消息/日志)满即丢并计数、每 1000 条提示一次;状态/订阅结果/发布回执异步补投。原无界通道在到达速率超过消费速率时内存无上界(单条最长 128KB)
- 重连改指数退避(1s→30s 封顶)+ 0~500ms 抖动,替代固定 2 秒的惊群式重试
- 测试连接的建连(读证书,阻塞 IO)移入 spawn_blocking,与 connect 一致

正确性与性能:
- session_params_changed 补齐 4 个 v5 CONNECT 属性(会话过期/接收上限/最大报文/主题别名),漏掉会让"重连后生效"提示静默失效;测试改逐字段防漏式断言
- ConnectionConfig / AliyunPreset 手写脱敏 Debug:password 与 AccessKeySecret/DeviceSecret 输出 <redacted>,杜绝一次 {:?} 把凭据送进 stderr 与 Sentry
- MqttRecord.user_properties 改 Arc<[(String,String)]>、应用日志改 Arc<LogEntry>:消息面板每帧 300 条、日志面板每帧 3000 条的深拷贝降为引用计数
- 阿里云 DeviceName 空值校验,避免拼出非法 clientId 后服务端报错难懂
This commit is contained in:
2026-09-30 10:15:40 +08:00
parent 2661a734ee
commit 9b6df8b9eb
15 changed files with 451 additions and 90 deletions
+38 -3
View File
@@ -119,7 +119,9 @@ impl SslConfig {
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
// Debug 手写而非 derive:`{:?}` 一次格式化就会把 `password` 送进 stderr 与 Sentry
//(panic 钩子会连同载荷一起上报),见文件底部的实现。
#[derive(Clone, Serialize, Deserialize)]
pub struct ConnectionConfig {
pub id: String,
pub name: String,
@@ -250,8 +252,9 @@ impl ConnectionConfig {
/// 与另一份配置相比,影响 broker 会话建立的关键参数是否变化。
/// 这些字段改动只有在重新建立连接后才会生效,用于提示用户重连。
/// WS 路径、遗嘱、TLS 配置、keep_alive 同样随 CONNECT 报文一次性生效,
/// 必须纳入比较。
/// WS 路径、遗嘱、TLS 配置、keep_alive 随 CONNECT 报文一次性生效;
/// MQTT 5 的四个 CONNECT 属性(会话过期/接收上限/最大报文/主题别名)同样如此,
/// 全部纳入比较——漏掉的字段会让"重连后生效"提示静默失效。
pub fn session_params_changed(&self, other: &ConnectionConfig) -> bool {
self.host != other.host
|| self.port != other.port
@@ -265,6 +268,10 @@ impl ConnectionConfig {
|| self.keep_alive != other.keep_alive
|| self.last_will != other.last_will
|| self.ssl != other.ssl
|| self.session_expiry_interval != other.session_expiry_interval
|| self.receive_maximum != other.receive_maximum
|| self.maximum_packet_size != other.maximum_packet_size
|| self.topic_alias_maximum != other.topic_alias_maximum
}
}
@@ -274,4 +281,32 @@ impl Default for ConnectionConfig {
}
}
/// 脱敏 Debug:`password` 以 `<redacted>` 输出。
/// 该结构在日志/panic 场景下可能被整体格式化(`{:?}`),derive 出的 Debug 会把
/// 明文密码带进 stderr 与 Sentry(main.rs 的 panic 钩子会上报),因此必须手写。
impl std::fmt::Debug for ConnectionConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ConnectionConfig")
.field("id", &self.id)
.field("name", &self.name)
.field("host", &self.host)
.field("port", &self.port)
.field("path", &self.path)
.field("protocol", &self.protocol)
.field("transport", &self.transport)
.field("client_id", &self.client_id)
.field("username", &self.username)
.field("password", &"<redacted>")
.field("clean_start", &self.clean_start)
.field("keep_alive", &self.keep_alive)
.field("auto_resubscribe", &self.auto_resubscribe)
.field("auto_reconnect", &self.auto_reconnect)
.field("auto_connect", &self.auto_connect)
.field("has_last_will", &self.last_will.is_some())
.field("group", &self.group)
.field("ssl", &"<redacted>")
.finish_non_exhaustive()
}
}
// ─── 订阅 ────────────────────────────────────────────────────────────────────
+3 -1
View File
@@ -94,7 +94,9 @@ pub struct MqttRecord {
pub retain: bool,
/// unix 毫秒
pub timestamp: i64,
pub user_properties: Vec<(String, String)>,
/// Arc 共享:消息列表每帧克隆数百条记录,Vec<(String,String)> 的深拷贝是
/// 每帧堆分配的主要来源,改为 Arc 后 clone 只增加引用计数
pub user_properties: Arc<[(String, String)]>,
pub content_type: Option<String>,
pub response_topic: Option<String>,
pub correlation_data: Option<String>,
+16 -1
View File
@@ -300,6 +300,21 @@ mod tests {
b.ssl.ca_file = "/tmp/ca.pem".into();
assert!(a.session_params_changed(&b), "TLS 配置改动应触发重连提示");
// MQTT 5 的 CONNECT 属性同样随报文一次性生效(此前遗漏,导致
// "配置已保存,重连后生效"的提示静默失效)
b = a.clone();
b.session_expiry_interval = a.session_expiry_interval + 1;
assert!(a.session_params_changed(&b), "会话过期间隔改动应触发重连提示");
b = a.clone();
b.receive_maximum = Some(10);
assert!(a.session_params_changed(&b), "接收上限改动应触发重连提示");
b = a.clone();
b.maximum_packet_size = Some(1024);
assert!(a.session_params_changed(&b), "最大报文长度改动应触发重连提示");
b = a.clone();
b.topic_alias_maximum = Some(5);
assert!(a.session_params_changed(&b), "主题别名上限改动应触发重连提示");
}
fn ring_record(seq: u64, payload_len: usize, raw_len: usize) -> MqttRecord {
@@ -312,7 +327,7 @@ mod tests {
qos: 0,
retain: false,
timestamp: 0,
user_properties: Vec::new(),
user_properties: Default::default(),
content_type: None,
response_topic: None,
correlation_data: None,