diff --git a/.changes/http-redirect-scope.md b/.changes/http-redirect-scope.md new file mode 100644 index 000000000..58e4616c1 --- /dev/null +++ b/.changes/http-redirect-scope.md @@ -0,0 +1,19 @@ +--- +"http": minor +--- + +**Security:** Added the `scopeRedirects` plugin configuration option, which checks the URL scope on every hop of a redirect chain instead of only on the URL requested by the frontend. Without it, a server on an allowed origin can redirect the request to any other origin - including `localhost` services, internal hosts and cloud metadata endpoints - and the plugin follows it, returning the response to the webview. + +```json +{ + "plugins": { + "http": { + "scopeRedirects": true + } + } +} +``` + +It is opt-in because a redirect to a URL that is not allowed by the scope now fails with `url not allowed on the configured scope` instead of being followed, so applications that rely on being redirected outside of their scope must add the redirect target to the scope. **This will become the default in v3.** + +Note that `tauri_plugin_http::init()` now returns `TauriPlugin>` instead of `TauriPlugin`. diff --git a/plugins/http/guest-js/index.ts b/plugins/http/guest-js/index.ts index 9426e35fe..5406e03f2 100644 --- a/plugins/http/guest-js/index.ts +++ b/plugins/http/guest-js/index.ts @@ -76,6 +76,10 @@ export interface ClientOptions { /** * Defines the maximum number of redirects the client should follow. * If set to 0, no redirects will be followed. + * + * When the `scopeRedirects` plugin configuration is enabled, every redirect must + * also be allowed by the configured scope, otherwise the request fails + * instead of being followed. */ maxRedirections?: number /** Timeout in milliseconds */ diff --git a/plugins/http/src/commands.rs b/plugins/http/src/commands.rs index 08df1509d..4387bae30 100644 --- a/plugins/http/src/commands.rs +++ b/plugins/http/src/commands.rs @@ -22,6 +22,10 @@ use crate::{ const HTTP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); +/// Matches the default [`reqwest`] redirect policy, used when the frontend does not +/// configure `maxRedirections`. +const DEFAULT_MAX_REDIRECTIONS: usize = 10; + struct ReqwestResponse(reqwest::Response); impl tauri::Resource for ReqwestResponse {} @@ -174,6 +178,56 @@ fn attach_proxy( Ok(builder) } +/// Builds the redirect policy for the request. +/// +/// When `scope` is [`Some`], **every** hop is checked against it. Validating only the URL the +/// frontend asked for is not enough: a server on an allowed origin can answer with a redirect to +/// any other origin (an open redirect, or a server the attacker controls), and following it would +/// give the webview access to a URL the scope denies. +/// +/// That check is opt-in through the `scopeRedirects` configuration because it breaks applications +/// that rely on being redirected outside of their scope. +// TODO(v3): always check the scope and take it by value instead of `Option` +fn redirect_policy(scope: Option, max_redirections: Option) -> Policy { + if max_redirections == Some(0) { + return Policy::none(); + } + + let max_redirections = max_redirections.unwrap_or(DEFAULT_MAX_REDIRECTIONS); + + let Some(scope) = scope else { + return Policy::limited(max_redirections); + }; + + Policy::custom(move |attempt| { + // the first URL in `previous` is the initial request, so it must be excluded + if attempt.previous().len() > max_redirections { + attempt.error("too many redirects") + } else if scope.is_allowed(attempt.url()) { + attempt.follow() + } else { + let url = attempt.url().clone(); + attempt.error(Error::UrlNotAllowed(url)) + } + }) +} + +/// [`reqwest`] wraps the error returned by the redirect policy, and its `Display` impl does not +/// include the source, so we unwrap our own scope error to keep the reason visible to the frontend. +fn map_request_error(error: reqwest::Error) -> Error { + if error.is_redirect() { + let mut source = std::error::Error::source(&error); + while let Some(err) = source { + if let Some(Error::UrlNotAllowed(url)) = err.downcast_ref::() { + return Error::UrlNotAllowed(url.clone()); + } + source = err.source(); + } + } + + Error::Network(error) +} + #[command] pub async fn fetch( webview: Webview, @@ -214,118 +268,115 @@ pub async fn fetch( match scheme { "http" | "https" => { - if Scope::new( + let scope = Scope::new( command_scope .allows() .iter() .chain(global_scope.allows()) + .cloned() .collect(), command_scope .denies() .iter() .chain(global_scope.denies()) + .cloned() .collect(), - ) - .is_allowed(&url) - { - let mut builder = reqwest::ClientBuilder::new(); + ); - if let Some(danger_config) = danger { - #[cfg(not(feature = "dangerous-settings"))] - { - #[cfg(debug_assertions)] - { - eprintln!("[\x1b[33mWARNING\x1b[0m] using dangerous settings requires `dangerous-settings` feature flag in your Cargo.toml"); - } - let _ = danger_config; - return Err(Error::DangerousSettings); - } - #[cfg(feature = "dangerous-settings")] - { - builder = builder - .danger_accept_invalid_certs(danger_config.accept_invalid_certs) - .danger_accept_invalid_hostnames(danger_config.accept_invalid_hostnames) - } - } - - if let Some(timeout) = connect_timeout { - builder = builder.connect_timeout(Duration::from_millis(timeout)); - } - - if let Some(max_redirections) = max_redirections { - builder = builder.redirect(if max_redirections == 0 { - Policy::none() - } else { - Policy::limited(max_redirections) - }); - } - - if let Some(proxy_config) = proxy { - builder = attach_proxy(proxy_config, builder)?; - } - - #[cfg(feature = "cookies")] - { - builder = builder.cookie_provider(state.cookies_jar.clone()); - } - - let mut request = builder.build()?.request(method.clone(), url); - - // POST and PUT requests should always have a 0 length content-length, - // if there is no body. https://fetch.spec.whatwg.org/#http-network-or-cache-fetch - if data.is_none() && matches!(method, Method::POST | Method::PUT) { - headers.append(header::CONTENT_LENGTH, HeaderValue::from_str("0")?); - } - - if headers.contains_key(header::RANGE) { - // https://fetch.spec.whatwg.org/#http-network-or-cache-fetch step 18 - // If httpRequest's header list contains `Range`, then append (`Accept-Encoding`, `identity`) - headers.append(header::ACCEPT_ENCODING, HeaderValue::from_str("identity")?); - } - - if !headers.contains_key(header::USER_AGENT) { - headers.append(header::USER_AGENT, HeaderValue::from_str(HTTP_USER_AGENT)?); - } - - // ensure we have an Origin header set - if cfg!(not(feature = "unsafe-headers")) || !headers.contains_key(header::ORIGIN) { - if let Ok(url) = webview.url() { - // The url crate returns OpaqueOrigin for tauri://localhost which serializes to "null" - let origin = if url.scheme() == "tauri" { - "tauri://localhost".to_string() - } else { - url.origin().ascii_serialization() - }; - headers.append(header::ORIGIN, HeaderValue::from_str(&origin)?); - } - } - - // In case empty origin is passed, remove it. Some services do not like Origin header - // so this way we can remove it in explicit way. The default behaviour is still to set it - if cfg!(feature = "unsafe-headers") - && headers.get(header::ORIGIN) == Some(&HeaderValue::from_static("")) - { - headers.remove(header::ORIGIN); - }; - - if let Some(data) = data { - request = request.body(data); - } - - request = request.headers(headers); - - #[cfg(feature = "tracing")] - tracing::trace!("{:?}", request); - - let fut = async move { request.send().await.map_err(Into::into) }; - - let mut resources_table = webview.resources_table(); - let rid = resources_table.add_request(Box::pin(fut)); - - Ok(rid) - } else { - Err(Error::UrlNotAllowed(url)) + if !scope.is_allowed(&url) { + return Err(Error::UrlNotAllowed(url)); } + + let mut builder = reqwest::ClientBuilder::new(); + + if let Some(danger_config) = danger { + #[cfg(not(feature = "dangerous-settings"))] + { + #[cfg(debug_assertions)] + { + eprintln!("[\x1b[33mWARNING\x1b[0m] using dangerous settings requires `dangerous-settings` feature flag in your Cargo.toml"); + } + let _ = danger_config; + return Err(Error::DangerousSettings); + } + #[cfg(feature = "dangerous-settings")] + { + builder = builder + .danger_accept_invalid_certs(danger_config.accept_invalid_certs) + .danger_accept_invalid_hostnames(danger_config.accept_invalid_hostnames) + } + } + + if let Some(timeout) = connect_timeout { + builder = builder.connect_timeout(Duration::from_millis(timeout)); + } + + let scope = state.config.scope_redirects.then_some(scope); + builder = builder.redirect(redirect_policy(scope, max_redirections)); + + if let Some(proxy_config) = proxy { + builder = attach_proxy(proxy_config, builder)?; + } + + #[cfg(feature = "cookies")] + { + builder = builder.cookie_provider(state.cookies_jar.clone()); + } + + let mut request = builder.build()?.request(method.clone(), url); + + // POST and PUT requests should always have a 0 length content-length, + // if there is no body. https://fetch.spec.whatwg.org/#http-network-or-cache-fetch + if data.is_none() && matches!(method, Method::POST | Method::PUT) { + headers.append(header::CONTENT_LENGTH, HeaderValue::from_str("0")?); + } + + if headers.contains_key(header::RANGE) { + // https://fetch.spec.whatwg.org/#http-network-or-cache-fetch step 18 + // If httpRequest's header list contains `Range`, then append (`Accept-Encoding`, `identity`) + headers.append(header::ACCEPT_ENCODING, HeaderValue::from_str("identity")?); + } + + if !headers.contains_key(header::USER_AGENT) { + headers.append(header::USER_AGENT, HeaderValue::from_str(HTTP_USER_AGENT)?); + } + + // ensure we have an Origin header set + if cfg!(not(feature = "unsafe-headers")) || !headers.contains_key(header::ORIGIN) { + if let Ok(url) = webview.url() { + // The url crate returns OpaqueOrigin for tauri://localhost which serializes to "null" + let origin = if url.scheme() == "tauri" { + "tauri://localhost".to_string() + } else { + url.origin().ascii_serialization() + }; + headers.append(header::ORIGIN, HeaderValue::from_str(&origin)?); + } + } + + // In case empty origin is passed, remove it. Some services do not like Origin header + // so this way we can remove it in explicit way. The default behaviour is still to set it + if cfg!(feature = "unsafe-headers") + && headers.get(header::ORIGIN) == Some(&HeaderValue::from_static("")) + { + headers.remove(header::ORIGIN); + }; + + if let Some(data) = data { + request = request.body(data); + } + + request = request.headers(headers); + + #[cfg(feature = "tracing")] + tracing::trace!("{:?}", request); + + let fut = async move { request.send().await.map_err(map_request_error) }; + + let mut resources_table = webview.resources_table(); + let rid = resources_table.add_request(Box::pin(fut)); + + Ok(rid) } "data" => { let data_url = @@ -486,3 +537,182 @@ fn is_unsafe_header(header: &HeaderName) -> bool { lower.starts_with("proxy-") || lower.starts_with("sec-") } } + +#[cfg(test)] +mod tests { + use std::{ + io::{BufRead, BufReader, Write}, + net::TcpListener, + sync::Arc, + }; + + use super::*; + + /// Test server with an open redirect: + /// + /// - `/redirect-external` answers a 302 to `http://127.0.0.1:{port}/secret`, which is a + /// different origin as far as the scope is concerned - the tests allow `localhost`; + /// - `/redirect/{n}` answers a 302 to `/redirect/{n-1}` on `localhost`, and `/redirect/0` + /// answers a 302 to `/target`, so the whole chain stays in scope; + /// - anything else answers a 200, so a bypass shows up as a successful response. + fn spawn_server() -> u16 { + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let port = listener.local_addr().unwrap().port(); + + std::thread::spawn(move || { + for stream in listener.incoming() { + let mut stream = stream.unwrap(); + + // the whole request must be consumed, otherwise closing the connection + // with unread data queued resets it before the client reads the response + let mut reader = BufReader::new(stream.try_clone().unwrap()); + let mut request_line = String::new(); + reader.read_line(&mut request_line).unwrap(); + loop { + let mut header = String::new(); + reader.read_line(&mut header).unwrap(); + if matches!(header.as_str(), "" | "\r\n" | "\n") { + break; + } + } + + let path = request_line.split(' ').nth(1).unwrap_or("/"); + let location = match path.strip_prefix("/redirect/") { + Some("0") => Some(format!("http://localhost:{port}/target")), + Some(remaining) => Some(format!( + "http://localhost:{port}/redirect/{}", + remaining.parse::().unwrap() - 1 + )), + None if path == "/redirect-external" => { + Some(format!("http://127.0.0.1:{port}/secret")) + } + None => None, + }; + + let response = match location { + Some(location) => format!( + "HTTP/1.1 302 Found\r\nLocation: {location}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n" + ), + None => "HTTP/1.1 200 OK\r\nContent-Length: 6\r\nConnection: close\r\n\r\nsecret" + .to_string(), + }; + + let _ = stream.write_all(response.as_bytes()); + let _ = stream.flush(); + } + }); + + port + } + + fn get( + url: &str, + scope: Option, + max_redirections: Option, + ) -> Result { + let client = reqwest::ClientBuilder::new() + .redirect(redirect_policy(scope, max_redirections)) + .build() + .unwrap(); + let request = client.get(url); + tauri::async_runtime::block_on( + async move { request.send().await.map_err(map_request_error) }, + ) + } + + fn localhost_scope(port: u16) -> Option { + let entry = Arc::new(format!("http://localhost:{port}/*").parse().unwrap()); + Some(Scope::new(vec![entry], Vec::new())) + } + + #[test] + fn redirect_outside_of_scope_is_denied() { + let port = spawn_server(); + + let err = get( + &format!("http://localhost:{port}/redirect-external"), + localhost_scope(port), + None, + ) + .unwrap_err(); + + match err { + Error::UrlNotAllowed(url) => { + assert_eq!(url.as_str(), format!("http://127.0.0.1:{port}/secret")) + } + e => panic!("expected the redirect to be denied by the scope, got {e:?}"), + } + } + + #[test] + fn redirect_outside_of_scope_is_followed_when_not_configured() { + let port = spawn_server(); + + // `scopeRedirects` is disabled, so only the URL requested by the frontend is checked + let response = get( + &format!("http://localhost:{port}/redirect-external"), + None, + None, + ) + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + } + + #[test] + fn redirect_denied_by_the_scope_is_denied() { + let port = spawn_server(); + let allow = Arc::new(format!("http://localhost:{port}/*").parse().unwrap()); + let deny = Arc::new(format!("http://localhost:{port}/target").parse().unwrap()); + let scope = Some(Scope::new(vec![allow], vec![deny])); + + let err = get(&format!("http://localhost:{port}/redirect/0"), scope, None).unwrap_err(); + + assert!(matches!(err, Error::UrlNotAllowed(_))); + } + + #[test] + fn redirect_inside_of_scope_is_followed() { + let port = spawn_server(); + + let response = get( + &format!("http://localhost:{port}/redirect/2"), + localhost_scope(port), + None, + ) + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + assert_eq!(response.url().path(), "/target"); + } + + #[test] + fn max_redirections_is_enforced() { + let port = spawn_server(); + + for scope in [localhost_scope(port), None] { + let err = get( + &format!("http://localhost:{port}/redirect/5"), + scope, + Some(2), + ) + .unwrap_err(); + + assert!(matches!(err, Error::Network(e) if e.is_redirect())); + } + } + + #[test] + fn zero_max_redirections_does_not_follow() { + let port = spawn_server(); + + let response = get( + &format!("http://localhost:{port}/redirect/0"), + localhost_scope(port), + Some(0), + ) + .unwrap(); + + assert_eq!(response.status(), StatusCode::FOUND); + } +} diff --git a/plugins/http/src/config.rs b/plugins/http/src/config.rs new file mode 100644 index 000000000..02f4e9285 --- /dev/null +++ b/plugins/http/src/config.rs @@ -0,0 +1,47 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::Deserialize; + +/// HTTP plugin configuration, defined on the `plugins > http` object of your `tauri.conf.json`. +#[derive(Debug, Clone, Default, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct Config { + /// Whether the scope is checked on every hop of a redirect chain instead of only on the + /// URL requested by the frontend. Defaults to `false`. + /// + /// When disabled, a server on an allowed origin can answer with a redirect to any other + /// origin - an open redirect, or a server an attacker controls - and the plugin follows it, + /// handing the webview a response from a URL the scope denies, such as a `localhost` + /// service, an internal host or a cloud metadata endpoint. + /// + /// When enabled, a redirect to a URL that is not allowed by the scope fails with + /// [`Error::UrlNotAllowed`](crate::Error::UrlNotAllowed) instead of being followed, so every + /// redirect target must also be part of the scope. This is opt-in because it breaks + /// applications that rely on being redirected outside of their configured scope. + // TODO(v3): enforce the scope on redirects by default and remove this option + #[serde(default)] + pub scope_redirects: bool, +} + +#[cfg(test)] +mod tests { + use super::Config; + + #[test] + fn deserializes_the_plugin_configuration() { + // the plugin configuration is optional, and the scope check is opt-in + let config: Option = serde_json::from_value(serde_json::Value::Null).unwrap(); + assert!(config.is_none()); + + let config: Config = serde_json::from_str("{}").unwrap(); + assert!(!config.scope_redirects); + + let config: Config = serde_json::from_str(r#"{ "scopeRedirects": true }"#).unwrap(); + assert!(config.scope_redirects); + + // a typo must not silently disable the scope check + assert!(serde_json::from_str::(r#"{ "scopeRedirect": true }"#).is_err()); + } +} diff --git a/plugins/http/src/lib.rs b/plugins/http/src/lib.rs index e5f5c2b4e..e33275476 100644 --- a/plugins/http/src/lib.rs +++ b/plugins/http/src/lib.rs @@ -43,6 +43,21 @@ //! - **tracing**: Adds request, response, and cookie-store diagnostics through `tracing`. //! - **unsafe-headers**: Allows webview requests to send any headers. //! - **dangerous-settings**: Allows dangerous client settings such as accepting invalid certificates or hostnames. +//! +//! ## Configuration +//! +//! See [`Config`] for the options that can be set on the `plugins > http` object of your +//! `tauri.conf.json`: +//! +//! ```json +//! { +//! "plugins": { +//! "http": { +//! "scopeRedirects": true +//! } +//! } +//! } +//! ``` pub use reqwest; use tauri::{ @@ -50,9 +65,11 @@ use tauri::{ Manager, Runtime, }; +pub use config::Config; pub use error::{Error, Result}; mod commands; +mod config; mod error; #[cfg(feature = "cookies")] mod reqwest_cookie_store; @@ -62,13 +79,14 @@ mod scope; const COOKIES_FILENAME: &str = ".cookies"; pub(crate) struct Http { + pub(crate) config: Config, #[cfg(feature = "cookies")] cookies_jar: std::sync::Arc, } -pub fn init() -> TauriPlugin { - Builder::::new("http") - .setup(|app, _| { +pub fn init() -> TauriPlugin> { + Builder::>::new("http") + .setup(|app, api| { #[cfg(feature = "cookies")] let cookies_jar = { use crate::reqwest_cookie_store::*; @@ -96,6 +114,7 @@ pub fn init() -> TauriPlugin { }; let state = Http { + config: api.config().clone().unwrap_or_default(), #[cfg(feature = "cookies")] cookies_jar: std::sync::Arc::new(cookies_jar), }; diff --git a/plugins/http/src/scope.rs b/plugins/http/src/scope.rs index e7638c2ce..a7745c2cd 100644 --- a/plugins/http/src/scope.rs +++ b/plugins/http/src/scope.rs @@ -59,16 +59,16 @@ impl<'de> Deserialize<'de> for Entry { } } -/// Scope for filesystem access. +/// Scope for HTTP access. #[derive(Debug)] -pub struct Scope<'a> { - allowed: Vec<&'a Arc>, - denied: Vec<&'a Arc>, +pub struct Scope { + allowed: Vec>, + denied: Vec>, } -impl<'a> Scope<'a> { +impl Scope { /// Creates a new scope from the scope configuration. - pub(crate) fn new(allowed: Vec<&'a Arc>, denied: Vec<&'a Arc>) -> Self { + pub(crate) fn new(allowed: Vec>, denied: Vec>) -> Self { Self { allowed, denied } } @@ -94,25 +94,24 @@ impl<'a> Scope<'a> { } #[cfg(test)] -mod tests { - use std::{str::FromStr, sync::Arc}; +impl std::str::FromStr for Entry { + type Err = urlpattern::quirks::Error; - use super::Entry; - - impl FromStr for Entry { - type Err = urlpattern::quirks::Error; - - fn from_str(s: &str) -> Result { - let pattern = super::parse_url_pattern(s)?; - Ok(Self { url: pattern }) - } + fn from_str(s: &str) -> Result { + let url = parse_url_pattern(s)?; + Ok(Self { url }) } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; #[test] fn denied_takes_precedence() { let allow = Arc::new("http://localhost:8080/file.png".parse().unwrap()); let deny = Arc::new("http://localhost:8080/*".parse().unwrap()); - let scope = super::Scope::new(vec![&allow], vec![&deny]); + let scope = super::Scope::new(vec![allow], vec![deny]); assert!(!scope.is_allowed(&"http://localhost:8080/file.png".parse().unwrap())); assert!(!scope.is_allowed(&"http://localhost:8080?framework=tauri".parse().unwrap())); } @@ -121,7 +120,7 @@ mod tests { fn fixed_url() { // plain URL let entry = Arc::new("http://localhost:8080".parse().unwrap()); - let scope = super::Scope::new(vec![&entry], Vec::new()); + let scope = super::Scope::new(vec![entry], Vec::new()); assert!(scope.is_allowed(&"http://localhost:8080".parse().unwrap())); assert!(scope.is_allowed(&"http://localhost:8080/".parse().unwrap())); assert!(scope.is_allowed(&"http://localhost:8080/file".parse().unwrap())); @@ -137,7 +136,7 @@ mod tests { fn fixed_path() { // URL with fixed path let entry = Arc::new("http://localhost:8080/file.png".parse().unwrap()); - let scope = super::Scope::new(vec![&entry], Vec::new()); + let scope = super::Scope::new(vec![entry], Vec::new()); assert!(scope.is_allowed(&"http://localhost:8080/file.png".parse().unwrap())); assert!(scope.is_allowed(&"http://localhost:8080/file.png?q=1".parse().unwrap())); @@ -150,7 +149,7 @@ mod tests { #[test] fn pattern_wildcard() { let entry = Arc::new("http://localhost:8080/*.png".parse().unwrap()); - let scope = super::Scope::new(vec![&entry], Vec::new()); + let scope = super::Scope::new(vec![entry], Vec::new()); assert!(scope.is_allowed(&"http://localhost:8080/file.png".parse().unwrap())); assert!(scope.is_allowed(&"http://localhost:8080/file.png#head".parse().unwrap())); @@ -167,7 +166,7 @@ mod tests { #[test] fn domain_wildcard() { let entry = Arc::new("http://*".parse().unwrap()); - let scope = super::Scope::new(vec![&entry], Vec::new()); + let scope = super::Scope::new(vec![entry], Vec::new()); assert!(scope.is_allowed(&"http://something.else".parse().unwrap())); assert!(scope.is_allowed(&"http://something.else#tauri".parse().unwrap())); @@ -182,7 +181,7 @@ mod tests { assert!(!scope.is_allowed(&"https://something.else".parse().unwrap())); let entry = Arc::new("http://*/*".parse().unwrap()); - let scope = super::Scope::new(vec![&entry], Vec::new()); + let scope = super::Scope::new(vec![entry], Vec::new()); assert!(scope.is_allowed(&"http://something.else".parse().unwrap())); assert!(scope.is_allowed(&"http://something.else/path/to/file".parse().unwrap())); @@ -191,7 +190,7 @@ mod tests { #[test] fn scheme_wildcard() { let entry = Arc::new("*://*".parse().unwrap()); - let scope = super::Scope::new(vec![&entry], Vec::new()); + let scope = super::Scope::new(vec![entry], Vec::new()); assert!(scope.is_allowed(&"http://something.else".parse().unwrap())); assert!(scope.is_allowed(&"http://something.else/path/to/file".parse().unwrap())); @@ -201,7 +200,7 @@ mod tests { assert!(scope.is_allowed(&"https://something.else?x=1#frag".parse().unwrap())); let entry = Arc::new("*://*/*".parse().unwrap()); - let scope = super::Scope::new(vec![&entry], Vec::new()); + let scope = super::Scope::new(vec![entry], Vec::new()); assert!(scope.is_allowed(&"http://something.else".parse().unwrap())); assert!(scope.is_allowed(&"http://something.else/path/to/file".parse().unwrap())); @@ -212,7 +211,7 @@ mod tests { #[test] fn validate_query() { let entry = Arc::new("https://tauri.app/path?x=*".parse().unwrap()); - let scope = super::Scope::new(vec![&entry], Vec::new()); + let scope = super::Scope::new(vec![entry], Vec::new()); assert!(scope.is_allowed(&"https://tauri.app/path?x=5".parse().unwrap())); @@ -222,7 +221,7 @@ mod tests { #[test] fn validate_hash() { let entry = Arc::new("https://tauri.app/path#frame*".parse().unwrap()); - let scope = super::Scope::new(vec![&entry], Vec::new()); + let scope = super::Scope::new(vec![entry], Vec::new()); assert!(scope.is_allowed(&"https://tauri.app/path#frame".parse().unwrap()));