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}