prfitem.rs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563
  1. use crate::utils::{dirs, help, resolve::VERSION, 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, Default)]
  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. /// profile web page url
  38. #[serde(skip_serializing_if = "Option::is_none")]
  39. pub home: Option<String>,
  40. /// the file data
  41. #[serde(skip)]
  42. pub file_data: Option<String>,
  43. }
  44. #[derive(Default, Debug, Clone, Deserialize, Serialize)]
  45. pub struct PrfSelected {
  46. pub name: Option<String>,
  47. pub now: Option<String>,
  48. }
  49. #[derive(Default, Debug, Clone, Copy, Deserialize, Serialize)]
  50. pub struct PrfExtra {
  51. pub upload: u64,
  52. pub download: u64,
  53. pub total: u64,
  54. pub expire: u64,
  55. }
  56. #[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
  57. pub struct PrfOption {
  58. /// for `remote` profile's http request
  59. /// see issue #13
  60. #[serde(skip_serializing_if = "Option::is_none")]
  61. pub user_agent: Option<String>,
  62. /// for `remote` profile
  63. /// use system proxy
  64. #[serde(skip_serializing_if = "Option::is_none")]
  65. pub with_proxy: Option<bool>,
  66. /// for `remote` profile
  67. /// use self proxy
  68. #[serde(skip_serializing_if = "Option::is_none")]
  69. pub self_proxy: Option<bool>,
  70. #[serde(skip_serializing_if = "Option::is_none")]
  71. pub update_interval: Option<u64>,
  72. /// for `remote` profile
  73. /// disable certificate validation
  74. /// default is `false`
  75. #[serde(skip_serializing_if = "Option::is_none")]
  76. pub danger_accept_invalid_certs: Option<bool>,
  77. pub merge: Option<String>,
  78. pub script: Option<String>,
  79. pub rules: Option<String>,
  80. pub proxies: Option<String>,
  81. pub groups: Option<String>,
  82. }
  83. impl PrfOption {
  84. pub fn merge(one: Option<Self>, other: Option<Self>) -> Option<Self> {
  85. match (one, other) {
  86. (Some(mut a), Some(b)) => {
  87. a.user_agent = b.user_agent.or(a.user_agent);
  88. a.with_proxy = b.with_proxy.or(a.with_proxy);
  89. a.self_proxy = b.self_proxy.or(a.self_proxy);
  90. a.danger_accept_invalid_certs = b
  91. .danger_accept_invalid_certs
  92. .or(a.danger_accept_invalid_certs);
  93. a.update_interval = b.update_interval.or(a.update_interval);
  94. a.merge = b.merge.or(a.merge);
  95. a.script = b.script.or(a.script);
  96. a.rules = b.rules.or(a.rules);
  97. a.proxies = b.proxies.or(a.proxies);
  98. a.groups = b.groups.or(a.groups);
  99. Some(a)
  100. }
  101. t => t.0.or(t.1),
  102. }
  103. }
  104. }
  105. impl PrfItem {
  106. /// From partial item
  107. /// must contain `itype`
  108. pub async fn from(item: PrfItem, file_data: Option<String>) -> Result<PrfItem> {
  109. if item.itype.is_none() {
  110. bail!("type should not be null");
  111. }
  112. match item.itype.unwrap().as_str() {
  113. "remote" => {
  114. if item.url.is_none() {
  115. bail!("url should not be null");
  116. }
  117. let url = item.url.as_ref().unwrap().as_str();
  118. let name = item.name;
  119. let desc = item.desc;
  120. PrfItem::from_url(url, name, desc, item.option).await
  121. }
  122. "local" => {
  123. let name = item.name.unwrap_or("Local File".into());
  124. let desc = item.desc.unwrap_or("".into());
  125. PrfItem::from_local(name, desc, file_data, item.option)
  126. }
  127. typ => bail!("invalid profile item type \"{typ}\""),
  128. }
  129. }
  130. /// ## Local type
  131. /// create a new item from name/desc
  132. pub fn from_local(
  133. name: String,
  134. desc: String,
  135. file_data: Option<String>,
  136. option: Option<PrfOption>,
  137. ) -> Result<PrfItem> {
  138. let uid = help::get_uid("L");
  139. let file = format!("{uid}.yaml");
  140. let opt_ref = option.as_ref();
  141. let update_interval = opt_ref.and_then(|o| o.update_interval);
  142. let mut merge = opt_ref.and_then(|o| o.merge.clone());
  143. let mut script = opt_ref.and_then(|o| o.script.clone());
  144. let mut rules = opt_ref.and_then(|o| o.rules.clone());
  145. let mut proxies = opt_ref.and_then(|o| o.proxies.clone());
  146. let mut groups = opt_ref.and_then(|o| o.groups.clone());
  147. if merge.is_none() {
  148. let merge_item = PrfItem::from_merge(None)?;
  149. Config::profiles().data().append_item(merge_item.clone())?;
  150. merge = merge_item.uid;
  151. }
  152. if script.is_none() {
  153. let script_item = PrfItem::from_script(None)?;
  154. Config::profiles().data().append_item(script_item.clone())?;
  155. script = script_item.uid;
  156. }
  157. if rules.is_none() {
  158. let rules_item = PrfItem::from_rules()?;
  159. Config::profiles().data().append_item(rules_item.clone())?;
  160. rules = rules_item.uid;
  161. }
  162. if proxies.is_none() {
  163. let proxies_item = PrfItem::from_proxies()?;
  164. Config::profiles()
  165. .data()
  166. .append_item(proxies_item.clone())?;
  167. proxies = proxies_item.uid;
  168. }
  169. if groups.is_none() {
  170. let groups_item = PrfItem::from_groups()?;
  171. Config::profiles().data().append_item(groups_item.clone())?;
  172. groups = groups_item.uid;
  173. }
  174. Ok(PrfItem {
  175. uid: Some(uid),
  176. itype: Some("local".into()),
  177. name: Some(name),
  178. desc: Some(desc),
  179. file: Some(file),
  180. url: None,
  181. selected: None,
  182. extra: None,
  183. option: Some(PrfOption {
  184. update_interval,
  185. merge,
  186. script,
  187. rules,
  188. proxies,
  189. groups,
  190. ..PrfOption::default()
  191. }),
  192. home: None,
  193. updated: Some(chrono::Local::now().timestamp() as usize),
  194. file_data: Some(file_data.unwrap_or(tmpl::ITEM_LOCAL.into())),
  195. })
  196. }
  197. /// ## Remote type
  198. /// create a new item from url
  199. pub async fn from_url(
  200. url: &str,
  201. name: Option<String>,
  202. desc: Option<String>,
  203. option: Option<PrfOption>,
  204. ) -> Result<PrfItem> {
  205. let opt_ref = option.as_ref();
  206. let with_proxy = opt_ref.map_or(false, |o| o.with_proxy.unwrap_or(false));
  207. let self_proxy = opt_ref.map_or(false, |o| o.self_proxy.unwrap_or(false));
  208. let accept_invalid_certs =
  209. opt_ref.map_or(false, |o| o.danger_accept_invalid_certs.unwrap_or(false));
  210. let user_agent = opt_ref.and_then(|o| o.user_agent.clone());
  211. let update_interval = opt_ref.and_then(|o| o.update_interval);
  212. let mut merge = opt_ref.and_then(|o| o.merge.clone());
  213. let mut script = opt_ref.and_then(|o| o.script.clone());
  214. let mut rules = opt_ref.and_then(|o| o.rules.clone());
  215. let mut proxies = opt_ref.and_then(|o| o.proxies.clone());
  216. let mut groups = opt_ref.and_then(|o| o.groups.clone());
  217. let mut builder = reqwest::ClientBuilder::new().use_rustls_tls().no_proxy();
  218. if merge.is_none() {
  219. let merge_item = PrfItem::from_merge(None)?;
  220. Config::profiles().data().append_item(merge_item.clone())?;
  221. merge = merge_item.uid;
  222. }
  223. if script.is_none() {
  224. let script_item = PrfItem::from_script(None)?;
  225. Config::profiles().data().append_item(script_item.clone())?;
  226. script = script_item.uid;
  227. }
  228. if rules.is_none() {
  229. let rules_item = PrfItem::from_rules()?;
  230. Config::profiles().data().append_item(rules_item.clone())?;
  231. rules = rules_item.uid;
  232. }
  233. if proxies.is_none() {
  234. let proxies_item = PrfItem::from_proxies()?;
  235. Config::profiles()
  236. .data()
  237. .append_item(proxies_item.clone())?;
  238. proxies = proxies_item.uid;
  239. }
  240. if groups.is_none() {
  241. let groups_item = PrfItem::from_groups()?;
  242. Config::profiles().data().append_item(groups_item.clone())?;
  243. groups = groups_item.uid;
  244. }
  245. // 使用软件自己的代理
  246. if self_proxy {
  247. let port = Config::verge()
  248. .latest()
  249. .verge_mixed_port
  250. .unwrap_or(Config::clash().data().get_mixed_port());
  251. let proxy_scheme = format!("http://127.0.0.1:{port}");
  252. if let Ok(proxy) = reqwest::Proxy::http(&proxy_scheme) {
  253. builder = builder.proxy(proxy);
  254. }
  255. if let Ok(proxy) = reqwest::Proxy::https(&proxy_scheme) {
  256. builder = builder.proxy(proxy);
  257. }
  258. if let Ok(proxy) = reqwest::Proxy::all(&proxy_scheme) {
  259. builder = builder.proxy(proxy);
  260. }
  261. }
  262. // 使用系统代理
  263. else if with_proxy {
  264. if let Ok(p @ Sysproxy { enable: true, .. }) = Sysproxy::get_system_proxy() {
  265. let proxy_scheme = format!("http://{}:{}", p.host, p.port);
  266. if let Ok(proxy) = reqwest::Proxy::http(&proxy_scheme) {
  267. builder = builder.proxy(proxy);
  268. }
  269. if let Ok(proxy) = reqwest::Proxy::https(&proxy_scheme) {
  270. builder = builder.proxy(proxy);
  271. }
  272. if let Ok(proxy) = reqwest::Proxy::all(&proxy_scheme) {
  273. builder = builder.proxy(proxy);
  274. }
  275. }
  276. }
  277. let version = match VERSION.get() {
  278. Some(v) => format!("clash-verge/v{}", v),
  279. None => "clash-verge/unknown".to_string(),
  280. };
  281. builder = builder.danger_accept_invalid_certs(accept_invalid_certs);
  282. builder = builder.user_agent(user_agent.unwrap_or(version));
  283. let resp = builder.build()?.get(url).send().await?;
  284. let status_code = resp.status();
  285. if !StatusCode::is_success(&status_code) {
  286. bail!("failed to fetch remote profile with status {status_code}")
  287. }
  288. let header = resp.headers();
  289. // parse the Subscription UserInfo
  290. let extra = match header.get("Subscription-Userinfo") {
  291. Some(value) => {
  292. let sub_info = value.to_str().unwrap_or("");
  293. Some(PrfExtra {
  294. upload: help::parse_str(sub_info, "upload").unwrap_or(0),
  295. download: help::parse_str(sub_info, "download").unwrap_or(0),
  296. total: help::parse_str(sub_info, "total").unwrap_or(0),
  297. expire: help::parse_str(sub_info, "expire").unwrap_or(0),
  298. })
  299. }
  300. None => None,
  301. };
  302. // parse the Content-Disposition
  303. let filename = match header.get("Content-Disposition") {
  304. Some(value) => {
  305. let filename = format!("{value:?}");
  306. let filename = filename.trim_matches('"');
  307. match help::parse_str::<String>(filename, "filename*") {
  308. Some(filename) => {
  309. let iter = percent_encoding::percent_decode(filename.as_bytes());
  310. let filename = iter.decode_utf8().unwrap_or_default();
  311. filename.split("''").last().map(|s| s.to_string())
  312. }
  313. None => match help::parse_str::<String>(filename, "filename") {
  314. Some(filename) => {
  315. let filename = filename.trim_matches('"');
  316. Some(filename.to_string())
  317. }
  318. None => None,
  319. },
  320. }
  321. }
  322. None => Some(
  323. crate::utils::help::get_last_part_and_decode(url).unwrap_or("Remote File".into()),
  324. ),
  325. };
  326. let update_interval = match update_interval {
  327. Some(val) => Some(val),
  328. None => match header.get("profile-update-interval") {
  329. Some(value) => match value.to_str().unwrap_or("").parse::<u64>() {
  330. Ok(val) => Some(val * 60), // hour -> min
  331. Err(_) => None,
  332. },
  333. None => None,
  334. },
  335. };
  336. let home = match header.get("profile-web-page-url") {
  337. Some(value) => {
  338. let str_value = value.to_str().unwrap_or("");
  339. Some(str_value.to_string())
  340. }
  341. None => None,
  342. };
  343. let uid = help::get_uid("R");
  344. let file = format!("{uid}.yaml");
  345. let name = name.unwrap_or(filename.unwrap_or("Remote File".into()));
  346. let data = resp.text_with_charset("utf-8").await?;
  347. // process the charset "UTF-8 with BOM"
  348. let data = data.trim_start_matches('\u{feff}');
  349. // check the data whether the valid yaml format
  350. let yaml = serde_yaml::from_str::<Mapping>(data)
  351. .context("the remote profile data is invalid yaml")?;
  352. if !yaml.contains_key("proxies") && !yaml.contains_key("proxy-providers") {
  353. bail!("profile does not contain `proxies` or `proxy-providers`");
  354. }
  355. Ok(PrfItem {
  356. uid: Some(uid),
  357. itype: Some("remote".into()),
  358. name: Some(name),
  359. desc,
  360. file: Some(file),
  361. url: Some(url.into()),
  362. selected: None,
  363. extra,
  364. option: Some(PrfOption {
  365. update_interval,
  366. merge,
  367. script,
  368. rules,
  369. proxies,
  370. groups,
  371. ..PrfOption::default()
  372. }),
  373. home,
  374. updated: Some(chrono::Local::now().timestamp() as usize),
  375. file_data: Some(data.into()),
  376. })
  377. }
  378. /// ## Merge type (enhance)
  379. /// create the enhanced item by using `merge` rule
  380. pub fn from_merge(uid: Option<String>) -> Result<PrfItem> {
  381. let mut id = help::get_uid("m");
  382. let mut template = tmpl::ITEM_MERGE_EMPTY.into();
  383. if let Some(uid) = uid {
  384. id = uid;
  385. template = tmpl::ITEM_MERGE.into();
  386. }
  387. let file = format!("{id}.yaml");
  388. Ok(PrfItem {
  389. uid: Some(id),
  390. itype: Some("merge".into()),
  391. name: None,
  392. desc: None,
  393. file: Some(file),
  394. url: None,
  395. selected: None,
  396. extra: None,
  397. option: None,
  398. home: None,
  399. updated: Some(chrono::Local::now().timestamp() as usize),
  400. file_data: Some(template),
  401. })
  402. }
  403. /// ## Script type (enhance)
  404. /// create the enhanced item by using javascript quick.js
  405. pub fn from_script(uid: Option<String>) -> Result<PrfItem> {
  406. let mut id = help::get_uid("s");
  407. if let Some(uid) = uid {
  408. id = uid;
  409. }
  410. let file = format!("{id}.js"); // js ext
  411. Ok(PrfItem {
  412. uid: Some(id),
  413. itype: Some("script".into()),
  414. name: None,
  415. desc: None,
  416. file: Some(file),
  417. url: None,
  418. home: None,
  419. selected: None,
  420. extra: None,
  421. option: None,
  422. updated: Some(chrono::Local::now().timestamp() as usize),
  423. file_data: Some(tmpl::ITEM_SCRIPT.into()),
  424. })
  425. }
  426. /// ## Rules type (enhance)
  427. pub fn from_rules() -> Result<PrfItem> {
  428. let uid = help::get_uid("r");
  429. let file = format!("{uid}.yaml"); // yaml ext
  430. Ok(PrfItem {
  431. uid: Some(uid),
  432. itype: Some("rules".into()),
  433. name: None,
  434. desc: None,
  435. file: Some(file),
  436. url: None,
  437. home: None,
  438. selected: None,
  439. extra: None,
  440. option: None,
  441. updated: Some(chrono::Local::now().timestamp() as usize),
  442. file_data: Some(tmpl::ITEM_RULES.into()),
  443. })
  444. }
  445. /// ## Proxies type (enhance)
  446. pub fn from_proxies() -> Result<PrfItem> {
  447. let uid = help::get_uid("p");
  448. let file = format!("{uid}.yaml"); // yaml ext
  449. Ok(PrfItem {
  450. uid: Some(uid),
  451. itype: Some("proxies".into()),
  452. name: None,
  453. desc: None,
  454. file: Some(file),
  455. url: None,
  456. home: None,
  457. selected: None,
  458. extra: None,
  459. option: None,
  460. updated: Some(chrono::Local::now().timestamp() as usize),
  461. file_data: Some(tmpl::ITEM_PROXIES.into()),
  462. })
  463. }
  464. /// ## Groups type (enhance)
  465. pub fn from_groups() -> Result<PrfItem> {
  466. let uid = help::get_uid("g");
  467. let file = format!("{uid}.yaml"); // yaml ext
  468. Ok(PrfItem {
  469. uid: Some(uid),
  470. itype: Some("groups".into()),
  471. name: None,
  472. desc: None,
  473. file: Some(file),
  474. url: None,
  475. home: None,
  476. selected: None,
  477. extra: None,
  478. option: None,
  479. updated: Some(chrono::Local::now().timestamp() as usize),
  480. file_data: Some(tmpl::ITEM_GROUPS.into()),
  481. })
  482. }
  483. /// get the file data
  484. pub fn read_file(&self) -> Result<String> {
  485. if self.file.is_none() {
  486. bail!("could not find the file");
  487. }
  488. let file = self.file.clone().unwrap();
  489. let path = dirs::app_profiles_dir()?.join(file);
  490. fs::read_to_string(path).context("failed to read the file")
  491. }
  492. /// save the file data
  493. pub fn save_file(&self, data: String) -> Result<()> {
  494. if self.file.is_none() {
  495. bail!("could not find the file");
  496. }
  497. let file = self.file.clone().unwrap();
  498. let path = dirs::app_profiles_dir()?.join(file);
  499. fs::write(path, data.as_bytes()).context("failed to save the file")
  500. }
  501. }