Skip to content

Commit 0ad1c65

Browse files
committed
feat(core): add http allowlist scope [TRI-008] (#24)
1 parent 239bba5 commit 0ad1c65

15 files changed

Lines changed: 143 additions & 27 deletions

File tree

.changes/http-refactor.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"tauri": patch
3+
---
4+
5+
`tauri::api::HttpRequestBuilder::new` now returns a `Result` to validate the url.

.changes/http-scope.md

Whitespace-only changes.

.changes/scope-config.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@
33
---
44

55
Adds `scope` glob array config under `tauri > allowlist > fs`.
6+
Adds `assetScope` glob array config under `tauri > allowlist > protocol`.
7+
Adds `scope` URL array config under `tauri > allowlist > http`.

.changes/scopes.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22
"tauri": patch
33
---
44

5-
Scopes the filesystem APIs from the webview access using `tauri.conf.json > tauri > allowlist > fs > scope`.
5+
Scopes the `filesystem` APIs from the webview access using `tauri.conf.json > tauri > allowlist > fs > scope`.
66
Scopes the `asset` protocol access using `tauri.conf.json > tauri > allowlist > protocol > assetScope`.
7+
Scopes the `http` APIs from the webview access using `tauri.conf.json > tauri > allowlist > http > scope`.

core/tauri-utils/src/config.rs

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -932,11 +932,19 @@ impl Allowlist for DialogAllowlistConfig {
932932
}
933933
}
934934

