Midea/src/cloud.rs

283 lines
8 KiB
Rust
Raw Normal View History

2023-09-23 09:41:49 +00:00
use std::{
collections::HashMap,
time::{Duration, SystemTime, UNIX_EPOCH},
};
use anyhow::{anyhow, Error, Result};
use base64::{engine::general_purpose, Engine};
use chrono::Local;
use rand::RngCore;
use reqwest::header::HeaderMap;
use serde::{Deserialize, Serialize};
use serde_json::{from_str, to_string};
use crate::cloud_security::CloudSecurity;
2023-09-22 16:06:37 +00:00
pub struct Cloud {
2023-09-23 09:41:49 +00:00
device_id: u64,
uid: Option<String>,
api_url: String,
access_token: Option<String>,
auth_base: String,
login_id: Option<String>,
account: String,
password: String,
security: CloudSecurity,
2023-09-22 16:06:37 +00:00
}
impl Cloud {
2023-09-23 09:41:49 +00:00
pub const APP_ID: &str = "1010";
2023-09-22 16:06:37 +00:00
pub const APP_KEY: &str = "ac21b9f9cbfe4ca5a88562ef25e2b768";
2023-09-23 09:41:49 +00:00
pub const API_URL: &str = "https://mp-prod.appsmb.com/mas/v5/app/proxy?alias=";
pub const APP_VERSION: &str = "3.0.2";
pub const SRC: &str = "10";
pub const IOT_KEY: &str = "meicloud";
pub const HMAC_KEY: &str = "PROD_VnoClJI9aikS8dyy";
// pub const IOT_KEY: &str = "9795516279659324117647275084689641883661667";
// pub const HMAC_KEY: &str = "117390035944627627450677220413733956185864939010425";
pub fn new(account: impl ToString, password: impl ToString, device_id: u64) -> Result<Self> {
Ok(Self {
device_id,
uid: None,
api_url: "https://mp-prod.appsmb.com/mas/v5/app/proxy?alias=".to_string(),
access_token: None,
auth_base: general_purpose::STANDARD_NO_PAD
.encode(format!("{}:{}", Self::APP_KEY, Self::IOT_KEY).as_bytes()),
login_id: None,
account: account.to_string(),
password: password.to_string(),
security: CloudSecurity::new(Self::APP_KEY, Self::IOT_KEY, Self::HMAC_KEY),
})
}
fn make_general_data(&self) -> HashMap<String, String> {
[
("appVersion", Self::APP_VERSION),
("src", Self::SRC),
("format", "2"),
(
"stamp",
Local::now().format("%Y%m%d%H%M%S").to_string().as_str(),
),
("platformId", "1"),
("deviceId", self.device_id.to_string().as_str()),
(
"reqId",
{
let mut d = [0; 16];
rand::thread_rng().fill_bytes(&mut d);
let mut s = String::new();
for b in d {
s += format!("{b:x}").as_str();
}
s
}
.as_str(),
),
("uid", self.uid.clone().unwrap_or_default().as_str()),
("clientType", "1"),
("appId", Self::APP_ID),
]
.into_iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect()
}
async fn api_request(
&self,
endpoint: &str,
data: HashMap<String, String>,
header: Option<HeaderMap>,
) -> Result<String> {
let mut header = header.unwrap_or_default();
let url = format!("{}{}", self.api_url, endpoint);
let dump_data = to_string(&data)?;
let random = SystemTime::now()
.duration_since(UNIX_EPOCH)?
.as_secs()
.to_string();
let sign = self.security.sign(&dump_data, &random);
header.insert("x-recipe-app", Self::APP_ID.parse().unwrap());
header.insert(
"authorization",
format!("Basic {}", self.auth_base).parse().unwrap(),
);
header.insert(
"content-type",
"application/json; charset=utf-8".parse().unwrap(),
);
header.insert("secretVersion", "1".parse().unwrap());
header.insert("sign", sign.parse().unwrap());
header.insert("random", random.parse().unwrap());
if let Some(access_token) = &self.access_token {
header.insert("accesstoken", access_token.parse().unwrap());
}
Ok(reqwest::Client::new()
.post(url)
.headers(header)
.body(dump_data)
.timeout(Duration::from_secs(10))
.send()
.await?
.text()
.await?)
}
async fn reroute(&mut self) -> Result<()> {
let mut data = self.make_general_data();
data.insert("userType".to_string(), "0".to_string());
data.insert("userName".to_string(), format!("{}", self.account));
let response: Response = from_str(
&self
.api_request("/v1/multicloud/platform/user/route", data, None)
.await?,
)?;
if let Some(api_url) = response.data.get("masUrl") {
self.api_url = api_url.clone();
}
Ok(())
}
async fn reqest_login_id(&mut self) -> Result<()> {
let mut data = self.make_general_data();
data.insert("loginAccount".to_string(), format!("{}", self.account));
let response: Response = from_str(
&self
.api_request("/v1/user/login/id/get", data, None)
.await?,
)?;
let login_id = response
.data
.get("loginId")
.cloned()
.ok_or(Error::msg("failed to request loginId"))?;
self.login_id = Some(login_id);
Ok(())
}
async fn login(&mut self) -> Result<()> {
self.reroute().await?;
self.reqest_login_id().await?;
let mut iot_data = self.make_general_data();
iot_data.remove("uid");
iot_data.insert(
"iampwd".to_string(),
self.security
.encrypt_iam_password(self.login_id.clone().unwrap(), self.password),
);
iot_data.insert("loginAccount".to_string(), self.account.clone());
iot_data.insert(
"password".to_string(),
self.security
.encrypt_password(self.login_id.clone().unwrap(), self.password),
);
let stamp = iot_data.get("stamp").unwrap().clone();
let iot_data_dump = to_string(&iot_data)?;
let mut data = HashMap::new();
data.insert("iotData".to_string(), iot_data_dump);
data.insert(
"data".to_string(),
to_string(&Data {
appKey: Self::APP_KEY,
deviceId: self.device_id.to_string().as_str(),
platform: "2",
})?,
);
data.insert("stamp".to_string(), stamp);
let response: Response = from_str(&self.api_request("/mj/user/login", data, None).await?)?;
self.uid = Some(
response
.data
.get("uid")
.cloned()
.ok_or(Error::msg("failed to request uid"))?,
);
self.access_token = Some(
response
.data
.get("accessToken")
.cloned()
.ok_or(Error::msg("failed to request uid"))?,
);
self.security.set_aes_keys(
&response
.data
.get("accessToken")
.cloned()
.ok_or(Error::msg("failed to request accessToken"))?,
&response
.data
.get("randomData")
.cloned()
.ok_or(Error::msg("failed to request randomData"))?,
);
Ok(())
}
}
#[cfg(test)]
mod test {
use anyhow::Result;
use super::Cloud;
use crate::discover::Startup;
#[tokio::test]
async fn test_login() -> Result<()> {
let devices = Startup::discover()?;
println!("{devices:#?}");
for device_info in devices {
let mut cloud = Cloud::new("michaelh.95@t-online.de", "Hoda.semi1", device_info.id)?;
let id = cloud.login_id().await?;
println!("{id:?}")
}
Ok(())
}
}
#[derive(Deserialize, Debug)]
struct Response {
msg: String,
code: i32,
pub data: HashMap<String, String>,
}
#[derive(Serialize, Debug)]
struct Data<'a> {
appKey: &'a str,
deviceId: &'a str,
platform: &'a str,
2023-09-22 16:06:37 +00:00
}