ho_api_rust_sdk/
lib.rs

1use std::time::Duration;
2
3use aes::Aes256;
4use block_modes::{block_padding::Pkcs7, BlockMode, Cbc};
5use chrono::Utc;
6use chrono_tz::Asia::Shanghai;
7use reqwest::{Client, Method, StatusCode};
8use serde::Deserialize;
9use serde_json::{json, to_string, Value};
10use sha1::{Digest, Sha1};
11use uuid::Uuid;
12
13type Aes256Cbc = Cbc<Aes256, Pkcs7>;
14
15#[derive(Debug, Deserialize)]
16struct ApiResult {
17    data: String,
18}
19
20#[derive(Debug)]
21pub enum ApiClientError {
22    ReqwestError(reqwest::Error),
23    SerdeJsonError(serde_json::Error),
24    AesError(block_modes::BlockModeError),
25    Utf8Error(std::string::FromUtf8Error),
26    HexError(hex::FromHexError),
27    InvalidConfig(String),
28}
29
30impl From<reqwest::Error> for ApiClientError {
31    fn from(err: reqwest::Error) -> Self {
32        ApiClientError::ReqwestError(err)
33    }
34}
35
36impl From<serde_json::Error> for ApiClientError {
37    fn from(err: serde_json::Error) -> Self {
38        ApiClientError::SerdeJsonError(err)
39    }
40}
41
42impl From<block_modes::BlockModeError> for ApiClientError {
43    fn from(err: block_modes::BlockModeError) -> Self {
44        ApiClientError::AesError(err)
45    }
46}
47
48impl From<std::string::FromUtf8Error> for ApiClientError {
49    fn from(err: std::string::FromUtf8Error) -> Self {
50        ApiClientError::Utf8Error(err)
51    }
52}
53
54impl From<hex::FromHexError> for ApiClientError {
55    fn from(err: hex::FromHexError) -> Self {
56        ApiClientError::HexError(err)
57    }
58}
59
60pub struct ApiClient {
61    config: ApiClientConfig,
62    cipher: Aes256Cbc,
63}
64
65#[derive(Clone)]
66pub struct ApiClientConfig {
67    pub app_id: String,
68    pub app_secret: String,
69    pub iv: String,
70    pub base_url: String,
71    pub content: String,
72}
73
74impl ApiClient {
75    pub fn new(config: ApiClientConfig) -> Result<Self, ApiClientError> {
76        let cipher = Aes256Cbc::new_from_slices(config.app_secret.as_bytes(), config.iv.as_bytes())
77            .map_err(|_| ApiClientError::InvalidConfig("AES config error".to_string()))?;
78        Ok(Self { config, cipher })
79    }
80
81    fn generate_nonce(&self) -> String {
82        Uuid::new_v4().to_string()
83    }
84
85    fn generate_signature(&self, nonce: &str, timestamp: i64, uri: &str, body: &str) -> String {
86        let sign_str = format!(
87            "{}{}{}{}{}{}",
88            self.config.app_id, nonce, timestamp, uri, body, self.config.app_secret
89        );
90
91        let mut hasher = Sha1::default();
92        hasher.update(sign_str.as_bytes());
93        let result = hasher.finalize();
94        format!("{:x}", result)
95    }
96
97    pub async fn send(&self, method: Method, uri: &str, body_option: Option<Value>) -> Result<String, ApiClientError> {
98        let nonce = self.generate_nonce();
99        let now = Utc::now().with_timezone(&Shanghai).timestamp_millis();
100        let body_str = match body_option.clone() {
101            Some(body) => to_string(&body)?,
102            None => "".to_string(),
103        };
104        let signature = self.generate_signature(&nonce, now, uri, &body_str);
105
106        let url = format!("{}{}{}", self.config.base_url, self.config.content, uri);
107
108        let client = Client::builder()
109            .timeout(Duration::from_secs(100))
110            .connect_timeout(Duration::from_secs(100))
111            .build()?;
112
113        let mut request = client
114            .request(method, &url)
115            .header("User-Agent", "H-RUST-SDK-1.0.0")
116            .header("HO-APP-ID", &self.config.app_id)
117            .header("HO-NONCE", &nonce)
118            .header("HO-TIMESTAMP", now.to_string())
119            .header("HO-SIGNATURE", &signature);
120
121        if let Some(body) = body_option {
122            request = request.json(&json!({ "data": to_string(&body)? }));
123        } else {
124            request = request.json(&json!({}));
125        }
126
127        let response = request.send().await?;
128        if response.status() != StatusCode::OK {
129            return Err(ApiClientError::ReqwestError(response.error_for_status().unwrap_err()));
130        }
131
132        let api_result: ApiResult = response.json().await?;
133        let hex_ciphertext = hex::decode(&api_result.data)?;
134        let decrypted_data = self.cipher.clone().decrypt_vec(&hex_ciphertext)?;
135        let decrypted_str = String::from_utf8(decrypted_data)?;
136        Ok(decrypted_str)
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use serde_json::json;
143    use tokio::test;
144
145    use super::*;
146
147    #[test]
148    async fn test_send_request() {
149        let config = ApiClientConfig {
150            app_id: "your app id".to_string(),
151            app_secret: "your app secret".to_string(),
152            iv: "you app iv".to_string(),
153            base_url: "https://server.zelaser.com".to_string(),
154            content: "/server/common/api".to_string(),
155        };
156
157        let client = ApiClient::new(config).expect("Failed to create API client");
158
159        let body = json!({
160            "key": "value",
161        });
162
163        match client.send(Method::GET, "/v1/lol/champion/skin?region=cn", Some(body)).await {
164            Ok(response) => {
165                println!("Response Body: {}", response);
166            }
167            Err(e) => eprintln!("Error: {:?}", e),
168        }
169    }
170}