prfitem.rs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  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)]
  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().use_rustls_tls().no_proxy();
  168. // 使用软件自己的代理
  169. if self_proxy {
  170. let port = Config::verge()
  171. .latest()
  172. .verge_mixed_port
  173. .unwrap_or(Config::clash().data().get_mixed_port());
  174. let proxy_scheme = format!("http://127.0.0.1:{port}");
  175. if let Ok(proxy) = reqwest::Proxy::http(&proxy_scheme) {
  176. builder = builder.proxy(proxy);
  177. }
  178. if let Ok(proxy) = reqwest::Proxy::https(&proxy_scheme) {
  179. builder = builder.proxy(proxy);
  180. }
  181. if let Ok(proxy) = reqwest::Proxy::all(&proxy_scheme) {
  182. builder = builder.proxy(proxy);
  183. }
  184. }
  185. // 使用系统代理
  186. else if with_proxy {
  187. match Sysproxy::get_system_proxy() {
  188. Ok(p @ Sysproxy { enable: true, .. }) => {
  189. let proxy_scheme = format!("http://{}:{}", p.host, p.port);
  190. if let Ok(proxy) = reqwest::Proxy::http(&proxy_scheme) {
  191. builder = builder.proxy(proxy);
  192. }
  193. if let Ok(proxy) = reqwest::Proxy::https(&proxy_scheme) {
  194. builder = builder.proxy(proxy);
  195. }
  196. if let Ok(proxy) = reqwest::Proxy::all(&proxy_scheme) {
  197. builder = builder.proxy(proxy);
  198. }
  199. }
  200. _ => {}
  201. };
  202. }
  203. let version = match VERSION.get() {
  204. Some(v) => format!("clash-verge/v{}", v),
  205. None => format!("clash-verge/unknown"),
  206. };
  207. builder = builder.user_agent(user_agent.unwrap_or(version));
  208. let resp = builder.build()?.get(url).send().await?;
  209. let status_code = resp.status();
  210. if !StatusCode::is_success(&status_code) {
  211. bail!("failed to fetch remote profile with status {status_code}")
  212. }
  213. let header = resp.headers();
  214. // parse the Subscription UserInfo
  215. let extra = match header.get("Subscription-Userinfo") {
  216. Some(value) => {
  217. let sub_info = value.to_str().unwrap_or("");
  218. Some(PrfExtra {
  219. upload: help::parse_str(sub_info, "upload").unwrap_or(0),
  220. download: help::parse_str(sub_info, "download").unwrap_or(0),
  221. total: help::parse_str(sub_info, "total").unwrap_or(0),
  222. expire: help::parse_str(sub_info, "expire").unwrap_or(0),
  223. })
  224. }
  225. None => None,
  226. };
  227. // parse the Content-Disposition
  228. let filename = match header.get("Content-Disposition") {
  229. Some(value) => {
  230. let filename = format!("{value:?}");
  231. let filename = filename.trim_matches('"');
  232. match help::parse_str::<String>(filename, "filename*") {
  233. Some(filename) => {
  234. let iter = percent_encoding::percent_decode(filename.as_bytes());
  235. let filename = iter.decode_utf8().unwrap_or_default();
  236. filename.split("''").last().map(|s| s.to_string())
  237. }
  238. None => match help::parse_str::<String>(filename, "filename") {
  239. Some(filename) => {
  240. let filename = filename.trim_matches('"');
  241. Some(filename.to_string())
  242. }
  243. None => None,
  244. },
  245. }
  246. }
  247. None => None,
  248. };
  249. // parse the profile-update-interval
  250. let option = match header.get("profile-update-interval") {
  251. Some(value) => match value.to_str().unwrap_or("").parse::<u64>() {
  252. Ok(val) => Some(PrfOption {
  253. update_interval: Some(val * 60), // hour -> min
  254. ..PrfOption::default()
  255. }),
  256. Err(_) => None,
  257. },
  258. None => None,
  259. };
  260. let uid = help::get_uid("r");
  261. let file = format!("{uid}.yaml");
  262. let name = name.unwrap_or(filename.unwrap_or("Remote File".into()));
  263. let data = resp.text_with_charset("utf-8").await?;
  264. // process the charset "UTF-8 with BOM"
  265. let data = data.trim_start_matches('\u{feff}');
  266. // check the data whether the valid yaml format
  267. let yaml = serde_yaml::from_str::<Mapping>(data)
  268. .context("the remote profile data is invalid yaml")?;
  269. if !yaml.contains_key("proxies") && !yaml.contains_key("proxy-providers") {
  270. bail!("profile does not contain `proxies` or `proxy-providers`");
  271. }
  272. Ok(PrfItem {
  273. uid: Some(uid),
  274. itype: Some("remote".into()),
  275. name: Some(name),
  276. desc,
  277. file: Some(file),
  278. url: Some(url.into()),
  279. selected: None,
  280. extra,
  281. option,
  282. updated: Some(chrono::Local::now().timestamp() as usize),
  283. file_data: Some(data.into()),
  284. })
  285. }
  286. /// ## Merge type (enhance)
  287. /// create the enhanced item by using `merge` rule
  288. pub fn from_merge(name: String, desc: String) -> Result<PrfItem> {
  289. let uid = help::get_uid("m");
  290. let file = format!("{uid}.yaml");
  291. Ok(PrfItem {
  292. uid: Some(uid),
  293. itype: Some("merge".into()),
  294. name: Some(name),
  295. desc: Some(desc),
  296. file: Some(file),
  297. url: None,
  298. selected: None,
  299. extra: None,
  300. option: None,
  301. updated: Some(chrono::Local::now().timestamp() as usize),
  302. file_data: Some(tmpl::ITEM_MERGE.into()),
  303. })
  304. }
  305. /// ## Script type (enhance)
  306. /// create the enhanced item by using javascript quick.js
  307. pub fn from_script(name: String, desc: String) -> Result<PrfItem> {
  308. let uid = help::get_uid("s");
  309. let file = format!("{uid}.js"); // js ext
  310. Ok(PrfItem {
  311. uid: Some(uid),
  312. itype: Some("script".into()),
  313. name: Some(name),
  314. desc: Some(desc),
  315. file: Some(file),
  316. url: None,
  317. selected: None,
  318. extra: None,
  319. option: None,
  320. updated: Some(chrono::Local::now().timestamp() as usize),
  321. file_data: Some(tmpl::ITEM_SCRIPT.into()),
  322. })
  323. }
  324. /// get the file data
  325. pub fn read_file(&self) -> Result<String> {
  326. if self.file.is_none() {
  327. bail!("could not find the file");
  328. }
  329. let file = self.file.clone().unwrap();
  330. let path = dirs::app_profiles_dir()?.join(file);
  331. fs::read_to_string(path).context("failed to read the file")
  332. }
  333. /// save the file data
  334. pub fn save_file(&self, data: String) -> Result<()> {
  335. if self.file.is_none() {
  336. bail!("could not find the file");
  337. }
  338. let file = self.file.clone().unwrap();
  339. let path = dirs::app_profiles_dir()?.join(file);
  340. fs::write(path, data.as_bytes()).context("failed to save the file")
  341. }
  342. }