use crate::crypto;
use crate::error::{NcmError, Result};
use crate::util::config::*;
use crate::util::cookie::{cookie_obj_to_string, cookie_to_json};
use crate::util::device::{generate_device_id, generate_wnmcid, random_hex};
use crate::util::ip::generate_random_chinese_ip;
use reqwest::header::{HeaderMap, HeaderValue, COOKIE, REFERER, USER_AGENT};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::sync::LazyLock;
static SPECIAL_STATUS_CODES: LazyLock<std::collections::HashSet<i64>> =
LazyLock::new(|| [201, 302, 400, 502, 800, 801, 802, 803].into());
static DEVICE_ID: LazyLock<String> = LazyLock::new(generate_device_id);
static WNMCID: LazyLock<String> = LazyLock::new(generate_wnmcid);
static DOMAIN_REGEX: LazyLock<regex_lite::Regex> =
LazyLock::new(|| regex_lite::Regex::new(r"\s*Domain=[^;]+;?").unwrap());
fn header_value(s: &str) -> HeaderValue {
HeaderValue::from_str(s).unwrap_or_else(|_| {
let safe: String = s
.chars()
.filter(|c| c.is_ascii() && !c.is_ascii_control())
.collect();
HeaderValue::from_str(&safe).unwrap_or_else(|_| HeaderValue::from_static(""))
})
}
#[derive(Debug, Clone, PartialEq, Default)]
pub enum CryptoType {
Weapi,
#[default]
Eapi,
Linuxapi,
Api, }
impl CryptoType {
pub fn as_str(&self) -> &str {
match self {
CryptoType::Weapi => "weapi",
CryptoType::Eapi => "eapi",
CryptoType::Linuxapi => "linuxapi",
CryptoType::Api => "api",
}
}
}
impl From<&str> for CryptoType {
fn from(s: &str) -> Self {
match s {
"weapi" => CryptoType::Weapi,
"linuxapi" => CryptoType::Linuxapi,
"api" => CryptoType::Api,
_ => CryptoType::Eapi,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct RequestOption {
pub crypto: CryptoType,
pub cookie: Option<String>,
pub ua: Option<String>,
pub proxy: Option<String>,
pub real_ip: Option<String>,
pub random_cn_ip: bool,
pub e_r: Option<bool>,
pub domain: Option<String>,
pub check_token: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApiResponse {
pub status: i64,
pub body: Value,
#[serde(default)]
pub cookie: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct ApiClient {
pub(crate) client: reqwest::Client,
cookie: Option<String>,
anonymous_token: Option<String>,
device_id: Option<String>,
}
impl ApiClient {
pub fn new(cookie: Option<String>) -> Self {
let client = reqwest::Client::builder()
.build()
.expect("Failed to create HTTP client");
Self {
client,
cookie,
anonymous_token: None,
device_id: None,
}
}
pub fn with_proxy(cookie: Option<String>, proxy_url: &str) -> Result<Self> {
let proxy = reqwest::Proxy::all(proxy_url)
.map_err(|e| NcmError::Unknown(format!("Invalid proxy URL: {}", e)))?;
let client = reqwest::Client::builder()
.proxy(proxy)
.build()
.map_err(NcmError::Http)?;
Ok(Self {
client,
cookie,
anonymous_token: None,
device_id: None,
})
}
pub fn set_cookie(&mut self, cookie: String) {
self.cookie = Some(cookie);
}
pub fn set_anonymous_token(&mut self, token: String) {
self.anonymous_token = Some(token);
}
pub fn set_device_id(&mut self, device_id: String) {
self.device_id = Some(device_id);
}
fn get_device_id(&self) -> &str {
self.device_id.as_deref().unwrap_or(&DEVICE_ID)
}
pub async fn request(
&self,
uri: &str,
data: Value,
options: RequestOption,
) -> Result<ApiResponse> {
let mut headers = HeaderMap::new();
let ip = options.real_ip.clone().or_else(|| {
if options.random_cn_ip {
Some(generate_random_chinese_ip())
} else {
None
}
});
if let Some(ref ip) = ip {
if let (Ok(real_ip), Ok(fwd)) = (HeaderValue::from_str(ip), HeaderValue::from_str(ip)) {
headers.insert("X-Real-IP", real_ip);
headers.insert("X-Forwarded-For", fwd);
}
}
let cookie_str = options
.cookie
.as_deref()
.or(self.cookie.as_deref())
.unwrap_or("");
let mut cookie_map = cookie_to_json(cookie_str);
let ntes_nuid = random_hex(16);
let os = get_os_config(cookie_map.get("os").map(|s| s.as_str()).unwrap_or("pc"));
let now_ts = chrono::Utc::now().timestamp_millis().to_string();
cookie_map
.entry("__remember_me".to_string())
.or_insert_with(|| "true".to_string());
cookie_map
.entry("ntes_kaola_ad".to_string())
.or_insert_with(|| "1".to_string());
cookie_map
.entry("_ntes_nnid".to_string())
.or_insert_with(|| format!("{},{}", ntes_nuid, now_ts));
cookie_map
.entry("_ntes_nuid".to_string())
.or_insert(ntes_nuid);
cookie_map
.entry("WNMCID".to_string())
.or_insert_with(|| WNMCID.clone());
cookie_map
.entry("WEVNSM".to_string())
.or_insert_with(|| "1.0.0".to_string());
cookie_map
.entry("osver".to_string())
.or_insert_with(|| os.osver.to_string());
cookie_map
.entry("deviceId".to_string())
.or_insert_with(|| self.get_device_id().to_string());
cookie_map
.entry("os".to_string())
.or_insert_with(|| os.os.to_string());
cookie_map
.entry("channel".to_string())
.or_insert_with(|| os.channel.to_string());
cookie_map
.entry("appver".to_string())
.or_insert_with(|| os.appver.to_string());
if !uri.contains("login") {
cookie_map
.entry("NMTID".to_string())
.or_insert_with(|| random_hex(8));
}
if !cookie_map.contains_key("MUSIC_U") {
if let Some(ref token) = self.anonymous_token {
cookie_map
.entry("MUSIC_A".to_string())
.or_insert_with(|| token.clone());
}
}
headers.insert(COOKIE, header_value(&cookie_obj_to_string(&cookie_map)));
let crypto_type = if options.crypto == CryptoType::Eapi && !ENCRYPT {
CryptoType::Api
} else {
options.crypto.clone()
};
let mut data = data;
let url: String;
let encrypt_data: HashMap<String, String>;
let domain = options.domain.as_deref().unwrap_or("");
let csrf_token = cookie_map.get("__csrf").cloned().unwrap_or_default();
match crypto_type {
CryptoType::Weapi => {
let ref_domain = if domain.is_empty() { DOMAIN } else { domain };
headers.insert(REFERER, header_value(ref_domain));
let ua = options
.ua
.as_deref()
.unwrap_or_else(|| choose_user_agent("weapi", "pc"));
headers.insert(USER_AGENT, header_value(ua));
data["csrf_token"] = Value::String(csrf_token);
encrypt_data = crypto::weapi(&data);
url = format!("{}/weapi/{}", ref_domain, &uri[5..]);
}
CryptoType::Linuxapi => {
let ua = options
.ua
.as_deref()
.unwrap_or_else(|| choose_user_agent("linuxapi", "linux"));
headers.insert(USER_AGENT, header_value(ua));
let ref_domain = if domain.is_empty() { DOMAIN } else { domain };
let linux_data = serde_json::json!({
"method": "POST",
"url": format!("{}{}", ref_domain, uri),
"params": data,
});
encrypt_data = crypto::linuxapi(&linux_data);
url = format!("{}/api/linux/forward", ref_domain);
}
CryptoType::Eapi | CryptoType::Api => {
let now_secs = chrono::Utc::now().timestamp().to_string();
let request_id = format!(
"{}_{:04}",
chrono::Utc::now().timestamp_millis(),
rand::random::<u16>() % 1000
);
let mut header_map: HashMap<String, String> = HashMap::new();
header_map.insert(
"osver".to_string(),
cookie_map.get("osver").cloned().unwrap_or_default(),
);
header_map.insert(
"deviceId".to_string(),
cookie_map.get("deviceId").cloned().unwrap_or_default(),
);
header_map.insert(
"os".to_string(),
cookie_map.get("os").cloned().unwrap_or_default(),
);
header_map.insert(
"appver".to_string(),
cookie_map.get("appver").cloned().unwrap_or_default(),
);
header_map.insert(
"versioncode".to_string(),
cookie_map
.get("versioncode")
.cloned()
.unwrap_or_else(|| "140".to_string()),
);
header_map.insert(
"mobilename".to_string(),
cookie_map.get("mobilename").cloned().unwrap_or_default(),
);
header_map.insert(
"buildver".to_string(),
cookie_map
.get("buildver")
.cloned()
.unwrap_or_else(|| now_secs[..10].to_string()),
);
header_map.insert(
"resolution".to_string(),
cookie_map
.get("resolution")
.cloned()
.unwrap_or_else(|| "1920x1080".to_string()),
);
header_map.insert("__csrf".to_string(), csrf_token.clone());
header_map.insert(
"channel".to_string(),
cookie_map.get("channel").cloned().unwrap_or_default(),
);
header_map.insert("requestId".to_string(), request_id);
if options.check_token {
header_map.insert("X-antiCheatToken".to_string(), CHECK_TOKEN.to_string());
}
if let Some(music_u) = cookie_map.get("MUSIC_U") {
header_map.insert("MUSIC_U".to_string(), music_u.clone());
}
if let Some(music_a) = cookie_map.get("MUSIC_A") {
header_map.insert("MUSIC_A".to_string(), music_a.clone());
}
let header_cookie_str = header_map
.iter()
.map(|(k, v)| format!("{}={}", urlencoding::encode(k), urlencoding::encode(v)))
.collect::<Vec<_>>()
.join("; ");
headers.insert(COOKIE, header_value(&header_cookie_str));
let ua = options
.ua
.as_deref()
.unwrap_or_else(|| choose_user_agent("api", "iphone"));
headers.insert(USER_AGENT, header_value(ua));
let api_domain = if domain.is_empty() {
API_DOMAIN
} else {
domain
};
if crypto_type == CryptoType::Eapi {
let header_value = serde_json::to_value(&header_map).unwrap();
data["header"] = header_value;
let e_r = options.e_r.unwrap_or(ENCRYPT_RESPONSE);
data["e_r"] = Value::Bool(e_r);
encrypt_data = crypto::eapi(uri, &data);
url = format!("{}/eapi/{}", api_domain, &uri[5..]);
} else {
encrypt_data = if let Value::Object(map) = &data {
map.iter()
.map(|(k, v)| {
(
k.clone(),
match v {
Value::String(s) => s.clone(),
_ => v.to_string(),
},
)
})
.collect()
} else {
HashMap::new()
};
url = format!("{}{}", api_domain, uri);
}
}
}
let body = serde_urlencoded::to_string(&encrypt_data)
.map_err(|e| NcmError::Unknown(e.to_string()))?;
headers.insert(
reqwest::header::CONTENT_TYPE,
HeaderValue::from_static("application/x-www-form-urlencoded"),
);
let response = if let Some(ref proxy_url) = options.proxy {
let proxy = reqwest::Proxy::all(proxy_url)
.map_err(|e| NcmError::Unknown(format!("Invalid proxy URL: {}", e)))?;
let proxy_client = reqwest::Client::builder()
.proxy(proxy)
.build()
.map_err(NcmError::Http)?;
proxy_client
.post(&url)
.headers(headers)
.body(body)
.send()
.await?
} else {
self.client
.post(&url)
.headers(headers)
.body(body)
.send()
.await?
};
let resp_cookies: Vec<String> = response
.headers()
.get_all("set-cookie")
.iter()
.filter_map(|v| v.to_str().ok())
.map(|s| {
DOMAIN_REGEX.replace_all(s, "").to_string()
})
.collect();
let e_r = options.e_r.unwrap_or(false);
let status_code = response.status().as_u16() as i64;
let body: Value = if crypto_type == CryptoType::Eapi && e_r {
let bytes = response.bytes().await?;
let hex_str = hex::encode_upper(&bytes);
crypto::eapi_res_decrypt(&hex_str).unwrap_or(Value::Null)
} else {
let text = response.text().await?;
serde_json::from_str(&text).unwrap_or(Value::String(text))
};
let mut status = body
.get("code")
.and_then(|c| {
c.as_i64()
.or_else(|| c.as_str().and_then(|s| s.parse().ok()))
})
.unwrap_or(status_code);
if SPECIAL_STATUS_CODES.contains(&status) {
status = 200;
}
if !(100..600).contains(&status) {
status = 400;
}
let answer = ApiResponse {
status,
body,
cookie: resp_cookies,
};
if status == 200 {
Ok(answer)
} else {
let msg = answer
.body
.get("msg")
.and_then(|m| m.as_str())
.unwrap_or("Unknown error")
.to_string();
Err(NcmError::from_api(status, msg))
}
}
}