prfitem.rs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  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. /// 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: u64,
  49. pub download: u64,
  50. pub total: u64,
  51. pub expire: u64,
  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. /// for `remote` profile
  70. /// disable certificate validation
  71. /// default is `false`
  72. #[serde(skip_serializing_if = "Option::is_none")]
  73. pub danger_accept_invalid_certs: Option<bool>,
  74. }
  75. impl PrfOption {
  76. pub fn merge(one: Option<Self>, other: Option<Self>) -> Option<Self> {
  77. match (one, other) {
  78. (Some(mut a), Some(b)) => {
  79. a.user_agent = b.user_agent.or(a.user_agent);
  80. a.with_proxy = b.with_proxy.or(a.with_proxy);
  81. a.self_proxy = b.self_proxy.or(a.self_proxy);
  82. a.danger_accept_invalid_certs = b.danger_accept_invalid_certs.or(a.danger_accept_invalid_certs);
  83. a.update_interval = b.update_interval.or(a.update_interval);
  84. Some(a)
  85. }
  86. t => t.0.or(t.1),
  87. }
  88. }
  89. }
  90. impl PrfItem {
  91. /// From partial item
  92. /// must contain `itype`
  93. pub async fn from(item: PrfItem, file_data: Option<String>) -> Result<PrfItem> {
  94. if item.itype.is_none() {
  95. bail!("type should not be null");
  96. }
  97. match item.itype.unwrap().as_str() {
  98. "remote" => {
  99. if item.url.is_none() {
  100. bail!("url should not be null");
  101. }
  102. let url = item.url.as_ref().unwrap().as_str();
  103. let name = item.name;
  104. let desc = item.desc;
  105. PrfItem::from_url(url, name, desc, item.option).await
  106. }
  107. "local" => {
  108. let name = item.name.unwrap_or("Local File".into());
  109. let desc = item.desc.unwrap_or("".into());
  110. PrfItem::from_local(name, desc, file_data)
  111. }
  112. "merge" => {
  113. let name = item.name.unwrap_or("Merge".into());
  114. let desc = item.desc.unwrap_or("".into());
  115. PrfItem::from_merge(name, desc)
  116. }
  117. "script" => {
  118. let name = item.name.unwrap_or("Script".into());
  119. let desc = item.desc.unwrap_or("".into());
  120. PrfItem::from_script(name, desc)
  121. }
  122. typ => bail!("invalid profile item type \"{typ}\""),
  123. }
  124. }
  125. /// ## Local type
  126. /// create a new item from name/desc
  127. pub fn from_local(name: String, desc: String, file_data: Option<String>) -> Result<PrfItem> {
  128. let uid = help::get_uid("l");
  129. let file = format!("{uid}.yaml");
  130. Ok(PrfItem {
  131. uid: Some(uid),
  132. itype: Some("local".into()),
  133. name: Some(name),
  134. desc: Some(desc),
  135. file: Some(file),
  136. url: None,
  137. selected: None,
  138. extra: None,
  139. option: None,
  140. updated: Some(chrono::Local::now().timestamp() as usize),
  141. file_data: Some(file_data.unwrap_or(tmpl::ITEM_LOCAL.into())),
  142. })
  143. }
  144. /// ## Remote type
  145. /// create a new item from url
  146. pub async fn from_url(
  147. url: &str,
  148. name: Option<String>,
  149. desc: Option<String>,
  150. option: Option<PrfOption>,
  151. ) -> Result<PrfItem> {
  152. let opt_ref = option.as_ref();
  153. let with_proxy = opt_ref.map_or(false, |o| o.with_proxy.unwrap_or(false));
  154. let self_proxy = opt_ref.map_or(false, |o| o.self_proxy.unwrap_or(false));
  155. let accept_invalid_certs = opt_ref.map_or(false, |o| o.danger_accept_invalid_certs.unwrap_or(false));
  156. let user_agent = opt_ref.and_then(|o| o.user_agent.clone());
  157. let update_interval = opt_ref.and_then(|o| o.update_interval);
  158. let mut builder = reqwest::ClientBuilder::new().use_rustls_tls().no_proxy();
  159. // 使用软件自己的代理
  160. if self_proxy {
  161. let port = Config::verge()
  162. .latest()
  163. .verge_mixed_port
  164. .unwrap_or(Config::clash().data().get_mixed_port());
  165. let proxy_scheme = format!("http://127.0.0.1:{port}");
  166. if let Ok(proxy) = reqwest::Proxy::http(&proxy_scheme) {
  167. builder = builder.proxy(proxy);
  168. }
  169. if let Ok(proxy) = reqwest::Proxy::https(&proxy_scheme) {
  170. builder = builder.proxy(proxy);
  171. }
  172. if let Ok(proxy) = reqwest::Proxy::all(&proxy_scheme) {
  173. builder = builder.proxy(proxy);
  174. }
  175. }
  176. // 使用系统代理
  177. else if with_proxy {
  178. if let Ok(p @ Sysproxy { enable: true, .. }) = Sysproxy::get_system_proxy() {
  179. let proxy_scheme = format!("http://{}:{}", p.host, p.port);
  180. if let Ok(proxy) = reqwest::Proxy::http(&proxy_scheme) {
  181. builder = builder.proxy(proxy);
  182. }
  183. if let Ok(proxy) = reqwest::Proxy::https(&proxy_scheme) {
  184. builder = builder.proxy(proxy);
  185. }
  186. if let Ok(proxy) = reqwest::Proxy::all(&proxy_scheme) {
  187. builder = builder.proxy(proxy);
  188. }
  189. }
  190. }
  191. let version = match VERSION.get() {
  192. Some(v) => format!("clash-verge/v{}", v),
  193. None => "clash-verge/unknown".to_string(),
  194. };
  195. builder = builder.danger_accept_invalid_certs(accept_invalid_certs);
  196. builder = builder.user_agent(user_agent.unwrap_or(version));
  197. let resp = builder.build()?.get(url).send().await?;
  198. let status_code = resp.status();
  199. if !StatusCode::is_success(&status_code) {
  200. bail!("failed to fetch remote profile with status {status_code}")
  201. }
  202. let header = resp.headers();
  203. // parse the Subscription UserInfo
  204. let extra = match header.get("Subscription-Userinfo") {
  205. Some(value) => {
  206. let sub_info = value.to_str().unwrap_or("");
  207. Some(PrfExtra {
  208. upload: help::parse_str(sub_info, "upload").unwrap_or(0),
  209. download: help::parse_str(sub_info, "download").unwrap_or(0),
  210. total: help::parse_str(sub_info, "total").unwrap_or(0),
  211. expire: help::parse_str(sub_info, "expire").unwrap_or(0),
  212. })
  213. }
  214. None => None,
  215. };
  216. // parse the Content-Disposition
  217. let filename = match header.get("Content-Disposition") {
  218. Some(value) => {
  219. let filename = format!("{value:?}");
  220. let filename = filename.trim_matches('"');
  221. match help::parse_str::<String>(filename, "filename*") {
  222. Some(filename) => {
  223. let iter = percent_encoding::percent_decode(filename.as_bytes());
  224. let filename = iter.decode_utf8().unwrap_or_default();
  225. filename.split("''").last().map(|s| s.to_string())
  226. }
  227. None => match help::parse_str::<String>(filename, "filename") {
  228. Some(filename) => {
  229. let filename = filename.trim_matches('"');
  230. Some(filename.to_string())
  231. }
  232. None => None,
  233. },
  234. }
  235. }
  236. None => Some(
  237. crate::utils::help::get_last_part_and_decode(url).unwrap_or("Remote File".into()),
  238. ),
  239. };
  240. let option = match update_interval {
  241. Some(val) => Some(PrfOption {
  242. update_interval: Some(val),
  243. ..PrfOption::default()
  244. }),
  245. None => match header.get("profile-update-interval") {
  246. Some(value) => match value.to_str().unwrap_or("").parse::<u64>() {
  247. Ok(val) => Some(PrfOption {
  248. update_interval: Some(val * 60), // hour -> min
  249. ..PrfOption::default()
  250. }),
  251. Err(_) => None,
  252. },
  253. None => None,
  254. },
  255. };
  256. let uid = help::get_uid("r");
  257. let file = format!("{uid}.yaml");
  258. let name = name.unwrap_or(filename.unwrap_or("Remote File".into()));
  259. let data = resp.text_with_charset("utf-8").await?;
  260. // process the charset "UTF-8 with BOM"
  261. let data = data.trim_start_matches('\u{feff}');
  262. // check the data whether the valid yaml format
  263. let yaml = serde_yaml::from_str::<Mapping>(data)
  264. .context("the remote profile data is invalid yaml")?;
  265. if !yaml.contains_key("proxies") && !yaml.contains_key("proxy-providers") {
  266. bail!("profile does not contain `proxies` or `proxy-providers`");
  267. }
  268. Ok(PrfItem {
  269. uid: Some(uid),
  270. itype: Some("remote".into()),
  271. name: Some(name),
  272. desc,
  273. file: Some(file),
  274. url: Some(url.into()),
  275. selected: None,
  276. extra,
  277. option,
  278. updated: Some(chrono::Local::now().timestamp() as usize),
  279. file_data: Some(data.into()),
  280. })
  281. }
  282. /// ## Merge type (enhance)
  283. /// create the enhanced item by using `merge` rule
  284. pub fn from_merge(name: String, desc: String) -> Result<PrfItem> {
  285. let uid = help::get_uid("m");
  286. let file = format!("{uid}.yaml");
  287. Ok(PrfItem {
  288. uid: Some(uid),
  289. itype: Some("merge".into()),
  290. name: Some(name),
  291. desc: Some(desc),
  292. file: Some(file),
  293. url: None,
  294. selected: None,
  295. extra: None,
  296. option: None,
  297. updated: Some(chrono::Local::now().timestamp() as usize),
  298. file_data: Some(tmpl::ITEM_MERGE.into()),
  299. })
  300. }
  301. /// ## Script type (enhance)
  302. /// create the enhanced item by using javascript quick.js
  303. pub fn from_script(name: String, desc: String) -> Result<PrfItem> {
  304. let uid = help::get_uid("s");
  305. let file = format!("{uid}.js"); // js ext
  306. Ok(PrfItem {
  307. uid: Some(uid),
  308. itype: Some("script".into()),
  309. name: Some(name),
  310. desc: Some(desc),
  311. file: Some(file),
  312. url: None,
  313. selected: None,
  314. extra: None,
  315. option: None,
  316. updated: Some(chrono::Local::now().timestamp() as usize),
  317. file_data: Some(tmpl::ITEM_SCRIPT.into()),
  318. })
  319. }
  320. /// get the file data
  321. pub fn read_file(&self) -> Result<String> {
  322. if self.file.is_none() {
  323. bail!("could not find the file");
  324. }
  325. let file = self.file.clone().unwrap();
  326. let path = dirs::app_profiles_dir()?.join(file);
  327. fs::read_to_string(path).context("failed to read the file")
  328. }
  329. /// save the file data
  330. pub fn save_file(&self, data: String) -> Result<()> {
  331. if self.file.is_none() {
  332. bail!("could not find the file");
  333. }
  334. let file = self.file.clone().unwrap();
  335. let path = dirs::app_profiles_dir()?.join(file);
  336. fs::write(path, data.as_bytes()).context("failed to save the file")
  337. }
  338. }