ipc.rs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. // Copyright 2019-2023 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. use std::sync::{Arc, Mutex};
  5. use crate::{Config, Runtime, Window};
  6. use url::Url;
  7. /// IPC access configuration for a remote domain.
  8. #[derive(Debug, Clone)]
  9. pub struct RemoteDomainAccessScope {
  10. scheme: Option<String>,
  11. domain: String,
  12. windows: Vec<String>,
  13. plugins: Vec<String>,
  14. enable_tauri_api: bool,
  15. }
  16. impl RemoteDomainAccessScope {
  17. /// Creates a new access scope.
  18. pub fn new(domain: impl Into<String>) -> Self {
  19. Self {
  20. scheme: None,
  21. domain: domain.into(),
  22. windows: Vec::new(),
  23. plugins: Vec::new(),
  24. enable_tauri_api: false,
  25. }
  26. }
  27. /// Sets the scheme of the URL to allow in this scope. By default, all schemes with the given domain are allowed.
  28. pub fn allow_on_scheme(mut self, scheme: impl Into<String>) -> Self {
  29. self.scheme.replace(scheme.into());
  30. self
  31. }
  32. /// Adds the given window label to the list of windows that uses this scope.
  33. pub fn add_window(mut self, window: impl Into<String>) -> Self {
  34. self.windows.push(window.into());
  35. self
  36. }
  37. /// Adds the given plugin to the allowed plugin list.
  38. pub fn add_plugin(mut self, plugin: impl Into<String>) -> Self {
  39. self.plugins.push(plugin.into());
  40. self
  41. }
  42. /// Enables access to the Tauri API.
  43. pub fn enable_tauri_api(mut self) -> Self {
  44. self.enable_tauri_api = true;
  45. self
  46. }
  47. /// The domain of the URLs that can access this scope.
  48. pub fn domain(&self) -> &str {
  49. &self.domain
  50. }
  51. /// The list of window labels that can access this scope.
  52. pub fn windows(&self) -> &Vec<String> {
  53. &self.windows
  54. }
  55. /// The list of plugins enabled by this scope.
  56. pub fn plugins(&self) -> &Vec<String> {
  57. &self.plugins
  58. }
  59. /// Whether this scope enables Tauri API access or not.
  60. pub fn enables_tauri_api(&self) -> bool {
  61. self.enable_tauri_api
  62. }
  63. }
  64. pub(crate) struct RemoteAccessError {
  65. pub matches_window: bool,
  66. pub matches_domain: bool,
  67. }
  68. /// IPC scope.
  69. #[derive(Clone)]
  70. pub struct Scope {
  71. remote_access: Arc<Mutex<Vec<RemoteDomainAccessScope>>>,
  72. }
  73. impl Scope {
  74. pub(crate) fn new(config: &Config) -> Self {
  75. #[allow(unused_mut)]
  76. let mut remote_access: Vec<RemoteDomainAccessScope> = config
  77. .tauri
  78. .security
  79. .dangerous_remote_domain_ipc_access
  80. .clone()
  81. .into_iter()
  82. .map(|s| RemoteDomainAccessScope {
  83. scheme: s.scheme,
  84. domain: s.domain,
  85. windows: s.windows,
  86. plugins: s.plugins,
  87. enable_tauri_api: s.enable_tauri_api,
  88. })
  89. .collect();
  90. Self {
  91. remote_access: Arc::new(Mutex::new(remote_access)),
  92. }
  93. }
  94. /// Adds the given configuration for remote access.
  95. ///
  96. /// # Examples
  97. ///
  98. /// ```
  99. /// use tauri::{Manager, scope::ipc::RemoteDomainAccessScope};
  100. /// tauri::Builder::default()
  101. /// .setup(|app| {
  102. /// app.ipc_scope().configure_remote_access(
  103. /// RemoteDomainAccessScope::new("tauri.app")
  104. /// .add_window("main")
  105. /// .enable_tauri_api()
  106. /// );
  107. /// Ok(())
  108. /// });
  109. /// ```
  110. pub fn configure_remote_access(&self, access: RemoteDomainAccessScope) {
  111. self.remote_access.lock().unwrap().push(access);
  112. }
  113. pub(crate) fn remote_access_for<R: Runtime>(
  114. &self,
  115. window: &Window<R>,
  116. url: &Url,
  117. ) -> Result<RemoteDomainAccessScope, RemoteAccessError> {
  118. let mut scope = None;
  119. let mut found_scope_for_window = false;
  120. let mut found_scope_for_domain = false;
  121. let label = window.label().to_string();
  122. for s in &*self.remote_access.lock().unwrap() {
  123. #[allow(unused_mut)]
  124. let mut matches_window = s.windows.contains(&label);
  125. let matches_scheme = s
  126. .scheme
  127. .as_ref()
  128. .map(|scheme| scheme == url.scheme())
  129. .unwrap_or(true);
  130. let matches_domain =
  131. matches_scheme && url.domain().map(|d| d == s.domain).unwrap_or_default();
  132. found_scope_for_window = found_scope_for_window || matches_window;
  133. found_scope_for_domain = found_scope_for_domain || matches_domain;
  134. if matches_window && matches_domain && scope.is_none() {
  135. scope.replace(s.clone());
  136. }
  137. }
  138. if let Some(s) = scope {
  139. Ok(s)
  140. } else {
  141. Err(RemoteAccessError {
  142. matches_window: found_scope_for_window,
  143. matches_domain: found_scope_for_domain,
  144. })
  145. }
  146. }
  147. }
  148. #[cfg(test)]
  149. mod tests {
  150. use serde::Serialize;
  151. use super::RemoteDomainAccessScope;
  152. use crate::{api::ipc::CallbackFn, test::MockRuntime, App, InvokePayload, Manager, Window};
  153. const PLUGIN_NAME: &str = "test";
  154. fn test_context(scopes: Vec<RemoteDomainAccessScope>) -> (App<MockRuntime>, Window<MockRuntime>) {
  155. let app = crate::test::mock_app();
  156. let window = app.get_window("main").unwrap();
  157. for scope in scopes {
  158. app.ipc_scope().configure_remote_access(scope);
  159. }
  160. (app, window)
  161. }
  162. fn assert_ipc_response<R: Serialize>(
  163. window: &Window<MockRuntime>,
  164. payload: InvokePayload,
  165. expected: Result<R, &str>,
  166. ) {
  167. let callback = payload.callback;
  168. let error = payload.error;
  169. window.clone().on_message(payload).unwrap();
  170. let mut num_tries = 0;
  171. let evaluated_script = loop {
  172. std::thread::sleep(std::time::Duration::from_millis(50));
  173. let evaluated_script = window.dispatcher().last_evaluated_script();
  174. if let Some(s) = evaluated_script {
  175. break s;
  176. }
  177. num_tries += 1;
  178. if num_tries == 20 {
  179. panic!("Response script not evaluated");
  180. }
  181. };
  182. let (expected_response, fn_name) = match expected {
  183. Ok(payload) => (serde_json::to_value(payload).unwrap(), callback),
  184. Err(payload) => (serde_json::to_value(payload).unwrap(), error),
  185. };
  186. let expected = format!(
  187. "window[\"_{}\"]({})",
  188. fn_name.0,
  189. crate::api::ipc::serialize_js(&expected_response).unwrap()
  190. );
  191. println!("Last evaluated script:");
  192. println!("{evaluated_script}");
  193. println!("Expected:");
  194. println!("{expected}");
  195. assert!(evaluated_script.contains(&expected));
  196. }
  197. fn path_is_absolute_payload() -> InvokePayload {
  198. let callback = CallbackFn(0);
  199. let error = CallbackFn(1);
  200. let mut payload = serde_json::Map::new();
  201. payload.insert(
  202. "path".into(),
  203. serde_json::Value::String(std::env::current_dir().unwrap().display().to_string()),
  204. );
  205. InvokePayload {
  206. cmd: "plugin:path|is_absolute".into(),
  207. tauri_module: None,
  208. callback,
  209. error,
  210. inner: serde_json::Value::Object(payload),
  211. }
  212. }
  213. fn plugin_test_payload() -> InvokePayload {
  214. let callback = CallbackFn(0);
  215. let error = CallbackFn(1);
  216. InvokePayload {
  217. cmd: format!("plugin:{PLUGIN_NAME}|doSomething"),
  218. tauri_module: None,
  219. callback,
  220. error,
  221. inner: Default::default(),
  222. }
  223. }
  224. #[test]
  225. fn scope_not_defined() {
  226. let (_app, window) = test_context(vec![RemoteDomainAccessScope::new("app.tauri.app")
  227. .add_window("other")
  228. .add_plugin("path")
  229. .enable_tauri_api()]);
  230. window.navigate("https://tauri.app".parse().unwrap());
  231. assert_ipc_response::<()>(
  232. &window,
  233. path_is_absolute_payload(),
  234. Err(&crate::window::ipc_scope_not_found_error_message(
  235. "main",
  236. "https://tauri.app/",
  237. )),
  238. );
  239. }
  240. #[test]
  241. fn scope_not_defined_for_window() {
  242. let (_app, window) = test_context(vec![RemoteDomainAccessScope::new("tauri.app")
  243. .add_window("second")
  244. .add_plugin("path")
  245. .enable_tauri_api()]);
  246. window.navigate("https://tauri.app".parse().unwrap());
  247. assert_ipc_response::<()>(
  248. &window,
  249. path_is_absolute_payload(),
  250. Err(&crate::window::ipc_scope_window_error_message("main")),
  251. );
  252. }
  253. #[test]
  254. fn scope_not_defined_for_url() {
  255. let (_app, window) = test_context(vec![RemoteDomainAccessScope::new("github.com")
  256. .add_window("main")
  257. .add_plugin("path")
  258. .enable_tauri_api()]);
  259. window.navigate("https://tauri.app".parse().unwrap());
  260. assert_ipc_response::<()>(
  261. &window,
  262. path_is_absolute_payload(),
  263. Err(&crate::window::ipc_scope_domain_error_message(
  264. "https://tauri.app/",
  265. )),
  266. );
  267. }
  268. #[test]
  269. fn subdomain_is_not_allowed() {
  270. let (_app, mut window) = test_context(vec![
  271. RemoteDomainAccessScope::new("tauri.app")
  272. .add_window("main")
  273. .add_plugin("path")
  274. .enable_tauri_api(),
  275. RemoteDomainAccessScope::new("sub.tauri.app")
  276. .add_window("main")
  277. .add_plugin("path")
  278. .enable_tauri_api(),
  279. ]);
  280. window.navigate("https://tauri.app".parse().unwrap());
  281. assert_ipc_response(&window, path_is_absolute_payload(), Ok(true));
  282. window.navigate("https://blog.tauri.app".parse().unwrap());
  283. assert_ipc_response::<()>(
  284. &window,
  285. path_is_absolute_payload(),
  286. Err(&crate::window::ipc_scope_domain_error_message(
  287. "https://blog.tauri.app/",
  288. )),
  289. );
  290. window.navigate("https://sub.tauri.app".parse().unwrap());
  291. assert_ipc_response(&window, path_is_absolute_payload(), Ok(true));
  292. window.window.label = "test".into();
  293. window.navigate("https://dev.tauri.app".parse().unwrap());
  294. assert_ipc_response::<()>(
  295. &window,
  296. path_is_absolute_payload(),
  297. Err(&crate::window::ipc_scope_not_found_error_message(
  298. "test",
  299. "https://dev.tauri.app/",
  300. )),
  301. );
  302. }
  303. #[test]
  304. fn subpath_is_allowed() {
  305. let (_app, window) = test_context(vec![RemoteDomainAccessScope::new("tauri.app")
  306. .add_window("main")
  307. .add_plugin("path")
  308. .enable_tauri_api()]);
  309. window.navigate("https://tauri.app/inner/path".parse().unwrap());
  310. assert_ipc_response(&window, path_is_absolute_payload(), Ok(true));
  311. }
  312. #[test]
  313. fn tauri_api_not_allowed() {
  314. let (_app, window) = test_context(vec![
  315. RemoteDomainAccessScope::new("tauri.app").add_window("main")
  316. ]);
  317. window.navigate("https://tauri.app".parse().unwrap());
  318. assert_ipc_response::<()>(
  319. &window,
  320. path_is_absolute_payload(),
  321. Err(crate::window::IPC_SCOPE_DOES_NOT_ALLOW),
  322. );
  323. }
  324. #[test]
  325. fn plugin_allowed() {
  326. let (_app, window) = test_context(vec![RemoteDomainAccessScope::new("tauri.app")
  327. .add_window("main")
  328. .add_plugin(PLUGIN_NAME)]);
  329. window.navigate("https://tauri.app".parse().unwrap());
  330. assert_ipc_response::<()>(
  331. &window,
  332. plugin_test_payload(),
  333. Err(&format!("plugin {PLUGIN_NAME} not found")),
  334. );
  335. }
  336. #[test]
  337. fn plugin_not_allowed() {
  338. let (_app, window) = test_context(vec![
  339. RemoteDomainAccessScope::new("tauri.app").add_window("main")
  340. ]);
  341. window.navigate("https://tauri.app".parse().unwrap());
  342. assert_ipc_response::<()>(
  343. &window,
  344. plugin_test_payload(),
  345. Err(crate::window::IPC_SCOPE_DOES_NOT_ALLOW),
  346. );
  347. }
  348. }