Files
mqttx/gpui-app/src/aliyun.rs
T
JoeAllen bf6937b776 feat: 重写为 GPUI 原生桌面应用并接入 Gitea 软件包发布
- gpui-kit 0.6 原生 UI:连接管理、消息流/日志、发布预设、全局变量、
  设置、阿里云连接预设、原生 Logo
- MQTT 5.0/3.1.1 引擎(rumqttc,TCP/TLS/WS/WSS),独立 tokio runtime
  + 事件泵,真实 broker 端到端测试
- 连接表单:保存前可一键测试连接(宽松校验,自动生成临时 Client ID),
  高级属性(认证/MQTT 5 属性/遗嘱)按需折叠显示,WS 路径仅 WS/WSS 显示
- bug 修复:Hex 奇数长度显式报错、测试连接唯一 Client ID、Base64/Hex
  发布发送真实字节、v5 发布记录携带 Content-Type/用户属性、预设应用
  清除旧 Content-Type、模板时间基准统一;补 8 个单元测试
- Gitea 发布:Generic Package 上传脚本(bash/PowerShell)+ Actions
  release 工作流(tag 触发,双平台构建),README 使用说明
2026-09-28 11:39:47 +08:00

153 lines
5.2 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 阿里云 IoT 平台一键接入预设(对标官方 MQTTX 的阿里云模板)。
//!
//! Token 模式鉴权参数:
//! - clientId: `{GroupId}@@@{DeviceId}`
//! - username: `Signature|{AccessKeyId}|{InstanceId}`
//! - password: base64(HMAC-SHA1(AccessKeySecret, clientId))
use base64::Engine as _;
use hmac::{Hmac, Mac};
use serde::{Deserialize, Serialize};
use sha1::Sha1;
use crate::model::{ConnectionConfig, ProtocolVersion, TransportKind};
type HmacSha1 = Hmac<Sha1>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum AliyunAuthMode {
/// Group ID + 设备 ID,使用账号 AccessKey 签名
#[default]
Token,
/// 一机一密:设备独立 DeviceName + DeviceSecret(直接以设备密钥鉴权)
DeviceCredential,
}
impl AliyunAuthMode {
pub const ALL: [Self; 2] = [Self::Token, Self::DeviceCredential];
pub fn label(self) -> &'static str {
match self {
Self::Token => "Group 令牌 (Token)",
Self::DeviceCredential => "一机一密",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AliyunPreset {
pub id: String,
pub name: String,
/// 分组(仅 UI 分类用)
#[serde(default)]
pub group: String,
/// 实例 ID,如 post-cn-4591dq2ra1i
pub instance_id: String,
/// 地域 host 后缀前的 region,如 cn-shanghai;留空则使用公共接入点
#[serde(default)]
pub region: String,
#[serde(default)]
pub auth_mode: AliyunAuthMode,
// Token 模式
#[serde(default)]
pub access_key_id: String,
#[serde(default)]
pub access_key_secret: String,
#[serde(default)]
pub group_id: String,
// 共用
pub device_id: String,
// 一机一密模式
#[serde(default)]
pub device_secret: String,
}
impl AliyunPreset {
pub fn new() -> Self {
Self {
id: uuid::Uuid::new_v4().to_string(),
name: "阿里云设备".into(),
group: String::new(),
instance_id: String::new(),
region: String::new(),
auth_mode: AliyunAuthMode::Token,
access_key_id: String::new(),
access_key_secret: String::new(),
group_id: String::new(),
device_id: format!("device_{}", &uuid::Uuid::new_v4().simple().to_string()[..8]),
device_secret: String::new(),
}
}
/// HMAC-SHA1(secret, client_id) 的 Base64 编码。
pub fn signature(secret: &str, client_id: &str) -> Result<String, String> {
let mut mac =
HmacSha1::new_from_slice(secret.as_bytes()).map_err(|e| format!("HMAC 初始化失败: {e}"))?;
mac.update(client_id.as_bytes());
Ok(base64::engine::general_purpose::STANDARD.encode(mac.finalize().into_bytes()))
}
/// 接入点 host:`{instanceId}.mqtt.iothub.aliyuncs.com`
/// (实例级接入点在各区域通用;region 为空时同样可用)
pub fn host(&self) -> String {
if self.region.is_empty() {
format!("{}.mqtt.iothub.aliyuncs.com", self.instance_id)
} else {
format!(
"{}.mqtt.{}.aliyuncs.com",
self.instance_id, self.region
)
}
}
/// 由预设生成一份连接配置(不写入 id/name 之外的覆盖项)。
pub fn to_connection(&self) -> Result<ConnectionConfig, String> {
let mut conn = ConnectionConfig::new();
conn.name = self.name.clone();
conn.host = self.host();
conn.port = TransportKind::Tcp.default_port();
conn.transport = TransportKind::Tcp;
conn.protocol = ProtocolVersion::V311;
conn.clean_start = true;
match self.auth_mode {
AliyunAuthMode::Token => {
if self.group_id.is_empty()
|| self.access_key_id.is_empty()
|| self.access_key_secret.is_empty()
{
return Err("Group ID、AccessKey ID 与 AccessKey Secret 不能为空".into());
}
let client_id = format!("{}@@@{}", self.group_id, self.device_id);
let password = Self::signature(&self.access_key_secret, &client_id)?;
conn.client_id = client_id;
conn.username =
format!("Signature|{}|{}", self.access_key_id, self.instance_id);
conn.password = password;
}
AliyunAuthMode::DeviceCredential => {
if self.device_secret.is_empty() {
return Err("设备密钥 (DeviceSecret) 不能为空".into());
}
// 一机一密三元组:clientId=设备名,username=设备名&实例,password=HMAC-SHA1(DeviceSecret, clientId)
let client_id = self.device_id.clone();
let password = Self::signature(&self.device_secret, &client_id)?;
conn.client_id = client_id;
conn.username = format!("{}&{}", self.device_id, self.instance_id);
conn.password = password;
}
}
Ok(conn)
}
}
impl Default for AliyunPreset {
fn default() -> Self {
Self::new()
}
}