prfitem.rs 13 KB

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