935+
/// HTTP API scope definition.
936+
/// It is a list of URLs that can be accessed by the webview when using the HTTP APIs.
937+
#[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize)]
938+
#[cfg_attr(feature = "schema", derive(JsonSchema))]
939+
pub struct HttpAllowlistScope(pub Vec<Url>);
940+
935941
/// Allowlist for the HTTP APIs.
936942
#[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize)]
937943
#[cfg_attr(feature = "schema", derive(JsonSchema))]
938944
#[serde(rename_all = "camelCase", deny_unknown_fields)]
939945
pub struct HttpAllowlistConfig {
946+
/// The access scope for the HTTP APIs.
947+
pub scope: HttpAllowlistScope,
940948
/// Use this flag to enable all HTTP API features.
941949
#[serde(default)]
942950
pub all: bool,
@@ -948,6 +956,7 @@ pub struct HttpAllowlistConfig {
948956
impl Allowlist for HttpAllowlistConfig {
949957
fn all_features() -> Vec<&'static str> {
950958
let allowlist = Self {
959+
scope: Default::default(),
951960
all: false,
952961
request: true,
953962
};
@@ -1653,6 +1662,12 @@ mod build {
16531662
quote! { ::std::path::PathBuf::from(#s) }
16541663
}
16551664

1665+
/// Creates a `Url` constructor `TokenStream`.
1666+
fn url_lit(url: &Url) -> TokenStream {
1667+
let url = url.as_str();
1668+
quote! { #url.parse().unwrap() }
1669+
}
1670+
16561671
/// Create a map constructor, mapping keys and values with other `TokenStream`s.
16571672
///
16581673
/// This function is pretty generic because the types of keys AND values get transformed.
@@ -1758,8 +1773,8 @@ mod build {
17581773
quote! { #prefix::App(#path) }
17591774
}
17601775
Self::External(url) => {
1761-
let url = url.as_str();
1762-
quote! { #prefix::External(#url.parse().unwrap()) }
1776+
let url = url_lit(url);
1777+
quote! { #prefix::External(#url) }
17631778
}
17641779
})
17651780
}
@@ -2041,12 +2056,27 @@ mod build {
20412056
}
20422057
}
20432058

2059+
impl ToTokens for HttpAllowlistScope {
2060+
fn to_tokens(&self, tokens: &mut TokenStream) {
2061+
let allowed_urls = vec_lit(&self.0, url_lit);
2062+
tokens.append_all(quote! { ::tauri::utils::config::HttpAllowlistScope(#allowed_urls) })
2063+
}
2064+
}
2065+
2066+
impl ToTokens for HttpAllowlistConfig {
2067+
fn to_tokens(&self, tokens: &mut TokenStream) {
2068+
let scope = &self.scope;
2069+
tokens.append_all(quote! { ::tauri::utils::config::HttpAllowlistConfig { scope: #scope, ..Default::default() } })
2070+
}
2071+
}
2072+
20442073
impl ToTokens for AllowlistConfig {
20452074
fn to_tokens(&self, tokens: &mut TokenStream) {
20462075
let fs = &self.fs;
20472076
let protocol = &self.protocol;
2077+
let http = &self.http;
20482078
tokens.append_all(
2049-
quote! { ::tauri::utils::config::AllowlistConfig { fs: #fs, protocol: #protocol, ..Default::default() } },
2079+
quote! { ::tauri::utils::config::AllowlistConfig { fs: #fs, protocol: #protocol, http: #http, ..Default::default() } },
20502080
)
20512081
}
20522082
}

core/tauri/src/api/error.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ pub enum Error {
7272
#[cfg(notification_all)]
7373
#[error(transparent)]
7474
Notification(#[from] notify_rust::error::Error),
75+
/// Url error.
76+
#[error(transparent)]
77+
Url(#[from] url::ParseError),
7578
/// failed to detect the current platform.
7679
#[error("failed to detect platform: {0}")]
7780
FailedToDetectPlatform(String),

core/tauri/src/api/http.rs

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use http::{header::HeaderName, Method};
88
use serde::{Deserialize, Serialize};
99
use serde_json::Value;
1010
use serde_repr::{Deserialize_repr, Serialize_repr};
11+
use url::Url;
1112

1213
use std::{collections::HashMap, path::PathBuf, time::Duration};
1314

@@ -139,7 +140,7 @@ impl Client {
139140
pub async fn send(&self, request: HttpRequestBuilder) -> crate::api::Result<Response> {
140141
let method = Method::from_bytes(request.method.to_uppercase().as_bytes())?;
141142

142-
let mut request_builder = self.0.request(method, &request.url);
143+
let mut request_builder = self.0.request(method, request.url.as_str());
143144

144145
if let Some(query) = request.query {
145146
request_builder = request_builder.query(&query);
@@ -265,7 +266,7 @@ pub struct HttpRequestBuilder {
265266
/// The request method (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, CONNECT or TRACE)
266267
pub method: String,
267268
/// The request URL
268-
pub url: String,
269+
pub url: Url,
269270
/// The request query params
270271
pub query: Option<HashMap<String, String>>,
271272
/// The request headers
@@ -280,16 +281,16 @@ pub struct HttpRequestBuilder {
280281

281282
impl HttpRequestBuilder {
282283
/// Initializes a new instance of the HttpRequestrequest_builder.
283-
pub fn new(method: impl Into<String>, url: impl Into<String>) -> Self {
284-
Self {
284+
pub fn new(method: impl Into<String>, url: impl AsRef<str>) -> crate::api::Result<Self> {
285+
Ok(Self {
285286
method: method.into(),
286-
url: url.into(),
287+
url: Url::parse(url.as_ref())?,
287288
query: None,
288289
headers: None,
289290
body: None,
290291
timeout: None,
291292
response_type: None,
292-
}
293+
})
293294
}
294295

295296
/// Sets the request parameters.
@@ -330,7 +331,7 @@ pub struct Response(ResponseType, reqwest::Response);
330331
/// The HTTP response.
331332
#[cfg(not(feature = "reqwest-client"))]
332333
#[derive(Debug)]
333-
pub struct Response(ResponseType, attohttpc::Response, String);
334+
pub struct Response(ResponseType, attohttpc::Response, Url);
334335

335336
impl Response {
336337
/// Reads the response as raw bytes.
@@ -346,7 +347,7 @@ impl Response {
346347
/// Reads the response and returns its info.
347348
pub async fn read(self) -> crate::api::Result<ResponseData> {
348349
#[cfg(feature = "reqwest-client")]
349-
let url = self.1.url().to_string();
350+
let url = self.1.url().clone();
350351
#[cfg(not(feature = "reqwest-client"))]
351352
let url = self.2;
352353

@@ -410,7 +411,7 @@ pub struct RawResponse {
410411
#[non_exhaustive]
411412
pub struct ResponseData {
412413
/// Response URL. Useful if it followed redirects.
413-
pub url: String,
414+
pub url: Url,
414415
/// Response status code.
415416
pub status: u16,
416417
/// Response headers.

core/tauri/src/app.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1025,6 +1025,8 @@ impl<R: Runtime> Builder<R> {
10251025
&env,
10261026
&app.config().tauri.allowlist.protocol.asset_scope,
10271027
),
1028+
#[cfg(http_request)]
1029+
http: crate::scope::HttpScope::for_http_api(&app.config().tauri.allowlist.http.scope),
10281030
});
10291031
app.manage(env);
10301032

core/tauri/src/endpoints/http.rs

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -66,17 +66,27 @@ impl Cmd {
6666

6767
#[module_command_handler(http_request, "http > request")]
6868
async fn http_request<R: Runtime>(
69-
_context: InvokeContext<R>,
69+
context: InvokeContext<R>,
7070
client_id: ClientId,
7171
options: Box<HttpRequestBuilder>,
7272
) -> crate::Result<ResponseData> {
73-
let client = clients()
74-
.lock()
75-
.unwrap()
76-
.get(&client_id)
77-
.ok_or(crate::Error::HttpClientNotInitialized)?
78-
.clone();
79-
let response = client.send(*options).await?;
80-
Ok(response.read().await?)
73+
use crate::Manager;
74+
if context
75+
.window
76+
.state::<crate::Scopes>()
77+
.http
78+
.is_allowed(&options.url)
79+
{
80+
let client = clients()
81+
.lock()
82+
.unwrap()
83+
.get(&client_id)
84+
.ok_or(crate::Error::HttpClientNotInitialized)?
85+
.clone();
86+
let response = client.send(*options).await?;
87+
Ok(response.read().await?)
88+
} else {
89+
Err(crate::Error::UrlNotAllowed(options.url))
90+
}
8191
}
8292
}

core/tauri/src/error.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ pub enum Error {
8787
/// The user did not allow sending notifications.
8888
#[error("sending notification was not allowed by the user")]
8989
NotificationNotAllowed,
90+
/// URL not allowed by the scope.
91+
#[error("url not allowed on the configured scope: {0}")]
92+
UrlNotAllowed(url::Url),
9093
}
9194

9295
impl From<serde_json::Error> for Error {

0 commit comments

Comments
 (0)