Merge commit from fork

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.
This commit is contained in:
Lucas Fernandes Nogueira
2026-09-19 21:01:48 -03:00
committed by GitHub
parent 1308bfa399
commit 1198a524b7
6 changed files with 448 additions and 130 deletions
+19
View File
@@ -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<R, Option<Config>>` instead of `TauriPlugin<R>`.
+4
View File
@@ -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 */
+330 -100
View File
@@ -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<Scope>, max_redirections: Option<usize>) -> 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::<Error>() {
return Error::UrlNotAllowed(url.clone());
}
source = err.source();
}
}
Error::Network(error)
}
#[command]
pub async fn fetch<R: Runtime>(
webview: Webview<R>,
@@ -214,118 +268,115 @@ pub async fn fetch<R: Runtime>(
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::<usize>().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<Scope>,
max_redirections: Option<usize>,
) -> Result<reqwest::Response> {
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<Scope> {
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);
}
}
+47
View File
@@ -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<Config> = 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::<Config>(r#"{ "scopeRedirect": true }"#).is_err());
}
}
+22 -3
View File
@@ -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<crate::reqwest_cookie_store::CookieStoreMutex>,
}
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::<R>::new("http")
.setup(|app, _| {
pub fn init<R: Runtime>() -> TauriPlugin<R, Option<Config>> {
Builder::<R, Option<Config>>::new("http")
.setup(|app, api| {
#[cfg(feature = "cookies")]
let cookies_jar = {
use crate::reqwest_cookie_store::*;
@@ -96,6 +114,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
};
let state = Http {
config: api.config().clone().unwrap_or_default(),
#[cfg(feature = "cookies")]
cookies_jar: std::sync::Arc::new(cookies_jar),
};
+26 -27
View File
@@ -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<Entry>>,
denied: Vec<&'a Arc<Entry>>,
pub struct Scope {
allowed: Vec<Arc<Entry>>,
denied: Vec<Arc<Entry>>,
}
impl<'a> Scope<'a> {
impl Scope {
/// Creates a new scope from the scope configuration.
pub(crate) fn new(allowed: Vec<&'a Arc<Entry>>, denied: Vec<&'a Arc<Entry>>) -> Self {
pub(crate) fn new(allowed: Vec<Arc<Entry>>, denied: Vec<Arc<Entry>>) -> 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<Self, Self::Err> {
let pattern = super::parse_url_pattern(s)?;
Ok(Self { url: pattern })
}
fn from_str(s: &str) -> Result<Self, Self::Err> {
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()));