- 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 使用说明
153 lines
5.2 KiB
Rust
153 lines
5.2 KiB
Rust
//! 阿里云 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()
|
||
}
|
||
}
|