use std::{collections::HashMap, sync::Arc};
use base64ct::{Base64, Encoding};
use cookie_store::{CookieStore, RawCookie};
use md5::{Digest, Md5};
use reqwest::{Client, Url};
use reqwest_cookie_store::CookieStoreMutex;
use serde::{Deserialize, Serialize};
use serde_json::{Number, Value};
use sha1::Sha1;
use tracing::trace;
use crate::util::random_id;
#[derive(Clone, Debug)]
pub struct Login {
client: Client,
server: Url,
username: String,
password_hash: String,
cookie_store: Arc<CookieStoreMutex>,
}
const LOGIN_SERVER: &str = "https://account.xiaomi.com/pass/";
const LOGIN_UA: &str = "APP/com.xiaomi.mihome APPV/6.0.103 iosPassportSDK/3.9.0 iOS/14.4 miHSTS";
impl Login {
pub fn new(username: impl Into<String>, password: impl AsRef<[u8]>) -> crate::Result<Self> {
let server = Url::parse(LOGIN_SERVER)?;
let mut cookie_store = CookieStore::new(None);
let device_id = random_device_id();
for (name, value) in [("sdkVersion", "3.9"), ("deviceId", &device_id)] {
let cookie = RawCookie::build((name, value)).path("/").build();
cookie_store.insert_raw(&cookie, &server)?;
trace!("预先添加 Cookies: {}", cookie);
}
let cookie_store = Arc::new(CookieStoreMutex::new(cookie_store));
let client = Client::builder()
.cookie_provider(Arc::clone(&cookie_store))
.user_agent(LOGIN_UA)
.build()?;
Ok(Self {
client,
server,
username: username.into(),
password_hash: hash_password(password),
cookie_store,
})
}
pub async fn login(&self) -> crate::Result<LoginResponse> {
let raw = self.raw_login().await?;
Ok(serde_json::from_value(raw)?)
}
pub async fn raw_login(&self) -> crate::Result<Value> {
let bytes = self
.client
.get(self.server.join("serviceLogin?sid=micoapi&_json=true")?)
.send()
.await?
.error_for_status()?
.bytes()
.await?;
let response = serde_json::from_slice(&bytes[11..])?;
trace!("尝试初步登录: {response}");
Ok(response)
}
pub async fn auth(&self, login_response: LoginResponse) -> crate::Result<AuthResponse> {
let raw = self.raw_auth(login_response).await?;
Ok(serde_json::from_value(raw)?)
}
pub async fn raw_auth(&self, login_response: LoginResponse) -> crate::Result<Value> {
let form = HashMap::from([
("_json", "true"),
("qs", &login_response.qs),
("sid", &login_response.sid),
("_sign", &login_response._sign),
("callback", &login_response.callback),
("user", &self.username),
("hash", &self.password_hash),
]);
let bytes = self
.client
.post(self.server.join("serviceLoginAuth2")?)
.form(&form)
.send()
.await?
.error_for_status()?
.bytes()
.await?;
let response = serde_json::from_slice(&bytes[11..])?;
trace!("尝试认证: {response}");
Ok(response)
}
pub async fn get_token(&self, auth_response: AuthResponse) -> crate::Result<Value> {
let client_sign = client_sign(&auth_response);
let url = Url::parse_with_params(&auth_response.location, [("clientSign", &client_sign)])?;
let response = self
.client
.get(url)
.send()
.await?
.error_for_status()?
.json()
.await?;
trace!("尝试获取 serviceToken: {response}");
Ok(response)
}
pub fn into_cookie_store(self) -> Arc<CookieStoreMutex> {
self.cookie_store
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct LoginResponse {
pub qs: String,
pub sid: String,
pub _sign: String,
pub callback: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AuthResponse {
pub location: String,
pub nonce: Number,
pub ssecurity: String,
}
fn random_device_id() -> String {
let mut device_id = random_id(16);
device_id.make_ascii_uppercase();
device_id
}
fn hash_password(password: impl AsRef<[u8]>) -> String {
let result = Md5::new().chain_update(password).finalize();
let mut result = base16ct::lower::encode_string(&result);
result.make_ascii_uppercase();
result
}
fn client_sign(payload: &AuthResponse) -> String {
let nsec = Sha1::new()
.chain_update("nonce=")
.chain_update(payload.nonce.to_string())
.chain_update("&")
.chain_update(&payload.ssecurity)
.finalize();
Base64::encode_string(&nsec)
}