Implement token and key from cloud
This commit is contained in:
parent
99819f36d9
commit
1cf3e17faa
5 changed files with 199 additions and 59 deletions
|
@ -19,3 +19,4 @@ reqwest = "0.11.20"
|
||||||
base64 = "0.21.4"
|
base64 = "0.21.4"
|
||||||
tokio = { version = "1.32.0", features=["macros", "rt-multi-thread"] }
|
tokio = { version = "1.32.0", features=["macros", "rt-multi-thread"] }
|
||||||
md5 = "0.7.0"
|
md5 = "0.7.0"
|
||||||
|
futures = "0.3.28"
|
||||||
|
|
196
src/cloud.rs
196
src/cloud.rs
|
@ -1,9 +1,10 @@
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
|
str::FromStr,
|
||||||
time::{SystemTime, UNIX_EPOCH},
|
time::{SystemTime, UNIX_EPOCH},
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::{Error, Result};
|
use anyhow::{anyhow, bail, Error, Result};
|
||||||
use base64::{engine::general_purpose, Engine};
|
use base64::{engine::general_purpose, Engine};
|
||||||
|
|
||||||
use chrono::Local;
|
use chrono::Local;
|
||||||
|
@ -101,13 +102,8 @@ impl Cloud {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn api_request(
|
async fn api_request(&self, endpoint: &str, dump_data: String) -> Result<String> {
|
||||||
&self,
|
let mut header = HeaderMap::default();
|
||||||
endpoint: &str,
|
|
||||||
dump_data: String,
|
|
||||||
header: Option<HeaderMap>,
|
|
||||||
) -> Result<String> {
|
|
||||||
let mut header = header.unwrap_or_default();
|
|
||||||
|
|
||||||
let url = format!("{}{}", self.api_url, endpoint);
|
let url = format!("{}{}", self.api_url, endpoint);
|
||||||
|
|
||||||
|
@ -159,15 +155,11 @@ impl Cloud {
|
||||||
data.insert("userType".to_string(), "0".to_string());
|
data.insert("userType".to_string(), "0".to_string());
|
||||||
data.insert("userName".to_string(), format!("{}", self.account));
|
data.insert("userName".to_string(), format!("{}", self.account));
|
||||||
|
|
||||||
let response = Self::parse_response(
|
let response = Response::from_str(
|
||||||
self.api_request(
|
&self
|
||||||
"/v1/multicloud/platform/user/route",
|
.api_request("/v1/multicloud/platform/user/route", to_string(&data)?)
|
||||||
to_string(&data)?,
|
.await?,
|
||||||
None,
|
)?;
|
||||||
)
|
|
||||||
.await?,
|
|
||||||
)
|
|
||||||
.ok_or(Error::msg("failed parsing response"))?;
|
|
||||||
|
|
||||||
if let Some(api_url) = response.normal_data().get("masUrl") {
|
if let Some(api_url) = response.normal_data().get("masUrl") {
|
||||||
self.api_url = api_url.clone();
|
self.api_url = api_url.clone();
|
||||||
|
@ -180,11 +172,11 @@ impl Cloud {
|
||||||
let mut data = self.make_general_data();
|
let mut data = self.make_general_data();
|
||||||
data.insert("loginAccount".to_string(), format!("{}", self.account));
|
data.insert("loginAccount".to_string(), format!("{}", self.account));
|
||||||
|
|
||||||
let response = Self::parse_response(
|
let response = Response::from_str(
|
||||||
self.api_request("/v1/user/login/id/get", to_string(&data)?, None)
|
&self
|
||||||
|
.api_request("/v1/user/login/id/get", to_string(&data)?)
|
||||||
.await?,
|
.await?,
|
||||||
)
|
)?;
|
||||||
.ok_or(Error::msg("failed parsing response"))?;
|
|
||||||
|
|
||||||
let login_id = response
|
let login_id = response
|
||||||
.normal_data()
|
.normal_data()
|
||||||
|
@ -233,57 +225,112 @@ impl Cloud {
|
||||||
|
|
||||||
let dump_i = to_string(&i).unwrap();
|
let dump_i = to_string(&i).unwrap();
|
||||||
|
|
||||||
let response =
|
let response = Response::from_str(&self.api_request("/mj/user/login", dump_i).await?)?;
|
||||||
Self::parse_response(self.api_request("/mj/user/login", dump_i, None).await?)
|
|
||||||
.ok_or(Error::msg("failed parsing response"))?;
|
|
||||||
|
|
||||||
self.uid = Some(response.nested_data().uid.clone());
|
self.uid = Some(response.login_data().uid.clone());
|
||||||
self.access_token = Some(response.nested_data().mdata.accessToken.clone());
|
self.access_token = Some(response.login_data().mdata.accessToken.clone());
|
||||||
self.security.set_aes_keys(
|
self.security.set_aes_keys(
|
||||||
&response.nested_data().accessToken,
|
&response.login_data().accessToken,
|
||||||
&response.nested_data().randomData,
|
&response.login_data().randomData,
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_response(s: String) -> Option<Response> {
|
pub async fn keys(&self, device_id: u64) -> Result<(String, String)> {
|
||||||
if let Ok(r) = from_str::<ResponseNumber>(&s) {
|
for method in [1, 2] {
|
||||||
return Some(Response::from(r));
|
let udp_id = CloudSecurity::udp_id(device_id, method);
|
||||||
|
|
||||||
|
let mut data = self.make_general_data();
|
||||||
|
data.insert("udpid".to_string(), udp_id.clone());
|
||||||
|
|
||||||
|
let response = Response::from_str(
|
||||||
|
&self
|
||||||
|
.api_request("/v1/iot/secure/getToken", to_string(&data)?)
|
||||||
|
.await?,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
for token in response.token_list() {
|
||||||
|
if token.udpId == udp_id {
|
||||||
|
return Ok((token.token.clone(), token.key.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(r) = from_str::<ResponseString>(&s) {
|
bail!("no keys found")
|
||||||
return Some(Response::from(r));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(r) = from_str::<ResponseMap>(&s) {
|
|
||||||
return Some(Response::from(r));
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum Response {
|
enum Response {
|
||||||
Normal(ResponseNumber),
|
Normal(ResponseNumber),
|
||||||
NestedMap(ResponseMap),
|
NestedMap(ResponseLogin),
|
||||||
|
TokenList(ResponseTokenList),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Response {
|
impl Response {
|
||||||
pub fn normal_data(&self) -> &HashMap<String, String> {
|
pub fn code(&self) -> i32 {
|
||||||
match self {
|
match self {
|
||||||
Self::Normal(n) => &n.data,
|
Self::Normal(n) => n.code,
|
||||||
Self::NestedMap(_) => panic!(),
|
Self::NestedMap(n) => n.code,
|
||||||
|
Self::TokenList(n) => n.code.parse().unwrap(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn nested_data(&self) -> &ResponseMapData {
|
pub fn normal_data(&self) -> &HashMap<String, String> {
|
||||||
match self {
|
match self {
|
||||||
Self::Normal(_) => panic!(),
|
Self::Normal(n) => &n.data,
|
||||||
Self::NestedMap(n) => &n.data,
|
_ => panic!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn login_data(&self) -> &ResponseLoginData {
|
||||||
|
match self {
|
||||||
|
Self::NestedMap(n) => &n.data,
|
||||||
|
_ => panic!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn token_list(&self) -> &[Token] {
|
||||||
|
match self {
|
||||||
|
Self::TokenList(n) => &n.data.tokenlist,
|
||||||
|
_ => panic!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Response {
|
||||||
|
type Err = Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
let resp_fn = |s| {
|
||||||
|
if let Ok(r) = from_str::<ResponseNumber>(s) {
|
||||||
|
return Ok(Response::from(r));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(r) = from_str::<ResponseString>(s) {
|
||||||
|
return Ok(Response::from(r));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(r) = from_str::<ResponseLogin>(s) {
|
||||||
|
return Ok(Response::from(r));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(r) = from_str::<ResponseTokenList>(s) {
|
||||||
|
return Ok(Response::from(r));
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(Error::msg("failed parsing response"))
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = resp_fn(s)?;
|
||||||
|
|
||||||
|
if response.code() != 0 {
|
||||||
|
bail!("Error return code: {}", response.code());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ResponseNumber> for Response {
|
impl From<ResponseNumber> for Response {
|
||||||
|
@ -302,12 +349,18 @@ impl From<ResponseString> for Response {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ResponseMap> for Response {
|
impl From<ResponseLogin> for Response {
|
||||||
fn from(value: ResponseMap) -> Self {
|
fn from(value: ResponseLogin) -> Self {
|
||||||
Self::NestedMap(value)
|
Self::NestedMap(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<ResponseTokenList> for Response {
|
||||||
|
fn from(value: ResponseTokenList) -> Self {
|
||||||
|
Self::TokenList(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
struct ResponseNumber {
|
struct ResponseNumber {
|
||||||
|
@ -325,16 +378,39 @@ struct ResponseString {
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
struct ResponseMap {
|
struct ResponseLogin {
|
||||||
msg: String,
|
msg: String,
|
||||||
code: i32,
|
code: i32,
|
||||||
pub data: ResponseMapData,
|
pub data: ResponseLoginData,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct ResponseTokenList {
|
||||||
|
msg: String,
|
||||||
|
code: String,
|
||||||
|
pub data: TokenList,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct TokenList {
|
||||||
|
tokenlist: Vec<Token>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct Token {
|
||||||
|
udpId: String,
|
||||||
|
key: String,
|
||||||
|
token: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
struct ResponseMapData {
|
struct ResponseLoginData {
|
||||||
randomData: String,
|
randomData: String,
|
||||||
uid: String,
|
uid: String,
|
||||||
accountId: String,
|
accountId: String,
|
||||||
|
@ -376,6 +452,9 @@ struct Data<'a> {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use futures::future::try_join;
|
||||||
|
|
||||||
|
use crate::Startup;
|
||||||
|
|
||||||
use super::Cloud;
|
use super::Cloud;
|
||||||
|
|
||||||
|
@ -408,4 +487,17 @@ mod test {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn keys() -> Result<()> {
|
||||||
|
let mut cloud = Cloud::new("michaelh.95@t-online.de", "Hoda.semi1")?;
|
||||||
|
|
||||||
|
let (_, devices) = try_join(cloud.login(), Startup::discover()).await?;
|
||||||
|
|
||||||
|
for device_info in devices {
|
||||||
|
let (_token, _key) = cloud.keys(device_info.id).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,6 +89,40 @@ impl CloudSecurity {
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
.join("")
|
.join("")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn udp_id(device_id: u64, method: u8) -> String {
|
||||||
|
let bytes_id: Vec<u8> = match method {
|
||||||
|
0 => device_id.to_be_bytes().into_iter().rev().collect(),
|
||||||
|
1 => {
|
||||||
|
let mut v = device_id.to_be_bytes().to_vec();
|
||||||
|
v.truncate(6);
|
||||||
|
|
||||||
|
v
|
||||||
|
}
|
||||||
|
1 => {
|
||||||
|
let mut v = device_id.to_be_bytes().to_vec();
|
||||||
|
v.truncate(6);
|
||||||
|
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => return String::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut data = Sha256::digest(&bytes_id);
|
||||||
|
|
||||||
|
for i in 0..16 {
|
||||||
|
data[i] ^= data[i + 16];
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut v: Vec<u8> = data.into_iter().collect();
|
||||||
|
v.truncate(16);
|
||||||
|
|
||||||
|
v.iter()
|
||||||
|
.map(|b| format!("{b:02x}"))
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join("")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -78,7 +78,7 @@ impl Startup {
|
||||||
|
|
||||||
const NUM_RETRIES: u8 = 5;
|
const NUM_RETRIES: u8 = 5;
|
||||||
|
|
||||||
pub fn discover() -> Result<Vec<DeviceInfo>> {
|
pub async fn discover() -> Result<Vec<DeviceInfo>> {
|
||||||
let socket = UdpSocket::bind("0.0.0.0:0")?;
|
let socket = UdpSocket::bind("0.0.0.0:0")?;
|
||||||
socket.set_broadcast(true)?;
|
socket.set_broadcast(true)?;
|
||||||
socket.set_read_timeout(Some(Duration::from_secs(2)))?;
|
socket.set_read_timeout(Some(Duration::from_secs(2)))?;
|
||||||
|
@ -184,18 +184,18 @@ mod test {
|
||||||
|
|
||||||
use super::{Device, Startup};
|
use super::{Device, Startup};
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn discover() -> Result<()> {
|
async fn discover() -> Result<()> {
|
||||||
let devices = Startup::discover()?;
|
let devices = Startup::discover().await?;
|
||||||
|
|
||||||
println!("{devices:#?}");
|
println!("{devices:#?}");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn connect() -> Result<()> {
|
async fn connect() -> Result<()> {
|
||||||
for device_info in Startup::discover()? {
|
for device_info in Startup::discover().await? {
|
||||||
Device::connect(device_info)?;
|
Device::connect(device_info)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
13
tst.json
Normal file
13
tst.json
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"code": "0",
|
||||||
|
"msg": "ok",
|
||||||
|
"data": {
|
||||||
|
"tokenlist": [
|
||||||
|
{
|
||||||
|
"udpId": "a79479839b272432f3bbf971f9faed30",
|
||||||
|
"key": "d9d856b06f5245349e96df80e063af989532e6ef03c34039b74bfa0754a0ddf0",
|
||||||
|
"token": "4D14C31771A353EB584566243552C5CC2CE55C06162E550E71A805949569F7E8FA659B5B4B6CC097475A1270A4CC3DB75D52782DE3D1E65FEEA0945C6FFC7B3C"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue