prfitem.rs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. use crate::utils::{dirs, help, tmpl};
  2. use anyhow::{bail, Context, Result};
  3. use reqwest::StatusCode;
  4. use serde::{Deserialize, Serialize};
  5. use serde_yaml::Mapping;
  6. use std::fs;
  7. use sysproxy::Sysproxy;
  8. use super::Config;
  9. #[derive(Debug, Clone, Deserialize, Serialize)]
  10. pub struct PrfItem {
  11. pub uid: Option<String>,
  12. /// profile item type
  13. /// enum value: remote | local | script | merge
  14. #[serde(rename = "type")]
  15. pub itype: Option<String>,
  16. /// profile name
  17. pub name: Option<String>,
  18. /// profile file
  19. pub file: Option<String>,
  20. /// profile description
  21. #[serde(skip_serializing_if = "Option::is_none")]
  22. pub desc: Option<String>,
  23. /// source url
  24. #[serde(skip_serializing_if = "Option::is_none")]
  25. pub url: Option<String>,
  26. /// selected information
  27. #[serde(skip_serializing_if = "Option::is_none")]
  28. pub selected: Option<Vec<PrfSelected>>,
  29. /// subscription user info
  30. #[serde(skip_serializing_if = "Option::is_none")]
  31. pub extra: Option<PrfExtra>,
  32. /// updated time
  33. pub updated: Option<usize>,
  34. /// some options of the item
  35. #[serde(skip_serializing_if = "Option::is_none")]
  36. pub option: Option<PrfOption>,
  37. /// the file data
  38. #[serde(skip)]
  39. pub file_data: Option<String>,
  40. }
  41. #[derive(Default, Debug, Clone, Deserialize, Serialize)]
  42. pub struct PrfSelected {
  43. pub name: Option<String>,
  44. pub now: Option<String>,
  45. }
  46. #[derive(Default, Debug, Clone, Copy, Deserialize, Serialize)]
  47. pub struct PrfExtra {
  48. pub upload: usize,
  49. pub download: usize,
  50. pub total: usize,
  51. pub expire: usize,
  52. }
  53. #[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
  54. pub struct PrfOption {
  55. /// for `remote` profile's http request
  56. /// see issue #13
  57. #[serde(skip_serializing_if = "Option::is_none")]
  58. pub user_agent: Option<String>,
  59. /// for `remote` profile
  60. /// use system proxy
  61. #[serde(skip_serializing_if = "Option::is_none")]
  62. pub with_proxy: Option<bool>,
  63. /// for `remote` profile
  64. /// use self proxy
  65. #[serde(skip_serializing_if = "Option::is_none")]
  66. pub self_proxy: Option<bool>,
  67. #[serde(skip_serializing_if = "Option::is_none")]
  68. pub update_interval: Option<u64>,
  69. }
  70. impl PrfOption {
  71. pub fn merge(one: Option<Self>, other: Option<Self>) -> Option<Self> {
  72. match (one, other) {
  73. (Some(mut a), Some(b)) => {
  74. a.user_agent = b.user_agent.or(a.user_agent);
  75. a.with_proxy = b.with_proxy.or(a.with_proxy);
  76. a.self_proxy = b.self_proxy.or(a.self_proxy);
  77. a.update_interval = b.update_interval.or(a.update_interval);
  78. Some(a)
  79. }
  80. t @ _ => t.0.or(t.1),
  81. }
  82. }
  83. }
  84. impl Default for PrfItem {
  85. fn default() -> Self {
  86. PrfItem {
  87. uid: None,
  88. itype: None,
  89. name: None,
  90. desc: None,
  91. file: None,
  92. url: None,
  93. selected: None,
  94. extra: None,
  95. updated: None,
  96. option: None,
  97. file_data: None,
  98. }
  99. }
  100. }
  101. impl PrfItem {
  102. /// From partial item
  103. /// must contain `itype`
  104. pub async fn from(item: PrfItem, file_data: Option<String>) -> Result<PrfItem> {
  105. if item.itype.is_none() {
  106. bail!("type should not be null");
  107. }
  108. match item.itype.unwrap().as_str() {
  109. "remote" => {
  110. if item.url.is_none() {
  111. bail!("url should not be null");
  112. }
  113. let url = item.url.as_ref().unwrap().as_str();
  114. let name = item.name;
  115. let desc = item.desc;
  116. PrfItem::from_url(url, name, desc, item.option).await
  117. }
  118. "local" => {
  119. let name = item.name.unwrap_or("Local File".into());
  120. let desc = item.desc.unwrap_or("".into());
  121. PrfItem::from_local(name, desc, file_data)
  122. }
  123. "merge" => {
  124. let name = item.name.unwrap_or("Merge".into());
  125. let desc = item.desc.unwrap_or("".into());
  126. PrfItem::from_merge(name, desc)
  127. }
  128. "script" => {
  129. let name = item.name.unwrap_or("Script".into());
  130. let desc = item.desc.unwrap_or("".into());
  131. PrfItem::from_script(name, desc)
  132. }
  133. typ @ _ => bail!("invalid profile item type \"{typ}\""),
  134. }
  135. }
  136. /// ## Local type
  137. /// create a new item from name/desc
  138. pub fn from_local(name: String, desc: String, file_data: Option<String>) -> Result<PrfItem> {
  139. let uid = help::get_uid("l");
  140. let file = format!("{uid}.yaml");
  141. Ok(PrfItem {
  142. uid: Some(uid),
  143. itype: Some("local".into()),
  144. name: Some(name),
  145. desc: Some(desc),
  146. file: Some(file),
  147. url: None,
  148. selected: None,
  149. extra: None,
  150. option: None,
  151. updated: Some(chrono::Local::now().timestamp() as usize),
  152. file_data: Some(file_data.unwrap_or(tmpl::ITEM_LOCAL.into())),
  153. })
  154. }
  155. /// ## Remote type
  156. /// create a new item from url
  157. pub async fn from_url(
  158. url: &str,
  159. name: Option<String>,
  160. desc: Option<String>,
  161. option: Option<PrfOption>,
  162. ) -> Result<PrfItem> {
  163. let opt_ref = option.as_ref();
  164. let with_proxy = opt_ref.map_or(false, |o| o.with_proxy.unwrap_or(false));
  165. let self_proxy = opt_ref.map_or(false, |o| o.self_proxy.unwrap_or(false));
  166. let user_agent = opt_ref.map_or(None, |o| o.user_agent.clone());
  167. let mut builder = reqwest::ClientBuilder::new().no_proxy();
  168. // 使用软件自己的代理
  169. if self_proxy {
  170. let port = Config::clash().data().get_info()?.port;
  171. let port = port.ok_or(anyhow::anyhow!("failed to get clash info port"))?;
  172. let proxy_scheme = format!("http://127.0.0.1:{port}");
  173. if let Ok(proxy) = reqwest::Proxy::http(&proxy_scheme) {
  174. builder = builder.proxy(proxy);
  175. }
  176. if let Ok(proxy) = reqwest::Proxy::https(&proxy_scheme) {
  177. builder = builder.proxy(proxy);
  178. }
  179. if let Ok(proxy) = reqwest::Proxy::all(&proxy_scheme) {
  180. builder = builder.proxy(proxy);
  181. }
  182. }
  183. // 使用系统代理
  184. else if with_proxy {
  185. match Sysproxy::get_system_proxy() {
  186. Ok(p @ Sysproxy { enable: true, .. }) => {
  187. let proxy_scheme = format!("http://{}:{}", p.host, p.port);
  188. if let Ok(proxy) = reqwest::Proxy::http(&proxy_scheme) {
  189. builder = builder.proxy(proxy);
  190. }
  191. if let Ok(proxy) = reqwest::Proxy::https(&proxy_scheme) {
  192. builder = builder.proxy(proxy);
  193. }
  194. if let Ok(proxy) = reqwest::Proxy::all(&proxy_scheme) {
  195. builder = builder.proxy(proxy);
  196. }
  197. }
  198. _ => {}
  199. };
  200. }
  201. let version = unsafe { dirs::APP_VERSION };
  202. let version = format!("clash-verge/{version}");
  203. builder = builder.user_agent(user_agent.unwrap_or(version));
  204. let resp = builder.build()?.get(url).send().await?;
  205. let status_code = resp.status();
  206. if !StatusCode::is_success(&status_code) {
  207. bail!("failed to fetch remote profile with status {status_code}")
  208. }
  209. let header = resp.headers();
  210. // parse the Subscription UserInfo
  211. let extra = match header.get("Subscription-Userinfo") {
  212. Some(value) => {
  213. let sub_info = value.to_str().unwrap_or("");
  214. Some(PrfExtra {
  215. upload: help::parse_str(sub_info, "upload=").unwrap_or(0),
  216. download: help::parse_str(sub_info, "download=").unwrap_or(0),
  217. total: help::parse_str(sub_info, "total=").unwrap_or(0),
  218. expire: help::parse_str(sub_info, "expire=").unwrap_or(0),
  219. })
  220. }
  221. None => None,
  222. };
  223. // parse the Content-Disposition
  224. let filename = match header.get("Content-Disposition") {
  225. Some(value) => {
  226. let filename = value.to_str().unwrap_or("");
  227. help::parse_str::<String>(filename, "filename=")
  228. }
  229. None => None,
  230. };
  231. // parse the profile-update-interval
  232. let option = match header.get("profile-update-interval") {
  233. Some(value) => match value.to_str().unwrap_or("").parse::<u64>() {
  234. Ok(val) => Some(PrfOption {
  235. update_interval: Some(val * 60), // hour -> min
  236. ..PrfOption::default()
  237. }),
  238. Err(_) => None,
  239. },
  240. None => None,
  241. };
  242. let uid = help::get_uid("r");
  243. let file = format!("{uid}.yaml");
  244. let name = name.unwrap_or(filename.unwrap_or("Remote File".into()));
  245. let data = resp.text_with_charset("utf-8").await?;
  246. // check the data whether the valid yaml format
  247. let yaml = serde_yaml::from_str::<Mapping>(&data) //
  248. .context("the remote profile data is invalid yaml")?;
  249. if !yaml.contains_key("proxies") && !yaml.contains_key("proxy-providers") {
  250. bail!("profile does not contain `proxies` or `proxy-providers`");
  251. }
  252. Ok(PrfItem {
  253. uid: Some(uid),
  254. itype: Some("remote".into()),
  255. name: Some(name),
  256. desc,
  257. file: Some(file),
  258. url: Some(url.into()),
  259. selected: None,
  260. extra,
  261. option,
  262. updated: Some(chrono::Local::now().timestamp() as usize),
  263. file_data: Some(data),
  264. })
  265. }
  266. /// ## Merge type (enhance)
  267. /// create the enhanced item by using `merge` rule
  268. pub fn from_merge(name: String, desc: String) -> Result<PrfItem> {
  269. let uid = help::get_uid("m");
  270. let file = format!("{uid}.yaml");
  271. Ok(PrfItem {
  272. uid: Some(uid),
  273. itype: Some("merge".into()),
  274. name: Some(name),
  275. desc: Some(desc),
  276. file: Some(file),
  277. url: None,
  278. selected: None,
  279. extra: None,
  280. option: None,
  281. updated: Some(chrono::Local::now().timestamp() as usize),
  282. file_data: Some(tmpl::ITEM_MERGE.into()),
  283. })
  284. }
  285. /// ## Script type (enhance)
  286. /// create the enhanced item by using javascript quick.js
  287. pub fn from_script(name: String, desc: String) -> Result<PrfItem> {
  288. let uid = help::get_uid("s");
  289. let file = format!("{uid}.js"); // js ext
  290. Ok(PrfItem {
  291. uid: Some(uid),
  292. itype: Some("script".into()),
  293. name: Some(name),
  294. desc: Some(desc),
  295. file: Some(file),
  296. url: None,
  297. selected: None,
  298. extra: None,
  299. option: None,
  300. updated: Some(chrono::Local::now().timestamp() as usize),
  301. file_data: Some(tmpl::ITEM_SCRIPT.into()),
  302. })
  303. }
  304. /// get the file data
  305. pub fn read_file(&self) -> Result<String> {
  306. if self.file.is_none() {
  307. bail!("could not find the file");
  308. }
  309. let file = self.file.clone().unwrap();
  310. let path = dirs::app_profiles_dir()?.join(file);
  311. fs::read_to_string(path).context("failed to read the file")
  312. }
  313. /// save the file data
  314. pub fn save_file(&self, data: String) -> Result<()> {
  315. if self.file.is_none() {
  316. bail!("could not find the file");
  317. }
  318. let file = self.file.clone().unwrap();
  319. let path = dirs::app_profiles_dir()?.join(file);
  320. fs::write(path, data.as_bytes()).context("failed to save the file")
  321. }
  322. /// get the data for enhanced mode
  323. pub fn to_enhance(&self) -> Option<ChainItem> {
  324. let itype = self.itype.as_ref()?.as_str();
  325. let file = self.file.clone()?;
  326. let uid = self.uid.clone().unwrap_or("".into());
  327. let path = dirs::app_profiles_dir().ok()?.join(file);
  328. if !path.exists() {
  329. return None;
  330. }
  331. match itype {
  332. "script" => Some(ChainItem {
  333. uid,
  334. data: ChainType::Script(fs::read_to_string(path).ok()?),
  335. }),
  336. "merge" => Some(ChainItem {
  337. uid,
  338. data: ChainType::Merge(help::read_merge_mapping(&path).ok()?),
  339. }),
  340. _ => None,
  341. }
  342. }
  343. }
  344. #[derive(Debug, Clone)]
  345. pub struct ChainItem {
  346. pub uid: String,
  347. pub data: ChainType,
  348. }
  349. #[derive(Debug, Clone)]
  350. pub enum ChainType {
  351. Merge(Mapping),
  352. Script(String),
  353. }