1use crate::crypto;
5use crate::error::{NcmError, Result};
6use crate::util::config::*;
7use crate::util::cookie::{cookie_obj_to_string, cookie_to_json};
8use crate::util::device::{generate_device_id, generate_wnmcid, random_hex};
9use crate::util::ip::generate_random_chinese_ip;
10
11use reqwest::header::{HeaderMap, HeaderValue, COOKIE, REFERER, USER_AGENT};
12use serde::{Deserialize, Serialize};
13use serde_json::Value;
14use std::collections::HashMap;
15use std::sync::LazyLock;
16
17static SPECIAL_STATUS_CODES: LazyLock<std::collections::HashSet<i64>> =
19 LazyLock::new(|| [201, 302, 400, 502, 800, 801, 802, 803].into());
20
21static DEVICE_ID: LazyLock<String> = LazyLock::new(generate_device_id);
23static WNMCID: LazyLock<String> = LazyLock::new(generate_wnmcid);
25static DOMAIN_REGEX: LazyLock<regex_lite::Regex> =
27 LazyLock::new(|| regex_lite::Regex::new(r"\s*Domain=[^;]+;?").unwrap());
28
29fn header_value(s: &str) -> HeaderValue {
31 HeaderValue::from_str(s).unwrap_or_else(|_| {
32 let safe: String = s
34 .chars()
35 .filter(|c| c.is_ascii() && !c.is_ascii_control())
36 .collect();
37 HeaderValue::from_str(&safe).unwrap_or_else(|_| HeaderValue::from_static(""))
38 })
39}
40
41#[derive(Debug, Clone, PartialEq, Default)]
43pub enum CryptoType {
44 Weapi,
45 #[default]
46 Eapi,
47 Linuxapi,
48 Api, }
50
51impl CryptoType {
52 pub fn as_str(&self) -> &str {
53 match self {
54 CryptoType::Weapi => "weapi",
55 CryptoType::Eapi => "eapi",
56 CryptoType::Linuxapi => "linuxapi",
57 CryptoType::Api => "api",
58 }
59 }
60}
61
62impl From<&str> for CryptoType {
63 fn from(s: &str) -> Self {
64 match s {
65 "weapi" => CryptoType::Weapi,
66 "linuxapi" => CryptoType::Linuxapi,
67 "api" => CryptoType::Api,
68 _ => CryptoType::Eapi,
69 }
70 }
71}
72
73#[derive(Debug, Clone, Default)]
75pub struct RequestOption {
76 pub crypto: CryptoType,
77 pub cookie: Option<String>,
78 pub ua: Option<String>,
79 pub proxy: Option<String>,
80 pub real_ip: Option<String>,
81 pub random_cn_ip: bool,
82 pub e_r: Option<bool>,
83 pub domain: Option<String>,
84 pub check_token: bool,
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct ApiResponse {
90 pub status: i64,
91 pub body: Value,
92 #[serde(default)]
93 pub cookie: Vec<String>,
94}
95
96#[derive(Debug, Clone)]
98pub struct ApiClient {
99 pub(crate) client: reqwest::Client,
100 cookie: Option<String>,
101 anonymous_token: Option<String>,
102 device_id: Option<String>,
104}
105
106impl ApiClient {
107 pub fn new(cookie: Option<String>) -> Self {
109 let client = reqwest::Client::builder()
110 .build()
111 .expect("Failed to create HTTP client");
112
113 Self {
114 client,
115 cookie,
116 anonymous_token: None,
117 device_id: None,
118 }
119 }
120
121 pub fn with_proxy(cookie: Option<String>, proxy_url: &str) -> Result<Self> {
123 let proxy = reqwest::Proxy::all(proxy_url)
124 .map_err(|e| NcmError::Unknown(format!("Invalid proxy URL: {}", e)))?;
125 let client = reqwest::Client::builder()
126 .proxy(proxy)
127 .build()
128 .map_err(NcmError::Http)?;
129
130 Ok(Self {
131 client,
132 cookie,
133 anonymous_token: None,
134 device_id: None,
135 })
136 }
137
138 pub fn set_cookie(&mut self, cookie: String) {
140 self.cookie = Some(cookie);
141 }
142
143 pub fn set_anonymous_token(&mut self, token: String) {
145 self.anonymous_token = Some(token);
146 }
147
148 pub fn set_device_id(&mut self, device_id: String) {
152 self.device_id = Some(device_id);
153 }
154
155 fn get_device_id(&self) -> &str {
157 self.device_id.as_deref().unwrap_or(&DEVICE_ID)
158 }
159
160 pub async fn request(
162 &self,
163 uri: &str,
164 data: Value,
165 options: RequestOption,
166 ) -> Result<ApiResponse> {
167 let mut headers = HeaderMap::new();
168
169 let ip = options.real_ip.clone().or_else(|| {
171 if options.random_cn_ip {
172 Some(generate_random_chinese_ip())
173 } else {
174 None
175 }
176 });
177
178 if let Some(ref ip) = ip {
179 if let (Ok(real_ip), Ok(fwd)) = (HeaderValue::from_str(ip), HeaderValue::from_str(ip)) {
180 headers.insert("X-Real-IP", real_ip);
181 headers.insert("X-Forwarded-For", fwd);
182 }
183 }
184
185 let cookie_str = options
187 .cookie
188 .as_deref()
189 .or(self.cookie.as_deref())
190 .unwrap_or("");
191 let mut cookie_map = cookie_to_json(cookie_str);
192
193 let ntes_nuid = random_hex(16);
195 let os = get_os_config(cookie_map.get("os").map(|s| s.as_str()).unwrap_or("pc"));
196 let now_ts = chrono::Utc::now().timestamp_millis().to_string();
197
198 cookie_map
199 .entry("__remember_me".to_string())
200 .or_insert_with(|| "true".to_string());
201 cookie_map
202 .entry("ntes_kaola_ad".to_string())
203 .or_insert_with(|| "1".to_string());
204 cookie_map
205 .entry("_ntes_nnid".to_string())
206 .or_insert_with(|| format!("{},{}", ntes_nuid, now_ts));
207 cookie_map
208 .entry("_ntes_nuid".to_string())
209 .or_insert(ntes_nuid);
210 cookie_map
211 .entry("WNMCID".to_string())
212 .or_insert_with(|| WNMCID.clone());
213 cookie_map
214 .entry("WEVNSM".to_string())
215 .or_insert_with(|| "1.0.0".to_string());
216 cookie_map
217 .entry("osver".to_string())
218 .or_insert_with(|| os.osver.to_string());
219 cookie_map
220 .entry("deviceId".to_string())
221 .or_insert_with(|| self.get_device_id().to_string());
222 cookie_map
223 .entry("os".to_string())
224 .or_insert_with(|| os.os.to_string());
225 cookie_map
226 .entry("channel".to_string())
227 .or_insert_with(|| os.channel.to_string());
228 cookie_map
229 .entry("appver".to_string())
230 .or_insert_with(|| os.appver.to_string());
231
232 if !uri.contains("login") {
234 cookie_map
235 .entry("NMTID".to_string())
236 .or_insert_with(|| random_hex(8));
237 }
238
239 if !cookie_map.contains_key("MUSIC_U") {
241 if let Some(ref token) = self.anonymous_token {
242 cookie_map
243 .entry("MUSIC_A".to_string())
244 .or_insert_with(|| token.clone());
245 }
246 }
247
248 headers.insert(COOKIE, header_value(&cookie_obj_to_string(&cookie_map)));
249
250 let crypto_type = if options.crypto == CryptoType::Eapi && !ENCRYPT {
252 CryptoType::Api
253 } else {
254 options.crypto.clone()
255 };
256
257 let mut data = data;
258 let url: String;
259 let encrypt_data: HashMap<String, String>;
260 let domain = options.domain.as_deref().unwrap_or("");
261
262 let csrf_token = cookie_map.get("__csrf").cloned().unwrap_or_default();
263
264 match crypto_type {
265 CryptoType::Weapi => {
266 let ref_domain = if domain.is_empty() { DOMAIN } else { domain };
267 headers.insert(REFERER, header_value(ref_domain));
268 let ua = options
269 .ua
270 .as_deref()
271 .unwrap_or_else(|| choose_user_agent("weapi", "pc"));
272 headers.insert(USER_AGENT, header_value(ua));
273
274 data["csrf_token"] = Value::String(csrf_token);
275 encrypt_data = crypto::weapi(&data);
276 url = format!("{}/weapi/{}", ref_domain, &uri[5..]);
277 }
278 CryptoType::Linuxapi => {
279 let ua = options
280 .ua
281 .as_deref()
282 .unwrap_or_else(|| choose_user_agent("linuxapi", "linux"));
283 headers.insert(USER_AGENT, header_value(ua));
284
285 let ref_domain = if domain.is_empty() { DOMAIN } else { domain };
286 let linux_data = serde_json::json!({
287 "method": "POST",
288 "url": format!("{}{}", ref_domain, uri),
289 "params": data,
290 });
291 encrypt_data = crypto::linuxapi(&linux_data);
292 url = format!("{}/api/linux/forward", ref_domain);
293 }
294 CryptoType::Eapi | CryptoType::Api => {
295 let now_secs = chrono::Utc::now().timestamp().to_string();
297 let request_id = format!(
298 "{}_{:04}",
299 chrono::Utc::now().timestamp_millis(),
300 rand::random::<u16>() % 1000
301 );
302
303 let mut header_map: HashMap<String, String> = HashMap::new();
304 header_map.insert(
305 "osver".to_string(),
306 cookie_map.get("osver").cloned().unwrap_or_default(),
307 );
308 header_map.insert(
309 "deviceId".to_string(),
310 cookie_map.get("deviceId").cloned().unwrap_or_default(),
311 );
312 header_map.insert(
313 "os".to_string(),
314 cookie_map.get("os").cloned().unwrap_or_default(),
315 );
316 header_map.insert(
317 "appver".to_string(),
318 cookie_map.get("appver").cloned().unwrap_or_default(),
319 );
320 header_map.insert(
321 "versioncode".to_string(),
322 cookie_map
323 .get("versioncode")
324 .cloned()
325 .unwrap_or_else(|| "140".to_string()),
326 );
327 header_map.insert(
328 "mobilename".to_string(),
329 cookie_map.get("mobilename").cloned().unwrap_or_default(),
330 );
331 header_map.insert(
332 "buildver".to_string(),
333 cookie_map
334 .get("buildver")
335 .cloned()
336 .unwrap_or_else(|| now_secs[..10].to_string()),
337 );
338 header_map.insert(
339 "resolution".to_string(),
340 cookie_map
341 .get("resolution")
342 .cloned()
343 .unwrap_or_else(|| "1920x1080".to_string()),
344 );
345 header_map.insert("__csrf".to_string(), csrf_token.clone());
346 header_map.insert(
347 "channel".to_string(),
348 cookie_map.get("channel").cloned().unwrap_or_default(),
349 );
350 header_map.insert("requestId".to_string(), request_id);
351
352 if options.check_token {
353 header_map.insert("X-antiCheatToken".to_string(), CHECK_TOKEN.to_string());
354 }
355
356 if let Some(music_u) = cookie_map.get("MUSIC_U") {
357 header_map.insert("MUSIC_U".to_string(), music_u.clone());
358 }
359 if let Some(music_a) = cookie_map.get("MUSIC_A") {
360 header_map.insert("MUSIC_A".to_string(), music_a.clone());
361 }
362
363 let header_cookie_str = header_map
364 .iter()
365 .map(|(k, v)| format!("{}={}", urlencoding::encode(k), urlencoding::encode(v)))
366 .collect::<Vec<_>>()
367 .join("; ");
368
369 headers.insert(COOKIE, header_value(&header_cookie_str));
370
371 let ua = options
372 .ua
373 .as_deref()
374 .unwrap_or_else(|| choose_user_agent("api", "iphone"));
375 headers.insert(USER_AGENT, header_value(ua));
376
377 let api_domain = if domain.is_empty() {
378 API_DOMAIN
379 } else {
380 domain
381 };
382
383 if crypto_type == CryptoType::Eapi {
384 let header_value = serde_json::to_value(&header_map).unwrap();
386 data["header"] = header_value;
387
388 let e_r = options.e_r.unwrap_or(ENCRYPT_RESPONSE);
389 data["e_r"] = Value::Bool(e_r);
390
391 encrypt_data = crypto::eapi(uri, &data);
392 url = format!("{}/eapi/{}", api_domain, &uri[5..]);
393 } else {
394 encrypt_data = if let Value::Object(map) = &data {
396 map.iter()
397 .map(|(k, v)| {
398 (
399 k.clone(),
400 match v {
401 Value::String(s) => s.clone(),
402 _ => v.to_string(),
403 },
404 )
405 })
406 .collect()
407 } else {
408 HashMap::new()
409 };
410 url = format!("{}{}", api_domain, uri);
411 }
412 }
413 }
414
415 let body = serde_urlencoded::to_string(&encrypt_data)
417 .map_err(|e| NcmError::Unknown(e.to_string()))?;
418
419 headers.insert(
421 reqwest::header::CONTENT_TYPE,
422 HeaderValue::from_static("application/x-www-form-urlencoded"),
423 );
424
425 let response = if let Some(ref proxy_url) = options.proxy {
427 let proxy = reqwest::Proxy::all(proxy_url)
428 .map_err(|e| NcmError::Unknown(format!("Invalid proxy URL: {}", e)))?;
429 let proxy_client = reqwest::Client::builder()
430 .proxy(proxy)
431 .build()
432 .map_err(NcmError::Http)?;
433 proxy_client
434 .post(&url)
435 .headers(headers)
436 .body(body)
437 .send()
438 .await?
439 } else {
440 self.client
441 .post(&url)
442 .headers(headers)
443 .body(body)
444 .send()
445 .await?
446 };
447
448 let resp_cookies: Vec<String> = response
450 .headers()
451 .get_all("set-cookie")
452 .iter()
453 .filter_map(|v| v.to_str().ok())
454 .map(|s| {
455 DOMAIN_REGEX.replace_all(s, "").to_string()
457 })
458 .collect();
459
460 let e_r = options.e_r.unwrap_or(false);
462 let status_code = response.status().as_u16() as i64;
463
464 let body: Value = if crypto_type == CryptoType::Eapi && e_r {
465 let bytes = response.bytes().await?;
466 let hex_str = hex::encode_upper(&bytes);
467 crypto::eapi_res_decrypt(&hex_str).unwrap_or(Value::Null)
468 } else {
469 let text = response.text().await?;
470 serde_json::from_str(&text).unwrap_or(Value::String(text))
471 };
472
473 let mut status = body
474 .get("code")
475 .and_then(|c| {
476 c.as_i64()
477 .or_else(|| c.as_str().and_then(|s| s.parse().ok()))
478 })
479 .unwrap_or(status_code);
480
481 if SPECIAL_STATUS_CODES.contains(&status) {
483 status = 200;
484 }
485
486 if !(100..600).contains(&status) {
488 status = 400;
489 }
490
491 let answer = ApiResponse {
492 status,
493 body,
494 cookie: resp_cookies,
495 };
496
497 if status == 200 {
498 Ok(answer)
499 } else {
500 let msg = answer
501 .body
502 .get("msg")
503 .and_then(|m| m.as_str())
504 .unwrap_or("Unknown error")
505 .to_string();
506 Err(NcmError::from_api(status, msg))
507 }
508 }
509}